๐ 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 languagePer-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