๐Ÿ“„ src/models/session.ts
D-OPEN SOVEREIGN

Documentation & Insights

Front-end boot orchestration for `@the_library/public/models`.

Framework-agnostic: resolves sidecar locations (local folder OR registry_v2 โ†’
Arweave), spins up the worker, **eagerly hydrates the full corpus into the
ORM**, and replays local patches. Framework adapters (e.g. the Solid
`bootstrapModelsV2`) wrap this with reactivity + navigation persistence.

Layer-clean: takes the registry data block structurally (no
`web3-registry-addresses` dependency) and the model constructors from the
caller (no constructor registry needed).
Per-objectType constructor the caller supplies (e.g. `{ type: Book.type, create: NewBookWithId }`).
Local `corpus-index.json` shape (the build artifact of `build-v2-corpus.ts`).
registry_v2 manifest data block: `objects[type][lang][tier] = [txid, โ€ฆ]`.
Map a local corpus folder + its `corpus-index.json` โ†’ `SidecarUrls[]` (shard 0).
Map a registry_v2 data block โ†’ `SidecarUrls[]` (Arweave gateway URLs, shard 0).
Fire-and-forget sidecar URL pre-fetches so the browser warms its HTTP cache
before `bootstrapModelsV2` starts the worker. Call this as early as possible
(e.g. from an inline `<script>` immediately after the UMD is loaded).
Safe to call multiple times โ€” already-in-flight URLs are skipped.
Second hydration pass: rebuilds every **inverse (`in`) relation** edge from
the forward (`has`) edges of all materialized records. The sidecar serializes
only the `has` direction (ยง3.7.1), so after merge `book.authors`/`tag.tags`
work but the inverse getters (`person.authorOf`, `tag.inTags`, `tag` parent
side) are empty until this runs. Idempotent. Returns records processed.
Eagerly materializes **every** record from the worker's parsed `SidecarSet[]`
into the ORM (records self-register via their constructor's `addRecord`),
**including all relations** โ€” forward `has` edges come from `mergeExtended`,
and a final `rebuildInverseRelations()` pass restores the inverse `in` side
so both directions are queryable. Use for full-corpus apps (book-manager);
large lazy apps may skip this and rely on `LoadAsync`.
Returns the number of records hydrated.
Primary session language
Per-objectType constructors, e.g. `[{type:Book.type,create:NewBookWithId}, โ€ฆ]`.
Worker bundle URL (default `/patch-worker.js`).
**Local mode** โ€” base URL/path serving the `bin/` folder + `corpus-index.json`
(book-manager dev: e.g. `/corpus_v2`). When set, the registry is ignored.
Pre-fetched local manifest (else fetched from `${localMode}/corpus-index.json`).
*Registry mode** (used when `localMode` is absent) โ€” resolved registry_v2 data block.
Full one-shot boot: resolve sidecar URLs (local folder or registry_v2 โ†’
Arweave) โ†’ init worker โ†’ eagerly hydrate the corpus โ†’ replay local patches.
Returns the worker's `{ sidecars, patches }`.
Installs a framework's reactivity system; may return a teardown.
Optional per-URL hook, fired after each Astro `<ClientRouter />` swap.
Escape hatch โ€” custom one-time boot, bypassing `bootModelsV2`'s source resolution.
Called once after the full boot completes (worker init + hydration + patch replay).
Receives the raw worker result: `sidecars` (parsed SidecarSet[]) and `patches`
(per-username QueueItem arrays). Use this instead of letting the result be discarded.
Framework-agnostic persistent-session bootstrap shared by the solid/vue/react
adapters. Each adapter passes its own reactivity installer; everything else โ€”
the once-only full boot (`bootModelsV2`), the shared boot promise, and the
Astro `<ClientRouter />` navigation persistence โ€” lives here so it is written
once and reused.

Idempotent: safe to call from every island / every `astro:page-load`.
Reactivity is (re)installed each call and after every `astro:after-swap`;
the heavy boot runs exactly once; in a pure SPA the swap listener never fires.
Test/HMR helper โ€” forgets the session so the next bootstrap re-inits.
1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library โ€” The World Library
4 * 
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 * 
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15/**
16 * Front-end boot orchestration for `@the_library/public/models`.
17 *
18 * Framework-agnostic: resolves sidecar locations (local folder OR registry_v2 โ†’
19 * Arweave), spins up the worker, **eagerly hydrates the full corpus into the
20 * ORM**, and replays local patches. Framework adapters (e.g. the Solid
21 * `bootstrapModelsV2`) wrap this with reactivity + navigation persistence.
22 *
23 * Layer-clean: takes the registry data block structurally (no
24 * `web3-registry-addresses` dependency) and the model constructors from the
25 * caller (no constructor registry needed).
26 */
27import type { Lang, SidecarUrls, uid } from "./interfaces.ts";
28import { setDefaultLanguage } from "./orm.ts";
29import type { Orm } from "./orm.ts";
30import type { SidecarSet } from "./worker/libs/initialLoad.ts";
31import type { AnyPatch } from "./binary.ts";
32import { extractRecordCols } from "./enriched-index.ts";
33import { InitPatchWorker, LoadLanguageWorkerCmd } from "./worker-patch-main.ts";
34import { applyRestoredPatches } from "./replay.ts";
35import { dump } from "./db.ts";
36import { scanTrash } from "./trash.ts";
37
38const DEFAULT_GATEWAY = "https://arweave.net";
39const stripSlash = (s: string) => s.replace(/\/+$/, "");
40const join = (base: string, file?: string) => (file ? `${stripSlash(base)}/${file}` : undefined);
41const txUrl = (gateway: string, txid?: string) => (txid ? `${stripSlash(gateway)}/${txid}` : undefined);
42
43/** Per-objectType constructor the caller supplies (e.g. `{ type: Book.type, create: NewBookWithId }`). */
44export interface ModelFactory {
45    type: number;
46    create: (id: uid) => Orm;
47}
48
49/** Local `corpus-index.json` shape (the build artifact of `build-v2-corpus.ts`). */
50export interface LocalCorpusIndex {
51    lang?: string;
52    objects: Record<string, {
53        tiers: { slim?: { file: string }[]; extended?: { file: string }[]; prov?: { file: string }[] };
54    }>;
55}
56
57/** registry_v2 manifest data block: `objects[type][lang][tier] = [txid, โ€ฆ]`. */
58export interface RegistryDataBlock {
59    objects?: Record<string, Record<string, { slim?: string[]; extended?: string[]; prov?: string[] }>>;
60}
61
62/** Map a local corpus folder + its `corpus-index.json` โ†’ `SidecarUrls[]` (shard 0). */
63export function localSidecarUrls(basePath: string, manifest: LocalCorpusIndex, selectedLangs: Lang[]): SidecarUrls[] {
64    const out: SidecarUrls[] = [];
65    for (const [type, obj] of Object.entries(manifest.objects ?? {})) {
66        for (const lang of selectedLangs) {
67            const slim = obj.tiers.slim?.find(t => t.file.startsWith(`${lang}_`));
68            const extended = obj.tiers.extended?.find(t => t.file.startsWith(`${lang}_`));
69            const prov = obj.tiers.prov?.find(t => t.file.startsWith(`${lang}_`));
70            if (!slim && !extended && !prov) continue;
71            out.push({
72                objectType: Number(type),
73                lang,
74                slimUrl: join(basePath, slim?.file),
75                extendedUrl: join(basePath, extended?.file),
76                provUrl: join(basePath, prov?.file),
77            });
78        }
79    }
80    return out;
81}
82
83/** Map a registry_v2 data block โ†’ `SidecarUrls[]` (Arweave gateway URLs, shard 0). */
84export function registrySidecarUrls(data: RegistryDataBlock, selectedLangs: Lang[], gateway: string = DEFAULT_GATEWAY): SidecarUrls[] {
85    const out: SidecarUrls[] = [];
86    for (const [type, byLang] of Object.entries(data.objects ?? {})) {
87        for (const lang of selectedLangs) {
88            const tiers = byLang[lang];
89            if (!tiers) continue;
90            out.push({
91                objectType: Number(type),
92                lang,
93                slimUrl: txUrl(gateway, tiers.slim?.[0]),
94                extendedUrl: txUrl(gateway, tiers.extended?.[0]),
95                provUrl: txUrl(gateway, tiers.prov?.[0]),
96            });
97        }
98    }
99    return out;
100}
101
102/**
103 * Fire-and-forget sidecar URL pre-fetches so the browser warms its HTTP cache
104 * before `bootstrapModelsV2` starts the worker. Call this as early as possible
105 * (e.g. from an inline `<script>` immediately after the UMD is loaded).
106 * Safe to call multiple times โ€” already-in-flight URLs are skipped.
107 */
108const _prefetched = new Set<string>();
109export function prefetchSidecars(
110    data: RegistryDataBlock,
111    langs: string[] = ['eng'],
112    gateway: string = DEFAULT_GATEWAY,
113): void {
114    const sidecars = registrySidecarUrls(data, langs, gateway);
115    for (const s of sidecars) {
116        for (const url of [s.slimUrl, s.extendedUrl, s.provUrl]) {
117            if (url && !_prefetched.has(url)) {
118                _prefetched.add(url);
119                fetch(url, { priority: 'high' } as RequestInit).catch(() => {});
120            }
121        }
122    }
123}
124
125/**
126 * Second hydration pass: rebuilds every **inverse (`in`) relation** edge from
127 * the forward (`has`) edges of all materialized records. The sidecar serializes
128 * only the `has` direction (ยง3.7.1), so after merge `book.authors`/`tag.tags`
129 * work but the inverse getters (`person.authorOf`, `tag.inTags`, `tag` parent
130 * side) are empty until this runs. Idempotent. Returns records processed.
131 */
132export function rebuildInverseRelations(): number {
133    let n = 0;
134    for (const cache of dump().values()) {
135        for (const rec of cache.values()) {
136            (rec as Orm).restoreInverses();
137            n++;
138        }
139    }
140    return n;
141}
142
143/**
144 * Eagerly materializes **every** record from the worker's parsed `SidecarSet[]`
145 * into the ORM (records self-register via their constructor's `addRecord`),
146 * **including all relations** โ€” forward `has` edges come from `mergeExtended`,
147 * and a final `rebuildInverseRelations()` pass restores the inverse `in` side
148 * so both directions are queryable. Use for full-corpus apps (book-manager);
149 * large lazy apps may skip this and rely on `LoadAsync`.
150 * Returns the number of records hydrated.
151 */
152export function hydrateAll(sets: SidecarSet[], factories: ModelFactory[]): number {
153    const byType = new Map(factories.map(f => [f.type, f.create] as const));
154    let count = 0;
155    for (const set of sets) {
156        const create = byType.get(set.objectType);
157        if (!create) continue;
158        for (const [id, rowIndex] of set.idIndex) {
159            const rec = create(id) as any;
160            if (set.slim) rec.mergeSlim(extractRecordCols(set.slim, rowIndex), 0, set.lang);
161            if (set.extended) rec.mergeExtended(extractRecordCols(set.extended, rowIndex), 0, set.lang);
162            if (set.prov) rec.mergeProv(extractRecordCols(set.prov, rowIndex), 0, set.lang);
163            count++;
164        }
165    }
166    // Forward edges (`_has`) are now set on every record; restore the inverse side.
167    rebuildInverseRelations();
168    // Boot-time (and per-loadLanguage) trash population โ€” live updates after
169    // this come from trash.ts's own reactivity-system registration.
170    scanTrash();
171    return count;
172}
173
174export interface BootModelsV2Options {
175    /** Primary session language */
176    userLang: Lang;
177    /** Per-objectType constructors, e.g. `[{type:Book.type,create:NewBookWithId}, โ€ฆ]`. */
178    models: ModelFactory[];
179    selectedLangs?: Lang[];
180    /** Worker bundle URL (default `/patch-worker.js`). */
181    workerPath?: string | URL;
182    /**
183     * **Local mode** โ€” base URL/path serving the `bin/` folder + `corpus-index.json`
184     * (book-manager dev: e.g. `/corpus_v2`). When set, the registry is ignored.
185     */
186    localMode?: string;
187    /** Pre-fetched local manifest (else fetched from `${localMode}/corpus-index.json`). */
188    localManifest?: LocalCorpusIndex;
189    /** **Registry mode** (used when `localMode` is absent) โ€” resolved registry_v2 data block. */
190    registry?: { dataBlock: RegistryDataBlock; gateway?: string };
191}
192
193/**
194 * Full one-shot boot: resolve sidecar URLs (local folder or registry_v2 โ†’
195 * Arweave) โ†’ init worker โ†’ eagerly hydrate the corpus โ†’ replay local patches.
196 * Returns the worker's `{ sidecars, patches }`.
197 */
198export async function bootModelsV2(opts: BootModelsV2Options): Promise<Awaited<ReturnType<typeof InitPatchWorker>>> {
199    setDefaultLanguage(opts.userLang);
200    const langs = opts.selectedLangs ?? [opts.userLang];
201
202    let sidecars: SidecarUrls[];
203    if (opts.localMode) {
204        let manifest: LocalCorpusIndex;
205        if (opts.localManifest) {
206            manifest = opts.localManifest;
207        } else {
208            const r = await fetch(`${stripSlash(opts.localMode)}/corpus-index.json`);
209            if (!r.ok) throw new Error(`local corpus-index.json fetch failed: ${r.status}`);
210            manifest = await r.json() as LocalCorpusIndex;
211        }
212        sidecars = localSidecarUrls(opts.localMode, manifest, langs);
213    } else if (opts.registry) {
214        sidecars = registrySidecarUrls(opts.registry.dataBlock, langs, opts.registry.gateway);
215    } else {
216        throw new Error("bootModelsV2: provide `localMode` (folder) or `registry` (data block)");
217    }
218
219    const data = await InitPatchWorker({ workerPath: opts.workerPath, sidecars, selectedLangs: langs });
220    const hydrated = hydrateAll(data.sidecars, opts.models);
221    await applyRestoredPatches((data.patches.mine ?? []) as unknown as AnyPatch[]);
222    console.log(`[models_v2] booted: ${hydrated} records (${opts.localMode ? `local ${opts.localMode}` : "registry"})`);
223    
224    _bootOptions = opts;
225    for (const lang of langs) _loadedLangs.add(lang);
226    
227    return data;
228}
229
230// โ”€โ”€โ”€ Dynamic Language Loading (SPEC ยง12.9) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
231
232let _bootOptions: BootModelsV2Options | null = null;
233const _loadedLangs = new Set<Lang>();
234
235export function hasLang(lang: Lang): boolean {
236    return _loadedLangs.has(lang);
237}
238
239export async function loadLanguage(lang: Lang): Promise<void> {
240    if (hasLang(lang)) return;
241    if (!_bootOptions) throw new Error("loadLanguage called before bootModelsV2");
242
243    // 1. Resolve SidecarUrls for this specific language
244    let sidecars: SidecarUrls[];
245    if (_bootOptions.localMode) {
246        let manifest: LocalCorpusIndex;
247        if (_bootOptions.localManifest) {
248            manifest = _bootOptions.localManifest;
249        } else {
250            const r = await fetch(`${stripSlash(_bootOptions.localMode)}/corpus-index.json`);
251            if (!r.ok) throw new Error(`local corpus-index.json fetch failed: ${r.status}`);
252            manifest = await r.json() as LocalCorpusIndex;
253        }
254        sidecars = localSidecarUrls(_bootOptions.localMode, manifest, [lang]);
255    } else if (_bootOptions.registry) {
256        sidecars = registrySidecarUrls(_bootOptions.registry.dataBlock, [lang], _bootOptions.registry.gateway);
257    } else {
258        throw new Error("No registry or localMode available");
259    }
260
261    // 2. Call worker to load language (Worker decodes sidecar, registers crosswalk maps, returns parsed cols)
262    const data = await LoadLanguageWorkerCmd(lang, sidecars);
263    
264    // 3. Hydrate the parsed rows into the ORM memory graph
265    hydrateAll(data.sidecars, _bootOptions.models);
266    
267    _loadedLangs.add(lang);
268    console.log(`[models_v2] dynamic language loaded: ${lang}`);
269}
270
271// โ”€โ”€โ”€ Persistent-session glue (shared by every framework adapter) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
272
273/** Installs a framework's reactivity system; may return a teardown. */
274export type ReactivityInstaller = () => (() => void) | void;
275
276export type BootResult = Awaited<ReturnType<typeof bootModelsV2>>;
277
278export interface SessionBootstrapOptions extends BootModelsV2Options {
279    /** Optional per-URL hook, fired after each Astro `<ClientRouter />` swap. */
280    onNavigate?: () => void;
281    /** Escape hatch โ€” custom one-time boot, bypassing `bootModelsV2`'s source resolution. */
282    init?: () => void | Promise<void>;
283    /**
284     * Called once after the full boot completes (worker init + hydration + patch replay).
285     * Receives the raw worker result: `sidecars` (parsed SidecarSet[]) and `patches`
286     * (per-username QueueItem arrays). Use this instead of letting the result be discarded.
287     */
288    onBoot?: (data: BootResult) => void | Promise<void>;
289}
290
291let _bootPromise: Promise<void> | null = null;
292let _swapBound = false;
293let _onNavigate: (() => void) | undefined;
294
295/**
296 * Framework-agnostic persistent-session bootstrap shared by the solid/vue/react
297 * adapters. Each adapter passes its own reactivity installer; everything else โ€”
298 * the once-only full boot (`bootModelsV2`), the shared boot promise, and the
299 * Astro `<ClientRouter />` navigation persistence โ€” lives here so it is written
300 * once and reused.
301 *
302 * Idempotent: safe to call from every island / every `astro:page-load`.
303 * Reactivity is (re)installed each call and after every `astro:after-swap`;
304 * the heavy boot runs exactly once; in a pure SPA the swap listener never fires.
305 */
306export const bootstrapSession = (
307    opts: SessionBootstrapOptions,
308    installReactivity: ReactivityInstaller,
309): Promise<void> => {
310    _onNavigate = opts.onNavigate;
311    installReactivity();
312
313    if (_bootPromise === null) {
314        _bootPromise = Promise.resolve()
315            .then(() => opts.init
316                ? opts.init()
317                : bootModelsV2(opts).then(data => opts.onBoot?.(data)))
318            .catch(err => { console.error("[models_v2] session init failed", err); });
319    } else {
320        _onNavigate?.();
321    }
322
323    if (typeof document !== "undefined" && !_swapBound) {
324        _swapBound = true;
325        document.addEventListener("astro:after-swap", () => {
326            installReactivity();
327            _onNavigate?.();
328        });
329    }
330
331    return _bootPromise;
332};
333
334/** Test/HMR helper โ€” forgets the session so the next bootstrap re-inits. */
335export const resetModelsV2Session = (): void => {
336    _bootPromise = null;
337    _onNavigate = undefined;
338    _bootOptions = null;
339    _loadedLangs.clear();
340};
341