feat(logs): logs données restructurés — identité en préfixe, trace résolution/barrière, inspection outbox

Chemin données bas-niveau du polyfill rendu lisible pour diagnostiquer en session live :

- Format identité-en-premier : `[<identity>][polyfill] OP shortNuri (label)` ;
  console.error épars (store-registry, inbox) unifiés au même préfixe.
- Trace (derrière le flag debug) : issue de la barrière ensureRepoOpen
  (synced|timed-out + durée) et résultat sémantique de chaque étage de résolution
  (resolvePointer/canonicalDoc/resolveAccount/resolveShimDoc/readScopeIndex).
- outbox-log.ts (nouveau) : inspection read-only de l'outbox hors-ligne au
  démarrage de session ; console.warn si non vide (anomalie, toujours visible),
  sous flag si vide. Le comptage seul est fiable (payloads BARE opaques côté JS).

Pas de changement fonctionnel. bun test 126 pass ; tsc 0 erreur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
Sylvain Duchesne
2026-07-14 10:37:46 +02:00
parent 3547967d37
commit 70de7afa3c
7 changed files with 207 additions and 33 deletions
+98
View File
@@ -0,0 +1,98 @@
/**
* outbox-log — read-only diagnostic inspection of `@ng-org/web`'s offline write
* outbox, at session bootstrap (polyfill-era, low-level-data-path trace).
*
* ── What this surfaces ──────────────────────────────────────────────────────
* `@ng-org/web` (the real injected SDK) queues writes made while offline/
* disconnected in an "outbox", persisted client-side in `sessionStorage` by the
* WASM verifier (see `sdk/rust/src/local_broker.rs` `JsStorageConfig::
* get_js_storage_config` in the `nextgraph-rs` core repo — read-only reference,
* NOT vendored here). A non-empty outbox at session start is an ANOMALY worth
* surfacing unconditionally: it means writes from a previous (disconnected)
* session are still queued and haven't reached the broker yet.
*
* ── sessionStorage key shapes (verified in the core repo, not guessed) ──────
* The outbox is keyed per LOCAL PEER id (`peer_id`, the persistent local peer's
* pubkey — NOT the ng-eventually shim's `account`/`identity` concept), via two
* key families written by `session_write`/read by `session_read`:
* - `ng_peer_last_seq@<peerId>` — the peer's last reserved seq number.
* - `ng_outboxes@<peerId>@start` — the seq number the outbox starts at.
* - `ng_outboxes@<peerId>@<00000-idx>` — one queued (base64url + BARE-encoded)
* event per zero-padded index, contiguous from 0 until the first miss (the
* exact shape `outbox_read_function` walks — see `local_broker.rs`).
* We don't know `peerId` ahead of time (it's internal to the injected SDK), so
* we DISCOVER it by scanning `sessionStorage` for `@start` markers instead of
* requiring it to be passed in — this also means the probe works unchanged
* however many peers/wallets the browser session has touched.
*
* ── Read-only, defensive, best-effort ────────────────────────────────────────
* This NEVER writes or deletes a key (unlike the real `outbox_read_function`,
* which drains on read) — it only counts. The queued event bytes are opaque
* (BARE-encoded Rust structs, base64url'd); decoding them to report concrete
* write TARGETS (topics/docs) would mean duplicating the WASM verifier's wire
* format in this polyfill, which is explicitly out of scope (SDK internals live
* in the `@ng-eventually/client`-independent core repo, per this repo's
* doctrine) — so only the pending COUNT is reported, never fabricated targets.
* `sessionStorage` access itself can throw (sandboxed iframe, disabled storage —
* see the exact error string handled in the core repo's `main.ts`
* `convert_error`), so the whole probe is wrapped in one try/catch: unavailable
* → skip silently, never throw into the caller.
*
* Polyfill-era; removed at the real multi-store migration alongside the rest of
* this low-level trace instrumentation.
*/
import { accessLogPrefix, logStage } from "./access-log";
/** Matches an outbox "start" marker key, capturing the peer id. */
const OUTBOX_START_KEY = /^ng_outboxes@(.+)@start$/;
/** Safety bound on the per-peer index walk, so a corrupted/mocked storage
* (e.g. a `@start` marker with no matching index gaps) can't spin forever.
* Real outboxes are queued-while-offline writes — nowhere near this size. */
const MAX_SCAN_PER_PEER = 10_000;
/**
* Inspect the outbox NOW and log its state — count only, never targets (see
* module doc). Non-empty → `console.warn`, ALWAYS printed (anomaly, not gated
* by the access-log flag). Empty → a normal {@link logStage} line, gated by the
* access-log flag like the rest of the low-level trace. Read-only: never
* mutates `sessionStorage`. Never throws.
*/
export function inspectOutbox(): void {
try {
const storage = (globalThis as any)?.sessionStorage;
if (!storage) return;
const peers = new Set<string>();
const length: number = storage.length ?? 0;
for (let i = 0; i < length; i++) {
const key = storage.key?.(i);
if (!key) continue;
const m = OUTBOX_START_KEY.exec(key);
const peerId = m?.[1];
if (peerId) peers.add(peerId);
}
let total = 0;
for (const peer of peers) {
let idx = 0;
while (idx < MAX_SCAN_PER_PEER) {
const idxKey = "ng_outboxes@" + peer + "@" + String(idx).padStart(5, "0");
if (storage.getItem(idxKey) === null) break;
total++;
idx++;
}
}
if (total > 0) {
// Anomaly: ALWAYS visible, regardless of the access-log flag.
console.warn(accessLogPrefix() + " OUTBOX " + total + " pending write(s)");
} else {
logStage("OUTBOX empty");
}
} catch {
// sessionStorage unavailable / access denied — skip silently. Diagnostic
// only, never a hard dependency of the read/write path.
}
}