chore(client): retirer la migration legacy + alléger les logs d'accès
- Migration des comptes legacy supprimée (migrateLegacyRecords + garde migratedInto + call-sites). Un wallet pré-fix (records store-root, pas de pointeur) provisionne simplement un doc-shim frais; contenu legacy ignoré (voulu, données = dev). La résolution barrière-autoritative + anti-fork (resolvePointer/ensureRepoOpen/ canonicalDoc/ensureInFlight/pointerGuard) est inchangée. - Logs d'accès SDK préfixés [polyfill] + NURI tronqué via shortNuri() (retire did:ng:o: et :v:…, garde 8 chars) → moins verbeux. Ex: [polyfill] [user1] READ vDlwbZio… (resolvePointer) → 1 triple-rows Tests: bun test unit 126/0. Docs (nextgraph-current-state/simulation/migration-guide) mis à jour (migration legacy retirée du modèle décrit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -61,12 +61,27 @@ function activeIdentity(): string {
|
||||
return getCurrentUser() ?? "(none)";
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorten a NURI for the access log: the full form (`did:ng:o:<RepoID>:v:<...>`,
|
||||
* ~100 chars) is too verbose to scan. Drop the `did:ng:o:` prefix and the `:v:<...>`
|
||||
* overlay suffix, and keep the first 8 chars of the RepoID + an ellipsis — short but
|
||||
* still identifiable (`vDlwbZio…`). A NURI that doesn't match the expected shape is
|
||||
* returned unchanged (best-effort, this is only a diagnostic label).
|
||||
*/
|
||||
export function shortNuri(nuri: string): string {
|
||||
const withoutPrefix = nuri.replace(/^did:ng:o:/, "");
|
||||
const repoId = withoutPrefix.split(":v:")[0] ?? withoutPrefix;
|
||||
return repoId.length > 8 ? repoId.slice(0, 8) + "…" : repoId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log one document access — but ONLY when {@link enabled}. Off → returns
|
||||
* immediately, prints nothing. Format:
|
||||
* `[<identity>] READ <nuri> (<label>)` — optionally with `<extra>` appended
|
||||
* (e.g. ` → 3 rows`, a strong signal a doc rendered data under an identity that
|
||||
* should see nothing).
|
||||
* `[polyfill] [<identity>] READ <shortNuri> (<label>)` — optionally with `<extra>`
|
||||
* appended (e.g. ` → 3 rows`, a strong signal a doc rendered data under an identity
|
||||
* that should see nothing). The `[polyfill]` prefix marks these as SDK-layer access
|
||||
* logs (distinct from the consumer app's own logs). The NURI is shortened by
|
||||
* {@link shortNuri} to keep the line scannable.
|
||||
*/
|
||||
export function logAccess(
|
||||
op: AccessOp,
|
||||
@@ -76,6 +91,6 @@ export function logAccess(
|
||||
): void {
|
||||
if (!enabled()) return;
|
||||
console.log(
|
||||
"[" + activeIdentity() + "] " + op + " " + nuri + " (" + label + ")" + (extra ?? ""),
|
||||
"[polyfill] [" + activeIdentity() + "] " + op + " " + shortNuri(nuri) + " (" + label + ")" + (extra ?? ""),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,9 +34,8 @@
|
||||
* BARRIER and reads the account AUTHORITATIVELY (0 = genuinely absent).
|
||||
*
|
||||
* A pointer FORK (two devices writing the pointer before either synced) is
|
||||
* BENIGN: the pointer is reconciled to a canonical doc-shim (lexicographically-
|
||||
* smallest NURI) and destroys no account data — worst case a doc-shim not chosen
|
||||
* is migrated forward on next resolve. This is why the OLD account-level retry
|
||||
* reconciled to a canonical doc-shim (lexicographically-smallest NURI) so every
|
||||
* device converges on the SAME doc-shim. This is why the OLD account-level retry
|
||||
* (`resolveAccountReliably` / `provisionRetry`) is GONE: the account read is now
|
||||
* barrier-authoritative, so it never needs to be retried to distinguish sync-lag
|
||||
* from absence. A micro-guard remains ONLY on the pointer read (one write-once
|
||||
@@ -201,9 +200,6 @@ export function resetRegistryCache(): void {
|
||||
accountCache.clear();
|
||||
shimDocNuri = null;
|
||||
shimDocInFlight = null;
|
||||
// The per-session migration guard is tied to the resolved doc-shim, which is
|
||||
// dropped here — so a switched wallet (or a test) re-runs the legacy scan.
|
||||
migratedInto.clear();
|
||||
}
|
||||
|
||||
// --- SPARQL result helpers ------------------------------------------------
|
||||
@@ -299,8 +295,7 @@ const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms
|
||||
* If MULTIPLE pointers exist (a pointer fork: two devices each wrote a pointer to
|
||||
* their own freshly-created doc-shim before either synced), reconcile to the
|
||||
* canonical (lexicographically-smallest) doc-shim NURI — content-addressed and
|
||||
* stable, so every device converges on the SAME doc-shim. A pointer fork is benign:
|
||||
* no account data is lost (migration folds any non-canonical doc-shim forward).
|
||||
* stable, so every device converges on the SAME doc-shim.
|
||||
*/
|
||||
async function resolvePointer(): Promise<Nuri> {
|
||||
const s = await session();
|
||||
@@ -379,15 +374,12 @@ async function createDoc(): Promise<Nuri> {
|
||||
* Steps (cached; runs at most once per session, concurrent callers share one):
|
||||
* 1. Read the pointer from the store-root (resolvePointer). If present → that is
|
||||
* the doc-shim; open it through its first-`State` BARRIER so subsequent account
|
||||
* reads are AUTHORITATIVE, then MIGRATE any legacy store-root records into it.
|
||||
* 2. No pointer → FIRST login (or a wallet from before the indirection):
|
||||
* reads are AUTHORITATIVE.
|
||||
* 2. No pointer → FIRST login:
|
||||
* a. create a fresh doc-shim (`doc_create`), which bootstraps the repo into the
|
||||
* session (`self.repos`) — so it is already open/synced in-session;
|
||||
* b. publish the pointer (store-root → the new doc-shim), once;
|
||||
* c. open it (barrier — trivially satisfied for a just-created in-session repo)
|
||||
* and MIGRATE any legacy AccountRecords found in the store-root graph into it
|
||||
* (see migrateLegacyRecords) — this is what guarantees NO existing account is
|
||||
* lost when a pre-indirection wallet is opened for the first time.
|
||||
* c. open it (barrier — trivially satisfied for a just-created in-session repo).
|
||||
* The BARRIER matters on RECONNECT (step 1, reading an EXISTING remote doc-shim);
|
||||
* on first-login creation it is a no-op, so the pointer is published first.
|
||||
*/
|
||||
@@ -401,20 +393,15 @@ async function resolveShimDoc(): Promise<Nuri> {
|
||||
// Open the doc-shim through its first-`State` barrier BEFORE any account read,
|
||||
// so a cold 0 on the doc-shim is authoritative (genuinely absent), not sync-lag.
|
||||
await ensureRepoOpen(existing);
|
||||
// A pointer that predates a migration still points at a doc-shim, but the LEGACY
|
||||
// store-root records (from before the indirection) may not have been folded in.
|
||||
// Migrate best-effort (idempotent) so no legacy account is ever stranded.
|
||||
await migrateLegacyRecords(existing);
|
||||
shimDocNuri = existing;
|
||||
return existing;
|
||||
}
|
||||
|
||||
// FIRST login (no pointer): create the doc-shim (bootstrapped in-session), publish
|
||||
// the pointer, then open (no-op barrier for a just-created repo) + migrate legacy.
|
||||
// the pointer, then open (no-op barrier for a just-created repo).
|
||||
const doc = await createDoc();
|
||||
await writePointer(doc);
|
||||
await ensureRepoOpen(doc);
|
||||
await migrateLegacyRecords(doc);
|
||||
shimDocNuri = doc;
|
||||
return doc;
|
||||
})();
|
||||
@@ -427,87 +414,6 @@ async function resolveShimDoc(): Promise<Nuri> {
|
||||
}
|
||||
}
|
||||
|
||||
// --- migration (legacy store-root records → the doc-shim) ------------------
|
||||
|
||||
// Guard so the (best-effort, O(all-accounts)) legacy scan runs at most once per
|
||||
// session per target doc-shim. Cleared with the rest via resetRegistryCache.
|
||||
const migratedInto = new Set<Nuri>();
|
||||
|
||||
/**
|
||||
* MIGRATION — fold any AccountRecords that live at the OLD location (the store-root
|
||||
* graph, where the pre-indirection shim wrote them) into the new doc-shim.
|
||||
*
|
||||
* ── Why this exists (do NOT lose an existing account) ──────────────────────
|
||||
* Before the indirection, `ensureAccount` wrote `<subj> a Account ; id ; docPublic ;
|
||||
* docProtected ; docPrivate` directly into the store-root graph. A wallet created
|
||||
* under that scheme has its accounts there, not in a doc-shim. When such a wallet is
|
||||
* first opened under the new scheme, resolveShimDoc creates/opens the doc-shim; this
|
||||
* function reads the store-root graph best-effort and RE-INSERTS every legacy record
|
||||
* into the doc-shim, so barrier-authoritative resolveAccount finds it — instead of
|
||||
* seeing 0 in the (fresh) doc-shim and provisioning a NEW forked account.
|
||||
*
|
||||
* ── Behaviour (precise) ────────────────────────────────────────────────────
|
||||
* - Best-effort: a store-root read failure (RepoNotFound on a truly fresh wallet)
|
||||
* is swallowed — there is simply nothing to migrate.
|
||||
* - Copy, don't move: legacy triples are LEFT in the store-root (never deleted), so
|
||||
* the migration is safe to re-run and an aborted migration never loses data. The
|
||||
* doc-shim becomes the authoritative read location; the store-root copy is inert.
|
||||
* - Idempotent: re-inserting the same `<subj> ... docPublic <X>` triples is a no-op
|
||||
* (RDF set semantics); and `migratedInto` skips the scan after the first run per
|
||||
* session. Re-running after new legacy writes (there should be none post-migration)
|
||||
* would simply re-copy them.
|
||||
* - Runs once per session before the first authoritative account read, so a legacy
|
||||
* account resolves on the very first resolveAccount after reconnect.
|
||||
*/
|
||||
async function migrateLegacyRecords(doc: Nuri): Promise<void> {
|
||||
if (migratedInto.has(doc)) return;
|
||||
migratedInto.add(doc);
|
||||
|
||||
const s = await session();
|
||||
const root = await rootNuri();
|
||||
// Read every legacy account record from the OLD store-root location.
|
||||
const query = `
|
||||
SELECT ?id ?docPublic ?docProtected ?docPrivate WHERE {
|
||||
GRAPH <${assertNuri(root)}> {
|
||||
?acc a <${P.type}> ;
|
||||
<${P.id}> ?id ;
|
||||
<${P.docPublic}> ?docPublic ;
|
||||
<${P.docProtected}> ?docProtected ;
|
||||
<${P.docPrivate}> ?docPrivate .
|
||||
}
|
||||
}`;
|
||||
let rows: Array<Record<string, { value: string }>> = [];
|
||||
try {
|
||||
await ensureRepoOpen(root);
|
||||
const result = await sparqlQuery(s.sessionId, query, undefined, root, "migrateLegacyRead");
|
||||
rows = readBindings(result);
|
||||
} catch (error) {
|
||||
// No legacy repo / nothing to read → nothing to migrate. Not an error.
|
||||
console.error("[storeRegistry] migrateLegacyRecords read (best-effort) failed:", error);
|
||||
return;
|
||||
}
|
||||
if (rows.length === 0) return;
|
||||
|
||||
// Group by account subject, pick the canonical doc per scope (fold any legacy fork
|
||||
// residue the SAME content-addressed way as resolveAccount), and re-insert into the
|
||||
// doc-shim. Grouping needs the subject; the SELECT above doesn't bind it, so we key
|
||||
// on the account id literal (stable per account) instead.
|
||||
const byId = new Map<string, Array<Record<string, { value: string }>>>();
|
||||
for (const row of rows) {
|
||||
const id = bindingValue(row, "id");
|
||||
if (!id) continue;
|
||||
const bucket = byId.get(id) ?? [];
|
||||
bucket.push(row);
|
||||
byId.set(id, bucket);
|
||||
}
|
||||
await ensureRepoOpen(doc);
|
||||
for (const [id, group] of byId) {
|
||||
const record = recordFromRows(group, id);
|
||||
if (!record.docPublic && !record.docProtected && !record.docPrivate) continue;
|
||||
await writeRecord(doc, record);
|
||||
}
|
||||
}
|
||||
|
||||
// --- shim load / account bootstrap ----------------------------------------
|
||||
|
||||
/** Load all accounts from the shim (the doc-shim) into the cache. */
|
||||
|
||||
Reference in New Issue
Block a user