Files
backspace/packages/web/src/utils/crossStoreResolvers.ts
T
Jannis Braun 4d2e50b55b fix(web): extract cross-store resolvers into neutral utility to break TDZ
instanceStore registers three resolver functions at module load —
setApiForOriginResolver, setUserIdForOriginResolver,
setOriginFromHostnameResolver — whose backing `let` bindings used to
live in spaceStore. When the module graph was entered from
instanceStore (e.g. JoinSpaceModal importing useInstanceStore) the
order became spaceStore → chatStore → useWebSocket → socialStore →
instanceStore (top-level setter call) while spaceStore was still
paused on its line-8 chatStore import, so the backing `let` had not
been reached yet and the setter crashed with
`Cannot access '_getApiForOrigin' before initialization`. This left
InviteModal.test.tsx and JoinSpace.test.tsx unable to even load their
suites once AudioManager was mocked away.

Move the three `let` bindings, their setters, their pure getters, plus
the WS-populated user-ID cache (`_myUserIdByOrigin`, setMyUserIdForOrigin,
getCachedUserIdForOrigin, clearMyUserIdCache) into
`packages/web/src/utils/crossStoreResolvers.ts`. The utility imports
nothing from `./stores/*`, so no back-edge exists. spaceStore re-exports
the public surface for backward compatibility with the many existing
import sites; instanceStore imports the setters directly from the
utility (the in-cycle re-export path does not resolve at module-init
time under vite-ssr, so a direct import is required for the top-level
setter calls).

spaceStore's remaining wrappers (resolveUserOrigin, getLayoutHomeOrigin,
getMyUserIdForOrigin) stay where they are — they combine the utility's
pure lookups with authStore state — but now delegate to the utility.

Also adds the AudioManager mock to InviteModal.test.tsx and
JoinSpace.test.tsx so their suites actually load (same pattern already
used in 5 other test files). Net test-suite result: 127/131 pass (up
from 121/121 — +6 newly unlockable). The 4 remaining JoinSpace
failures are pre-existing stale UI-text assertions (the placeholder was
expanded and the submit button was made disable-when-empty) made
visible by the suite now loading; they're orthogonal to this change
and handed back for a separate triage.

Closes backlog #27.
2026-04-23 02:46:19 +02:00

100 lines
4.4 KiB
TypeScript

// Cross-store resolvers for federation identity and API client routing.
//
// This module exists SOLELY to break a temporal-dead-zone (TDZ) cycle between
// spaceStore and instanceStore. instanceStore runs top-level `setXResolver`
// calls at module load; the backing `let` bindings used to live in spaceStore.
// When the import graph is entered from instanceStore (e.g. via a component
// like JoinSpaceModal that imports `useInstanceStore` directly) — or any chain
// that resolves in an order where spaceStore is mid-load when instanceStore's
// top-level code runs — the `let _getApiForOrigin = null` declaration has not
// been evaluated yet, so the setter crashes with
// `Cannot access '_getApiForOrigin' before initialization`.
//
// The fix: hold the resolvers, their setters, and the local user-ID cache
// in this neutral module. It imports nothing from any store, so no back-edge
// exists and module load order between spaceStore / instanceStore / anything
// else cannot cause TDZ. spaceStore re-exports the public surface for
// backward compatibility with existing import sites.
//
// IMPORTANT: do not add imports from any `./stores/*` module here. Doing so
// re-creates the exact cycle this module was carved out to break.
import { api, BackspaceApiClient } from '../api/client';
// ─── API client resolution ────────────────────────────────────────────────────
// Registered by instanceStore on import; maps an origin to the instance-
// specific API client. `'' | null | undefined` origin always returns the home
// `api` client.
let _getApiForOrigin: ((origin: string) => BackspaceApiClient) | null = null;
export function setApiForOriginResolver(
resolver: (origin: string) => BackspaceApiClient
): void {
_getApiForOrigin = resolver;
}
export function getApiForOrigin(origin: string): BackspaceApiClient {
if (!origin || !_getApiForOrigin) return api;
return _getApiForOrigin(origin);
}
// ─── Hostname → origin resolution (federation) ────────────────────────────────
// Registered by instanceStore on import; maps a federated user's `homeInstance`
// hostname (e.g. "remote.example.com") to a full origin URL
// (e.g. "https://remote.example.com") by looking up connected instances.
let _resolveOriginFromHostname: ((hostname: string) => string) | null = null;
export function setOriginFromHostnameResolver(
resolver: (hostname: string) => string
): void {
_resolveOriginFromHostname = resolver;
}
/**
* Pure hostname→origin lookup. Returns '' if the resolver is not yet
* registered or the hostname is unknown. Callers that need to combine this
* with `window.location.host` or `authStore` state should layer on top
* (see `resolveUserOrigin` / `getLayoutHomeOrigin` in spaceStore).
*/
export function resolveOriginFromHostname(hostname: string): string {
return _resolveOriginFromHostname?.(hostname) ?? '';
}
// ─── User ID resolution (federation) ──────────────────────────────────────────
// Registered by instanceStore on import; maps an origin to the local user's
// ID on that instance. Used as a fallback for `getMyUserIdForOrigin` when the
// direct WS-populated cache below has not yet been filled.
let _getUserIdForOrigin: ((origin: string) => string | undefined) | null = null;
export function setUserIdForOriginResolver(
resolver: (origin: string) => string | undefined
): void {
_getUserIdForOrigin = resolver;
}
export function resolveUserIdFromInstances(origin: string): string | undefined {
return _getUserIdForOrigin?.(origin);
}
// ─── Direct user-ID cache (populated from WS ready events) ────────────────────
// Bypasses the instanceStore resolver for reliable federation support even
// while `instanceStore.instances[].user` is still a placeholder mid-connection.
const _myUserIdByOrigin = new Map<string, string>();
export function setMyUserIdForOrigin(origin: string, userId: string): void {
_myUserIdByOrigin.set(origin, userId);
}
export function getCachedUserIdForOrigin(origin: string): string | undefined {
return _myUserIdByOrigin.get(origin);
}
/** Clears the WS-populated user-ID cache. Called by spaceStore.reset() on logout. */
export function clearMyUserIdCache(): void {
_myUserIdByOrigin.clear();
}