WIP: open/subscribe repos before anchored cold-start reads (branche, non mergée)

Défaut visé : sur une session verifier fraîche (reconnexion), la lecture ancrée
tape des repos pas encore ouverts dans self.repos → 0 ligne. Nouveau module
`open-repo.ts` : `ensureReposOpen`/`ensureRepoOpen` ouvrent/souscrivent un repo
via la primitive existante `subscribeDoc` (doc_subscribe) et attendent le push
d'état initial (borné, sans polling) AVANT la lecture ancrée. Câblé en amont de
`readUnion` (read-model) et `readScopeIndex` (store-registry). Idempotent par
session (Set des NURI ouverts + Map in-flight ; ré-ouverture si la session
injectée change). No-op si le `ng` injecté n'a pas doc_subscribe (fake unitaire).

État — NON MERGÉ, incomplet :
- AIDE le PROTECTED : la participation remonte au cold-start (mesuré app, timing
  un peu bruité).
- N'ADRESSE PAS l'accueil PUBLIC : `readScopeIndex` de l'index public rend 0 même
  côté écrivain même-session — le fix ouvre le repo mais l'index reste vide. Le
  code d'index lib est prouvé scope-symétrique, donc la cause du public est
  ailleurs (broker/store ou chemin app), À MESURER SOUS BROKER (actuellement
  injoignable). Nécessité du fix elle-même non prouvée en e2e lib (broker down).

gate broker-indépendant : tsc --noEmit propre ; bun test 91 pass (worker).
Inclut le scaffolding e2e de repro reconnexion (broker/run/sdk-entry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-08 10:46:20 +02:00
parent d8c36bac3b
commit 1825d4d72f
6 changed files with 402 additions and 6 deletions
+15 -5
View File
@@ -43,6 +43,7 @@
import { docCreate, sparqlUpdate, sparqlQuery } from "./docs";
import { getCaps, getCurrentUser, getStoreRegistryDeps } from "./polyfill";
import { ensureReposOpen } from "./open-repo";
import { assertNuri } from "./sparql";
import type { Nuri } from "./types";
@@ -88,11 +89,13 @@ async function sessionId(): Promise<string> {
* This is O(1) in the doc's own size and independent of the rest of the (possibly
* bloated / shared) session store — it never iterates other graphs.
*
* All same-session repos (every doc `doc_create`d this session — in the mono-wallet
* polyfill, all of them) are already in `self.repos`, so the anchored query resolves
* the repo directly with no separate open. A genuinely-absent repo throws
* `RepoNotFound` here, in isolation, and the doc is skipped — never aborting the
* others (unlike the ORM fan-out). Returns the doc's rows, or `[]` on failure.
* COLD-START (fresh session, same persistent wallet): the repo is NOT in
* `self.repos` until something opens it, and an anchored query against an unopened
* repo silently returns 0 rows (never `RepoNotFound`). {@link readUnion} therefore
* opens the batch's repos ({@link ensureReposOpen}) BEFORE this read runs, so the
* anchored query resolves a same-session repo directly. A genuinely-absent repo
* still yields `[]` (in isolation, never aborting the others). Returns the doc's
* rows, or `[]` on failure.
*
* At the real multi-store migration this becomes a real sync: opening a per-user
* store repo by cap is a native broker fetch (`verifier.rs:1423` `OpenRepo` TODO).
@@ -137,6 +140,13 @@ export async function readUnion(docs: Nuri[]): Promise<UnionSubject[]> {
const unique = [...new Set(docs.filter(Boolean))];
if (unique.length === 0) return [];
// COLD-START heal (polyfill-era): on a fresh session over a persistent wallet the
// target repos are not yet in `self.repos`, so an anchored read would return 0
// rows. Open/subscribe each repo ONCE (idempotent, per session) and await its
// initial-state push before the anchored reads. No-op once opened / when the
// injected `ng` has no `doc_subscribe` (unit fake). See open-repo.ts.
await ensureReposOpen(unique);
// One anchored query per doc, in parallel, tolerant (a bad doc yields []).
const perDoc = await Promise.all(
unique.map(async (d) => ({ doc: assertNuri(d), rows: await readDoc(sid, d) })),