refactor(data): route entities by scope via the SDK — no store ids in the app
Festipod now treats @ng-eventually/client as a finished NextGraph SDK: the app decides only each entity's logical scope (events/PdR public, profiles/ participations protected, settings private) and calls the lib by scope. The old mono-store default and the FESTIPOD_MULTISTORE path collapse into ONE scope path. Removed every physical-store leak from the app data-plane (ngGraph, registration, FestipodDataContext, NextGraphContext, useShapeWithDefaults): no more did🆖${store_id} construction. The session is handed to the lib only at the sanctioned injection point (ngSession/configureStoreRegistry). Product behavior unchanged. @data 20/20; build + tsc clean. (_debt.md included; the T03.e doctrine pass settles accumulated doc-debt.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,17 +26,8 @@ import {
|
||||
import { useNextGraph } from './NextGraphContext';
|
||||
import { useAccount, normalizeUsername } from './AccountContext';
|
||||
import { applyIsolation } from '../utils/isolation';
|
||||
import { ensureAccount, resolveReadGraphs, resolveWriteGraph, createEntityDoc, listEntityDocs } from '../utils/storeRegistry';
|
||||
import { resolveScopeGraph, listEntityDocs } from '../utils/storeRegistry';
|
||||
import { useShapeWithDefaults, type ShapeScope } from '../hooks/useShapeWithDefaults';
|
||||
|
||||
// Multi-document mode (storeRegistry): one document per (account × scope),
|
||||
// mirroring the target per-user stores. Default OFF — the validated mono-store
|
||||
// path stays the default until the multi-document path is broker-validated.
|
||||
// Flip via FESTIPOD_MULTISTORE=1 at build time. See brief_2026-06-15_shared-wallet-shim.
|
||||
// Browser-safe env read: the bundler inlines process.env.NODE_ENV but NOT
|
||||
// custom vars, so a bare `process.env.FESTIPOD_MULTISTORE` throws
|
||||
// "process is not defined" in the browser harness. Guard it.
|
||||
const MULTISTORE = typeof process !== 'undefined' && process?.env?.FESTIPOD_MULTISTORE === '1';
|
||||
import {
|
||||
FpEventShapeType,
|
||||
FpUserProfileShapeType,
|
||||
@@ -257,45 +248,32 @@ function useLocalData(empty?: boolean): FestipodDataContextValue {
|
||||
function useNgData(): FestipodDataContextValue {
|
||||
const { session } = useNextGraph();
|
||||
const { username } = useAccount();
|
||||
// Mono-store scopes (MULTISTORE off). Two native stores of the shared wallet:
|
||||
// - privateNuri: kept as the anchor for the inbox shim (host inbox deposits)
|
||||
// and for private settings. Opens the repo in the verifier.
|
||||
// - protectedNuri: T02.h (axe A) — the SHAREABLE domain entities (events,
|
||||
// profiles, participations) now READ from and WRITE to the real protected
|
||||
// native store, representative of the target per-user wallet. Verified to
|
||||
// open for ORM reads+writes exactly like private (round-trip, no
|
||||
// RepoNotFound — see protected-store.feature). Subscribing useShape with
|
||||
// this NURI opens its repo in the verifier (required for writes).
|
||||
const privateNuri = session ? `did:ng:${session.private_store_id}` : undefined;
|
||||
const protectedNuri = session ? `did:ng:${session.protected_store_id}` : undefined;
|
||||
|
||||
// Multi-document state (storeRegistry): read fan-out (all accounts' docs per
|
||||
// scope) and the current account's write docs. Populated by the effect below.
|
||||
const [readGraphs, setReadGraphs] = useState<{ public: string[]; protected: string[] }>({ public: [], protected: [] });
|
||||
const [writeGraphs, setWriteGraphs] = useState<{ protected?: string }>({});
|
||||
// The app speaks ONLY in logical scopes — it holds no store id and builds no
|
||||
// `did:ng:${…}` NURI. It asks the SDK (`resolveScopeGraph(scope)`) for the
|
||||
// opaque graph NURI of each scope; the SDK owns the physical placement (today
|
||||
// it resolves the shareable domain scopes to the shared wallet's native
|
||||
// stores — its internal detail). `ready` gates the effects on the session.
|
||||
const ready = !!session;
|
||||
|
||||
// Scope-resolved graphs (from the SDK). Domain entities: events → public,
|
||||
// profiles + participations → protected. Populated by the effect below.
|
||||
const [scopeGraphs, setScopeGraphs] = useState<{ public?: string; protected?: string }>({});
|
||||
useEffect(() => {
|
||||
if (!MULTISTORE || !privateNuri || !username) return;
|
||||
if (!ready) return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
await ensureAccount(username); // create this account's index docs on first sight
|
||||
// Public = per-entity documents (events/PdR) listed by the public index.
|
||||
// Protected = grouped in each account's protected index document.
|
||||
const [pub, prot, wProt] = await Promise.all([
|
||||
listEntityDocs('public'),
|
||||
resolveReadGraphs('protected'),
|
||||
resolveWriteGraph(username, 'protected'),
|
||||
const [pub, prot] = await Promise.all([
|
||||
resolveScopeGraph('public'),
|
||||
resolveScopeGraph('protected'),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
setReadGraphs({ public: pub, protected: prot });
|
||||
setWriteGraphs({ protected: wProt });
|
||||
if (!cancelled) setScopeGraphs({ public: pub, protected: prot });
|
||||
} catch (err) {
|
||||
console.error('[FestipodData] storeRegistry init failed:', err);
|
||||
console.error('[FestipodData] scope resolution failed:', err);
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, [privateNuri, username]);
|
||||
}, [ready]);
|
||||
|
||||
// --- Public discovery (T02.e): cross-account fan-out, ALWAYS on ------------
|
||||
// Materialize the cross-account source of PUBLIC entities so a user discovers
|
||||
@@ -313,7 +291,7 @@ function useNgData(): FestipodDataContextValue {
|
||||
// shim IS populated (multi-account staging), discovery unions those events in.
|
||||
const [discoveryGraphs, setDiscoveryGraphs] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
if (!privateNuri) return;
|
||||
if (!ready) return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
@@ -324,18 +302,14 @@ function useNgData(): FestipodDataContextValue {
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, [privateNuri, username]);
|
||||
}, [ready, username]);
|
||||
const discoveryScope: ShapeScope = discoveryGraphs.length ? { graphs: discoveryGraphs } : undefined;
|
||||
|
||||
// Scope per entity: events live in the PUBLIC docs, profiles + participations
|
||||
// in the PROTECTED docs. Mono-store mode collapses all to the PROTECTED native
|
||||
// store (T02.h, axe A) — the shareable domain entities read from there.
|
||||
const publicScope: ShapeScope = MULTISTORE
|
||||
? (readGraphs.public.length ? { graphs: readGraphs.public } : undefined)
|
||||
: protectedNuri;
|
||||
const protectedScope: ShapeScope = MULTISTORE
|
||||
? (readGraphs.protected.length ? { graphs: readGraphs.protected } : undefined)
|
||||
: protectedNuri;
|
||||
// Scope per entity: events read/write the PUBLIC scope, profiles +
|
||||
// participations the PROTECTED scope. Both are opaque SDK-resolved graph NURIs
|
||||
// (the SDK owns placement) — the app never sees a store id.
|
||||
const publicScope: ShapeScope = scopeGraphs.public;
|
||||
const protectedScope: ShapeScope = scopeGraphs.protected;
|
||||
|
||||
// useShapeWithDefaults: show EMPTY data until NG populates (no seed defaults)
|
||||
const emptyEvents: FpEventData[] = [];
|
||||
@@ -388,9 +362,8 @@ function useNgData(): FestipodDataContextValue {
|
||||
const hasTriedAutoSeed = useRef(false);
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'production') return;
|
||||
if (MULTISTORE) return; // seed targets the private store; multi-doc seeding is a separate concern
|
||||
if (hasTriedAutoSeed.current) return;
|
||||
if (!privateNuri) return;
|
||||
if (!ready) return;
|
||||
const t = setTimeout(() => {
|
||||
hasTriedAutoSeed.current = true;
|
||||
if (eventsShape.ngSet.size === 0 && usersShape.ngSet.size === 0) {
|
||||
@@ -405,7 +378,7 @@ function useNgData(): FestipodDataContextValue {
|
||||
}
|
||||
}, 3000);
|
||||
return () => clearTimeout(t);
|
||||
}, [privateNuri, eventsShape.ngSet, usersShape.ngSet, participationsShape.ngSet]);
|
||||
}, [ready, eventsShape.ngSet, usersShape.ngSet, participationsShape.ngSet]);
|
||||
|
||||
// --- Derived ---
|
||||
// Resolve current user from the chosen account username (the perceived login);
|
||||
@@ -428,12 +401,12 @@ function useNgData(): FestipodDataContextValue {
|
||||
[events, currentUserId],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!privateNuri || hostedEventIds.length === 0) return;
|
||||
if (!ready || hostedEventIds.length === 0) return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
// In the polyfill every host inbox resolves to the shared private store,
|
||||
// so read it ONCE and let the curator filter deposits per hosted event.
|
||||
// The SDK resolves the inbox anchor for the current session; read it ONCE
|
||||
// and let the curator filter deposits per hosted event.
|
||||
const targetInbox = await hostInboxNuri('');
|
||||
const all: FpNotificationData[] = [];
|
||||
for (const evId of hostedEventIds) {
|
||||
@@ -454,7 +427,7 @@ function useNgData(): FestipodDataContextValue {
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [privateNuri, hostedEventIds.join('|')]);
|
||||
}, [ready, hostedEventIds.join('|')]);
|
||||
|
||||
// Isolation (staging realism): the app honors the matrix in connected mode —
|
||||
// participations/connections narrowed to self + connections. See isolation.ts.
|
||||
@@ -472,24 +445,16 @@ function useNgData(): FestipodDataContextValue {
|
||||
'| selectedEvent:', selectedEvent?.title ?? '(none)');
|
||||
|
||||
// --- Mutations (NG) ---
|
||||
// Participations stay GROUPED in the account's protected index document.
|
||||
// Mono-store mode writes to the PROTECTED native store (T02.h, axe A) — the
|
||||
// same store the domain read scopes subscribe, so writes round-trip.
|
||||
const protectedGraph = (MULTISTORE ? writeGraphs.protected : undefined) || protectedNuri || '';
|
||||
// Writes target the SCOPE-resolved graphs (opaque SDK NURIs — no store id).
|
||||
// Participations + profiles → protected scope; events → public scope. The
|
||||
// read scopes subscribe the same graphs, so writes round-trip.
|
||||
const protectedGraph = protectedScope || '';
|
||||
const publicGraph = publicScope || '';
|
||||
|
||||
const createEvent = useCallback(async (event: Omit<FpEventData, 'id'>): Promise<FpEventData> => {
|
||||
console.log('[FestipodData] createEvent (NG):', event.title);
|
||||
// Per-entity: in multistore each event is its OWN document. Mono-store: the
|
||||
// private store. (Multistore create reactivity is best-effort — the new doc
|
||||
// is appended to the read fan-out so it shows after re-subscribe.)
|
||||
const eventGraph = MULTISTORE
|
||||
? await createEntityDoc(username || '', 'public')
|
||||
: (protectedNuri || '');
|
||||
if (MULTISTORE && eventGraph) {
|
||||
setReadGraphs(prev =>
|
||||
prev.public.includes(eventGraph) ? prev : { ...prev, public: [...prev.public, eventGraph] },
|
||||
);
|
||||
}
|
||||
// Events live in the PUBLIC scope (SDK-resolved graph — no store id).
|
||||
const eventGraph = publicGraph;
|
||||
eventsShape.ngSet.add({
|
||||
"@graph": eventGraph, "@type": "http://festipod.org/Event", "@id": "",
|
||||
title: event.title, description: event.description, date: event.date,
|
||||
@@ -506,7 +471,7 @@ function useNgData(): FestipodDataContextValue {
|
||||
setSelectedEventId(addedEvent["@id"]);
|
||||
}
|
||||
return { ...event, id: addedEvent?.["@id"] || `ng-pending-${Date.now()}` };
|
||||
}, [protectedGraph, eventsShape.ngSet, participationsShape.ngSet, currentUserId, protectedNuri, username]);
|
||||
}, [protectedGraph, publicGraph, eventsShape.ngSet, participationsShape.ngSet, currentUserId, username]);
|
||||
|
||||
const updateEvent = useCallback((id: string, updates: Partial<FpEventData>) => {
|
||||
console.log('[FestipodData] updateEvent (NG):', id, updates);
|
||||
@@ -529,7 +494,7 @@ function useNgData(): FestipodDataContextValue {
|
||||
console.log('[FestipodData] Already participating, skipping');
|
||||
return;
|
||||
}
|
||||
// 1) Persist the Participation (reactive ORM set — mono-store default path).
|
||||
// 1) Persist the Participation (reactive ORM set — protected scope graph).
|
||||
participationsShape.ngSet.add({
|
||||
"@graph": protectedGraph, "@type": "http://festipod.org/Participation", "@id": "",
|
||||
event: eventId, user: uid, isConfirmed: true,
|
||||
@@ -571,8 +536,8 @@ function useNgData(): FestipodDataContextValue {
|
||||
// deletion is a SPARQL DELETE via the real injected `ng` (docs.sparqlUpdate),
|
||||
// which removes the Participation server-side so it does NOT come back after
|
||||
// re-sync. The delete targets the participation's own @graph (the doc it lives
|
||||
// in) — mono-store: the private store; multistore: the protected write doc —
|
||||
// and is identified by the participation's OWN subject IRI (ngPart["@id"]),
|
||||
// in) — falling back to the protected SCOPE graph (SDK-resolved, no store id)
|
||||
// — and is identified by the participation's OWN subject IRI (ngPart["@id"]),
|
||||
// not a string-match on the object IRIs (the F2 bug: object string-match could
|
||||
// hit 0 rows on IRI-form drift → silent no-op → resurrection).
|
||||
const graphNuri = ngPart["@graph"] || protectedGraph;
|
||||
|
||||
@@ -40,11 +40,11 @@ export function NextGraphProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
sessionPromise
|
||||
.then((s) => {
|
||||
console.log('[NG] Session obtained, stores:', {
|
||||
private: s.private_store_id,
|
||||
protected: s.protected_store_id,
|
||||
public: s.public_store_id,
|
||||
});
|
||||
// The session (incl. native store ids) is handed to the SDK at the
|
||||
// sanctioned injection point (ngSession/storeRegistry). The app context
|
||||
// itself does not surface or manipulate store ids — it only tracks the
|
||||
// connection status and the opaque session handle.
|
||||
console.log('[NG] Session obtained — connected');
|
||||
setNgSession(s);
|
||||
setStatus('connected');
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { inbox, docs, escapeLiteral, escapeIri, assertNuri } from '@ng-eventually/client';
|
||||
import { sessionPromise } from '../utils/ngSession';
|
||||
import { resolveInboxAnchor } from '../utils/storeRegistry';
|
||||
import type { FpNotificationData } from './types';
|
||||
|
||||
/** Notification IRI/type constants (mirror the SHEX Notification shape). */
|
||||
@@ -64,20 +65,18 @@ function mintDepositUid(): string {
|
||||
* Resolve the inbox document NURI for a meeting point / host.
|
||||
*
|
||||
* Preference order: the explicit MeetingPoint `inbox` NURI (SHEX field, T02.a)
|
||||
* when known → else the shared-wallet PRIVATE STORE NURI. The lib's `inbox`
|
||||
* uses `targetInbox` as BOTH the RDF graph AND the SPARQL anchor, and the anchor
|
||||
* must be a REAL repo NURI (a `urn:` is rejected as `InvalidNuri` by the broker)
|
||||
* — so in the polyfill every host inbox physically resolves to the shared wallet
|
||||
* private store; deposits are DISCRIMINATED by their `eventId` payload (the
|
||||
* curator filters per event). At migration this becomes the host's native inbox
|
||||
* NURI and the deposits move to per-host docs. Async because the session (hence
|
||||
* the private_store_id) is resolved lazily.
|
||||
* when known → else the SDK-resolved inbox anchor for the current session
|
||||
* (`resolveInboxAnchor()`). The app asks the SDK for the anchor by intent and
|
||||
* holds NO store id: the SDK owns where deposits physically land (today: the
|
||||
* shared wallet's private store — a real repo NURI, required because the broker
|
||||
* rejects a `urn:` anchor; deposits are discriminated by their `eventId`
|
||||
* payload, the curator filters per event). At migration the SDK returns the
|
||||
* host's native inbox NURI and this call is unchanged.
|
||||
*/
|
||||
export async function hostInboxNuri(eventId: string, explicitInbox?: string): Promise<string> {
|
||||
void eventId; // reserved: per-event inbox docs at migration
|
||||
if (explicitInbox) return explicitInbox;
|
||||
const { private_store_id } = await sessionPromise;
|
||||
return `did:ng:${private_store_id}`;
|
||||
return resolveInboxAnchor();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* useShapeWithDefaults — wrapper around NextGraph ORM's useShape.
|
||||
* useShapeWithDefaults — wrapper around the SDK ORM's useShape.
|
||||
*
|
||||
* Subscribes to the private store via did:ng:<private_store_id> scope,
|
||||
* which opens the store repo in the verifier (required for writes).
|
||||
* Maps results to app types. If the NG set is empty, returns defaults.
|
||||
* Subscribes to a SCOPE-resolved graph NURI (obtained from the SDK by logical
|
||||
* scope — the app holds no store id), which opens the repo in the verifier
|
||||
* (required for writes). Maps results to app types. If the NG set is empty,
|
||||
* returns defaults.
|
||||
*
|
||||
* Must only be called when NG is connected (inside NgDataProvider).
|
||||
*/
|
||||
@@ -18,9 +19,9 @@ export interface ShapeWithDefaults<NgT extends BaseType, AppT> {
|
||||
}
|
||||
|
||||
/**
|
||||
* `scope` is either a single store/document NURI (mono-store mode) or a
|
||||
* `{ graphs }` set of document NURIs (multi-document mode — storeRegistry).
|
||||
* `useShape` accepts both natively.
|
||||
* `scope` is either a single scope-resolved graph NURI (from the SDK) or a
|
||||
* `{ graphs }` set of document NURIs (a read fan-out). `useShape` accepts both
|
||||
* natively. Either way the value is opaque to the app — it never builds it.
|
||||
*/
|
||||
export type ShapeScope = string | { graphs: string[] } | undefined;
|
||||
|
||||
@@ -31,8 +32,8 @@ export function useShapeWithDefaults<NgT extends BaseType, AppT>(
|
||||
mapFromNg: (item: NgT) => AppT,
|
||||
shapesReady: boolean,
|
||||
): ShapeWithDefaults<NgT, AppT> {
|
||||
// Mono-store: a single store NURI opens the repo in the verifier (enables
|
||||
// writes). Multi-document: a { graphs } scope subscribes to several docs.
|
||||
// A single scope-resolved graph NURI opens the repo in the verifier (enables
|
||||
// writes); a { graphs } scope subscribes to several docs (read fan-out).
|
||||
const ngSet = useShape(shapeType, storeNuri as any) as DeepSignalSet<NgT>;
|
||||
const usingDefaults = !shapesReady;
|
||||
const items = usingDefaults ? defaults : [...ngSet].map(item => mapFromNg(item as unknown as NgT));
|
||||
|
||||
+10
-15
@@ -1,24 +1,20 @@
|
||||
/**
|
||||
* NextGraph graph NURI management.
|
||||
*
|
||||
* Returns the PROTECTED store NURI as @graph for ORM entity creation of the
|
||||
* SHAREABLE domain entities (events, profiles, participations). T02.h (axe A)
|
||||
* switched the default domain scope from the private store to the real
|
||||
* protected native store (`did:ng:${protected_store_id}`) — the store
|
||||
* representative of the target per-user wallet. Verified empirically: the
|
||||
* protected store opens for ORM reads AND writes the same way private does
|
||||
* (round-trip probe, no RepoNotFound). Like private, subscribing useShape with
|
||||
* the protected store NURI as scope opens its repo in the verifier, and writes
|
||||
* target the same NURI. (Private stays the anchor for the inbox shim + settings.)
|
||||
* Returns the graph NURI where the SHAREABLE domain entities (events, profiles,
|
||||
* participations) are created via the ORM. These entities live in the PROTECTED
|
||||
* scope, so the NURI is resolved by SCOPE through the SDK
|
||||
* (`resolveScopeGraph('protected')`) — the app holds NO physical store id and
|
||||
* builds NO `did:ng:${store_id}` NURI. The SDK owns the placement/resolution.
|
||||
*/
|
||||
|
||||
import { sessionPromise } from './ngSession';
|
||||
import { resolveScopeGraph } from './storeRegistry';
|
||||
|
||||
let cachedGraphNuri: string | undefined;
|
||||
|
||||
/**
|
||||
* Get the graph NURI for adding shareable ORM entities.
|
||||
* Uses the protected store NURI (T02.h; was the private store before).
|
||||
* Resolves the PROTECTED scope via the SDK (was a raw store NURI before T03.g).
|
||||
*/
|
||||
export async function ensureGraphNuri(
|
||||
...sets: Iterable<{ readonly "@graph": string }>[]
|
||||
@@ -36,9 +32,8 @@ export async function ensureGraphNuri(
|
||||
}
|
||||
}
|
||||
|
||||
// Use PROTECTED store NURI (the repo is opened by useShape with this scope).
|
||||
const session = await sessionPromise;
|
||||
cachedGraphNuri = `did:ng:${session.protected_store_id}`;
|
||||
console.log('[ngGraph] Using protected store as graph:', cachedGraphNuri);
|
||||
// Resolve the PROTECTED scope's graph via the SDK (opaque NURI — no store id).
|
||||
cachedGraphNuri = await resolveScopeGraph('protected');
|
||||
console.log('[ngGraph] Using protected scope as graph:', cachedGraphNuri);
|
||||
return cachedGraphNuri;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function init(): Promise<void> {
|
||||
async (event: any) => {
|
||||
session = event.session;
|
||||
session!.ng ??= realNg;
|
||||
console.log('[NG session] Connected — private_store:', session!.private_store_id);
|
||||
console.log('[NG session] Connected');
|
||||
resolveSessionPromise(session!);
|
||||
initNgSignals(realNg, session!);
|
||||
},
|
||||
|
||||
@@ -48,7 +48,16 @@ export function entityScope(kind: EntityKind): Scope {
|
||||
configureStoreRegistry({
|
||||
getSession: async () => {
|
||||
const session = await sessionPromise;
|
||||
return { sessionId: session.session_id, privateStoreId: session.private_store_id };
|
||||
// Sanctioned injection point: the session (incl. the three native store ids)
|
||||
// is handed to the lib HERE and nowhere else. The lib owns physical placement
|
||||
// and resolves scope → store internally; the rest of the app speaks only in
|
||||
// logical scopes and never touches a store id / builds a `did:ng:${…}` NURI.
|
||||
return {
|
||||
sessionId: session.session_id,
|
||||
privateStoreId: session.private_store_id,
|
||||
protectedStoreId: session.protected_store_id,
|
||||
publicStoreId: session.public_store_id,
|
||||
};
|
||||
},
|
||||
normalizeUser: normalizeUsername,
|
||||
});
|
||||
@@ -64,6 +73,10 @@ export const {
|
||||
allAccounts,
|
||||
resolveReadGraphs,
|
||||
resetRegistryCache,
|
||||
// SDK-shaped scope resolvers — the app asks by scope, the lib resolves
|
||||
// placement (no store-id ever crosses the boundary).
|
||||
resolveScopeGraph,
|
||||
resolveInboxAnchor,
|
||||
} = libStoreRegistry;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user