0455a408b6
L'objectif acté était deux appels spécifiques au polyfill, voire un. Il en publiait
quatre. Chacun des trois de trop était une raison que la BIBLIOTHÈQUE a, pas un besoin
qu'une application a :
- **`configureStoreRegistry`** existait parce qu'il y a deux internes à câbler — le SDK
injecté d'un côté, la session de l'autre. Vu de l'appelant, les deux disent « voici ce
qu'il te faut pour tourner ». Replié dans `configure`, qui prend désormais
`getSession` / `normalizeId` / `pointerGuard`.
- **`setCurrentUser`** n'a plus lieu d'être publié depuis que le portail d'accès est
passé dans le polyfill : c'est lui qui pose l'identité. Et une application qui nomme
sa propre identité est exactement le geste qui inverse le modèle — il ne doit pas
exister d'appel publié vers lequel se tourner. Le harnais e2e, lui, joue plusieurs
identités sur une même page ; il y accède par le chemin interne, ce qu'un harnais a
le droit de faire et une application non.
- **`connectedUser`** est maintenant attendu DANS `ensureIdentity`. Ce n'était pas une
commodité : la suite applicative avait montré qu'une app devait l'attendre elle-même,
sinon une note qu'on venait de lui partager se lisait comme illisible. J'avais traité
le symptôme dans l'app d'exemple ; le défaut était côté bibliothèque. En amont, ouvrir
la session EST la connexion — aucune application n'attend un second appel.
Reste donc `configure({ … })`, plus `await ensureIdentity()` dont le site d'appel
survit à la migration : une application attendra toujours une session avant de rendre.
Le test étendu hier a fait son travail : les deux contrôles de contrat sont passés au
rouge sur `configureStoreRegistry`, `connectedUser` et `StoreRegistryDeps` dès que la
surface a bougé.
180 tests unitaires, e2e 40/40 (3,4 min) et applicatif 10/10 (0,8 min).
146 lines
5.5 KiB
TypeScript
146 lines
5.5 KiB
TypeScript
import { test, expect, mock, afterAll } from "bun:test";
|
|
import { subscribeDoc, subscribeDocs } from "../src/surface/subscribe";
|
|
import { configure } from "../src/index";
|
|
import { configureStoreRegistry } from "../src/shared-wallet/bootstrap";
|
|
import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
|
import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
|
|
|
// subscribeDoc/subscribeDocs wrap the REAL injected `ng.doc_subscribe`. This
|
|
// suite injects a fake `ng` whose `doc_subscribe` records the callback per doc
|
|
// and hands back an unsubscribe, so we can assert routing + isolation without a
|
|
// broker. Restore the un-configured state at the end.
|
|
afterAll(() => {
|
|
resetConfig();
|
|
resetStoreRegistry();
|
|
});
|
|
|
|
const SESSION: RegistrySession = { sessionId: "sid-1", privateStoreId: "PRIV" };
|
|
|
|
/**
|
|
* A fake reactive `ng`: `doc_subscribe(nuri, sid, cb)` registers `cb` for `nuri`,
|
|
* fires it once (initial State push), and returns an unsubscribe. `push(nuri)`
|
|
* drives a later change to that doc's subscribers. A per-doc `failFor` set makes
|
|
* `doc_subscribe` reject for chosen NURIs (a not-yet-synced doc).
|
|
*/
|
|
function makeFakeNg(failFor: Set<string> = new Set()) {
|
|
const subs = new Map<string, Set<(r: unknown) => void>>();
|
|
const doc_subscribe = mock(async (nuri: string, _sid: unknown, cb: (r: unknown) => void) => {
|
|
if (failFor.has(nuri)) throw new Error(`RepoNotFound: ${nuri}`);
|
|
let set = subs.get(nuri);
|
|
if (!set) {
|
|
set = new Set();
|
|
subs.set(nuri, set);
|
|
}
|
|
set.add(cb);
|
|
// Initial State push, delivered async (as the real RPC does).
|
|
queueMicrotask(() => cb({ V0: { State: { doc: nuri } } }));
|
|
return () => set!.delete(cb);
|
|
});
|
|
const push = (nuri: string): void => {
|
|
for (const cb of subs.get(nuri) ?? []) cb({ V0: { Patch: { doc: nuri } } });
|
|
};
|
|
const isSubscribed = (nuri: string): boolean => (subs.get(nuri)?.size ?? 0) > 0;
|
|
return { doc_subscribe, push, isSubscribed, _subs: subs };
|
|
}
|
|
|
|
function inject(failFor?: Set<string>) {
|
|
const ng = makeFakeNg(failFor);
|
|
configure({ ng: ng as any, useShape: (() => {}) as any });
|
|
// Synchronous fake store → no sync lag; disable the anti-fork retry backoff.
|
|
configureStoreRegistry({ getSession: async () => SESSION });
|
|
return ng;
|
|
}
|
|
|
|
const A = "did:ng:o:docA";
|
|
const B = "did:ng:o:docB";
|
|
|
|
const tick = () => new Promise((r) => setTimeout(r, 5));
|
|
|
|
test("subscribeDoc calls ng.doc_subscribe with (nuri, sessionId, callback)", async () => {
|
|
const ng = inject();
|
|
const onChange = mock(() => {});
|
|
subscribeDoc(A, onChange);
|
|
await tick();
|
|
expect(ng.doc_subscribe).toHaveBeenCalledTimes(1);
|
|
const call = ng.doc_subscribe.mock.calls[0]!;
|
|
expect(call[0]).toBe(A);
|
|
expect(call[1]).toBe("sid-1"); // sessionId from the injected session
|
|
expect(typeof call[2]).toBe("function"); // the callback
|
|
});
|
|
|
|
test("subscribeDoc routes the initial State push and every later change", async () => {
|
|
const ng = inject();
|
|
const seen: unknown[] = [];
|
|
subscribeDoc(A, (r) => seen.push(r));
|
|
await tick();
|
|
expect(seen).toHaveLength(1); // initial State push
|
|
ng.push(A);
|
|
ng.push(A);
|
|
expect(seen).toHaveLength(3); // + 2 patches
|
|
});
|
|
|
|
test("subscribeDoc unsubscribe stops further callbacks", async () => {
|
|
const ng = inject();
|
|
const seen: unknown[] = [];
|
|
const stop = subscribeDoc(A, (r) => seen.push(r));
|
|
await tick();
|
|
expect(seen).toHaveLength(1);
|
|
stop();
|
|
expect(ng.isSubscribed(A)).toBe(false); // real unsubscribe was invoked
|
|
ng.push(A); // ignored — no subscriber
|
|
expect(seen).toHaveLength(1);
|
|
});
|
|
|
|
test("subscribeDoc unsubscribe BEFORE async setup resolves cancels cleanly", async () => {
|
|
const ng = inject();
|
|
const seen: unknown[] = [];
|
|
const stop = subscribeDoc(A, (r) => seen.push(r));
|
|
stop(); // before the microtask/promise setup resolved
|
|
await tick();
|
|
// The subscription was cancelled the moment setup resolved: no callbacks, and
|
|
// no lingering subscriber.
|
|
expect(seen).toHaveLength(0);
|
|
expect(ng.isSubscribed(A)).toBe(false);
|
|
});
|
|
|
|
test("subscribeDocs fans out one subscription per doc and reports the source nuri", async () => {
|
|
const ng = inject();
|
|
const seen: Array<[string, unknown]> = [];
|
|
subscribeDocs([A, B], (nuri, r) => seen.push([nuri, r]));
|
|
await tick();
|
|
// Two initial pushes, one per doc.
|
|
expect(seen.map((s) => s[0]).sort()).toEqual([A, B]);
|
|
ng.push(B);
|
|
expect(seen.filter((s) => s[0] === B)).toHaveLength(2); // initial + patch
|
|
expect(seen.filter((s) => s[0] === A)).toHaveLength(1); // isolated: A didn't fire
|
|
});
|
|
|
|
test("subscribeDocs isolates a failing doc — the others still fire", async () => {
|
|
const ng = inject(new Set([A])); // A's subscription throws (RepoNotFound)
|
|
const seen: Array<[string, unknown]> = [];
|
|
subscribeDocs([A, B], (nuri, r) => seen.push([nuri, r]));
|
|
await tick();
|
|
// A failed to subscribe (logged, not thrown); B is unaffected and fired.
|
|
expect(seen.map((s) => s[0])).toEqual([B]);
|
|
ng.push(B);
|
|
expect(seen.filter((s) => s[0] === B)).toHaveLength(2);
|
|
});
|
|
|
|
test("subscribeDocs unsubscribe tears down all subscriptions", async () => {
|
|
const ng = inject();
|
|
const stop = subscribeDocs([A, B], () => {});
|
|
await tick();
|
|
expect(ng.isSubscribed(A)).toBe(true);
|
|
expect(ng.isSubscribed(B)).toBe(true);
|
|
stop();
|
|
expect(ng.isSubscribed(A)).toBe(false);
|
|
expect(ng.isSubscribed(B)).toBe(false);
|
|
});
|
|
|
|
test("subscribeDocs deduplicates repeated NURIs", async () => {
|
|
const ng = inject();
|
|
subscribeDocs([A, A, A], () => {});
|
|
await tick();
|
|
expect(ng.doc_subscribe).toHaveBeenCalledTimes(1);
|
|
});
|