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:
@@ -103,6 +103,15 @@ function makeFake(opts?: { holdState?: boolean }) {
|
||||
const p = m[1] ?? "urn:ng-eventually:shim:Account";
|
||||
const o = m[2] !== undefined ? unescapeLiteral(m[2]) : (m[3] ?? "");
|
||||
quads.push({ g, s, p, o });
|
||||
// The doc-shim (named by the write-once pointer triple) is INFRASTRUCTURE, like
|
||||
// the store-root: `doc_create` bootstrapped it into the session, so its barrier
|
||||
// `State` is immediately available. Pre-release it so `holdState` (which gates the
|
||||
// per-ENTITY docs the tests control) never blocks the doc-shim open. The pointer is
|
||||
// published BEFORE the doc-shim barrier open (resolveShimDoc first-login order).
|
||||
if (p === "urn:ng-eventually:shim:shimDoc") {
|
||||
released.add(o);
|
||||
for (const sub of subs) if (sub.nuri === o) sub.cb({ V0: { State: {} } });
|
||||
}
|
||||
}
|
||||
// A write to a subscribed doc fires a Patch push (reactivity signal).
|
||||
for (const sub of subs) {
|
||||
@@ -114,9 +123,16 @@ function makeFake(opts?: { holdState?: boolean }) {
|
||||
const sparql_query = mock(async (...a: unknown[]) => {
|
||||
const query = a[1] as string;
|
||||
const anchor = a[3] as string | undefined;
|
||||
// Pointer SELECT (store-root -> doc-shim).
|
||||
if (query.includes("<urn:ng-eventually:shim:shimDoc>")) {
|
||||
const bindings = quads
|
||||
.filter((q) => q.g === anchor && q.p === "urn:ng-eventually:shim:shimDoc")
|
||||
.map((q) => ({ shimDoc: { value: q.o } }));
|
||||
return { results: { bindings } };
|
||||
}
|
||||
if (query.includes("<urn:ng-eventually:shim:id>")) {
|
||||
const subjM = query.match(
|
||||
/GRAPH <[^>]+>\s*\{\s*<([^>]+)>\s+a\s+<urn:ng-eventually:shim:Account>/,
|
||||
/<([^>]+)>\s+a\s+<urn:ng-eventually:shim:Account>/,
|
||||
);
|
||||
const onlySubject = subjM ? subjM[1]! : null;
|
||||
const bySubject = new Map<string, Record<string, string>>();
|
||||
|
||||
Reference in New Issue
Block a user