refactor(layout): extraire le magasin d'injection vers shared-wallet/bootstrap
L'entrée publique `/polyfill` portait le magasin d'injection (le `ng` injecté, les dépendances du registre, QUI est connecté, le singleton `CapRegistry`). Tout module interne l'importait donc pour atteindre la config — ce qui faisait de l'entrée une dépendance du code qu'elle publie, avec les cycles `polyfill` <-> `connect` et `polyfill` <-> `inbox`. Le magasin rejoint `shared-wallet/` : rien n'est injecté en amont, l'app importe le SDK et « qui suis-je » est la session — il n'y a pas de relais d'utilisateur courant parce qu'un wallet a exactement un user. Ce module est la forme de cette absence, il s'évapore en entier à la migration. L'entrée ne fait plus que ré-exporter. Plus aucun module interne n'importe `polyfill`.
This commit is contained in:
@@ -34,7 +34,7 @@
|
|||||||
* them and this drains each in turn.
|
* them and this drains each in turn.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getCaps, getCurrentUser } from "../polyfill";
|
import { getCaps, getCurrentUser } from "../shared-wallet/bootstrap";
|
||||||
import { myInboxes, readLinks, resolveAccount } from "../shared-wallet/account-registry";
|
import { myInboxes, readLinks, resolveAccount } from "../shared-wallet/account-registry";
|
||||||
import { processInbox } from "../surface/inbox";
|
import { processInbox } from "../surface/inbox";
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { mustNotAttempt } from "./reach";
|
import { mustNotAttempt } from "./reach";
|
||||||
import { getConfig, getStoreRegistryDeps } from "../polyfill";
|
import { getConfig, getStoreRegistryDeps } from "../shared-wallet/bootstrap";
|
||||||
import { subscribeDocUnguarded, type Unsubscribe } from "../surface/subscribe";
|
import { subscribeDocUnguarded, type Unsubscribe } from "../surface/subscribe";
|
||||||
import { logStage, shortNuri } from "../shared-wallet/access-log";
|
import { logStage, shortNuri } from "../shared-wallet/access-log";
|
||||||
import type { Nuri } from "../model/types";
|
import type { Nuri } from "../model/types";
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
* At migration this module disappears: the boundary becomes the wallet itself.
|
* At migration this module disappears: the boundary becomes the wallet itself.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getCaps } from "../polyfill";
|
import { getCaps } from "../shared-wallet/bootstrap";
|
||||||
import { targetOf } from "../model/nuri";
|
import { targetOf } from "../model/nuri";
|
||||||
import type { Nuri } from "../model/types";
|
import type { Nuri } from "../model/types";
|
||||||
|
|
||||||
|
|||||||
+24
-215
@@ -1,223 +1,32 @@
|
|||||||
/**
|
/**
|
||||||
* The polyfill bootstrap — the ONLY non-SDK surface of the client.
|
* `@ng-eventually/client/polyfill` — the polyfill-era door.
|
||||||
*
|
*
|
||||||
* It injects the REAL SDK and the polyfill settings; afterwards the SDK-shaped
|
* Everything importable here disappears at migration, and that is the point of the
|
||||||
* exports (`ng`, `useShape`, `inbox`) behave as drop-ins. This is exposed at the
|
* separate entry: what an application imports from this path is exactly what it will
|
||||||
* subpath `@ng-eventually/client/polyfill` so the main entry
|
* have to delete. The SDK-identical entry (`@ng-eventually/client`) carries the other
|
||||||
* (`@ng-eventually/client`) stays a **pure, SDK-identical** surface. Everything
|
* promise — a target counterpart for every symbol.
|
||||||
* here is removed at migration.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { NgLike, UseShapeLike, Nuri, PrincipalId, ReadCap } from "./model/types";
|
|
||||||
import type { RegistrySession } from "./shared-wallet/account-registry";
|
|
||||||
import { CapRegistry } from "./emulated-verifier/caps";
|
|
||||||
import { setAccessLog } from "./shared-wallet/access-log";
|
|
||||||
import { inspectOutbox } from "./shared-wallet/outbox-log";
|
|
||||||
import { startConnect } from "./emulated-verifier/connect";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consumer-injected dependencies of the storeRegistry (polyfill-era). The
|
|
||||||
* registry itself is generic (it knows only native scopes); the consumer wires
|
|
||||||
* up how to reach the shared-wallet session and how to normalize an identity id
|
|
||||||
* used as the shim key. Removed at migration along with the whole shim.
|
|
||||||
*/
|
|
||||||
export interface StoreRegistryDeps {
|
|
||||||
/** Resolve the current shared-wallet session (id + private-store anchor). */
|
|
||||||
getSession: () => Promise<RegistrySession>;
|
|
||||||
/** Normalize an identity id for shim keying. Default: trim (identity-ish). */
|
|
||||||
normalizeId?: (id: string) => string;
|
|
||||||
/**
|
|
||||||
* POINTER micro-guard budget. The account records now live in a subscribable
|
|
||||||
* doc-shim (`did:ng:o:...`) reached through a well-known write-once POINTER triple
|
|
||||||
* in the store-root graph. The doc-shim read is barrier-AUTHORITATIVE, so accounts
|
|
||||||
* need NO retry (this replaces the deleted account-level `provisionRetry`). The
|
|
||||||
* ONLY residual sync-lag window is the store-root pointer read itself — one
|
|
||||||
* write-once triple. This bounded guard re-reads JUST that pointer a few times if a
|
|
||||||
* fresh cold read misses it; it can never provision or fork an account (worst case:
|
|
||||||
* a couple extra reads before an existing pointer is seen). Enable it where the REAL
|
|
||||||
* broker is used (app + e2e). Left UNSET (the default) → `attempts: 1` = single
|
|
||||||
* read, keeping the synchronous unit fakes fast and unchanged.
|
|
||||||
*/
|
|
||||||
pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EventuallyConfig {
|
|
||||||
/** The REAL `@ng-org/web` `ng` (injected to avoid a hard import / alias loop). */
|
|
||||||
ng: NgLike;
|
|
||||||
/** The REAL `@ng-org/orm` `useShape`. */
|
|
||||||
useShape: UseShapeLike;
|
|
||||||
/** Shared-wallet credentials — polyfill only (one wallet for everyone). */
|
|
||||||
sharedWallet?: { name: string; secret: string };
|
|
||||||
/** Initial current user; may also be set later via {@link setCurrentUser}. */
|
|
||||||
currentUser?: PrincipalId;
|
|
||||||
/**
|
|
||||||
* Turn on the OFF-by-default document access log (see {@link ./access-log}):
|
|
||||||
* every real read/write is printed, prefixed by the active identity, to
|
|
||||||
* diagnose the shared-wallet isolation leak. Also enablable without a code
|
|
||||||
* change via the env var `NG_EVENTUALLY_ACCESS_LOG=1`. Default: false.
|
|
||||||
*/
|
|
||||||
debugAccessLog?: boolean;
|
|
||||||
/** REAL `@ng-org/web` `init` (lifecycle) — forwarded by the lib's `init()`. */
|
|
||||||
init?: (...args: any[]) => any;
|
|
||||||
/** REAL `@ng-org/orm` `initNg` (ORM signals) — forwarded by the lib's `initNg()`. */
|
|
||||||
initNg?: (...args: any[]) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cfg: EventuallyConfig | null = null;
|
|
||||||
let currentUser: PrincipalId | null = null;
|
|
||||||
/** Required fields of StoreRegistryDeps after defaults are applied. `pointerGuard`
|
|
||||||
* defaults to `{ attempts: 1 }` (single read) when the consumer leaves it unset. */
|
|
||||||
type ResolvedRegistryDeps = Required<
|
|
||||||
Pick<StoreRegistryDeps, "getSession" | "normalizeId" | "pointerGuard">
|
|
||||||
>;
|
|
||||||
let registryDeps: ResolvedRegistryDeps | null = null;
|
|
||||||
/**
|
|
||||||
* The map key of the current identity — deliberately NOT the raw id.
|
|
||||||
*
|
*
|
||||||
* A virtual user IS a shim account, and the shim keys accounts by the
|
* This file only RE-EXPORTS. The implementation lives with the fate it belongs to:
|
||||||
* consumer-injected `normalizeId` ("@Alice" and "alice" are ONE account, with one
|
* the injection store and the current-user relay in `shared-wallet/bootstrap.ts`
|
||||||
* set of scope documents). This record must key the same way, or a consumer that
|
* (no counterpart, evaporates), the cap registry in `emulated-verifier/caps.ts`
|
||||||
* spells its own id differently between two calls gets a SECOND record and stops
|
* (stands in for the verifier's own bookkeeping). Keeping the store here made the
|
||||||
* reading its own documents — the caps are filed under one spelling and looked up
|
* published entry a dependency of the code it publishes.
|
||||||
* under the other. Falls back to the raw id while the registry deps are not yet
|
|
||||||
* configured (nothing can be filed before that anyway).
|
|
||||||
*/
|
*/
|
||||||
function capsHolder(): PrincipalId | null {
|
|
||||||
if (currentUser === null) return null;
|
|
||||||
return registryDeps ? registryDeps.normalizeId(currentUser) : currentUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
export type { StoreRegistryDeps, EventuallyConfig } from "./shared-wallet/bootstrap";
|
||||||
* The emulated cap registry — one record PER identity (per virtual user),
|
export {
|
||||||
* resolved through {@link capsHolder} on every call. So switching identity
|
configure,
|
||||||
* SWITCHES heldByHolder (nothing to reset, nothing wiped); see `caps.ts`. Empty until
|
getConfig,
|
||||||
* the first cap is issued, and while it is empty the read filter passes through
|
resetConfig,
|
||||||
* (no regression).
|
configureStoreRegistry,
|
||||||
*/
|
getStoreRegistryDeps,
|
||||||
let caps = new CapRegistry(capsHolder);
|
resetStoreRegistry,
|
||||||
|
setCurrentUser,
|
||||||
export function configure(c: EventuallyConfig): void {
|
getCurrentUser,
|
||||||
cfg = c;
|
getCaps,
|
||||||
currentUser = c.currentUser ?? null;
|
capFor,
|
||||||
setAccessLog(c.debugAccessLog ?? false);
|
resetCaps,
|
||||||
}
|
} from "./shared-wallet/bootstrap";
|
||||||
|
|
||||||
/** @internal — used by the SDK-shaped wrappers to reach the injected real SDK. */
|
|
||||||
export function getConfig(): EventuallyConfig {
|
|
||||||
if (!cfg) throw new Error("[ng-eventually] configure() must be called before use");
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Reset the injected config back to un-configured (mainly for tests, so a
|
|
||||||
* suite that calls configure() can restore the not-configured guard state). */
|
|
||||||
export function resetConfig(): void {
|
|
||||||
cfg = null;
|
|
||||||
currentUser = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wire the storeRegistry's consumer-injected dependencies (session + identity-id
|
|
||||||
* normalization). Must be called before any storeRegistry.* use. Separate from
|
|
||||||
* {@link configure} because it's storeRegistry-specific and, like the shim,
|
|
||||||
* disappears at migration.
|
|
||||||
*/
|
|
||||||
export function configureStoreRegistry(deps: StoreRegistryDeps): void {
|
|
||||||
// Fire the outbox inspection (Volet 3 of the low-level data-path trace) once,
|
|
||||||
// on the FIRST successful `getSession()` resolution — the most reliable
|
|
||||||
// "a session is established" signal available: every low-level reader/writer
|
|
||||||
// (store-registry, open-repo, read-model, subscribe, inbox) reaches its
|
|
||||||
// session through this SAME injected `getSession`, so wrapping it HERE catches
|
|
||||||
// the first success from whichever caller happens to run first, instead of
|
|
||||||
// tying the probe to one particular call site. Only on SUCCESS (an error
|
|
||||||
// propagates untouched, exactly as before) and only ONCE per
|
|
||||||
// `configureStoreRegistry()` call (a fresh session config → a fresh check).
|
|
||||||
let outboxInspected = false;
|
|
||||||
const getSession = async (): Promise<RegistrySession> => {
|
|
||||||
const session = await deps.getSession();
|
|
||||||
if (!outboxInspected) {
|
|
||||||
outboxInspected = true;
|
|
||||||
inspectOutbox();
|
|
||||||
}
|
|
||||||
return session;
|
|
||||||
};
|
|
||||||
registryDeps = {
|
|
||||||
getSession,
|
|
||||||
normalizeId: deps.normalizeId ?? ((id: string) => id.trim()),
|
|
||||||
// Default: single read (no re-read). Only the real-broker consumers (app + e2e)
|
|
||||||
// opt into the bounded pointer micro-guard; unit fakes stay synchronous.
|
|
||||||
pointerGuard: deps.pointerGuard ?? { attempts: 1 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @internal — used by the storeRegistry to reach its injected dependencies. */
|
|
||||||
export function getStoreRegistryDeps(): ResolvedRegistryDeps {
|
|
||||||
if (!registryDeps) {
|
|
||||||
throw new Error("[ng-eventually] configureStoreRegistry() must be called before use");
|
|
||||||
}
|
|
||||||
return registryDeps;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Reset storeRegistry deps (mainly for tests). */
|
|
||||||
export function resetStoreRegistry(): void {
|
|
||||||
registryDeps = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current identity id — who the SDK is reading/writing as. In the target
|
|
||||||
* this is the wallet user established at wallet-import time; here the consumer
|
|
||||||
* relays that id through this call so the read filter and the inbox `from` know
|
|
||||||
* who is acting. Passing `null` clears it (no identity yet, e.g. during startup).
|
|
||||||
*/
|
|
||||||
export function setCurrentUser(id: PrincipalId | null): void {
|
|
||||||
const changed = currentUser !== id;
|
|
||||||
currentUser = id;
|
|
||||||
// Connecting a user is what triggers inbox processing — the library's job, not
|
|
||||||
// the app's. Fire-and-forget: this setter is synchronous and every consumer calls
|
|
||||||
// it from synchronous code, so the work announces itself through the cap
|
|
||||||
// registry's change signal instead of making callers await. See `connect.ts`.
|
|
||||||
//
|
|
||||||
// Gated on the registry being configured, and that is not a test convenience: an
|
|
||||||
// identity set before the session resolves has nothing to restore and no inbox to
|
|
||||||
// reach, so firing would be I/O that can only fail. The consumer's real sequence
|
|
||||||
// is `configureStoreRegistry` then `setCurrentUser`; anything else can call
|
|
||||||
// `connectedUser()` explicitly.
|
|
||||||
if (changed && id !== null && registryDeps !== null) startConnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCurrentUser(): PrincipalId | null {
|
|
||||||
return currentUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The emulated cap registry — what the current identity holds, plus the emulated
|
|
||||||
* public store. The read filter and the read-model consult it. */
|
|
||||||
export function getCaps(): CapRegistry {
|
|
||||||
return caps;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Do I hold the cap of `nuri`? — the held-caps lookup, the ONLY way a cap is
|
|
||||||
* obtained besides being given one. Returns `undefined` when what I hold has none;
|
|
||||||
* that is the whole answer the model can give (there is no "may P read D?").
|
|
||||||
*
|
|
||||||
* Shorthand for `getCaps().capFor(nuri)`, exposed because it is the surface the
|
|
||||||
* consumer actually uses.
|
|
||||||
*/
|
|
||||||
export function capFor(nuri: Nuri): ReadCap | undefined {
|
|
||||||
return caps.capFor(nuri);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drop EVERY holder's caps (tests / a fresh wallet). This is **not** what an identity
|
|
||||||
* change does: switching identity switches heldByHolder, it never wipes one — if it
|
|
||||||
* wiped, durability would be a lie and per-session re-declaration would come back
|
|
||||||
* under another name. Nothing in the library calls this on `setCurrentUser`.
|
|
||||||
*/
|
|
||||||
export function resetCaps(): void {
|
|
||||||
// Clear IN PLACE rather than rebuilding: whoever subscribed to the registry's
|
|
||||||
// change signal (`watchShape`) stays subscribed to the live instance instead of
|
|
||||||
// silently holding a listener on an orphaned one.
|
|
||||||
caps.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap surface — polyfill-era (caps are emulated now; native at migration).
|
// Cap surface — polyfill-era (caps are emulated now; native at migration).
|
||||||
// Re-exported here so the whole polyfill API lives under /polyfill. `shareCap`
|
// Re-exported here so the whole polyfill API lives under /polyfill. `shareCap`
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
* migration where the broker/verifier enforces isolation natively.
|
* migration where the broker/verifier enforces isolation natively.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getCurrentUser } from "../polyfill";
|
import { getCurrentUser } from "./bootstrap";
|
||||||
|
|
||||||
/** Access kind: a document READ or a document WRITE. */
|
/** Access kind: a document READ or a document WRITE. */
|
||||||
export type AccessOp = "READ" | "WRITE";
|
export type AccessOp = "READ" | "WRITE";
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
|
|
||||||
import { sparqlUpdate, sparqlQuery } from "../surface/docs";
|
import { sparqlUpdate, sparqlQuery } from "../surface/docs";
|
||||||
import { physicalCreate, physicalQuery, physicalUpdate } from "./physical";
|
import { physicalCreate, physicalQuery, physicalUpdate } from "./physical";
|
||||||
import { getCaps, getCurrentUser, getStoreRegistryDeps } from "../polyfill";
|
import { getCaps, getCurrentUser, getStoreRegistryDeps } from "./bootstrap";
|
||||||
import { ensureRepoOpen } from "../emulated-verifier/open-repo";
|
import { ensureRepoOpen } from "../emulated-verifier/open-repo";
|
||||||
import { ensurePhysicalRepoOpen, subscribePhysicalDoc } from "./physical";
|
import { ensurePhysicalRepoOpen, subscribePhysicalDoc } from "./physical";
|
||||||
import { escapeLiteral, escapeIri, assertNuri } from "../surface/sparql";
|
import { escapeLiteral, escapeIri, assertNuri } from "../surface/sparql";
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
/**
|
||||||
|
* The injection store — where the consumer application plugs the real SDK in, and
|
||||||
|
* where the emulation keeps the state that only exists because one wallet hosts every
|
||||||
|
* identity: the injected `ng`/`useShape`, the registry dependencies, WHO is currently
|
||||||
|
* connected, and the `CapRegistry` singleton keyed by that holder.
|
||||||
|
*
|
||||||
|
* **NO COUNTERPART at any layer, by construction.** Upstream nothing is injected: the
|
||||||
|
* app imports the SDK, and "who am I" is the session — there is no current-user relay
|
||||||
|
* because a wallet has exactly one user. This module is the shape of that absence, so
|
||||||
|
* it belongs with the shared-wallet machinery and evaporates whole at migration.
|
||||||
|
*
|
||||||
|
* Extracted from `polyfill.ts` on 2026-08-03. Before that, every internal module
|
||||||
|
* imported the published ENTRY to reach the config, which made the entry a dependency
|
||||||
|
* of the code it publishes — cycles `polyfill` <-> `connect` and `polyfill` <-> `inbox`.
|
||||||
|
* The entry now only re-exports; the internals import this module instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NgLike, UseShapeLike, Nuri, PrincipalId, ReadCap } from "../model/types";
|
||||||
|
import type { RegistrySession } from "./account-registry";
|
||||||
|
import { CapRegistry } from "../emulated-verifier/caps";
|
||||||
|
import { setAccessLog } from "./access-log";
|
||||||
|
import { inspectOutbox } from "./outbox-log";
|
||||||
|
import { startConnect } from "../emulated-verifier/connect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumer-injected dependencies of the storeRegistry (polyfill-era). The
|
||||||
|
* registry itself is generic (it knows only native scopes); the consumer wires
|
||||||
|
* up how to reach the shared-wallet session and how to normalize an identity id
|
||||||
|
* used as the shim key. Removed at migration along with the whole shim.
|
||||||
|
*/
|
||||||
|
export interface StoreRegistryDeps {
|
||||||
|
/** Resolve the current shared-wallet session (id + private-store anchor). */
|
||||||
|
getSession: () => Promise<RegistrySession>;
|
||||||
|
/** Normalize an identity id for shim keying. Default: trim (identity-ish). */
|
||||||
|
normalizeId?: (id: string) => string;
|
||||||
|
/**
|
||||||
|
* POINTER micro-guard budget. The account records now live in a subscribable
|
||||||
|
* doc-shim (`did:ng:o:...`) reached through a well-known write-once POINTER triple
|
||||||
|
* in the store-root graph. The doc-shim read is barrier-AUTHORITATIVE, so accounts
|
||||||
|
* need NO retry (this replaces the deleted account-level `provisionRetry`). The
|
||||||
|
* ONLY residual sync-lag window is the store-root pointer read itself — one
|
||||||
|
* write-once triple. This bounded guard re-reads JUST that pointer a few times if a
|
||||||
|
* fresh cold read misses it; it can never provision or fork an account (worst case:
|
||||||
|
* a couple extra reads before an existing pointer is seen). Enable it where the REAL
|
||||||
|
* broker is used (app + e2e). Left UNSET (the default) → `attempts: 1` = single
|
||||||
|
* read, keeping the synchronous unit fakes fast and unchanged.
|
||||||
|
*/
|
||||||
|
pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventuallyConfig {
|
||||||
|
/** The REAL `@ng-org/web` `ng` (injected to avoid a hard import / alias loop). */
|
||||||
|
ng: NgLike;
|
||||||
|
/** The REAL `@ng-org/orm` `useShape`. */
|
||||||
|
useShape: UseShapeLike;
|
||||||
|
/** Shared-wallet credentials — polyfill only (one wallet for everyone). */
|
||||||
|
sharedWallet?: { name: string; secret: string };
|
||||||
|
/** Initial current user; may also be set later via {@link setCurrentUser}. */
|
||||||
|
currentUser?: PrincipalId;
|
||||||
|
/**
|
||||||
|
* Turn on the OFF-by-default document access log (see {@link ./access-log}):
|
||||||
|
* every real read/write is printed, prefixed by the active identity, to
|
||||||
|
* diagnose the shared-wallet isolation leak. Also enablable without a code
|
||||||
|
* change via the env var `NG_EVENTUALLY_ACCESS_LOG=1`. Default: false.
|
||||||
|
*/
|
||||||
|
debugAccessLog?: boolean;
|
||||||
|
/** REAL `@ng-org/web` `init` (lifecycle) — forwarded by the lib's `init()`. */
|
||||||
|
init?: (...args: any[]) => any;
|
||||||
|
/** REAL `@ng-org/orm` `initNg` (ORM signals) — forwarded by the lib's `initNg()`. */
|
||||||
|
initNg?: (...args: any[]) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cfg: EventuallyConfig | null = null;
|
||||||
|
let currentUser: PrincipalId | null = null;
|
||||||
|
/** Required fields of StoreRegistryDeps after defaults are applied. `pointerGuard`
|
||||||
|
* defaults to `{ attempts: 1 }` (single read) when the consumer leaves it unset. */
|
||||||
|
type ResolvedRegistryDeps = Required<
|
||||||
|
Pick<StoreRegistryDeps, "getSession" | "normalizeId" | "pointerGuard">
|
||||||
|
>;
|
||||||
|
let registryDeps: ResolvedRegistryDeps | null = null;
|
||||||
|
/**
|
||||||
|
* The map key of the current identity — deliberately NOT the raw id.
|
||||||
|
*
|
||||||
|
* A virtual user IS a shim account, and the shim keys accounts by the
|
||||||
|
* consumer-injected `normalizeId` ("@Alice" and "alice" are ONE account, with one
|
||||||
|
* set of scope documents). This record must key the same way, or a consumer that
|
||||||
|
* spells its own id differently between two calls gets a SECOND record and stops
|
||||||
|
* reading its own documents — the caps are filed under one spelling and looked up
|
||||||
|
* under the other. Falls back to the raw id while the registry deps are not yet
|
||||||
|
* configured (nothing can be filed before that anyway).
|
||||||
|
*/
|
||||||
|
function capsHolder(): PrincipalId | null {
|
||||||
|
if (currentUser === null) return null;
|
||||||
|
return registryDeps ? registryDeps.normalizeId(currentUser) : currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The emulated cap registry — one record PER identity (per virtual user),
|
||||||
|
* resolved through {@link capsHolder} on every call. So switching identity
|
||||||
|
* SWITCHES heldByHolder (nothing to reset, nothing wiped); see `caps.ts`. Empty until
|
||||||
|
* the first cap is issued, and while it is empty the read filter passes through
|
||||||
|
* (no regression).
|
||||||
|
*/
|
||||||
|
let caps = new CapRegistry(capsHolder);
|
||||||
|
|
||||||
|
export function configure(c: EventuallyConfig): void {
|
||||||
|
cfg = c;
|
||||||
|
currentUser = c.currentUser ?? null;
|
||||||
|
setAccessLog(c.debugAccessLog ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal — used by the SDK-shaped wrappers to reach the injected real SDK. */
|
||||||
|
export function getConfig(): EventuallyConfig {
|
||||||
|
if (!cfg) throw new Error("[ng-eventually] configure() must be called before use");
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset the injected config back to un-configured (mainly for tests, so a
|
||||||
|
* suite that calls configure() can restore the not-configured guard state). */
|
||||||
|
export function resetConfig(): void {
|
||||||
|
cfg = null;
|
||||||
|
currentUser = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire the storeRegistry's consumer-injected dependencies (session + identity-id
|
||||||
|
* normalization). Must be called before any storeRegistry.* use. Separate from
|
||||||
|
* {@link configure} because it's storeRegistry-specific and, like the shim,
|
||||||
|
* disappears at migration.
|
||||||
|
*/
|
||||||
|
export function configureStoreRegistry(deps: StoreRegistryDeps): void {
|
||||||
|
// Fire the outbox inspection (Volet 3 of the low-level data-path trace) once,
|
||||||
|
// on the FIRST successful `getSession()` resolution — the most reliable
|
||||||
|
// "a session is established" signal available: every low-level reader/writer
|
||||||
|
// (store-registry, open-repo, read-model, subscribe, inbox) reaches its
|
||||||
|
// session through this SAME injected `getSession`, so wrapping it HERE catches
|
||||||
|
// the first success from whichever caller happens to run first, instead of
|
||||||
|
// tying the probe to one particular call site. Only on SUCCESS (an error
|
||||||
|
// propagates untouched, exactly as before) and only ONCE per
|
||||||
|
// `configureStoreRegistry()` call (a fresh session config → a fresh check).
|
||||||
|
let outboxInspected = false;
|
||||||
|
const getSession = async (): Promise<RegistrySession> => {
|
||||||
|
const session = await deps.getSession();
|
||||||
|
if (!outboxInspected) {
|
||||||
|
outboxInspected = true;
|
||||||
|
inspectOutbox();
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
registryDeps = {
|
||||||
|
getSession,
|
||||||
|
normalizeId: deps.normalizeId ?? ((id: string) => id.trim()),
|
||||||
|
// Default: single read (no re-read). Only the real-broker consumers (app + e2e)
|
||||||
|
// opt into the bounded pointer micro-guard; unit fakes stay synchronous.
|
||||||
|
pointerGuard: deps.pointerGuard ?? { attempts: 1 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal — used by the storeRegistry to reach its injected dependencies. */
|
||||||
|
export function getStoreRegistryDeps(): ResolvedRegistryDeps {
|
||||||
|
if (!registryDeps) {
|
||||||
|
throw new Error("[ng-eventually] configureStoreRegistry() must be called before use");
|
||||||
|
}
|
||||||
|
return registryDeps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset storeRegistry deps (mainly for tests). */
|
||||||
|
export function resetStoreRegistry(): void {
|
||||||
|
registryDeps = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current identity id — who the SDK is reading/writing as. In the target
|
||||||
|
* this is the wallet user established at wallet-import time; here the consumer
|
||||||
|
* relays that id through this call so the read filter and the inbox `from` know
|
||||||
|
* who is acting. Passing `null` clears it (no identity yet, e.g. during startup).
|
||||||
|
*/
|
||||||
|
export function setCurrentUser(id: PrincipalId | null): void {
|
||||||
|
const changed = currentUser !== id;
|
||||||
|
currentUser = id;
|
||||||
|
// Connecting a user is what triggers inbox processing — the library's job, not
|
||||||
|
// the app's. Fire-and-forget: this setter is synchronous and every consumer calls
|
||||||
|
// it from synchronous code, so the work announces itself through the cap
|
||||||
|
// registry's change signal instead of making callers await. See `connect.ts`.
|
||||||
|
//
|
||||||
|
// Gated on the registry being configured, and that is not a test convenience: an
|
||||||
|
// identity set before the session resolves has nothing to restore and no inbox to
|
||||||
|
// reach, so firing would be I/O that can only fail. The consumer's real sequence
|
||||||
|
// is `configureStoreRegistry` then `setCurrentUser`; anything else can call
|
||||||
|
// `connectedUser()` explicitly.
|
||||||
|
if (changed && id !== null && registryDeps !== null) startConnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentUser(): PrincipalId | null {
|
||||||
|
return currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The emulated cap registry — what the current identity holds, plus the emulated
|
||||||
|
* public store. The read filter and the read-model consult it. */
|
||||||
|
export function getCaps(): CapRegistry {
|
||||||
|
return caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do I hold the cap of `nuri`? — the held-caps lookup, the ONLY way a cap is
|
||||||
|
* obtained besides being given one. Returns `undefined` when what I hold has none;
|
||||||
|
* that is the whole answer the model can give (there is no "may P read D?").
|
||||||
|
*
|
||||||
|
* Shorthand for `getCaps().capFor(nuri)`, exposed because it is the surface the
|
||||||
|
* consumer actually uses.
|
||||||
|
*/
|
||||||
|
export function capFor(nuri: Nuri): ReadCap | undefined {
|
||||||
|
return caps.capFor(nuri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop EVERY holder's caps (tests / a fresh wallet). This is **not** what an identity
|
||||||
|
* change does: switching identity switches heldByHolder, it never wipes one — if it
|
||||||
|
* wiped, durability would be a lie and per-session re-declaration would come back
|
||||||
|
* under another name. Nothing in the library calls this on `setCurrentUser`.
|
||||||
|
*/
|
||||||
|
export function resetCaps(): void {
|
||||||
|
// Clear IN PLACE rather than rebuilding: whoever subscribed to the registry's
|
||||||
|
// change signal (`watchShape`) stays subscribed to the live instance instead of
|
||||||
|
// silently holding a listener on an orphaned one.
|
||||||
|
caps.clear();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
* split once each user opens their own wallet.
|
* split once each user opens their own wallet.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getConfig } from "../polyfill";
|
import { getConfig } from "./bootstrap";
|
||||||
import { logAccess } from "./access-log";
|
import { logAccess } from "./access-log";
|
||||||
import { subscribeDocUnguarded } from "../surface/subscribe";
|
import { subscribeDocUnguarded } from "../surface/subscribe";
|
||||||
import { openRepoUnguarded } from "../emulated-verifier/open-repo";
|
import { openRepoUnguarded } from "../emulated-verifier/open-repo";
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* app's storeRegistry usage), so this is a drop-in for those raw calls.
|
* app's storeRegistry usage), so this is a drop-in for those raw calls.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getCaps, getConfig } from "../polyfill";
|
import { getCaps, getConfig } from "../shared-wallet/bootstrap";
|
||||||
import { logAccess, enabled as accessLogEnabled } from "../shared-wallet/access-log";
|
import { logAccess, enabled as accessLogEnabled } from "../shared-wallet/access-log";
|
||||||
import { isNuri } from "../model/nuri";
|
import { isNuri } from "../model/nuri";
|
||||||
import { assertMayReach } from "../emulated-verifier/reach";
|
import { assertMayReach } from "../emulated-verifier/reach";
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
import { depositInto, sparqlQuery } from "./docs";
|
import { depositInto, sparqlQuery } from "./docs";
|
||||||
import { subscribeDoc } from "./subscribe";
|
import { subscribeDoc } from "./subscribe";
|
||||||
import { ensureRepoOpen } from "../emulated-verifier/open-repo";
|
import { ensureRepoOpen } from "../emulated-verifier/open-repo";
|
||||||
import { getCaps, getCurrentUser, getStoreRegistryDeps } from "../polyfill";
|
import { getCaps, getCurrentUser, getStoreRegistryDeps } from "../shared-wallet/bootstrap";
|
||||||
import { addLink, documentInboxAddress, isOwnInbox } from "../shared-wallet/account-registry";
|
import { addLink, documentInboxAddress, isOwnInbox } from "../shared-wallet/account-registry";
|
||||||
import { escapeLiteral } from "./sparql";
|
import { escapeLiteral } from "./sparql";
|
||||||
import { hasReadCap } from "../model/nuri";
|
import { hasReadCap } from "../model/nuri";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* a hook point later (e.g. opening the shared wallet on `init`).
|
* a hook point later (e.g. opening the shared wallet on `init`).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getConfig } from "../polyfill";
|
import { getConfig } from "../shared-wallet/bootstrap";
|
||||||
|
|
||||||
/** Forwards to the real `@ng-org/web` `init`. */
|
/** Forwards to the real `@ng-org/web` `init`. */
|
||||||
export function init(...args: any[]): any {
|
export function init(...args: any[]): any {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* surface stays identical to `@ng-org/web`'s `ng`.
|
* surface stays identical to `@ng-org/web`'s `ng`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getConfig, getCaps, getCurrentUser } from "../polyfill";
|
import { getConfig, getCaps, getCurrentUser } from "../shared-wallet/bootstrap";
|
||||||
import type { Nuri } from "../model/types";
|
import type { Nuri } from "../model/types";
|
||||||
|
|
||||||
export function makeNg(): Record<string, any> {
|
export function makeNg(): Record<string, any> {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { docCreate, sparqlUpdate, sparqlQuery } from "./docs";
|
import { docCreate, sparqlUpdate, sparqlQuery } from "./docs";
|
||||||
import { getCaps, getStoreRegistryDeps } from "../polyfill";
|
import { getCaps, getStoreRegistryDeps } from "../shared-wallet/bootstrap";
|
||||||
import { mustNotAttempt } from "../emulated-verifier/reach";
|
import { mustNotAttempt } from "../emulated-verifier/reach";
|
||||||
import { ensureReposOpen } from "../emulated-verifier/open-repo";
|
import { ensureReposOpen } from "../emulated-verifier/open-repo";
|
||||||
import { assertNuri } from "./sparql";
|
import { assertNuri } from "./sparql";
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
* builds a set of these with per-doc error isolation to preserve that property.
|
* builds a set of these with per-doc error isolation to preserve that property.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getConfig, getStoreRegistryDeps } from "../polyfill";
|
import { getConfig, getStoreRegistryDeps } from "../shared-wallet/bootstrap";
|
||||||
import { assertMayReach } from "../emulated-verifier/reach";
|
import { assertMayReach } from "../emulated-verifier/reach";
|
||||||
import type { Nuri } from "../model/types";
|
import type { Nuri } from "../model/types";
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* only delivers documents whose cap the wallet holds.
|
* only delivers documents whose cap the wallet holds.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getConfig, getCaps } from "../polyfill";
|
import { getConfig, getCaps } from "../shared-wallet/bootstrap";
|
||||||
import { makeReadFilteredView } from "../emulated-verifier/read-filter";
|
import { makeReadFilteredView } from "../emulated-verifier/read-filter";
|
||||||
|
|
||||||
export function useShape(shapeType: unknown, scope: unknown): unknown {
|
export function useShape(shapeType: unknown, scope: unknown): unknown {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
* `isError` fires ONLY on a real thrown exception in the pipeline.
|
* `isError` fires ONLY on a real thrown exception in the pipeline.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getCaps, getCurrentUser } from "../polyfill";
|
import { getCaps, getCurrentUser } from "../shared-wallet/bootstrap";
|
||||||
import { ensureReposOpen, getSyncState } from "../emulated-verifier/open-repo";
|
import { ensureReposOpen, getSyncState } from "../emulated-verifier/open-repo";
|
||||||
import { readUnion, type UnionSubject } from "./read-model";
|
import { readUnion, type UnionSubject } from "./read-model";
|
||||||
import { subscribeDoc, type Unsubscribe } from "./subscribe";
|
import { subscribeDoc, type Unsubscribe } from "./subscribe";
|
||||||
|
|||||||
Reference in New Issue
Block a user