fix(client): résolution de compte barrière-autoritative — fin du fork à la reconnexion
Bug: à la reconnexion, resolveAccount lisait le shim depuis le store-root (did🆖${privateStoreId}), NON abonnable → pas de barrière first-State → un "0 rows" à froid est ambigu → le retry (resolveAccountReliably/provisionRetry) échoue → nouveau compte provisionné → FORK → données du compte invisibles. Cause NextGraph (vérifiée nextgraph-rs): "trouvable-sans-lookup" (store-root) et "abonnable" (did:ng:o:<RepoID aléatoire>) sont DISJOINTS — pas de doc à la fois devinable et attendable → une résolution shim purement barrière est impossible. Fix (indirection pointeur → doc-shim abonnable): - Les AccountRecord migrent dans un doc-shim doc_create'd (did:ng:o:..., a une barrière). - Un pointeur écrit-une-fois dans le store-root (<shim:root> <shim:shimDoc> <docShim>) le nomme. resolveShimDoc lit le pointeur → ensureRepoOpen(docShim) [barrière] → lecture de compte AUTORITATIVE (cold 0 = absent pour de vrai). Retry de compte SUPPRIMÉ. - Micro-garde résiduel (pointerGuard, ex-provisionRetry) sur le SEUL triple pointeur écrit-une-fois; ne peut jamais forker un compte; fork de pointeur réconcilié au doc-shim canonique (lexicographiquement-min), sans perte. - Migration: migrateLegacyRecords copie (pas déplace) les comptes de l'ancien store-root vers le doc-shim avant toute conclusion "absent"; idempotent; wallet neuf → no-op. Tests: unit 128/128, e2e réel 42/42 (CONTRACT 2 = non-fork du compte à la reconnexion), red-before/green-after prouvé. Docs: nextgraph-current-state (antagonisme + indirection), simulation, migration-guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -119,11 +119,18 @@ function makeFakeNg() {
|
||||
const sparql_query = mock(async (...a: unknown[]) => {
|
||||
const query = a[1] as string;
|
||||
const anchor = a[3] as string | undefined;
|
||||
// Shim account SELECT. Two shapes: the full scan (`?acc a <Account>`) and
|
||||
// the TARGETED bounded resolve (`<subj> a <Account>`), which binds one
|
||||
// subject — honour that subject filter so the bounded query is O(1)/exact.
|
||||
// Pointer SELECT: `<shim:root> <shim:shimDoc> ?shimDoc` in the store-root graph.
|
||||
if (query.includes(`<${SHIM}:shimDoc>`)) {
|
||||
const bindings = quads
|
||||
.filter((q) => q.g === anchor && q.p === `${SHIM}:shimDoc`)
|
||||
.map((q) => ({ shimDoc: { value: q.o } }));
|
||||
return { results: { bindings } };
|
||||
}
|
||||
// Shim account SELECT (anchored to the doc-shim, no GRAPH wrapper). Two shapes:
|
||||
// the full scan (`?acc a <Account>`) and the TARGETED bounded resolve (`<subj> a
|
||||
// <Account>`) — honour that subject filter so the bounded query is O(1)/exact.
|
||||
if (query.includes(`<${SHIM}:id>`)) {
|
||||
const subjM = query.match(new RegExp(`GRAPH <[^>]+>\\s*\\{\\s*<([^>]+)>\\s+a\\s+<${SHIM}:Account>`));
|
||||
const subjM = query.match(new RegExp(`<([^>]+)>\\s+a\\s+<${SHIM}:Account>`));
|
||||
const onlySubject = subjM ? subjM[1]! : null;
|
||||
const bySubject = new Map<string, Record<string, string>>();
|
||||
for (const q of quads) {
|
||||
@@ -188,8 +195,6 @@ function inject() {
|
||||
configureStoreRegistry({
|
||||
getSession: async () => SESSION,
|
||||
normalizeId: (u) => u.trim().replace(/^@+/, "").toLowerCase(),
|
||||
// Synchronous fake store → no sync lag; disable the anti-fork retry backoff.
|
||||
provisionRetry: { attempts: 1 },
|
||||
});
|
||||
resetRegistryCache();
|
||||
setCurrentUser(null);
|
||||
@@ -203,8 +208,8 @@ beforeEach(() => {
|
||||
|
||||
test("submitToIndex creates the @index special account on first sight (3 docs)", async () => {
|
||||
await submitToIndex({ nuri: "did:ng:o:event1", title: "Concert" });
|
||||
// ensureAccount('@index') created its 3 scope docs.
|
||||
expect(fake.doc_create).toHaveBeenCalledTimes(3);
|
||||
// ensureAccount('@index') created its 3 scope docs + 1 doc-shim (first login).
|
||||
expect(fake.doc_create).toHaveBeenCalledTimes(4);
|
||||
// The deposit landed in the @index public document (its inbox).
|
||||
const depositCall = fake.sparql_update.mock.calls.find((c) =>
|
||||
(c[1] as string).includes(`${INBOX}:Deposit`),
|
||||
|
||||
Reference in New Issue
Block a user