@the_library/public 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +684 -0
  2. package/README.md +735 -1101
  3. package/THIRD_PARTY_NOTICES +489 -0
  4. package/assets/branding/pond-enterprise-logo.svg +3 -14
  5. package/dist/{Book-K86JRm42.d.ts → Book-CYyj_y_4.d.ts} +22 -1
  6. package/dist/{bookSearchService-BmFHrvsv.d.ts → bookSearchService-YQskauJS.d.ts} +1 -1
  7. package/dist/branding/index.js +6 -1
  8. package/dist/branding/index.js.map +1 -1
  9. package/dist/{chunk-3XUKOVEF.js → chunk-34UDKDMR.js} +7 -2
  10. package/dist/chunk-34UDKDMR.js.map +1 -0
  11. package/dist/{chunk-7G3BMGV2.js → chunk-3ALQKSIN.js} +11 -1
  12. package/dist/chunk-3ALQKSIN.js.map +1 -0
  13. package/dist/{chunk-PV3X54EU.js → chunk-AADFZOUD.js} +6 -1
  14. package/dist/chunk-AADFZOUD.js.map +1 -0
  15. package/dist/{chunk-FN52YV7L.js → chunk-DCTN7BWB.js} +6 -1
  16. package/dist/chunk-DCTN7BWB.js.map +1 -0
  17. package/dist/{chunk-GWQNULP4.js → chunk-HZZP2YTW.js} +7 -2
  18. package/dist/chunk-HZZP2YTW.js.map +1 -0
  19. package/dist/chunk-OKIGFADW.js +7 -0
  20. package/dist/chunk-OKIGFADW.js.map +1 -0
  21. package/dist/{chunk-D4DXGA2W.js → chunk-QL7HY3IK.js} +7 -2
  22. package/dist/chunk-QL7HY3IK.js.map +1 -0
  23. package/dist/{chunk-46FQHZNM.js → chunk-SB7IRHJD.js} +6 -1
  24. package/dist/chunk-SB7IRHJD.js.map +1 -0
  25. package/dist/{chunk-OELHBR4F.js → chunk-TQW56CCL.js} +7 -2
  26. package/dist/chunk-TQW56CCL.js.map +1 -0
  27. package/dist/{chunk-SD3INWZF.js → chunk-YRTDYXAD.js} +6 -1
  28. package/dist/chunk-YRTDYXAD.js.map +1 -0
  29. package/dist/downloader/dl2-sw.js +6 -1
  30. package/dist/downloader/dl2-sw.js.map +1 -1
  31. package/dist/downloader/file-worker-v2.js +5 -0
  32. package/dist/downloader/file-worker-v2.js.map +1 -1
  33. package/dist/downloader/index.d.ts +13 -1
  34. package/dist/downloader/index.js +6 -1
  35. package/dist/downloader/index.js.map +1 -1
  36. package/dist/downloader/mu-worker-v2.js +5 -0
  37. package/dist/downloader/mu-worker-v2.js.map +1 -1
  38. package/dist/downloader/react.js +6 -1
  39. package/dist/downloader/react.js.map +1 -1
  40. package/dist/downloader/solid.js +6 -1
  41. package/dist/downloader/solid.js.map +1 -1
  42. package/dist/downloader/sw.js +6 -1
  43. package/dist/downloader/sw.js.map +1 -1
  44. package/dist/downloader/vue.js +6 -1
  45. package/dist/downloader/vue.js.map +1 -1
  46. package/dist/global/public.global.js +33 -23
  47. package/dist/global/public.manifest.json +0 -1
  48. package/dist/models/astro.js +6 -1
  49. package/dist/models/astro.js.map +1 -1
  50. package/dist/models/index.d.ts +3 -4
  51. package/dist/models/index.js +6 -1
  52. package/dist/models/index.js.map +1 -1
  53. package/dist/models/models.d.ts +2 -3
  54. package/dist/models/models.js +6 -1
  55. package/dist/models/patch-worker.js +10 -0
  56. package/dist/models/patch-worker.js.map +1 -1
  57. package/dist/models/react.js +6 -1
  58. package/dist/models/react.js.map +1 -1
  59. package/dist/models/solid.js +6 -1
  60. package/dist/models/solid.js.map +1 -1
  61. package/dist/models/vue.d.ts +2 -3
  62. package/dist/models/vue.js +6 -1
  63. package/dist/models/vue.js.map +1 -1
  64. package/dist/web3/auth.js +5 -0
  65. package/dist/web3/auth.js.map +1 -1
  66. package/dist/web3/index.js +6 -1
  67. package/dist/web3/index.js.map +1 -1
  68. package/dist/web3/network.js +6 -1
  69. package/dist/web3/registry.js +6 -1
  70. package/dist/web3/vue.js +6 -1
  71. package/dist/web3/vue.js.map +1 -1
  72. package/package.json +18 -17
  73. package/dist/chunk-3XUKOVEF.js.map +0 -1
  74. package/dist/chunk-46FQHZNM.js.map +0 -1
  75. package/dist/chunk-7G3BMGV2.js.map +0 -1
  76. package/dist/chunk-D4DXGA2W.js.map +0 -1
  77. package/dist/chunk-EMA2QJTW.js +0 -2
  78. package/dist/chunk-EMA2QJTW.js.map +0 -1
  79. package/dist/chunk-FN52YV7L.js.map +0 -1
  80. package/dist/chunk-GWQNULP4.js.map +0 -1
  81. package/dist/chunk-OELHBR4F.js.map +0 -1
  82. package/dist/chunk-PV3X54EU.js.map +0 -1
  83. package/dist/chunk-SD3INWZF.js.map +0 -1
package/README.md CHANGED
@@ -1,760 +1,559 @@
1
- # @the_library/public — Complete API Documentation
1
+ # @the_library/public
2
2
 
3
- This document provides exhaustive API reference for all exports from `@the_library/public` and its subpaths. It is organized by subpath/module and includes every function, type, interface, and composable with full parameter and return type documentation.
3
+ The JavaScript SDK of the World Library: download and read books stored on Arweave, work with the library's records (books, people, tags…), and read and write the library's smart contracts. One ESM package with a separate entry point per area, for browsers (Web Workers, IndexedDB, Cache Storage) and, for the Web3 and registry parts, Node.
4
4
 
5
- ---
6
-
7
- ## Table of Contents
8
-
9
- 1. [Downloader API](#1-downloader-api--the_libraryPublicdownloader)
10
- - [Initialization](#initialization)
11
- - [File Operations](#file-operations)
12
- - [Metadata & Storage](#metadata--storage)
13
- - [Cache Management](#cache-management)
14
- - [Cache Store API](#cache-store-api)
15
- - [Keys & Naming](#keys--naming)
16
- - [Framework Adapters](#framework-adapters-downloaderframeworknames)
17
- - [Service Worker API](#service-worker-api--the_libraryPublicdownloadersw)
18
-
19
- 2. [Models API](#2-models-api--the_libraryPublicmodels)
20
- - [Language Semantics](#language-semantics-every-value-is-per-language)
21
- - [Core Types & Registry](#core-types--registry)
22
- - [Record Cache & Reactivity](#record-cache--reactivity)
23
- - [Generated Models](#generated-models)
24
- - [Schema Registry](#schema-registry)
25
- - [Trash Index](#trash-index)
26
- - [Stats & Local Analytics](#stats--local-analytics)
27
- - [Framework Adapters](#framework-adapters-modelsframeworknames)
28
- - [Patch & Session Management](#patch--session-management)
29
-
30
- 3. [Web3 API](#3-web3-api--the_libraryPublicweb3)
31
- - [Session API](#31-session-api)
32
- - [Client API](#32-client-api)
33
- - [Registry API](#33-registry-api)
34
- - [Subscription API](#34-subscription-api)
35
- - [Contracts API](#35-contracts-api)
36
- - [Events API](#36-events-api)
37
- - [Wallets API](#37-wallets-api)
38
- - [Projects API](#38-projects-api)
39
- - [Chain & Network Configuration](#39-chain--network-configuration)
40
- - [Vue Composables](#310-vue-composables--the_libraryPublicweb3vue)
41
-
42
- 4. [Branding API](#4-branding-api--the_libraryPublicbranding)
5
+ This README explains [why the SDK is designed the way it is](#why-it-is-designed-this-way), gives short [how-to guides](#how-to-guides), and is the API reference, organised by entry point. The reference covers the public surface and the functions an app normally calls; the type declarations shipped in `dist/` are the complete, authoritative list.
43
6
 
44
- ---
45
-
46
- ## 1. Downloader API (`@the_library/public/downloader`)
7
+ **Licence:** D-Open Code Sovereign Licence 1.1 (unified edition), `LicenseRef-DOCSL-1.1`. See [`LICENSE`](./LICENSE) and the [full text](https://registry.worldbibliotech.com/licence). In short:
47
8
 
48
- The Downloader manages parallelized, cross-worker PDF and image fetching, decryption, decompression, and rendering using Web Workers and IndexedDB/Cache API storage backends.
49
-
50
- ### Initialization
9
+ - non-commercial use of unmodified copies (bundling, minifying and caching are fine);
10
+ - public, anonymous, read-only access: writing to the library's contracts through this SDK is reserved to the services the licensor authorises;
11
+ - by using the package you accept the licence, including its liquidated-damages clause for modifying the code or redirecting its contract addresses (section 16.4).
51
12
 
52
- #### `InitDownloaderV2(options?: InitOptions): Promise<void>`
13
+ Third-party components keep their own licences: [`THIRD_PARTY_NOTICES`](./THIRD_PARTY_NOTICES). For commercial use or a modified build: admin@pondenterprise.com.
53
14
 
54
- Single-call initialization of the download orchestrator. Must be called once at app boot before calling any file-fetch functions. Idempotent — subsequent calls are no-ops.
15
+ ## Install
55
16
 
56
- ```typescript
57
- import { InitDownloaderV2 } from '@the_library/public/downloader';
58
-
59
- await InitDownloaderV2({
60
- gateways: ['https://arweave.net', 'https://arweave.dev'],
61
- requestPersistence: true,
62
- concurrency: { pdf: 3, preview: 6 },
63
- mupdfUrl: '/js/mupdf.js',
64
- });
17
+ ```sh
18
+ npm install @the_library/public @the_library/network-config
65
19
  ```
66
20
 
67
- **Parameters:**
68
- - `options.gateways?: string[]` — Array of Arweave gateway URLs to try in sequence. Defaults to `['https://arweave.net']`.
69
- - `options.requestPersistence?: boolean` — If `true`, asks the browser for persistent storage via `navigator.storage.persist()`, which may prevent auto-eviction of cached files. Defaults to `false`.
70
- - `options.concurrency?: { pdf: number; preview: number }` — Limits simultaneous downloads: `pdf` caps concurrent PDF fetches, `preview` caps concurrent page-image renders. Defaults to `{ pdf: 3, preview: 6 }`.
71
- - `options.mupdfUrl?: string` — URL to `mupdf.js` (WASM runtime). Must be adjacent to `mupdf-wasm.js` and `mupdf-wasm.wasm` in the app's `public/` directory. Defaults to `'/js/mupdf.js'`.
72
-
73
- **Returns:** `Promise<void>` — resolves once worker threads are initialized.
74
-
75
- **Throws:**
76
- - If `InitDownloaderV2` is called simultaneously from multiple call sites, the first promise is returned (idempotency guard).
77
- - Worker thread setup errors (e.g., broken WASM URL) surface on the returned promise.
78
-
79
- ---
80
-
81
- #### `preloadWorkersV2(): void`
21
+ - `@the_library/network-config` is already a dependency; install it yourself as well because you import from it directly (chain IDs, `getChain`, `EVM_CHAINS`, …), which strict installers such as pnpm require.
22
+ - `vue`, `react` and `solid-js` are optional peer dependencies: install the one whose adapters you import.
23
+ - ESM only (`import`), with TypeScript declarations.
82
24
 
83
- Eagerly constructs worker threads so the worker JS bundles download in the background. Call as early as possible (e.g., during app shell initialization before the app mounts components).
25
+ ## Entry points
84
26
 
85
- ```typescript
86
- import { preloadWorkersV2 } from '@the_library/public/downloader';
27
+ | Import | What it gives you |
28
+ |---|---|
29
+ | `@the_library/public/downloader` | Download, cache and render book PDFs, covers and pages ([§1](#1-downloader-api-the_librarypublicdownloader)) |
30
+ | `@the_library/public/downloader/vue` · `/react` · `/solid` | Reactive download state ([§1](#framework-adapters-downloadervuereactsolid)) |
31
+ | `@the_library/public/downloader/sw` | Optional offline service worker ([§1](#service-worker-api-the_librarypublicdownloadersw)) |
32
+ | `@the_library/public/models` | The record engine: cache, reactivity, patches, search, trash, stats ([§2](#2-models-api-the_librarypublicmodels)) |
33
+ | `@the_library/public/models/models` | Generated model classes: `Book`, `Person`, `Tag`, `ClassificationSystem`, `Post` ([§2](#generated-models)) |
34
+ | `@the_library/public/models/vue` · `/react` · `/solid` · `/astro` | Session bootstrap and framework views ([§2](#framework-adapters-modelsvuereactsolidastro)) |
35
+ | `@the_library/public/web3` | EVM read/write clients, session, wallets, subscriptions, projects ([§3](#3-web3-api-the_librarypublicweb3)) |
36
+ | `@the_library/public/web3/network` | `toViemChain` ([§3.9](#39-chain--network-configuration)) |
37
+ | `@the_library/public/web3/registry` | Registry manifest loader ([§3.3](#33-registry-api)) |
38
+ | `@the_library/public/web3/vue` | Vue composables: `bootstrapWebSDK`, `useWeb3Session`, `useChain`, … ([§3.10](#310-vue-composables-the_librarypublicweb3vue)) |
39
+ | `@the_library/public/web3/auth` | Signed admin-batch and deletion-consent messages ([§3.11](#311-auth-api-the_librarypublicweb3auth)) |
40
+ | `@the_library/public/branding` | Shared branding and contact data ([§4](#4-branding-api-the_librarypublicbranding)) |
41
+ | `@the_library/public/branding/assets/*` | Brand images (logos, favicon) |
87
42
 
88
- // Call immediately when app boots, before any heavy imports:
89
- preloadWorkersV2();
90
- ```
43
+ `@the_library/public/models.js` and `@the_library/public/models/models.js` are aliases of `/models` and `/models/models`.
91
44
 
92
- **Parameters:** None
45
+ The package also ships `dist/global/public.global.js`, a single browser script that exposes the main entry points (and `@the_library/network-config`) on `window.TheLibraryPublic`, keyed by import path, with their export names in `dist/global/public.manifest.json`. It expects Vue on `window.Vue`.
93
46
 
94
- **Returns:** `void` (synchronous; starts background downloads, doesn't wait for them)
47
+ Chain IDs in the examples come from `@the_library/network-config` (`DEFAULT_CHAIN_ID` for production, `DEFAULT_STAGING_CHAIN_ID` for the testnet). Don't hard-code them: the networks behind those roles change.
95
48
 
96
49
  ---
97
50
 
98
- ### File Operations
99
-
100
- #### `GetPdf(txId: string, options?: { priority?: 'low' | 'high' }): Promise<Response>`
101
-
102
- Fetches, decrypts, and decompresses a PDF by Arweave TX ID. Returns a `Response` object wrapping the decrypted bytes.
51
+ ## Why it is designed this way
103
52
 
104
- ```typescript
105
- import { GetPdf, GetObjectUrl } from '@the_library/public/downloader';
53
+ This SDK is the engineering foundation of:
106
54
 
107
- const response = await GetPdf(txId);
108
- const blob = await response.blob();
109
- const objectUrl = await GetObjectUrl(response); // for <iframe> or <embed>
110
- ```
55
+ - [**datapond.earth**](https://datapond.earth) — the framework: *UniMarc & Web3*, a web3 data framework for the public domain;
56
+ - [**worldbibliotech.com**](https://worldbibliotech.com) — the World Library, the live library built on it;
57
+ - [**wordlibrary.net**](https://wordlibrary.net) — the anti-cheat idea behind the library's rankings, in plain language;
58
+ - the upcoming *MARC work (STARMARC).
111
59
 
112
- **Parameters:**
113
- - `txId: string` — Arweave TX ID identifying the encrypted PDF.
114
- - `options.priority?: 'low' | 'high'` — Queue priority. Defaults to `'low'`. Use `'high'` for user-initiated "open book" actions to bump ahead of background prefetches.
60
+ The aim is to bring sovereign data to public libraries. It does so by taking the design of a public library — one shared collection, a personal copy for every reader, public records, open to everyone — as the blueprint for a public web3 framework. Each principle below says what the idea means and which part of the code carries it.
115
61
 
116
- **Returns:** `Promise<Response>` — HTTP `Response` object (status 200) wrapping the decrypted PDF bytes. Use `.blob()`, `.arrayBuffer()`, or `.text()` to read the body.
117
-
118
- **Throws:**
119
- - If all gateways are unreachable or return 404/500, rejects with a `NetworkError` or `NotFoundError`.
120
- - If decryption fails, rejects with a `DecryptionError`.
121
-
122
- **Caching:** The result is cached by `txId` in the browser's Cache API (under `CACHES.pdf`). Subsequent calls return the cached response synchronously.
123
-
124
- ---
62
+ ### A library catalogue, built from library standards
125
63
 
126
- #### `GetImage(txId: string, pageOverride?: number): Promise<Response>`
64
+ Records are described with the identifiers and classifications that library catalogues already use. These are defined once in [`@the_library/canonical-schemas`](https://www.npmjs.com/package/@the_library/canonical-schemas) and implemented in [`@the_library/standard-adapters`](https://www.npmjs.com/package/@the_library/standard-adapters):
127
65
 
128
- Renders a page preview image (JPEG/PNG) via the mupdf worker. By default, renders the first page of a PDF. Returns a `Response` wrapping the image bytes.
66
+ - **identifiers:** ISBN, ISSN, ISNI, ORCID;
67
+ - **classifications:** Dewey, Library of Congress, UDC, Chinese, BBK, Thai and Colon.
129
68
 
130
- ```typescript
131
- import { GetImage } from '@the_library/public/downloader';
69
+ This is what "UNIMARC compatible by design" means here. A book, its editions, its authors and its subjects line up with the fields of a UNIMARC (and MARC 21) bibliographic record: see the [correspondence table](https://www.npmjs.com/package/@the_library/canonical-schemas#unimarc-and-marc-21). These packages don't include a UNIMARC record converter.
132
70
 
133
- // Get the cover image (first page)
134
- const coverResponse = await GetImage(txId);
135
- const imageBlob = await coverResponse.blob();
136
- const imgSrc = URL.createObjectURL(imageBlob);
71
+ ### Sovereign data: the records live on Gnosis and Arweave
137
72
 
138
- // Get a specific page
139
- const page5Image = await GetImage(txId, 5);
140
- ```
73
+ - **The shared catalogue** is published on Arweave as compact binary sidecars, found through the registry manifest ([§3.3](#33-registry-api)).
74
+ - **Every change a reader makes** — an edit, a deletion, a rating — is a patch. It is saved by `Factory.save()` under the reader's own account on Gnosis.
75
+ - **Book files** come from Arweave and are kept in the browser (Cache Storage and IndexedDB), with an optional offline service worker ([§1](#service-worker-api-the_librarypublicdownloadersw)).
76
+ - **The parts that are not on-chain:**
77
+ - `registry.worldbibliotech.com` serves the registry manifest, with the on-chain `AddressRegistry` as fallback;
78
+ - AWS Lambdas create accounts from an email address, top up gas, and send the recovery and deletion-consent emails;
79
+ - Arweave gateways serve the files;
80
+ - price oracles are used on chains that have one.
141
81
 
142
- **Parameters:**
143
- - `txId: string` — Arweave TX ID of the PDF.
144
- - `pageOverride?: number` — 1-indexed page number. Defaults to `1` (first page).
82
+ ### One shared collection, your own copy
145
83
 
146
- **Returns:** `Promise<Response>` — HTTP `Response` object (status 200) wrapping JPEG/PNG bytes.
84
+ Everyone starts from the same canonical collection. A reader's edits change their own copy, not anyone else's. Their patches are kept under their account (the records' `creatorUsername`), and other readers only receive them by subscribing to that reader ([§3.4](#34-subscription-api)).
147
85
 
148
- **Throws:**
149
- - Same as `GetPdf` (gateway errors, decryption failures).
150
- - If `pageOverride` is out of range (e.g., page 1000 of a 10-page PDF), rejects with `RangeError`.
86
+ ### Anti-cheat: curation, not likes
151
87
 
152
- **Caching:** Rendered images are cached in `CACHES.preview` under the key `{txId}_page{n}`. Cached preview images persist across sessions.
88
+ The ranking idea ([wordlibrary.net](https://wordlibrary.net)) is to count what readers remove, not what they like. A fake like hides in a crowd. Faking curation takes many coordinated deletions, and those leave a public trail.
153
89
 
154
- ---
90
+ What this SDK provides is that trail. Deletions and moves are ordinary patches on the reader's account (the `deleted`/`safe` flags, see [Trash Index](#trash-index)). They are saved publicly on Gnosis, and anyone can read them through the read client and subscription sync. The ranking itself is computed outside this package.
155
91
 
156
- #### `EnsurePdf(txId: string): Promise<void>`
92
+ ### Identity: an account, not a person
157
93
 
158
- Pre-downloads and caches a PDF without needing the `Response` object back. Useful for background prefetching or "warm up" operations.
94
+ - **On-chain, an account is** an address, a username, a user ID, a country and a language.
95
+ - **The email address is never stored on-chain in clear**, only as a hash. That hash is used for account recovery and for email-confirmed deletion ([§3.11](#311-auth-api-the_librarypublicweb3auth)).
96
+ - **Accounts are pseudonymous, not anonymous.** A reader's saved actions, including visits, downloads, ratings and shares, are public under their account. The aggregate statistics (`StatsPivotStorage`) carry no user ID ([Stats](#stats--local-analytics)).
97
+ - **The wallet key** is kept in the browser's `localStorage`, unencrypted ([Private Key Storage](#private-key-storage)).
159
98
 
160
- ```typescript
161
- import { EnsurePdf } from '@the_library/public/downloader';
99
+ ### Free to use
162
100
 
163
- // Background prefetch (fire and forget)
164
- EnsurePdf(txId).catch(e => console.warn('Prefetch failed:', e));
101
+ There are no paywalls, login walls or ads; that is a condition of the licence. Reading, searching and downloading need no account.
165
102
 
166
- // Wait for it
167
- await EnsurePdf(txId);
168
- ```
103
+ Saving to the chain needs a member account. A library can create accounts in bulk, or a reader can take the $2 `account` membership. Either way, when the account is created or restored, the project tops its wallet up with gas to the chain's `memberFundingTarget`, so members don't buy gas themselves. Donations are voluntary.
169
104
 
170
- **Parameters:**
171
- - `txId: string` — Arweave TX ID of the PDF.
105
+ ### Built to last
172
106
 
173
- **Returns:** `Promise<void>` — resolves once the PDF is cached.
174
-
175
- **Throws:** Same as `GetPdf`.
107
+ - **Property IDs are permanent:** they come from an append-only dictionary ([canonical-schemas](https://www.npmjs.com/package/@the_library/canonical-schemas#changing-a-definition)).
108
+ - **A storage contract is never redeployed empty:** a new version is migrated with every record.
109
+ - **Book files on Arweave are permanent.**
110
+ - **No network is hard-coded:** apps take their chains from `@the_library/network-config` by role (production, staging).
176
111
 
177
112
  ---
178
113
 
179
- #### `EnsurePageImage(txId: string, page: number): Promise<void>`
180
-
181
- Pre-renders and caches a specific page image without returning the bytes. Used to warm up page-image cache before the user scrolls to that page.
182
-
183
- ```typescript
184
- import { EnsurePageImage } from '@the_library/public/downloader';
185
-
186
- // Render pages 2-5 in the background while user reads page 1
187
- for (let i = 2; i <= 5; i++) {
188
- EnsurePageImage(txId, i).catch(e => console.warn(`Page ${i} prefetch failed`));
189
- }
190
- ```
191
-
192
- **Parameters:**
193
- - `txId: string` — Arweave TX ID.
194
- - `page: number` — 1-indexed page number.
195
-
196
- **Returns:** `Promise<void>` — resolves once the image is cached.
114
+ ## How-to guides
197
115
 
198
- **Throws:** Same as `GetImage`.
116
+ Each recipe is type-checked against the published package.
199
117
 
200
- ---
118
+ ### Open a book
201
119
 
202
- #### `DownloadPdfToDisk(txId: string, filename?: string): Promise<void>`
203
-
204
- Triggers a browser "Save As" dialog and writes the decrypted PDF to the user's local Downloads folder.
120
+ Show a book's cover, then open its PDF. Serve `file-worker-v2.js` and `mu-worker-v2.js` (from `dist/downloader/`) at your site's root.
205
121
 
206
122
  ```typescript
207
- import { DownloadPdfToDisk } from '@the_library/public/downloader';
208
-
209
- await DownloadPdfToDisk(txId, 'My Book Title.pdf');
210
- ```
211
-
212
- **Parameters:**
213
- - `txId: string` — Arweave TX ID.
214
- - `filename?: string` — Suggested filename for the browser's save dialog. If omitted, the browser uses a default name.
215
-
216
- **Returns:** `Promise<void>` — resolves once the download is initiated (does not wait for the user to confirm the save dialog).
217
-
218
- **Throws:**
219
- - Same as `GetPdf` (fetch/decrypt errors).
220
- - If the browser blocks the download (e.g., security policy), the promise rejects with a `SecurityError`.
221
-
222
- ---
223
-
224
- #### `GetObjectUrl(response: Response): Promise<string>`
123
+ import { preloadWorkersV2, GetImage, GetPdf, GetObjectUrl, revokeObjectUrl } from '@the_library/public/downloader';
124
+ import { Book } from '@the_library/public/models/models';
225
125
 
226
- Converts a `GetPdf` or `GetImage` response to a blob URL suitable for `<iframe src="">`, `<embed src="">`, or `<img src="">`.
126
+ preloadWorkersV2(); // start the workers early
227
127
 
228
- ```typescript
229
- import { GetPdf, GetObjectUrl } from '@the_library/public/downloader';
128
+ const book = await Book.LoadAsync(bookId); // bookId: bigint
129
+ img.src = await GetObjectUrl(await GetImage(book.previewTxId));
230
130
 
231
- const pdfResponse = await GetPdf(txId);
232
- const blobUrl = await GetObjectUrl(pdfResponse);
233
- // Now use: <iframe :src="blobUrl" />
131
+ const edition = book.getDefaultInstance('eng'); // the edition in a language
132
+ if (edition) frame.src = await GetObjectUrl(await GetPdf(edition.downloadTxId));
133
+ // when the reader closes it: revokeObjectUrl(frame.src)
234
134
  ```
235
135
 
236
- **Parameters:**
237
- - `response: Response` — A `Response` object returned by `GetPdf` or `GetImage`.
238
-
239
- **Returns:** `Promise<string>` — A `blob:` URL (e.g., `blob:https://example.com/...`).
240
-
241
- ---
136
+ ### Load the catalogue
242
137
 
243
- #### `revokeObjectUrl(blobUrl: string): void`
244
-
245
- Explicitly revokes a blob URL created by `GetObjectUrl` to release memory.
138
+ Read the published registry, then boot the models from the collection it points to. Serve `patch-worker.js` (from `dist/models/`) at your site's root.
246
139
 
247
140
  ```typescript
248
- const blobUrl = await GetObjectUrl(response);
249
- // ... later ...
250
- revokeObjectUrl(blobUrl);
251
- ```
252
-
253
- **Parameters:**
254
- - `blobUrl: string` — The blob URL to revoke.
255
-
256
- **Returns:** `void`
141
+ import { addressLoader } from '@the_library/public/web3';
142
+ import { bootstrapModelsV2 } from '@the_library/public/models/vue'; // or /react, /solid, /astro
143
+ import { Book, Tag, Person, NewBookWithId, NewTagWithId, NewPersonWithId } from '@the_library/public/models/models';
257
144
 
258
- **Note:** Blob URLs are automatically revoked when the page navigates away or the tab closes. Manual revocation is only necessary to free memory during a long-running session (e.g., a single-page app that opens/closes many books).
145
+ await addressLoader.loadManifest('production');
146
+ const corpus = addressLoader.getCollectionCorpus('production');
147
+ if (!corpus) throw new Error('no collection in the registry manifest');
259
148
 
260
- ---
149
+ await bootstrapModelsV2({
150
+ userLang: 'eng',
151
+ models: [
152
+ { type: Book.type, create: NewBookWithId },
153
+ { type: Tag.type, create: NewTagWithId },
154
+ { type: Person.type, create: NewPersonWithId },
155
+ ],
156
+ registry: { dataBlock: corpus, gateway: 'https://arweave.net' },
157
+ });
261
158
 
262
- #### `storageEstimate(): Promise<{ usage: number; quota: number }>`
159
+ const title = (await Book.LoadAsync(bookId)).getTitle('eng');
160
+ ```
263
161
 
264
- Queries the browser's storage quota and current usage.
162
+ ### Read the chain
265
163
 
266
164
  ```typescript
267
- import { storageEstimate } from '@the_library/public/downloader';
165
+ import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
166
+ import { getReadClient } from '@the_library/public/web3';
268
167
 
269
- const { usage, quota } = await storageEstimate();
270
- const percentUsed = (usage / quota) * 100;
271
- console.log(`${percentUsed.toFixed(1)}% of storage in use`);
168
+ const client = getReadClient(DEFAULT_CHAIN_ID);
169
+ await client.ready; // contract addresses and ABIs loaded
170
+ const members = await client.bouncerStorage.nbAccounts();
272
171
  ```
273
172
 
274
- **Parameters:** None
275
-
276
- **Returns:** `Promise<{ usage: number; quota: number }>` — `usage` is bytes currently used by Cache API + IndexedDB, `quota` is the browser's total storage quota in bytes.
277
-
278
- ---
279
-
280
- ### Metadata & Storage
281
-
282
- #### `getBookData(txId: string): Promise<BookData | undefined>`
283
-
284
- Reads cached book metadata (title, page count, etc.) from IndexedDB.
173
+ ### Start a Vue app with accounts
285
174
 
286
175
  ```typescript
287
- import { getBookData } from '@the_library/public/downloader';
176
+ import { createApp } from 'vue';
177
+ import { bootstrapWebSDK, BootstrapResultKey } from '@the_library/public/web3/vue';
288
178
 
289
- const bookData = await getBookData(txId);
290
- if (bookData) {
291
- console.log(`${bookData.title} has ${bookData.pages} pages`);
292
- }
179
+ const result = await bootstrapWebSDK({ environment: 'production' });
180
+ const app = createApp(App);
181
+ app.provide(BootstrapResultKey, result);
182
+ app.mount('#app');
183
+
184
+ // in a component
185
+ import { useWeb3Session } from '@the_library/public/web3/vue';
186
+ const { activeAccount, registeredAccounts } = useWeb3Session();
293
187
  ```
294
188
 
295
- **Parameters:**
296
- - `txId: string` — Arweave TX ID.
189
+ ### Save a reader's changes
297
190
 
298
- **Returns:** `Promise<BookData | undefined>` — Book metadata if cached, `undefined` if no metadata has been hydrated yet.
191
+ Edits made through the models are queued on the device; this sends them to the chain under the active account.
299
192
 
300
- **Type: `BookData`**
301
193
  ```typescript
302
- interface BookData {
303
- txId: string;
304
- title: string;
305
- pages: number;
306
- // ... other fields set by the mupdf worker
194
+ import { sessionStore, hasUnsavedPatches, saveMyLevel } from '@the_library/public/web3';
195
+
196
+ const me = sessionStore.getState().active;
197
+ if (me && await hasUnsavedPatches(me.account, me.chainId)) {
198
+ await saveMyLevel(me.wallet, me.chainId, me.account);
307
199
  }
308
200
  ```
309
201
 
310
- ---
311
-
312
- #### `getAllBookData(): Promise<BookData[]>`
202
+ ### Work offline
313
203
 
314
- Retrieves all cached book metadata records from IndexedDB.
204
+ Register the optional service worker (`dl2-sw.js`, served at `/dl2-sw.js`). Cached books then load from plain URLs, with or without a network.
315
205
 
316
206
  ```typescript
317
- import { getAllBookData } from '@the_library/public/downloader';
207
+ import { registerDownloaderSW } from '@the_library/public/downloader/sw';
318
208
 
319
- const allBooks = await getAllBookData();
320
- console.log(`${allBooks.length} books cached`);
209
+ const status = await registerDownloaderSW({ allowedOrigins: ['example.org'] });
210
+ if (status.registered) img.src = `/ar/${txId}`; // served from Cache Storage when cached
321
211
  ```
322
212
 
323
- **Parameters:** None
324
-
325
- **Returns:** `Promise<BookData[]>` — Array of all cached `BookData` records. Empty array if no books have been hydrated yet.
326
-
327
213
  ---
328
214
 
329
- #### `getDownloadRecord(txId: string): Promise<DownloadRecord | undefined>`
215
+ ## Table of Contents
330
216
 
331
- Reads the download progress record for a TX ID.
217
+ - [Why it is designed this way](#why-it-is-designed-this-way)
218
+ - [How-to guides](#how-to-guides)
332
219
 
333
- ```typescript
334
- import { getDownloadRecord } from '@the_library/public/downloader';
220
+ 1. [Downloader API](#1-downloader-api-the_librarypublicdownloader)
221
+ - [Initialization](#initialization)
222
+ - [File Operations](#file-operations)
223
+ - [Metadata & Storage](#metadata--storage)
224
+ - [Download State](#download-state)
225
+ - [Keys & Naming](#keys--naming)
226
+ - [Framework Adapters (/downloader/{vue,react,solid})](#framework-adapters-downloadervuereactsolid)
227
+ - [Service Worker API](#service-worker-api-the_librarypublicdownloadersw)
335
228
 
336
- const record = await getDownloadRecord(txId);
337
- if (record) {
338
- console.log(`${record.loaded}/${record.total} bytes downloaded`);
339
- }
340
- ```
229
+ 2. [Models API](#2-models-api-the_librarypublicmodels)
230
+ - [Language Semantics (every value is per-language)](#language-semantics-every-value-is-per-language)
231
+ - [Core Types & Registry](#core-types--registry)
232
+ - [Record Cache & Reactivity](#record-cache--reactivity)
233
+ - [Generated Models](#generated-models)
234
+ - [Schema Registry](#schema-registry)
235
+ - [Trash Index](#trash-index)
236
+ - [Stats & Local Analytics](#stats--local-analytics)
237
+ - [Framework Adapters (/models/{vue,react,solid,astro})](#framework-adapters-modelsvuereactsolidastro)
238
+ - [Patch & Session Management](#patch--session-management)
341
239
 
342
- **Parameters:**
343
- - `txId: string` — Arweave TX ID.
240
+ 3. [Web3 API](#3-web3-api-the_librarypublicweb3)
241
+ - [Session API](#31-session-api)
242
+ - [Client API](#32-client-api)
243
+ - [Registry API](#33-registry-api)
244
+ - [Subscription API](#34-subscription-api)
245
+ - [Contracts API](#35-contracts-api)
246
+ - [Events API](#36-events-api)
247
+ - [Wallets API](#37-wallets-api)
248
+ - [Projects API](#38-projects-api)
249
+ - [Chain & Network Configuration](#39-chain--network-configuration)
250
+ - [Vue Composables](#310-vue-composables-the_librarypublicweb3vue)
251
+ - [Auth API](#311-auth-api-the_librarypublicweb3auth)
344
252
 
345
- **Returns:** `Promise<DownloadRecord | undefined>`
253
+ 4. [Branding API](#4-branding-api-the_librarypublicbranding)
346
254
 
347
- **Type: `DownloadRecord`**
348
- ```typescript
349
- interface DownloadRecord {
350
- txId: string;
351
- loaded: number; // bytes downloaded so far
352
- total: number; // total file size in bytes
353
- timestamp: number; // last update time (ms since epoch)
354
- }
355
- ```
255
+ [Appendix: Type Quick Reference](#appendix-type-quick-reference)
256
+ - [Download Types](#download-types)
257
+ - [Models Types](#models-types)
258
+ - [Web3 Types](#web3-types)
259
+ - [Branding Types](#branding-types)
356
260
 
357
261
  ---
358
262
 
359
- #### `getAllDownloadRecords(): Promise<DownloadRecord[]>`
360
-
361
- Retrieves all download progress records.
362
-
363
- ```typescript
364
- import { getAllDownloadRecords } from '@the_library/public/downloader';
365
-
366
- const records = await getAllDownloadRecords();
367
- console.log(`${records.length} active/recent downloads`);
368
- ```
263
+ ## 1. Downloader API (`@the_library/public/downloader`)
369
264
 
370
- **Parameters:** None
265
+ The downloader fetches book PDFs from Arweave, renders cover and page images in Web Workers (mupdf), and keeps everything in the browser's Cache Storage, with metadata and download records in IndexedDB. Every file is identified by its Arweave transaction ID (`txId`). When storage use passes 80% of the browser's quota, the least recently used PDFs are deleted until it is back under 65%; the open book and [pinned](#setpinnedpdfstxids-string-void) books are never deleted.
371
266
 
372
- **Returns:** `Promise<DownloadRecord[]>` — Array of all cached progress records.
267
+ The workers are separate files shipped in `dist/downloader/`: `file-worker-v2.js` and `mu-worker-v2.js`, loaded from your site's root (`/file-worker-v2.js`, `/mu-worker-v2.js`), and the optional service worker `dl2-sw.js` (`/dl2-sw.js`). Page rendering also needs `mupdf.js`, with `mupdf-wasm.js` and `mupdf-wasm.wasm` next to it, at `mupdfUrl`.
373
268
 
374
- ---
375
-
376
- ### Cache Management
269
+ ### Initialization
377
270
 
378
- #### `downloadState`
271
+ #### `InitDownloaderV2(opts?: InitOptions): Promise<void>`
379
272
 
380
- The canonical, synchronous, framework-agnostic store for all file state. Every framework adapter (`vue`, `react`, `solid`) is a thin reactive wrapper around this store.
273
+ Starts the download orchestrator. Every file function below calls it for you, so calling it yourself is only needed to pass options or to start early. Repeated calls return the first call's promise; a failed start can be retried.
381
274
 
382
275
  ```typescript
383
- import { downloadState } from '@the_library/public/downloader';
276
+ import { InitDownloaderV2 } from '@the_library/public/downloader';
384
277
 
385
- // Subscribe to changes
386
- const unsubscribe = downloadState.subscribe((changedTxId) => {
387
- console.log(`File state changed for ${changedTxId}`);
278
+ await InitDownloaderV2({
279
+ gateways: ['https://arweave.net', 'https://permagate.io'],
280
+ concurrency: { pdf: 3, preview: 6 },
281
+ mupdfUrl: '/js/mupdf.js',
388
282
  });
389
-
390
- // Read current state
391
- const fileState = downloadState.get(txId);
392
- if (fileState?.phase === 'done') {
393
- console.log('Download complete');
394
- }
395
-
396
- // Read render state (page images, book metadata)
397
- const renderState = downloadState.getRender(txId);
398
- console.log(`${renderState.pages.size} page images cached`);
399
-
400
- // Global summary
401
- const summary = downloadState.summary();
402
- console.log(`${summary.downloading} active downloads`);
403
283
  ```
404
284
 
405
- **Methods:**
406
-
407
- - `downloadState.get(txId: string): FileState | undefined` — Returns current state of one file.
408
-
409
- - `downloadState.getRender(txId: string): RenderState | undefined` — Returns render/page state for one file.
410
-
411
- - `downloadState.summary(): DownloadSummary` — Returns global aggregate (total files, total bytes, etc.).
412
-
413
- - `downloadState.subscribe(callback: (txId: string) => void): () => void` — Registers a listener. Returns unsubscribe function.
414
-
415
- **Type: `FileState`**
416
- ```typescript
417
- interface FileState {
418
- phase: 'unknown' | 'queued' | 'downloading' | 'done' | 'error';
419
- loaded: number;
420
- total: number;
421
- error?: string;
422
- progress?: number; // 0-1
423
- }
424
- ```
425
-
426
- **Type: `RenderState`**
427
- ```typescript
428
- interface RenderState {
429
- bookData?: BookData;
430
- pages: Map<number, FilePhase>; // page number -> 'done' | 'error' | ...
431
- }
432
- ```
285
+ **`InitOptions`** (all optional):
286
+ - `gateways: string[]` — Arweave gateways, tried in order. Default `['https://arweave.net', 'https://permagate.io', 'https://ar-io.dev']`.
287
+ - `requestPersistence: boolean` — ask the browser for persistent storage (`navigator.storage.persist()`), so cached books aren't evicted under storage pressure. Default `true`.
288
+ - `concurrency: { pdf: number; preview: number }` — how many PDFs and how many cover images download at once. Default `{ pdf: 3, preview: 6 }`.
289
+ - `muWorkerTimeout: number` — milliseconds before a render request to the mupdf worker is abandoned. Default `60000`.
290
+ - `mupdfUrl: string` — URL of `mupdf.js`. Default `'/js/mupdf.js'`.
433
291
 
434
292
  ---
435
293
 
436
- #### `deletePdf(txId: string): Promise<boolean>`
294
+ #### `preloadWorkersV2(): void`
437
295
 
438
- Removes a cached PDF from Cache API storage.
296
+ Creates the two worker threads immediately, so their scripts download while the app is still starting. Call it as early as possible.
439
297
 
440
298
  ```typescript
441
- import { deletePdf } from '@the_library/public/downloader';
299
+ import { preloadWorkersV2 } from '@the_library/public/downloader';
442
300
 
443
- const deleted = await deletePdf(txId);
444
- console.log(deleted ? 'Deleted' : 'Not found in cache');
301
+ preloadWorkersV2();
445
302
  ```
446
303
 
447
- **Parameters:**
448
- - `txId: string` — Arweave TX ID.
449
-
450
- **Returns:** `Promise<boolean>` — `true` if deletion succeeded, `false` if the PDF was not cached.
451
-
452
304
  ---
453
305
 
454
- #### `deletePreview(txId: string): Promise<boolean>`
306
+ ### File Operations
455
307
 
456
- Removes all cached page preview images for a TX ID.
308
+ #### `GetPdf(txId: string): Promise<Response>`
309
+
310
+ Returns the book's PDF as a `Response`, from the cache when it's there, otherwise after downloading it. The book becomes the "open book", which pruning never removes.
457
311
 
458
312
  ```typescript
459
- import { deletePreview } from '@the_library/public/downloader';
313
+ import { GetPdf, GetObjectUrl } from '@the_library/public/downloader';
460
314
 
461
- await deletePreview(txId);
315
+ const response = await GetPdf(txId);
316
+ const url = await GetObjectUrl(response); // for <iframe> or <embed>
462
317
  ```
463
318
 
464
- **Parameters:**
465
- - `txId: string` — Arweave TX ID.
466
-
467
- **Returns:** `Promise<boolean>` — `true` if deletion succeeded.
319
+ **Throws** if the download fails on every gateway, or `DownloadQuotaExceededError` if the browser refused to store it.
468
320
 
469
321
  ---
470
322
 
471
- #### `deleteDownloadRecord(txId: string): Promise<void>`
323
+ #### `GetImage(txId: string): Promise<Response>`
472
324
 
473
- Removes the download progress record from IndexedDB.
325
+ Returns the book's cover (preview) image. Calls for different books are limited by `concurrency.preview`, and repeated calls for the same book share one download, so it's safe to call for every book in a list.
474
326
 
475
327
  ```typescript
476
- import { deleteDownloadRecord } from '@the_library/public/downloader';
328
+ import { GetImage } from '@the_library/public/downloader';
477
329
 
478
- await deleteDownloadRecord(txId);
330
+ const cover = await GetImage(txId);
331
+ img.src = URL.createObjectURL(await cover.blob());
479
332
  ```
480
333
 
481
- **Parameters:**
482
- - `txId: string` — Arweave TX ID.
483
-
484
- **Returns:** `Promise<void>`
485
-
486
334
  ---
487
335
 
488
- ### Cache Store API
489
-
490
- Raw cache and IndexedDB access (rarely called directly; use the higher-level APIs above when possible).
491
-
492
- #### `openPdfCache(): Promise<Cache>`
336
+ #### `EnsurePageImage(txId: string, page: number): Promise<Response>`
493
337
 
494
- Gets the Cache API store for PDFs.
338
+ Renders one page of the PDF to an image (downloading the PDF first if needed), caches it, and returns it.
495
339
 
496
340
  ```typescript
497
- const cache = await openPdfCache();
498
- const keys = await cache.keys();
499
- ```
500
-
501
- #### `openPreviewCache(): Promise<Cache>`
502
-
503
- Gets the Cache API store for preview images.
504
-
505
- #### `openPagesCache(): Promise<Cache>`
506
-
507
- Gets the Cache API store for individual page images.
508
-
509
- #### `matchPdf(txId: string): Promise<Response | undefined>`
510
-
511
- Low-level cache lookup for a single PDF.
512
-
513
- #### `matchPreview(txId: string): Promise<Response | undefined>`
514
-
515
- Low-level cache lookup for a cover/first-page image.
516
-
517
- #### `matchPage(txId: string, pageNumber: number): Promise<Response | undefined>`
518
-
519
- Low-level cache lookup for a specific page image.
341
+ import { EnsurePageImage } from '@the_library/public/downloader';
520
342
 
521
- #### `putPdf(txId: string, response: Response): Promise<void>`
343
+ const page2 = await EnsurePageImage(txId, 2);
344
+ ```
522
345
 
523
- Low-level cache write for a PDF.
346
+ ---
524
347
 
525
- #### `putPreview(txId: string, response: Response): Promise<void>`
348
+ #### `EnsurePdf(txId: string): Promise<void>`
526
349
 
527
- Low-level cache write for a cover image.
350
+ Downloads and caches the PDF without returning it, e.g. to prefetch the next book.
528
351
 
529
- #### `putPage(txId: string, pageNumber: number, response: Response): Promise<void>`
352
+ ---
530
353
 
531
- Low-level cache write for a page image.
354
+ #### `EnsurePageCount(txId: string): Promise<number>`
532
355
 
533
- #### `pdfKeys(): Promise<string[]>`
356
+ Returns the PDF's page count, downloading and parsing the PDF if needed.
534
357
 
535
- Lists all cached PDF keys.
358
+ ---
536
359
 
537
- #### `previewKeys(): Promise<string[]>`
360
+ #### `DownloadPdfToDisk(txId: string, filename: string): Promise<void>`
538
361
 
539
- Lists all cached preview-image keys.
362
+ Saves the PDF to the user's device through the browser's normal download. `.pdf` is appended to `filename` if it's missing.
540
363
 
541
- #### `pageKeys(): Promise<string[]>`
364
+ ```typescript
365
+ import { DownloadPdfToDisk } from '@the_library/public/downloader';
542
366
 
543
- Lists all cached page-image keys.
367
+ await DownloadPdfToDisk(txId, 'My Book Title');
368
+ ```
544
369
 
545
370
  ---
546
371
 
547
- ### Keys & Naming
548
-
549
- #### `pdfKey(txId: string): string`
372
+ #### `GetObjectUrl(resp: Response): Promise<string>` and `revokeObjectUrl(url: string): void`
550
373
 
551
- Generates the cache key for a PDF.
374
+ Turn a response from the functions above into a `blob:` URL, and release it when you no longer need it.
552
375
 
553
376
  ```typescript
554
- const key = pdfKey(txId); // e.g., "arweave_pdf_{txId}"
377
+ const url = await GetObjectUrl(await GetPdf(txId));
378
+ // … later
379
+ revokeObjectUrl(url);
555
380
  ```
556
381
 
557
- #### `previewKey(txId: string): string`
382
+ ---
558
383
 
559
- Generates the cache key for a cover image.
384
+ #### `SetPinnedPdfs(txIds: string[]): void`
560
385
 
561
- #### `pageKey(txId: string, pageNumber: number): string`
386
+ Protects these books from pruning, e.g. a shelf the user marked for offline reading. Replaces the previous list; the open book stays protected regardless.
562
387
 
563
- Generates the cache key for a specific page.
388
+ ---
564
389
 
565
- ```typescript
566
- const key = pageKey(txId, 5); // e.g., "arweave_page_5_{txId}"
567
- ```
390
+ #### `storageEstimate(): Promise<StorageEstimate>`
568
391
 
569
- #### `CACHES` (constant)
392
+ The browser's own storage figures (`navigator.storage.estimate()`): `usage` and `quota` in bytes.
570
393
 
571
394
  ```typescript
572
- import { CACHES } from '@the_library/public/downloader';
573
-
574
- // CACHES = {
575
- // pdf: 'dl2-pdf-cache-v1',
576
- // preview: 'dl2-preview-cache-v1',
577
- // pages: 'dl2-pages-cache-v1'
578
- // }
579
- ```
580
-
581
- #### `CACHE_VERSION` (constant)
395
+ import { storageEstimate } from '@the_library/public/downloader';
582
396
 
583
- ```typescript
584
- import { CACHE_VERSION } from '@the_library/public/downloader';
585
- // CACHE_VERSION = 1
397
+ const { usage = 0, quota = 1 } = await storageEstimate();
398
+ console.log(`${((usage / quota) * 100).toFixed(1)}% of storage in use`);
586
399
  ```
587
400
 
588
- #### `DL2_SW_VERSION` (constant)
589
-
590
- Service worker build version. Used internally to detect when the SW script has been updated.
591
-
592
401
  ---
593
402
 
594
- ### Framework Adapters (`/downloader/{vue,react,solid}`)
403
+ #### `DownloadQuotaExceededError`
595
404
 
596
- #### Vue Composables
405
+ Thrown (rejected) when the browser refused to store a download because storage is full. It has a `txId` property. Use it to stop retrying and ask the user to free space, instead of treating it like a network error.
597
406
 
598
407
  ```typescript
599
- import {
600
- useFileStatus,
601
- useRenderStatus,
602
- useGlobalProgress,
603
- usePruning
604
- } from '@the_library/public/downloader/vue';
605
-
606
- // Reactive ref for one file's download state
607
- const status = useFileStatus(txId); // ComputedRef<FileState | undefined>
608
-
609
- // Reactive ref for render/page state
610
- const render = useRenderStatus(txId); // ComputedRef<RenderState | undefined>
408
+ import { GetPdf, DownloadQuotaExceededError } from '@the_library/public/downloader';
611
409
 
612
- // Global download progress
613
- const progress = useGlobalProgress(); // ComputedRef<DownloadSummary>
614
-
615
- // LRU pruning activity
616
- const pruning = usePruning(); // ComputedRef<PruningState>
410
+ try {
411
+ await GetPdf(txId);
412
+ } catch (e) {
413
+ if (e instanceof DownloadQuotaExceededError) showStorageFullMessage(e.txId);
414
+ else throw e;
415
+ }
617
416
  ```
618
417
 
619
- #### React Hooks
418
+ ---
620
419
 
621
- ```typescript
622
- import {
623
- useFileStatus,
624
- useRenderStatus,
625
- useGlobalProgress,
626
- usePruning
627
- } from '@the_library/public/downloader/react';
420
+ ### Metadata & Storage
628
421
 
629
- // Synchronized external store hooks
630
- const status = useFileStatus(txId); // FileState | undefined
631
- const render = useRenderStatus(txId); // RenderState | undefined
632
- const progress = useGlobalProgress(); // DownloadSummary
633
- const pruning = usePruning(); // PruningState
634
- ```
422
+ #### `getBookData(txId: string): Promise<BookData | undefined>`
635
423
 
636
- #### Solid.js Primitives
424
+ The page dimensions recorded once the PDF has been parsed, or `undefined` before that.
637
425
 
638
426
  ```typescript
639
- import {
640
- useFileStatus,
641
- useRenderStatus,
642
- useGlobalProgress,
643
- usePruning
644
- } from '@the_library/public/downloader/solid';
645
-
646
- // Solid createMemo/createSignal-based reactivity
647
- const [status] = useFileStatus(txId);
648
- const [render] = useRenderStatus(txId);
649
- const [progress] = useGlobalProgress();
650
- const [pruning] = usePruning();
427
+ interface BookData {
428
+ txId: string;
429
+ dimensions: Array<{ width: number; height: number }>; // one entry per page
430
+ parsed: boolean;
431
+ }
651
432
  ```
652
433
 
653
434
  ---
654
435
 
655
- ### Service Worker API (`@the_library/public/downloader/sw`)
656
-
657
- Installs a service worker that intercepts PDF/image requests, serving cached responses offline and persisting downloads across tab reloads.
436
+ ### Download State
658
437
 
659
- #### `registerDownloaderSW(): Promise<ServiceWorkerState>`
438
+ #### `downloadState`
660
439
 
661
- Registers (or reuses) the downloader service worker.
440
+ The synchronous store behind every framework adapter: the state of each file, the pages rendered for each book, and pruning activity.
662
441
 
663
442
  ```typescript
664
- import { registerDownloaderSW } from '@the_library/public/downloader/sw';
665
-
666
- const state = await registerDownloaderSW();
667
- if (state.active) {
668
- console.log('Service worker active and controlling pages');
669
- }
670
- ```
443
+ import { downloadState } from '@the_library/public/downloader';
671
444
 
672
- **Parameters:** None
445
+ const unsubscribe = downloadState.subscribe((changedKey) => { /* re-read what you show */ });
673
446
 
674
- **Returns:** `Promise<ServiceWorkerState>`
447
+ downloadState.get(txId); // FileState
448
+ downloadState.getRender(txId); // RenderState: { bookData?, pages: Map<page, FilePhase> }
449
+ downloadState.summary(); // { active, queued, loaded, total } across all files
450
+ downloadState.pruning; // PruningState: { active, freedBytes, removedCount }
451
+ ```
675
452
 
676
- **Type: `ServiceWorkerState`**
677
453
  ```typescript
678
- interface ServiceWorkerState {
679
- registered: boolean; // SW is registered
680
- active: boolean; // SW is controlling this page
681
- waiting: boolean; // Update pending
682
- installing: boolean; // New registration in progress
683
- controller: ServiceWorkerContainer | null;
454
+ type FilePhase = 'unknown' | 'queued' | 'downloading' | 'done' | 'error';
455
+
456
+ interface FileState {
457
+ phase: FilePhase;
458
+ loaded?: number; // bytes so far
459
+ total?: number; // bytes, when known
460
+ error?: string;
684
461
  }
685
462
  ```
686
463
 
464
+ Progress is also announced as window events, `ARWEAVE_DOWNLOAD_PROGRESS` (`detail: DownloadProgressDetail` — `{ txId, loaded, total? }`) and `ARWEAVE_DOWNLOAD_DONE` (`detail: DownloadDoneDetail` — `{ txId, mime: 'pdf' | 'preview' }`).
465
+
687
466
  ---
688
467
 
689
- #### `swStatus(): Promise<ServiceWorkerState>`
468
+ ### Keys & Naming
690
469
 
691
- Checks current service worker status without re-registering.
470
+ The cache names and keys, for code that reads Cache Storage directly (the service worker does):
692
471
 
693
472
  ```typescript
694
- import { swStatus } from '@the_library/public/downloader/sw';
473
+ import { CACHES, CACHE_VERSION, pdfKey, previewKey, pageKey } from '@the_library/public/downloader';
695
474
 
696
- const state = await swStatus();
475
+ CACHES; // { pdf: 'dl2-pdf-v1', preview: 'dl2-preview-v1', pages: 'dl2-pages-v1' }
476
+ CACHE_VERSION; // 1
477
+ pdfKey(txId); // '/ar/<txId>' (in CACHES.pdf)
478
+ previewKey(txId); // '/ar/<txId>' (in CACHES.preview)
479
+ pageKey(txId, 5); // '/pages/<txId>/5'
697
480
  ```
698
481
 
699
- **Parameters:** None
700
-
701
- **Returns:** `Promise<ServiceWorkerState>`
482
+ `DL2_SW_VERSION` is the build's version stamp for the service worker.
702
483
 
703
484
  ---
704
485
 
705
- #### `clearDownloaderCaches(): Promise<void>`
486
+ ### Framework Adapters (`/downloader/{vue,react,solid}`)
706
487
 
707
- Drops all Cache API entries (PDFs, page images, previews), but keeps IndexedDB metadata records intact.
488
+ The same four hooks in each framework, wrapping `downloadState`:
708
489
 
709
490
  ```typescript
710
- import { clearDownloaderCaches } from '@the_library/public/downloader/sw';
491
+ import { useFileStatus, useRenderStatus, useGlobalProgress, usePruning } from '@the_library/public/downloader/vue';
711
492
 
712
- await clearDownloaderCaches();
713
- console.log('Cache storage cleared, IndexedDB preserved');
493
+ const status = useFileStatus(txId); // ComputedRef<FileState>
494
+ const render = useRenderStatus(txId); // ComputedRef<RenderState>
495
+ const progress = useGlobalProgress(); // ComputedRef<{ active, queued, loaded, total }>
496
+ const pruning = usePruning(); // ComputedRef<PruningState>
714
497
  ```
715
498
 
716
- **Parameters:** None
717
-
718
- **Returns:** `Promise<void>`
499
+ - `@the_library/public/downloader/react` returns the values themselves (`FileState`, `RenderState`, …), re-rendering on change.
500
+ - `@the_library/public/downloader/solid` returns accessors: `const status = useFileStatus(txId); status().phase`.
719
501
 
720
502
  ---
721
503
 
722
- #### `nukeDownloaderStorage(): Promise<void>`
504
+ ### Service Worker API (`@the_library/public/downloader/sw`)
505
+
506
+ An optional service worker (`/dl2-sw.js`, registered with scope `/`) answers same-origin `GET` requests for `/ar/<txId>` (a PDF or cover) and `/pages/<txId>/<n>` (a rendered page) from Cache Storage, fetching `/ar/` files from Arweave when they aren't cached yet. Plain `<img src="/ar/…">` and `<iframe src="/ar/…">` tags then work offline. Add `?dl2-bypass=1` to a URL to skip the service worker for that request.
507
+
508
+ #### `registerDownloaderSW(opts?: SwGateOptions): Promise<SwStatus>`
723
509
 
724
- Full reset: deletes everything — Cache API entries, IndexedDB records, service worker registration. Use when the user logs out or to recover storage quota.
510
+ Registers the service worker, unless something stops it; `reason` in the result says what:
511
+
512
+ - the page's host isn't in `allowedOrigins` (default `['datapond.earth', 'localhost']`, subdomains included) → `'origin-not-allowed'`;
513
+ - in development (`import.meta.env.DEV`), only when `enableInDev: true` **and** `localStorage['dl2:sw'] === 'on'` → otherwise `'dev-mode'`;
514
+ - kill switches: `?dl2-sw=off` in the URL or `localStorage['dl2:sw'] = 'off'` unregisters it (`'kill-switch-off'`); `?dl2-sw=purge` also erases all downloader storage (`'kill-switch-purge'`).
725
515
 
726
516
  ```typescript
727
- import { nukeDownloaderStorage } from '@the_library/public/downloader/sw';
517
+ import { registerDownloaderSW } from '@the_library/public/downloader/sw';
728
518
 
729
- await nukeDownloaderStorage();
730
- console.log('All download data erased');
519
+ const status = await registerDownloaderSW({ allowedOrigins: ['example.org'] });
520
+ if (!status.registered) console.info('Downloader SW not active:', status.reason);
731
521
  ```
732
522
 
733
- **Parameters:** None
523
+ ```typescript
524
+ interface SwGateOptions { allowedOrigins?: string[]; enableInDev?: boolean }
734
525
 
735
- **Returns:** `Promise<void>`
526
+ interface SwStatus {
527
+ supported: boolean; // the browser has service workers
528
+ registered: boolean;
529
+ controlling: boolean; // a service worker controls this page
530
+ scope?: string;
531
+ version?: string;
532
+ reason?: string; // why it isn't registered
533
+ }
534
+ ```
736
535
 
737
- ---
536
+ #### `swStatus(): Promise<SwStatus>`
738
537
 
739
- #### `unregisterDownloaderSW(): Promise<void>`
538
+ The current status, without registering anything.
740
539
 
741
- Unregisters the service worker.
540
+ #### `unregisterDownloaderSW(opts?: { clearCaches?: boolean; reload?: boolean }): Promise<boolean>`
742
541
 
743
- ```typescript
744
- import { unregisterDownloaderSW } from '@the_library/public/downloader/sw';
542
+ Unregisters the service worker; resolves `true` if one was removed. `clearCaches` also deletes the downloader's caches; `reload` reloads the page if a worker was removed.
745
543
 
746
- await unregisterDownloaderSW();
747
- ```
544
+ #### `clearDownloaderCaches(): Promise<void>`
748
545
 
749
- **Parameters:** None
546
+ Deletes every downloader cache (all Cache Storage entries named `dl2-…`). IndexedDB records are kept.
547
+
548
+ #### `nukeDownloaderStorage(): Promise<void>`
750
549
 
751
- **Returns:** `Promise<void>`
550
+ Full reset: unregisters the service worker, deletes the caches, and deletes the downloader's IndexedDB database.
752
551
 
753
552
  ---
754
553
 
755
554
  ## 2. Models API (`@the_library/public/models`)
756
555
 
757
- The Models API provides the in-memory ORM, record cache, patch/replay engine, and schema registry for Sanctuary's data model. The root subpath exports only the framework-agnostic core; session bootstrap and reactive views are framework-specific and live under `/models/{vue,react,solid,astro}`.
556
+ The Models API provides the in-memory ORM, record cache, patch/replay engine, and schema registry for the library's data model. The root subpath exports only the framework-agnostic core; session bootstrap and reactive views are framework-specific and live under `/models/{vue,react,solid,astro}`.
758
557
 
759
558
  ### Language Semantics (every value is per-language)
760
559
 
@@ -794,38 +593,39 @@ Why:
794
593
 
795
594
  ### Core Types & Registry
796
595
 
797
- #### `Registry` (singleton, exported from `registry.ts`)
596
+ #### `Registry` (singleton)
798
597
 
799
- Holds enriched `ModelDefinition` for every registered object type — property IDs, field schemas, and relations.
598
+ Holds the `ModelDefinition` of every model type: its props, instance fields and relations, as compiled from `@the_library/canonical-schemas`.
800
599
 
801
600
  ```typescript
802
601
  import { Registry, type ModelDefinition } from '@the_library/public/models';
602
+ import { Book } from '@the_library/public/models/models';
803
603
 
804
- // Get schema by numeric type ID
805
604
  const bookSchema: ModelDefinition = Registry.get(Book.type);
806
- // bookSchema.name, bookSchema.typePrefix, bookSchema.props, bookSchema.instanceFields, ...
807
-
808
- // Get schema by class name
809
- const byName = Registry.getByName('Book');
605
+ const byName = Registry.getByName('Book'); // ModelDefinition | undefined
810
606
  ```
811
607
 
812
608
  **Methods:**
813
609
 
814
- - `Registry.get(typeId: number): ModelDefinition` — Returns the enriched schema for a type. Throws if `typeId` is not registered.
815
-
816
- - `Registry.getByName(name: string): ModelDefinition` — Returns the schema for a named type (e.g., 'Book'). Throws if no type with that name is registered.
817
-
818
- - `Registry.all(): ModelDefinition[]` — Returns all registered type schemas.
610
+ - `Registry.get(id: number): ModelDefinition` — the schema for a type ID. An unknown ID logs a warning and returns an empty `Generic` placeholder.
611
+ - `Registry.getByName(name: string): ModelDefinition | undefined` — the schema for a model name such as `'Book'`.
612
+ - `Registry.register(schema: ModelDefinition): void` and `Registry.clear(): void` — used by the generated models and by tests.
819
613
 
820
614
  **Type: `ModelDefinition`**
821
615
  ```typescript
822
616
  interface ModelDefinition {
823
- name: string; // e.g., 'Book'
824
- type: number; // numeric type ID
825
- typePrefix: string; // e.g., 'B' for Book
826
- props: PropertyDefinition[]; // field schemas
827
- instanceFields: InstanceFieldInfo[]; // relation/reference fields
828
- relations: RelationDefinition[];
617
+ name: string; // e.g. 'Book'
618
+ typePrefix: string;
619
+ objectId: number; // the numeric type ID (Book.type)
620
+ multipleInstances: boolean;
621
+ uidStrategy?: string | string[];
622
+ instanceUidStrategy?: string | string[];
623
+ isPrivate?: boolean; // default true: creation patches are hidden from public reads
624
+ standardPks?: Record<string, string>;
625
+ props: SchemaProp[];
626
+ instanceFields: SchemaInstanceField[];
627
+ relations: SchemaRelation[];
628
+ indexes?: SchemaIndex[];
829
629
  }
830
630
  ```
831
631
 
@@ -833,204 +633,128 @@ interface ModelDefinition {
833
633
 
834
634
  ### Record Cache & Reactivity
835
635
 
836
- #### `configureDb(options?: { cacheLimit?: number }): void`
636
+ Loaded records live in an in-memory LRU cache, one per model type. Record IDs are `bigint` (`uid`). Every function below takes an optional `creatorUsername`. Canonical records are shared by everyone; user-created records (IDs with the top bit set) are kept per creator, and without `creatorUsername` the default set with `setDefaultCreatorUsername()` is used (an error if none is set; `hasRecord` returns `false` instead).
637
+
638
+ #### `configureDb(opts: { cacheLimit?: number }): void`
837
639
 
838
- Sets the in-memory LRU cache size. Call before loading any records.
640
+ Sets how many records of each type the cache keeps (default `10000`). Call it before loading records.
839
641
 
840
642
  ```typescript
841
643
  import { configureDb } from '@the_library/public/models';
842
644
 
843
- // Lower cache limit for mobile devices
844
- configureDb({ cacheLimit: 500 });
645
+ configureDb({ cacheLimit: 2000 }); // e.g. on mobile
845
646
  ```
846
647
 
847
- **Parameters:**
848
- - `options.cacheLimit?: number` — Maximum number of records to hold in memory. Default is `2000`. Older records are evicted when the limit is exceeded.
849
-
850
- **Returns:** `void`
851
-
852
648
  ---
853
649
 
854
- #### `getRecord(typeId: number, id: string): any`
650
+ #### `getRecord(objectType: number, id: bigint, creatorUsername?: string): any`
855
651
 
856
- Synchronously retrieves a record from the cache.
652
+ Returns a cached record synchronously. **Throws** if it isn't in the cache (never loaded, or evicted); check with `hasRecord`, or load it with the model's `LoadAsync`.
857
653
 
858
654
  ```typescript
859
- import { getRecord } from '@the_library/public/models';
860
- import { Book } from '@the_library/public/models';
655
+ import { getRecord, hasRecord } from '@the_library/public/models';
656
+ import { Book } from '@the_library/public/models/models';
861
657
 
862
- if (hasRecord(Book.type, bookId)) {
863
- const book = getRecord(Book.type, bookId);
864
- console.log(book.title);
865
- }
658
+ const book = hasRecord(Book.type, bookId) ? getRecord(Book.type, bookId) : await Book.LoadAsync(bookId);
866
659
  ```
867
660
 
868
- **Parameters:**
869
- - `typeId: number` — The model type ID (e.g., `Book.type`).
870
- - `id: string` — The record UID.
871
-
872
- **Returns:** The record object (e.g., a `Book` instance) if cached.
873
-
874
- **Throws:** If the record is not in cache (LRU eviction or never loaded). Use `hasRecord()` to check first, or call the model's `LoadAsync()` method to load from disk.
875
-
876
661
  ---
877
662
 
878
- #### `addRecord(typeId: number, id: string, record: any): void`
879
-
880
- Adds or updates a record in the cache.
881
-
882
- ```typescript
883
- import { addRecord } from '@the_library/public/models';
884
-
885
- addRecord(Book.type, bookId, bookInstance);
886
- ```
887
-
888
- **Parameters:**
889
- - `typeId: number` — Model type ID.
890
- - `id: string` — Record UID.
891
- - `record: any` — The record object.
663
+ #### `addRecord(objectType: number, obj: { id: bigint }, creatorUsername?: string): void`
892
664
 
893
- **Returns:** `void`
665
+ Puts a record object into the cache (replacing one with the same `id`).
894
666
 
895
667
  ---
896
668
 
897
- #### `removeRecord(typeId: number, id: string): void`
898
-
899
- Removes a record from the cache.
900
-
901
- ```typescript
902
- import { removeRecord } from '@the_library/public/models';
903
-
904
- removeRecord(Book.type, bookId);
905
- ```
906
-
907
- **Parameters:**
908
- - `typeId: number` — Model type ID.
909
- - `id: string` — Record UID.
669
+ #### `removeRecord(objectType: number, id: bigint, creatorUsername?: string): void`
910
670
 
911
- **Returns:** `void`
671
+ Drops a record from the cache.
912
672
 
913
673
  ---
914
674
 
915
- #### `hasRecord(typeId: number, id: string): boolean`
916
-
917
- Checks if a record is currently cached.
918
-
919
- ```typescript
920
- import { hasRecord } from '@the_library/public/models';
921
-
922
- if (hasRecord(Book.type, bookId)) {
923
- // safe to call getRecord()
924
- }
925
- ```
926
-
927
- **Parameters:**
928
- - `typeId: number` — Model type ID.
929
- - `id: string` — Record UID.
675
+ #### `hasRecord(objectType: number, id: bigint, creatorUsername?: string): boolean`
930
676
 
931
- **Returns:** `boolean`
677
+ Whether the record is in the cache right now.
932
678
 
933
679
  ---
934
680
 
935
- #### `pinRecord(typeId: number, id: string): void`
681
+ #### `pinRecord(objectType: number, id: bigint, creatorUsername?: string, reason?: string): void`
936
682
 
937
- Pins a record so it won't be evicted from the LRU cache. Use for the currently-open record, search results, etc.
683
+ Keeps a record in the cache until it is unpinned, e.g. the book that's open. A record stays pinned while any `reason` (default `'lifecycle'`) still pins it, so independent callers use different reasons; pinning twice with the same reason needs only one unpin.
938
684
 
939
685
  ```typescript
940
- import { pinRecord } from '@the_library/public/models';
686
+ import { pinRecord, unpinRecord } from '@the_library/public/models';
941
687
 
942
- // User opened a book — don't evict it while they're reading
943
- pinRecord(Book.type, openBookId);
688
+ pinRecord(Book.type, openBookId, undefined, 'reader');
689
+ // when the reader closes:
690
+ unpinRecord(Book.type, openBookId, undefined, 'reader');
944
691
  ```
945
692
 
946
- **Parameters:**
947
- - `typeId: number` — Model type ID.
948
- - `id: string` — Record UID.
949
-
950
- **Returns:** `void`
951
-
952
693
  ---
953
694
 
954
- #### `unpinRecord(typeId: number, id: string): void`
695
+ #### `unpinRecord(objectType: number, id: bigint, creatorUsername?: string, reason?: string): void`
955
696
 
956
- Unpins a record so it can be evicted again.
957
-
958
- ```typescript
959
- import { unpinRecord } from '@the_library/public/models';
960
-
961
- // User closed the book
962
- unpinRecord(Book.type, openBookId);
963
- ```
964
-
965
- **Parameters:**
966
- - `typeId: number` — Model type ID.
967
- - `id: string` — Record UID.
968
-
969
- **Returns:** `void`
697
+ Removes a pin added with the same `reason`; the record can then be evicted again.
970
698
 
971
699
  ---
972
700
 
973
- #### `addReactivitySystem(system: ReactivitySystem): void`
701
+ #### `addReactivitySystem(sys: ReactivitySystem): () => void`
974
702
 
975
- Installs a framework-agnostic reactivity hook that fires when records change. Framework adapters call this once at boot.
703
+ Registers a reactivity system and returns a function that unregisters it. Several can be registered at once (one per framework); the framework adapters' `install…Reactivity()` functions call this for you.
976
704
 
977
705
  ```typescript
978
706
  import { addReactivitySystem } from '@the_library/public/models';
979
707
 
980
- addReactivitySystem({
981
- track: (typeId, id) => { /* called when record added/changed */ },
982
- trigger: (typeId, id) => { /* called when record needs re-render */ },
708
+ const remove = addReactivitySystem({
709
+ track: (obj) => { /* a record was read: subscribe to it */ },
710
+ trigger: (obj) => { /* a record changed: re-render its readers */ },
983
711
  });
984
712
  ```
985
713
 
986
714
  ---
987
715
 
988
- #### `setReactivitySystem(system: ReactivitySystem): void`
716
+ #### `setReactivitySystem(sys: ReactivitySystem | null): void`
989
717
 
990
- Replaces the current reactivity system. Typically called by framework adapters during initialization.
718
+ Replaces all registered systems with `sys`, or removes them all with `null`. Meant for single-framework apps and tests; use `addReactivitySystem` when frameworks coexist.
991
719
 
992
720
  ---
993
721
 
994
722
  ### Generated Models
995
723
 
996
- Generated model classes (e.g., `Book`, `Person`, `Tag`) are built from your schema via `pnpm generate` and exported from both the root (`@the_library/public/models`) and the dedicated `/models/models` subpath.
724
+ The model classes (`Book`, `Person`, `Tag`, `ClassificationSystem`, `Post`) are generated from `@the_library/canonical-schemas` and exported from `@the_library/public/models/models` (and from `@the_library/public/models`). Every value is read and written per language (see [Language Semantics](#language-semantics-every-value-is-per-language)).
997
725
 
998
726
  ```typescript
999
- import { Book, Person, NewBookWithId, ModelTypes } from '@the_library/public/models';
1000
- // equivalently: from '@the_library/public/models/models'
727
+ import { Book, NewBookWithId, ModelTypes } from '@the_library/public/models/models';
1001
728
 
1002
- // Create a new record in memory (not yet persisted)
1003
- const book = NewBookWithId('book:abc123', 'username');
1004
- book.title = 'A Study in Scarlet';
1005
- book.author = 'Sir Arthur Conan Doyle';
729
+ // Load a record (from the cache, or IndexedDB / the network when it isn't cached)
730
+ const book = await Book.LoadAsync(bookId); // bookId: bigint
731
+ book.getTitle('eng');
1006
732
 
1007
- // Load an existing record from disk
1008
- const loaded = await Book.LoadAsync('book:abc123');
733
+ // Synchronous access when it's known to be cached
734
+ if (Book.Has(bookId)) Book.Load(bookId);
1009
735
 
1010
- // Check type at runtime
1011
- if (loaded instanceof Book) {
1012
- console.log('It is a book');
1013
- }
736
+ // Create a record in memory and edit it
737
+ const draft = NewBookWithId(newId);
738
+ draft.setTitle('A Study in Scarlet', 'eng');
739
+ draft.validateIsbn(); // one validate<Standard>() per standard the model uses
1014
740
 
1015
- // Generic record hydration (type unknown at compile time)
1016
- const typeId = 10; // some numeric type
1017
- const RecordClass = ModelTypes[typeId];
1018
- const generic = new RecordClass();
741
+ // Model class by numeric type ID
742
+ const BookClass = ModelTypes[Book.type];
1019
743
  ```
1020
744
 
1021
- **Key Functions:**
1022
-
1023
- - `NewBookWithId(id: string, creatorUsername: string): Book` — Factory to create a new Book record in memory.
745
+ **Key members:**
1024
746
 
1025
- - `Book.LoadAsync(id: string): Promise<Book>` — Loads a Book by UID from IndexedDB.
1026
-
1027
- - `ModelTypes: { [typeId: number]: typeof ModelClass }` — Maps numeric type IDs to model classes.
747
+ - `Book.type: number` — the model's type ID.
748
+ - `Book.LoadAsync(id: bigint, creatorUsername?: string): Promise<Book>`, `Book.Load(id, creatorUsername?): Book` (throws when not cached) and `Book.Has(id, creatorUsername?): boolean`.
749
+ - `NewBookWithId(id: bigint, creatorUsername?: string): Book` — a new record in memory; `NewPersonWithId`, `NewTagWithId`, `NewClassificationSystemWithId` and `NewPostWithId` work the same way.
750
+ - `get<Prop>(lang)` / `set<Prop>(value, lang)` for every prop, e.g. `getCopyrightStatus('eng')`; the plain property (`book.title`, `book.copyrightStatus`) reads and writes the default language.
751
+ - `ModelTypes: Record<number, ModelClass>` — model classes by type ID.
1028
752
 
1029
753
  ---
1030
754
 
1031
755
  ### Schema Registry
1032
756
 
1033
- The `Registry` singleton (above) holds `ModelDefinition` for each type. Accessed via `Registry.get(typeId)` or `Registry.getByName(name)`.
757
+ The `Registry` singleton ([above](#registry-singleton)) holds the `ModelDefinition` of each type: `Registry.get(typeId)` or `Registry.getByName(name)`.
1034
758
 
1035
759
  ---
1036
760
 
@@ -1069,7 +793,7 @@ const unsubscribe = trashStore.subscribe(Tag.type, () => {
1069
793
 
1070
794
  **Methods:**
1071
795
 
1072
- - `trashStore.list(objectType: number, usernames?: string[]): number[]` — Merges trashed ids across `usernames`. Empty array (default) merges **every** bucketed username for that type.
796
+ - `trashStore.list(objectType: number, usernames?: string[]): uid[]` — Merges trashed ids across `usernames`. Empty array (default) merges **every** bucketed username for that type.
1073
797
 
1074
798
  - `trashStore.count(objectType: number, usernames?: string[]): number` — Same merge semantics as `list()`; returns the count.
1075
799
 
@@ -1082,7 +806,7 @@ const unsubscribe = trashStore.subscribe(Tag.type, () => {
1082
806
  **Type: `TrashStore`**
1083
807
  ```typescript
1084
808
  interface TrashStore {
1085
- list(objectType: number, usernames?: string[]): number[];
809
+ list(objectType: number, usernames?: string[]): uid[];
1086
810
  count(objectType: number, usernames?: string[]): number;
1087
811
  usernames(objectType: number): string[];
1088
812
  subscribe(objectType: number, listener: () => void): () => void;
@@ -1103,7 +827,12 @@ Full (re)scan — walks every currently-hydrated record and reconciles trash mem
1103
827
 
1104
828
  ### Stats & Local Analytics
1105
829
 
1106
- Per-record engagement tracking (visits, downloads, ratings, shares) for any object type. Stats are **local-only** — they use the same patch/queue/IndexedDB pipeline as everything else in `models_v2`, but are never serialized to the EVM and never leave the device. There is no server-side aggregation; each device's `StatsStore` is a private, ephemeral index rebuilt from that device's own patch history on every boot.
830
+ Per-record engagement tracking (visits, downloads, ratings, shares) for any object type. Each action is applied to the in-memory `StatsStore` at once and queued like any other edit; on the next save it goes to the chain (all except `VisitPart`, which stays on the device):
831
+
832
+ - **Under the reader's account** — the action words are stored with the rest of the save in `BackupStorage`, so they are public under that account's identity (an account, not a real name) and come back to the reader's other devices when the save is restored.
833
+ - **Aggregated, without user IDs** — `Factory.save()` also feeds them to `StatsPivotStorage`, a global weekly pivot of visit, download, rating and share counts per record, with no user ID.
834
+
835
+ `StatsStore` itself is a per-device index, rebuilt on every boot from the patches in IndexedDB (local and restored).
1107
836
 
1108
837
  ```typescript
1109
838
  import { Stats, PopulateStatsStore, DownloadsByType, VisitsByType, FavoritesByTypes } from '@the_library/public/models';
@@ -1111,7 +840,7 @@ import { Stats, PopulateStatsStore, DownloadsByType, VisitsByType, FavoritesByTy
1111
840
 
1112
841
  **Why this exists / how it fits the patch pipeline:**
1113
842
  - `Orm` (the base class every generated model — `Book`, `Tag`, `Person`, ... — extends) **also extends `Stats`**. So `book.visit()`, `book.download()`, `book.rating = Rating.Keep`, `book.ratePart(3, Rating.Keep)`, `book.share(socialMediaId)`, and `book.favoriteParts()` are available directly on every record with no separate lookup step.
1114
- - Each call builds a `LocalOp` (discriminated by `action: ActionTypes`, not `operation: PatchOperation` like EVM patches — see `isEvmPatch()` in `interfaces.ts`), applies it to the in-memory `StatsStore` immediately, and pushes it through `PatchInstance.addPatch()` so it is queued and persisted to IndexedDB exactly like a property edit — just never included when patches are serialized for chain submission.
843
+ - Each call builds a `LocalOp` (discriminated by `action: ActionTypes`, not `operation: PatchOperation` like EVM patches — see `isEvmPatch()` in `interfaces.ts`), applies it to the in-memory `StatsStore` immediately, and pushes it through `PatchInstance.addPatch()` so it is queued and persisted to IndexedDB exactly like a property edit. When patches are serialized for the chain, `localOpToActionPatch()` turns it into an `ActionOperation` word (`VisitPart` has no opcode left and stays local); `actionPatchToLocalOp()` reverses this for restored saves.
1115
844
  - On boot, `InitPatchWorker()` (`worker-patch-main.ts`) loads every locally-stored patch for the current user, filters to the non-EVM ones (`!isEvmPatch(p)`), and replays them through `PopulateStatsStore()` to reconstruct `StatsStore` from scratch — the module-level `Map` holds nothing across a page reload until this runs.
1116
845
 
1117
846
  #### `StatsStore` (internal, not exported)
@@ -1204,7 +933,7 @@ type StatSummary = {
1204
933
  }
1205
934
  ```
1206
935
 
1207
- **Type: `ActionTypes`** (local-only op discriminator — do not confuse with EVM `PatchOperation`)
936
+ **Type: `ActionTypes`** (local op discriminator — do not confuse with EVM `PatchOperation`; on the chain these become `ActionOperation` words)
1208
937
  ```typescript
1209
938
  enum ActionTypes {
1210
939
  Visit = 0,
@@ -1234,253 +963,119 @@ enum Rating {
1234
963
 
1235
964
  ### Framework Adapters (`/models/{vue,react,solid,astro}`)
1236
965
 
1237
- #### Vue Composables
966
+ Each adapter exports the session functions below plus that framework's reactive views. The session functions are the same in all four:
1238
967
 
1239
- ```typescript
1240
- import {
1241
- bootstrapModelsV2,
1242
- useLevel,
1243
- useDraft,
1244
- useTrash,
1245
- resetModelsV2Session
1246
- } from '@the_library/public/models/vue';
1247
- ```
968
+ | Export | Vue | React | Solid | Astro |
969
+ |---|---|---|---|---|
970
+ | `bootstrapModelsV2`, `resetModelsV2Session`, `clearLocalPatches`, `loadSearch` | ✓ | ✓ | ✓ | ✓ |
971
+ | `install…Reactivity` | `installVueReactivity` | `installReactReactivity` | `installSolidReactivity` | `installVanillaReactivity` |
972
+ | `useLevel`, `useTrash` | ✓ | ✓ | ✓ | — |
973
+ | `useDraft` | ✓ | — | ✓ | — |
974
+ | other | `useBookSearch`, `useStatsTick` | `useModelUpdate` | `getAllPatches`, `hasPartition`, `loadPartition` | `subscribeRecord` |
1248
975
 
1249
- **`bootstrapModelsV2(options: BootstrapOptions): Promise<void>`**
976
+ #### `bootstrapModelsV2(opts: SessionBootstrapOptions): Promise<void>`
1250
977
 
1251
- Initializes the models subsystem once at app boot. Idempotent across Astro `<ClientRouter />` navigations.
978
+ Starts the models session once at app boot: loads the corpus into the patch worker and hydrates the records. Calling it again (for example on every Astro `<ClientRouter />` navigation) doesn't boot twice.
1252
979
 
1253
980
  ```typescript
981
+ import { bootstrapModelsV2 } from '@the_library/public/models/vue';
982
+ import { Book, NewBookWithId } from '@the_library/public/models/models';
983
+
1254
984
  await bootstrapModelsV2({
1255
- userLang: 'en',
1256
- models: [{ type: Book.type, create: NewBookWithId }],
1257
- localMode: '/corpus_v2', // OR registry: { dataBlock, gateway }
1258
- onBoot: (data) => console.log(`booted ${data.sidecars.length} sidecars`),
985
+ userLang: 'eng',
986
+ models: [{ type: Book.type, create: (id) => NewBookWithId(id) }],
987
+ localMode: '/corpus_v2', // or: registry: { dataBlock, gateway }
988
+ onBoot: (data) => { /* booted */ },
1259
989
  });
1260
990
  ```
1261
991
 
1262
- **Parameters:**
1263
- - `userLang: string` — User's preferred language (ISO 639-1 or custom code).
1264
- - `models: ModelFactory[]` — Array of `{ type: number; create: (id, username) => ModelInstance }`.
1265
- - `localMode?: string` — Folder path to serve corpus from (dev mode, mutually exclusive with `registry`).
1266
- - `registry?: { dataBlock: string; gateway: string }` — Arweave data block & gateway (production mode).
1267
- - `onBoot?: (data: BootData) => void` — Callback fired when bootstrap completes.
992
+ **`SessionBootstrapOptions`:**
993
+ - `userLang: Lang` — the user's language.
994
+ - `models: ModelFactory[]` — `{ type: number; create: (id: bigint) => Orm }` for each model to hydrate.
995
+ - `selectedLangs?: Lang[]` — further languages to load.
996
+ - `localMode?: string` / `localManifest?: LocalCorpusIndex` — serve the corpus from a local folder holding `bin/` and `corpus-index.json` (development, book-manager). When set, `registry` is ignored.
997
+ - `registry?: { dataBlock: RegistryDataBlock; gateway?: string }` — load it from Arweave, as published in the registry manifest (production).
998
+ - `workerPath?: string | URL` — URL of the patch worker (default `/patch-worker.js`).
999
+ - `loadAiSidecars?: boolean` — also load the AI results sidecars.
1000
+ - `search?: boolean` — load the Book search index in the background after boot (default `true`; never blocks or fails boot).
1001
+ - `onBoot?(data)`, `onBootError?(err)`, `init?()`, `onNavigate?()` — lifecycle callbacks.
1268
1002
 
1269
- **Returns:** `Promise<void>` — resolves once all sidecars are hydrated.
1003
+ #### `resetModelsV2Session(): void`
1270
1004
 
1271
- ---
1005
+ Clears the session and the cached level state, for logout and tests/HMR.
1272
1006
 
1273
- **`useLevel(username?: string): Ref<LevelData>`**
1007
+ #### `clearLocalPatches()`
1274
1008
 
1275
- Reactive per-username level state and corpus metadata.
1009
+ Deletes every locally queued, unsaved edit from IndexedDB (for all users) — "discard my local edits". Reload the page right after it.
1276
1010
 
1277
- ```typescript
1278
- import { useLevel } from '@the_library/public/models/vue';
1011
+ #### `loadSearch(lang?: Lang): Promise<void>`
1279
1012
 
1280
- const { level, progress } = useLevel('alice').value;
1281
- console.log(`Alice's level: ${level}`);
1282
- ```
1013
+ Starts (or retries) loading the search index for a language. Boot starts it on its own when `search` is enabled.
1283
1014
 
1284
- **Parameters:**
1285
- - `username?: string` — Username. Defaults to the currently-logged-in user.
1015
+ ---
1286
1016
 
1287
- **Returns:** `Ref<LevelData>`
1017
+ #### `useLevel(username: string)`
1018
+
1019
+ The user's level: `{ level, progress, points }` (`level` starts at 1, `progress` is 0–100 toward the next level). Vue returns a `ComputedRef<LevelState>`, React the `LevelState`, Solid an `Accessor<LevelState>`.
1288
1020
 
1289
- **Type: `LevelData`**
1290
1021
  ```typescript
1291
- interface LevelData {
1292
- username: string;
1293
- level: number;
1294
- progress: number; // 0-1
1295
- lastUpdated: number; // timestamp
1296
- }
1022
+ import { useLevel } from '@the_library/public/models/vue';
1023
+
1024
+ const level = useLevel(username);
1025
+ level.value.level;
1297
1026
  ```
1298
1027
 
1299
1028
  ---
1300
1029
 
1301
- **`useDraft(getter: () => ModelInstance): { draft, isDirty, commit, rollback }`**
1030
+ #### `useDraft(model)`
1302
1031
 
1303
- Tracks edits to a model instance before committing them back to the original.
1032
+ Edits a record on a copy, then applies or discards the changes. Vue takes a `Ref` or a getter; Solid a getter.
1304
1033
 
1305
1034
  ```typescript
1306
1035
  import { useDraft } from '@the_library/public/models/vue';
1307
1036
 
1308
1037
  const { draft, isDirty, commit, rollback } = useDraft(() => book);
1309
-
1310
- // Edit the draft
1311
1038
  draft.value.title = 'New Title';
1312
-
1313
- // Check if unsaved changes exist
1314
- if (isDirty.value) {
1315
- await commit(); // apply to original book
1316
- // or rollback() to discard
1317
- }
1039
+ if (isDirty.value) commit(); // or rollback()
1318
1040
  ```
1319
1041
 
1320
- **Parameters:**
1321
- - `getter: () => ModelInstance` — A function returning the record to edit (usually a computed ref).
1322
-
1323
- **Returns:**
1324
- ```typescript
1325
- {
1326
- draft: Ref<ModelInstance>; // editable proxy of the original
1327
- isDirty: Ref<boolean>; // true if any properties differ
1328
- commit: () => Promise<void>; // apply changes to original
1329
- rollback: () => void; // discard changes
1330
- }
1331
- ```
1042
+ Returns `{ draft, isDirty, commit: () => void, rollback: () => void }` (`ComputedRef`s in Vue, accessors in Solid).
1332
1043
 
1333
1044
  ---
1334
1045
 
1335
- **`useTrash(objectType: number, usernames?: string[]): { count: Ref<number>; getIds: () => number[] }`**
1046
+ #### `useTrash(objectType: number, usernames?: string[])`
1336
1047
 
1337
- Reactive view over the global [Trash Index](#trash-index) for one object type, merged across `usernames`. Population and live updates are entirely automatic — nothing to register, nothing to feed it.
1048
+ A live view of the [Trash Index](#trash-index) for one model type, merged across `usernames` (default `[]`: every known username). It fills and updates itself.
1338
1049
 
1339
1050
  ```typescript
1340
1051
  import { useTrash } from '@the_library/public/models/vue';
1341
1052
  import { Tag } from '@the_library/public/models/models';
1342
1053
 
1343
- const { count, getIds } = useTrash(Tag.type); // merges every username (canonical catalog trash)
1344
- ```
1345
-
1346
- **Parameters:**
1347
- - `objectType: number` — Model type ID (e.g. `Tag.type`).
1348
- - `usernames?: string[]` — Usernames to merge. Defaults to `[]` (every known username — see the `'canonical'` vs `'mine'` note in [Trash Index](#trash-index)).
1349
-
1350
- **Returns:**
1351
- ```typescript
1352
- {
1353
- count: Ref<number>;
1354
- getIds: () => number[];
1355
- }
1356
- ```
1357
-
1358
- ---
1359
-
1360
- **`resetModelsV2Session(): void`**
1361
-
1362
- Clears the session and cached level state. Used for logout and testing/HMR.
1363
-
1364
- ```typescript
1365
- import { resetModelsV2Session } from '@the_library/public/models/vue';
1366
-
1367
- resetModelsV2Session();
1368
- ```
1369
-
1370
- ---
1371
-
1372
- #### React Hooks
1373
-
1374
- Same API as Vue, but exported from `@the_library/public/models/react`:
1375
-
1376
- ```typescript
1377
- import {
1378
- bootstrapModelsV2,
1379
- useLevel,
1380
- useModelUpdate,
1381
- useTrash,
1382
- resetModelsV2Session
1383
- } from '@the_library/public/models/react';
1384
- ```
1385
-
1386
- **`useLevel(username?: string): LevelData`**
1387
-
1388
- Hook returning the current level data. Re-renders when level changes.
1389
-
1390
- **`useModelUpdate(record: ModelInstance): boolean`**
1391
-
1392
- Subscribes to updates for a single record, triggering re-render on change. Returns `true` if subscription is active.
1393
-
1394
- **`useTrash(objectType: number, usernames?: string[]): { count: number; getIds: () => number[] }`**
1395
-
1396
- Re-renders the calling component whenever the global [Trash Index](#trash-index) changes for `objectType`, merged across `usernames` (defaults to `[]` — every known username). Population and live updates are entirely automatic.
1397
-
1398
- ```typescript
1399
- import { useTrash } from '@the_library/public/models/react';
1400
- import { Tag } from '@the_library/public/models/models';
1401
-
1402
- const { count, getIds } = useTrash(Tag.type);
1403
- ```
1404
-
1405
- **Note:** React does **not** export `useDraft` (no fine-grained reactivity primitive like Vue's `Ref`). Use manual imperative edits or a state manager instead.
1406
-
1407
- ---
1408
-
1409
- #### Solid.js Primitives
1410
-
1411
- ```typescript
1412
- import {
1413
- bootstrapModelsV2,
1414
- useLevel,
1415
- subscribeRecord,
1416
- useTrash,
1417
- resetModelsV2Session
1418
- } from '@the_library/public/models/solid';
1054
+ const { count, getIds } = useTrash(Tag.type); // count: ComputedRef<number>, getIds(): bigint[]
1419
1055
  ```
1420
1056
 
1421
- **`useLevel(username?: string): () => LevelData`**
1422
-
1423
- Signal factory returning the current level data.
1424
-
1425
- **`subscribeRecord(record: ModelInstance, callback: (record) => void): () => void`**
1426
-
1427
- Manual subscription to record updates. Returns unsubscribe function.
1428
-
1429
- **`useTrash(objectType: number, usernames?: string[]): { count: () => number; getIds: () => number[] }`**
1430
-
1431
- Signal accessor over the global [Trash Index](#trash-index) for one object type, merged across `usernames` (defaults to `[]` — every known username). Population and live updates are entirely automatic.
1432
-
1433
- ```typescript
1434
- import { useTrash } from '@the_library/public/models/solid';
1435
- import { Tag } from '@the_library/public/models/models';
1436
-
1437
- const { count, getIds } = useTrash(Tag.type);
1438
- return <span>{count()}</span>;
1439
- ```
1057
+ In React `count` is a number (the component re-renders on change), in Solid an accessor.
1440
1058
 
1441
1059
  ---
1442
1060
 
1443
- #### Astro / Static Generation
1444
-
1445
- ```typescript
1446
- import {
1447
- bootstrapModelsV2,
1448
- resetModelsV2Session,
1449
- subscribeRecord
1450
- } from '@the_library/public/models/astro';
1451
- ```
1452
-
1453
- **`bootstrapModelsV2(options): Promise<void>`**
1454
-
1455
- Same as Vue/React.
1456
-
1457
- **`subscribeRecord(record, callback): () => void`**
1458
-
1459
- Manual subscription (islands use this for client-side updates).
1061
+ #### Framework-specific views
1460
1062
 
1461
- **Note:** Astro does **not** export `useLevel`, `useDraft`, or `useTrash` — use vanilla JS on the client (`trashStore` directly from `@the_library/public/models` works fine, it's framework-agnostic).
1063
+ - **`useBookSearch(options?)`** (Vue) — everything a search box needs over the Book search index: bind `text`, set `filters` and `sort`, read the results, which update as the user types and as the index changes. Results are record IDs: resolve them with `Book.Has(id)` / `Book.Load(id)`. Options: `lang`, `pageSize`, `facets`, `debounceMs`.
1064
+ - **`useStatsTick()`** (Vue) — a `ShallowRef<number>` that changes whenever the local stats change; read it inside a `computed` that uses `VisitsByType` / `DownloadsByType` / `FavoritesByTypes`.
1065
+ - **`useModelUpdate(record)`** (React) — re-renders the component when `record` changes. React can't track reads during render, so components subscribe to each record they show.
1066
+ - **`getAllPatches()`, `hasPartition(basePath)`, `loadPartition(…)`** (Solid) — list queued patches; check for and merge an extra local corpus partition (e.g. book-manager's `deleted/` archive) into the running session.
1067
+ - **`subscribeRecord(record, cb: () => void): () => void`** (Astro) — calls `cb` when `record` changes; returns the unsubscribe function. Astro has no `useLevel`, `useDraft` or `useTrash`: use `trashStore` from `@the_library/public/models` directly on the client.
1462
1068
 
1463
1069
  ---
1464
1070
 
1465
1071
  ### Patch & Session Management
1466
1072
 
1467
- Low-level APIs (not normally called directly; used by `bootstrapModelsV2`):
1073
+ Low-level functions behind `bootstrapModelsV2` and saving; apps rarely call them:
1468
1074
 
1469
- #### `serializePatches(patches: Patch[]): Uint8Array`
1470
-
1471
- Encodes patches to binary for transmission or storage.
1472
-
1473
- #### `deserializePatches(data: Uint8Array): Patch[]`
1474
-
1475
- Decodes binary patches.
1476
-
1477
- #### `applyRestoredPatches(patches: Patch[], models: ModelFactory[]): void`
1478
-
1479
- Applies a set of deserialized patches to the in-memory records.
1480
-
1481
- #### `getPatchInstanceByAccount(username: string): Patches`
1482
-
1483
- Retrieves the patch store for a specific account.
1075
+ - **`serializePatches(fromIndex: number, toIndex: number): Promise<EvmPayload>`** — packs the queued patches in that sequence window (`fromIndex` exclusive, `toIndex` inclusive) into the payload sent to the chain.
1076
+ - **`deserializePatches(username: string, payload: EvmPayload, source?: PatchSource): Promise<void>`** — unpacks a user's payload read from the chain and applies it.
1077
+ - **`applyRestoredPatches(ops: AnyPatch[], creatorUsername?: string): Promise<{ applied: number; skipped: number }>`** — replays operations onto the records in memory. Records not loaded yet are skipped; their operations stay in IndexedDB and apply when the record is next loaded. Safe to repeat.
1078
+ - **`getPatchInstanceByAccount(account: string): Patches`** — the patch queue of one account.
1484
1079
 
1485
1080
  ---
1486
1081
 
@@ -1514,7 +1109,13 @@ const unsubscribe = sessionStore.subscribe(() => {
1514
1109
 
1515
1110
  - `sessionStore.getState(): SessionState` — Returns current state: `{ active: Web3SessionAccount | null; known: Web3SessionAccount[] }`.
1516
1111
 
1517
- - `sessionStore.subscribe(callback: () => void): () => void` — Registers a listener, returns unsubscribe function.
1112
+ - `sessionStore.subscribe(listener: () => void): () => void` — Registers a listener, returns unsubscribe function.
1113
+
1114
+ - Writes: `setActive(account | null)`, `addKnown(account)`, `updateAccount(address, chainId, patch)`, `removeAccount(address, chainId)`, `clearAll()`, `updateProjectVotes(address, chainId, projectId, amount)`.
1115
+
1116
+ - Save counters: `lastSyncCounters(address, chainId)` → `{ lastPatchIndex, lastRestorationIndex }`, `setLastSavedPatchIndex(…)`, `setLastRestorationIndex(…)`.
1117
+
1118
+ - Persistence: `persist()` / `load()` write and read the known accounts in `localStorage`.
1518
1119
 
1519
1120
  **Type: `Web3SessionAccount`**
1520
1121
  ```typescript
@@ -1522,9 +1123,14 @@ interface Web3SessionAccount {
1522
1123
  account: string; // hex address
1523
1124
  chainId: number;
1524
1125
  wallet: WalletType; // 'metamask' | 'private_key'
1525
- username?: string;
1526
- userId?: number;
1126
+ username: string;
1127
+ userId: number;
1527
1128
  hasAccount: boolean; // registered on-chain
1129
+ lastPatchIndex: number; // last local patch sequence saved to the chain
1130
+ lastRestorationIndex: number; // last on-chain save restored on this device
1131
+ country?: string;
1132
+ language?: string;
1133
+ lastUpdated?: number; // when it was last read from the chain
1528
1134
  level?: number;
1529
1135
  projectBalance?: {
1530
1136
  contributions: number;
@@ -1532,31 +1138,29 @@ interface Web3SessionAccount {
1532
1138
  remaining: number;
1533
1139
  };
1534
1140
  donationsPerProject?: number[];
1535
- metadata?: {
1536
- country?: string;
1537
- language?: string;
1538
- lastSyncTime?: number;
1539
- };
1540
1141
  }
1541
1142
  ```
1542
1143
 
1543
1144
  ---
1544
1145
 
1545
- #### `syncAccount(address: string, chainId: number, walletType: WalletType): Promise<Web3SessionAccount>`
1146
+ #### `syncAccount(address: string, chainId: number, wallet: WalletType, environment?: 'production' | 'staging', forceRefresh?: boolean): Promise<Web3SessionAccount>`
1546
1147
 
1547
1148
  Hydrates a wallet account after connection by pulling `hasAccount`, `username`, `level`, and other on-chain data. Also kicks off background subscription sync for the account's `userId`.
1548
1149
 
1549
1150
  ```typescript
1151
+ import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
1550
1152
  import { syncAccount } from '@the_library/public/web3';
1551
1153
 
1552
- const account = await syncAccount('0xabc...', 146, 'metamask');
1154
+ const account = await syncAccount('0xabc...', DEFAULT_CHAIN_ID, 'metamask');
1553
1155
  console.log(`${account.username} is level ${account.level}`);
1554
1156
  ```
1555
1157
 
1556
1158
  **Parameters:**
1557
1159
  - `address: string` — Wallet address (hex).
1558
- - `chainId: number` — Chain ID (146, 14601, etc.).
1559
- - `walletType: WalletType` — `'metamask'` or `'private_key'`.
1160
+ - `chainId: number` — Chain ID, e.g. `DEFAULT_CHAIN_ID` (production) or `DEFAULT_STAGING_CHAIN_ID` (testnet), both from `@the_library/network-config`.
1161
+ - `wallet: WalletType` — `'metamask'` or `'private_key'`.
1162
+ - `environment?: 'production' | 'staging'` — which deployment to read. Defaults to the session's environment (`getSessionEnvironment()`).
1163
+ - `forceRefresh?: boolean` — re-read the chain even if the account was already hydrated. Default `false`: an account already in `sessionStore` is returned as is.
1560
1164
 
1561
1165
  **Returns:** `Promise<Web3SessionAccount>` — The hydrated account record.
1562
1166
 
@@ -1569,19 +1173,7 @@ console.log(`${account.username} is level ${account.level}`);
1569
1173
 
1570
1174
  #### `useWeb3Session()` (Vue composable)
1571
1175
 
1572
- Reactive wrapper over `sessionStore`.
1573
-
1574
- ```typescript
1575
- import { useWeb3Session } from '@the_library/public/web3/vue';
1576
-
1577
- const {
1578
- accounts, // Ref<Web3SessionAccount[]> — all known accounts
1579
- registeredAccounts, // Ref<Web3SessionAccount[]> — only hasAccount=true
1580
- activeAccount, // Ref<Web3SessionAccount | null>
1581
- isConnecting, // Ref<boolean>
1582
- setActive, // (account: Web3SessionAccount) => void
1583
- } = useWeb3Session();
1584
- ```
1176
+ Reactive wrapper over `sessionStore`; see [3.10 Vue Composables](#useweb3session-useweb3sessionreturn).
1585
1177
 
1586
1178
  ---
1587
1179
 
@@ -1594,9 +1186,10 @@ Typed read and write adapters over deployed contracts.
1594
1186
  Gets the read-only client for a chain. Blocks until contract addresses/ABIs are loaded from the registry.
1595
1187
 
1596
1188
  ```typescript
1189
+ import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
1597
1190
  import { getReadClient } from '@the_library/public/web3';
1598
1191
 
1599
- const client = getReadClient(146);
1192
+ const client = getReadClient(DEFAULT_CHAIN_ID);
1600
1193
  await client.ready; // wait for registry load
1601
1194
 
1602
1195
  const balance = await client.projectManagerStorage.balanceOfAddress('0xabc...');
@@ -1605,7 +1198,7 @@ const nbAccounts = await client.bouncerStorage.nbAccounts();
1605
1198
  ```
1606
1199
 
1607
1200
  **Parameters:**
1608
- - `chainId: number` — EVM chain ID (146, 14601, 84532, etc.).
1201
+ - `chainId: number` — EVM chain ID, e.g. `DEFAULT_CHAIN_ID` or `DEFAULT_STAGING_CHAIN_ID` from `@the_library/network-config`.
1609
1202
  - `environment?: 'production' | 'staging'` — Defaults to `'production'`. Use `'staging'` to read staging-deployed contract addresses.
1610
1203
 
1611
1204
  **Returns:** `ReadOnlyDomainAPI` — Typed interface to all contract reads.
@@ -1616,16 +1209,20 @@ interface ReadOnlyDomainAPI {
1616
1209
  readonly chainId: number;
1617
1210
  readonly ready: Promise<void>;
1618
1211
  readonly bouncerStorage: BouncerStorageReadAPI;
1619
- readonly archivistStorage: ArchivistStorageReadAPI;
1620
1212
  readonly scientistStorage: ScientistStorageReadAPI;
1213
+ readonly archivistStorage: ArchivistStorageReadAPI;
1621
1214
  readonly projectManagerStorage: ProjectManagerStorageReadAPI;
1622
1215
  readonly factory: FactoryReadAPI;
1216
+ readonly accountManager: AccountManagerReadAPI;
1623
1217
  readonly addressRegistry: AddressRegistryReadAPI;
1624
1218
  readonly subscriptionManager: SubscriptionManagerReadAPI;
1625
1219
  readonly libraryRegistryStorage: LibraryRegistryStorageReadAPI;
1626
1220
  readonly publicLibraryStorage: PublicLibraryStorageReadAPI;
1221
+ readonly dSafeHRStorage: DSafeHRStorageReadAPI;
1222
+ readonly relationshipIndexerStorage: RelationshipIndexerReadAPI;
1627
1223
  getUsdPrice(): Promise<number>;
1628
1224
  getRegistryStatus(): Promise<any>;
1225
+ getNativeBalance(address: string): Promise<bigint>;
1629
1226
  }
1630
1227
  ```
1631
1228
 
@@ -1786,53 +1383,64 @@ If a transaction throws and the run-lock needs to be cleared manually, call `res
1786
1383
 
1787
1384
  #### Chain Metadata
1788
1385
 
1789
- ```typescript
1790
- import { getChain, EVM_CHAINS, DEFAULT_CHAIN_ID } from '@the_library/public/web3';
1386
+ Chain metadata comes from `@the_library/network-config` (install it next to this package). `@the_library/public/web3` only adds `toViemChain`.
1791
1387
 
1792
- const chain = getChain(146);
1793
- // chain.name, chain.symbol, chain.shortName, chain.logo, chain.explorer,
1794
- // chain.rpc, chain.main (true for mainnet), chain.deployed, chain.stagingDeployed
1388
+ ```typescript
1389
+ import { getChain, EVM_CHAINS, DEFAULT_CHAIN_ID, DEFAULT_STAGING_CHAIN_ID } from '@the_library/network-config';
1390
+ import { toViemChain } from '@the_library/public/web3'; // also at '@the_library/public/web3/network'
1795
1391
 
1796
- const deployedChains = Object.values(EVM_CHAINS).filter(c => c.deployed);
1392
+ const chain = getChain(DEFAULT_CHAIN_ID); // production chain (testnet: DEFAULT_STAGING_CHAIN_ID)
1393
+ const viemChain = toViemChain(chain); // viem `Chain` for createPublicClient & co.
1394
+ const deployedChains = Object.values(EVM_CHAINS).filter((c) => c.deployed);
1797
1395
  ```
1798
1396
 
1799
1397
  **Type: `EvmChainConfig`**
1800
1398
  ```typescript
1801
1399
  interface EvmChainConfig {
1802
- name: string; // 'Sonic' | 'Sonic Blaze' | 'Base Sepolia'
1803
- shortName: string; // 'TREE' | 'WATER' | ... (mainnet/testnet shorthand codenames, not the chain name)
1804
1400
  chainId: number;
1805
- symbol: string; // 'S' | ...
1806
- logo: string; // URL to chain logo
1807
- explorer: string; // Block explorer URL
1808
- rpc: string[]; // RPC endpoints
1809
- main: boolean; // Mainnet vs testnet
1810
- deployed: boolean; // Contracts deployed on this chain
1811
- stagingDeployed?: boolean;
1401
+ name: string; // e.g. 'Gnosis Mainnet'
1402
+ symbol: string; // native coin, e.g. 'XDAI'
1403
+ shortName: string; // 'TREE' (mainnet) | 'WATER' (testnet): codenames, not the chain name
1404
+ decimals: number;
1405
+ explorer: string; // block explorer URL
1406
+ rpc: string; // RPC endpoint
1407
+ website: string;
1408
+ main: boolean; // mainnet or testnet
1409
+ brand: string; // brand name, e.g. 'GNOSIS'
1410
+ price: ChainPrice; // { stable: true } (coin = $1) or { pythOracle, usdPriceId }
1411
+ memberFundingTarget: string; // native amount a member wallet is topped up to
1412
+ staffFundingTarget: string; // native amount a new library-staff wallet receives
1413
+ addresses?: { addressRegistry?: string; stagingAddressRegistry?: string; [contract: string]: string | undefined };
1414
+ deployed?: boolean; // production contracts deployed on this chain
1415
+ stagingDeployed?: boolean; // staging contracts deployed on this chain
1812
1416
  }
1813
1417
  ```
1814
1418
 
1419
+ `addresses` is a bootstrap copy, used only to reach the chain when the registry manifest can't be loaded; read deployed addresses through the [Registry API](#33-registry-api).
1420
+
1815
1421
  ---
1816
1422
 
1817
1423
  #### Vue Composable: `useChain(chainId)`
1818
1424
 
1819
1425
  ```typescript
1426
+ import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
1820
1427
  import { useChain } from '@the_library/public/web3/vue';
1821
1428
 
1822
- const { chain, client, usdPrice, refreshPrice, isMainnet, chainRouteQuery } = useChain(146);
1429
+ const { chain, client, usdPrice, isPriceLoading, refreshPrice, isMainnet, isDeployed, chainRouteQuery } = useChain(DEFAULT_CHAIN_ID);
1823
1430
  // chain: EvmChainConfig
1824
- // client: ReadOnlyDomainAPI (ready to use)
1825
- // usdPrice: Ref<number> — live USD price polling
1826
- // refreshPrice: () => Promise<void>
1827
- // isMainnet: boolean
1828
- // chainRouteQuery: { chainId: '146' } — useful for router links
1431
+ // client: ReadOnlyDomainAPI
1432
+ // usdPrice: Ref<number | null> — USD price of the native coin
1433
+ // isPriceLoading: Ref<boolean>
1434
+ // refreshPrice(): Promise<void>
1435
+ // isMainnet, isDeployed: ComputedRef<boolean>
1436
+ // chainRouteQuery(extra?) — { chainId, ...extra } for router links
1829
1437
  ```
1830
1438
 
1831
1439
  ---
1832
1440
 
1833
1441
  ### 3.3 Registry API
1834
1442
 
1835
- Dynamic contract address and ABI resolution from the on-chain `AddressRegistry`.
1443
+ Contract addresses, ABIs and collection pointers, read from the published registry manifest (`https://registry.worldbibliotech.com/<env>/manifest.json`), falling back to the on-chain `AddressRegistry` when the manifest can't be loaded.
1836
1444
 
1837
1445
  #### `addressLoader` (singleton)
1838
1446
 
@@ -1854,13 +1462,13 @@ const abi = addressLoader.getAbi(chainId, 'Factory');
1854
1462
 
1855
1463
  **Methods:**
1856
1464
 
1857
- - `addressLoader.subscribe(callback: (chainId, LoadingStatus) => void): () => void` — Listen for load progress.
1465
+ - `addressLoader.subscribe(listener: (chainId: string | number, status: LoadingStatus) => void): () => void` — Listen for load progress.
1858
1466
 
1859
- - `addressLoader.getAddress(chainId: number, contractName: string): string | undefined` — Resolved contract address, or `undefined` if not yet loaded.
1467
+ - `addressLoader.getAddress(chainId: string | number, name: string, environment?: 'production' | 'staging'): string | undefined` — Resolved contract address, or `undefined` if not yet loaded.
1860
1468
 
1861
- - `addressLoader.getAbi(chainId: number, contractName: string): any[] | undefined` — Resolved ABI array, or `undefined` if not yet loaded.
1469
+ - `addressLoader.getAbi(chainId: string | number, name: string, environment?: 'production' | 'staging'): any` — Resolved ABI, or `undefined` if not yet loaded.
1862
1470
 
1863
- - `addressLoader.loadManifest(environment: 'production' | 'staging'): Promise<Manifest>` — Manually load the registry manifest (auto-called by clients).
1471
+ - `addressLoader.loadManifest(environment?: 'production' | 'staging'): Promise<Manifest>` — Manually load the registry manifest (auto-called by clients).
1864
1472
 
1865
1473
  ---
1866
1474
 
@@ -1874,14 +1482,15 @@ Override the default registry host URL (normally `https://registry.worldbibliote
1874
1482
 
1875
1483
  Walks the on-chain subscription graph (who-follows-whom) and incrementally fetches patches.
1876
1484
 
1877
- #### `getSubscriptionClient(chainId: number): SubscriptionClient`
1485
+ #### `getSubscriptionClient(chainId: number, environment?: 'production' | 'staging'): SubscriptionClient`
1878
1486
 
1879
1487
  Gets a chain-scoped subscription client.
1880
1488
 
1881
1489
  ```typescript
1490
+ import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
1882
1491
  import { getSubscriptionClient } from '@the_library/public/web3';
1883
1492
 
1884
- const subs = getSubscriptionClient(146);
1493
+ const subs = getSubscriptionClient(DEFAULT_CHAIN_ID);
1885
1494
 
1886
1495
  // First sync: full BFS walk + patch fetch
1887
1496
  const { patches, cursor, truncated } = await subs.sync(rootUserId);
@@ -1896,13 +1505,13 @@ const score = await subs.getScore(userId);
1896
1505
 
1897
1506
  **Methods:**
1898
1507
 
1899
- - `subs.sync(rootUserId: number): Promise<SyncResult>` — Walk subscription graph starting from a user, fetch all patches.
1508
+ - `subs.sync(rootUserId: number, opts?: SyncOptions): Promise<SyncResult>` — Walk subscription graph starting from a user, fetch all patches.
1900
1509
 
1901
- - `subs.syncSince(cursor: SyncCursor): Promise<SyncResult>` — Incremental sync using a persisted cursor.
1510
+ - `subs.syncSince(cursor: SyncCursor, opts?: SyncOptions): Promise<SyncResult>` — Incremental sync using a persisted cursor.
1902
1511
 
1903
1512
  - `subs.getSubscriptions(userId: number): Promise<number[]>` — List of users this user subscribes to.
1904
1513
 
1905
- - `subs.getScore(userId: number): Promise<number>` — Social score for a user.
1514
+ - `subs.getScore(userId: number): Promise<bigint>` — Social score for a user.
1906
1515
 
1907
1516
  **Type: `SyncResult`**
1908
1517
  ```typescript
@@ -1918,91 +1527,87 @@ interface SyncResult {
1918
1527
  #### `cursorStore` (IndexedDB persistence)
1919
1528
 
1920
1529
  ```typescript
1921
- import { cursorStore } from '@the_library/public/web3';
1530
+ import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
1531
+ import { cursorStore, cursorKey } from '@the_library/public/web3';
1922
1532
 
1923
- const cursor = await cursorStore.get(userId);
1924
- const result = cursor
1533
+ // one cursor per environment, chain and user
1534
+ const key = cursorKey('production', DEFAULT_CHAIN_ID, userId);
1535
+ const cursor = await cursorStore.get(key);
1536
+ const result = cursor
1925
1537
  ? await subs.syncSince(cursor)
1926
1538
  : await subs.sync(userId);
1927
- await cursorStore.set(userId, result.cursor);
1539
+ await cursorStore.set(key, result.cursor);
1928
1540
  ```
1929
1541
 
1930
1542
  **Methods:**
1931
1543
 
1932
- - `cursorStore.get(userId: number): Promise<SyncCursor | null>` — Retrieve persisted cursor, or `null` if never synced.
1544
+ - `cursorStore.get(key: string): Promise<SyncCursor | undefined>` — Retrieve the persisted cursor, or `undefined` if never synced.
1933
1545
 
1934
- - `cursorStore.set(userId: number, cursor: SyncCursor): Promise<void>` — Save cursor for next sync.
1546
+ - `cursorStore.set(key: string, cursor: SyncCursor): Promise<void>` — Save the cursor for the next sync.
1935
1547
 
1936
- ---
1548
+ - `cursorKey(environment, chainId, userId): string` — the key. The same `userId` exists on each environment and chain, each with its own subscription history, so a bare user ID would mix their cursors.
1937
1549
 
1938
- #### Vue Composable: `useSubscriptionSync(userId)`
1550
+ ---
1939
1551
 
1940
- ```typescript
1941
- import { useSubscriptionSync } from '@the_library/public/web3/vue';
1552
+ #### Vue Composable: `useSubscriptionSync(userId, chainId, environment)`
1942
1553
 
1943
- const { cursor, patches, isLoading, refresh } = useSubscriptionSync(userId);
1944
- // cursor: Ref<SyncCursor>
1945
- // patches: Ref<Patch[]>
1946
- // isLoading: Ref<boolean>
1947
- // refresh: () => Promise<void>
1948
- ```
1554
+ See [3.10 Vue Composables](#usesubscriptionsyncuserid-number-chainid-number-environment-production--staging).
1949
1555
 
1950
1556
  ---
1951
1557
 
1952
1558
  ### 3.5 Contracts API
1953
1559
 
1954
- Static ABI exports for contracts that don't use dynamic resolution.
1560
+ ABIs bundled with the package, for the contracts the SDK must reach before (or without) the registry: `addressRegistryAbi`, `dSafeHRStorageAbi`, `relationshipIndexerStorageAbi` and `pythAbi`. They are generated from the deployed contracts; every other ABI comes from the registry (`addressLoader.getAbi`, `getContractAbi`).
1955
1561
 
1956
1562
  ```typescript
1957
- import { addressRegistryAbi, pythAbi } from '@the_library/public/web3';
1958
-
1959
- // Use with viem directly:
1563
+ import { addressRegistryAbi } from '@the_library/public/web3';
1960
1564
  import { createPublicClient, http } from 'viem';
1961
1565
 
1962
- const client = createPublicClient({
1963
- chain: /*...*/,
1964
- transport: http()
1965
- });
1566
+ const client = createPublicClient({ chain: /* toViemChain(getChain(…)) */, transport: http() });
1966
1567
 
1967
- const data = await client.readContract({
1968
- address: '0x...',
1568
+ const [factory] = await client.readContract({
1569
+ address: '0x…', // the AddressRegistry, from the registry manifest
1969
1570
  abi: addressRegistryAbi,
1970
1571
  functionName: 'getLatestContract',
1971
1572
  args: ['Factory'],
1972
1573
  });
1973
1574
  ```
1974
1575
 
1576
+ `getContractDisplayName(name)` and `CONTRACT_DISPLAY_NAMES` give each contract's public name (e.g. `StatsPivotStorage` → "The Records Room").
1577
+
1975
1578
  ---
1976
1579
 
1977
1580
  ### 3.6 Events API
1978
1581
 
1979
- Typed pub/sub bus for wallet and transaction state notifications.
1582
+ Typed pub/sub bus for wallet, session and transaction notifications.
1980
1583
 
1981
1584
  #### `WalletEvents` (typed pub/sub)
1982
1585
 
1586
+ Every `on…` function returns its unsubscribe function; each has a matching `emit…`.
1587
+
1983
1588
  ```typescript
1984
- import { WalletEvents, type WalletState, type WalletError } from '@the_library/public/web3';
1589
+ import { WalletEvents, WalletState } from '@the_library/public/web3';
1985
1590
 
1986
1591
  const unsubWalletState = WalletEvents.onWalletStateEvent((e) => {
1987
- // e: { wallet, chainId, event: WalletState, functionName?, timestamp }
1988
- if (e.event === 'TRANSACTION_STARTED') showSpinner();
1989
- if (e.event === 'TRANSACTION_DONE') hideSpinner();
1990
- if (e.event === 'ACCOUNT_SYNCED') updateUI();
1592
+ // e: { wallet, chainId?, timestamp, event: WalletState, functionName?, functionArgs? }
1593
+ if (e.event === WalletState.TRANSACTION_STARTED) showSpinner();
1594
+ if (e.event === WalletState.TRANSACTION_DONE) hideSpinner();
1595
+ if (e.event === WalletState.READY) updateUI();
1991
1596
  });
1992
1597
 
1993
1598
  const unsubError = WalletEvents.onWalletError((e) => {
1994
- // e: { wallet, chainId, reason: WalletError, timestamp }
1599
+ // e: { wallet, chainId?, timestamp, reason: WalletError | WalletUxError, args? }
1995
1600
  toast.error(e.reason);
1996
1601
  });
1997
1602
 
1998
1603
  const unsubSaveRestored = WalletEvents.onSaveRestored((e) => {
1999
- // e: { compressedActions, indexes, stringIndexes, timestamp }
2000
- // Patches from on-chain state that outran local edits
1604
+ // e: { wallet, chainId?, timestamp, username, account, newRestorationIndex, appliedCount }
1605
+ // saves from another device were restored into this one
2001
1606
  });
2002
1607
 
2003
1608
  const unsubPatchesSynced = WalletEvents.onSubscriptionPatchesSynced((e) => {
2004
- // e: { rootUserId, patches: Patch[], timestamp }
2005
- // Subscription graph patches fetched in background
1609
+ // e: { rootUserId, chainId, appliedCount, timestamp }
1610
+ // subscription sync merged patches from the readers this user follows
2006
1611
  });
2007
1612
 
2008
1613
  // Unsubscribe
@@ -2012,24 +1617,21 @@ unsubSaveRestored();
2012
1617
  unsubPatchesSynced();
2013
1618
  ```
2014
1619
 
1620
+ The other events: `onSessionUpdated` and `onWalletSessionDeleted` (`{ account, chainId, wallet }`), `onChainChanged`, and `onDisconnected` (`{ …, reason? }`).
1621
+
2015
1622
  **Enums:**
2016
1623
 
2017
- **`WalletState`** — transaction lifecycle events:
2018
- - `'WALLET_CONNECTED'`
2019
- - `'WALLET_DISCONNECTED'`
2020
- - `'TRANSACTION_STARTED'`
2021
- - `'TRANSACTION_DONE'`
2022
- - `'TRANSACTION_FAILED'`
2023
- - `'ACCOUNT_SYNCED'`
2024
- - `'SUBSCRIPTION_SYNC_STARTED'`
2025
- - `'SUBSCRIPTION_SYNC_DONE'`
1624
+ **`WalletState`** — connector and transaction lifecycle:
1625
+ - setup: `INITIALIZING`, `READY`, `INTERNAL_ERROR`, `TIMEOUT`, `WALLET_INSTALLED`
1626
+ - availability: `NOT_CONNECTED`, `NOT_INSTALLED`, `NOT_ALLOWED`, `NOT_SUPPORTED`, `NOT_READY`
1627
+ - `CONNECT_STARTED` / `CONNECT_DONE` / `CONNECT_FAILED`
1628
+ - `UPDATING_STATE_STARTED` / `UPDATING_STATE_DONE` / `UPDATING_STATE_FAILED`
1629
+ - `TRANSACTION_STARTED` / `TRANSACTION_DONE` / `TRANSACTION_FAILED`
1630
+ - `CHAIN_CHANGED_STARTED` / `CHAIN_CHANGED_DONE` / `CHAIN_CHANGED_FAILED`
1631
+
1632
+ **`WalletError`** — `NO_ACCOUNTS`, `USER_REJECTED`, `INSUFFICIENT_FUNDS`, `LOGIN_INTO_WALLET`, `REQUEST_ALREADY_PENDING`, `UNKNOWN_ERROR`, `CONFIG_MISSING`, `NOT_SUPPORTED`, `NOT_CONNECTED`, `NOT_INSTALLED`, `NOT_ALLOWED`, `NETWORK_NOT_SUPPORTED`.
2026
1633
 
2027
- **`WalletError`** — error reasons:
2028
- - `'NETWORK_ERROR'`
2029
- - `'TRANSACTION_REJECTED'`
2030
- - `'INSUFFICIENT_BALANCE'`
2031
- - `'UNKNOWN_ERROR'`
2032
- - etc. (see source for full list)
1634
+ **`WalletUxError`** — something the user has to do in their wallet: `NO_ACCOUNT_SWITCH`, `NO_NETWORK_SWITCH`, `NETWORK_NOT_SUPPORTED`, `NO_INTERNET`, `NO_DISCONNECT`, `NO_ACCOUNT_DISCONNECT`.
2033
1635
 
2034
1636
  ---
2035
1637
 
@@ -2042,7 +1644,9 @@ const bus = getEventBus();
2042
1644
  const unsubscribe = bus.on('topic', (data) => { /* handle */ });
2043
1645
  bus.emit('topic', data);
2044
1646
  bus.once('topic', (data) => { /* one-time listener */ });
2045
- bus.clear('topic'); // remove all listeners
1647
+ bus.off('topic', listener);
1648
+ bus.listenerCount('topic');
1649
+ bus.clear('topic'); // remove all listeners (all topics when called without one)
2046
1650
  ```
2047
1651
 
2048
1652
  ---
@@ -2110,10 +1714,11 @@ if (!meta.isInstalled() && meta.isInstallable()) {
2110
1714
  #### Private Key Storage
2111
1715
 
2112
1716
  ```typescript
1717
+ import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
2113
1718
  import { storePrivateKey, hasPrivateKey, hasAnyPrivateKey } from '@the_library/public/web3';
2114
1719
 
2115
- storePrivateKey(146, '0x1234...'); // raw hex, no '0x' prefix required
2116
- if (hasPrivateKey(146)) {
1720
+ storePrivateKey(DEFAULT_CHAIN_ID, '0x1234...'); // raw hex, no '0x' prefix required
1721
+ if (hasPrivateKey(DEFAULT_CHAIN_ID)) {
2117
1722
  // PrivateKeyConnector can connect to this chain
2118
1723
  }
2119
1724
 
@@ -2122,7 +1727,7 @@ if (hasAnyPrivateKey()) {
2122
1727
  }
2123
1728
  ```
2124
1729
 
2125
- **Warning:** Private keys are stored in plaintext localStorage. **Only use for dev/test wallets, never for real user funds.**
1730
+ **Warning:** Private keys are stored in plaintext localStorage. `storeEncryptedPrivateKey`, `getDecryptedPrivateKey` and `hasEncryptedPrivateKey` are compatibility aliases of the three functions above: they do **not** encrypt anything. A key stored this way is readable by any script running on the page, so keep only small balances (such as the gas top-up of a member wallet) on these wallets.
2126
1731
 
2127
1732
  ---
2128
1733
 
@@ -2135,13 +1740,14 @@ Project discovery and voting.
2135
1740
  Loads a single project by ID with language fallback. Cached automatically.
2136
1741
 
2137
1742
  ```typescript
1743
+ import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
2138
1744
  import { loadProject } from '@the_library/public/web3';
2139
1745
 
2140
- const project = await loadProject(146, 'eng', 5);
1746
+ const project = await loadProject(DEFAULT_CHAIN_ID, 'eng', 5);
2141
1747
  console.log(`${project.version.name} — ${project.balance} tokens`);
2142
1748
 
2143
1749
  // Force re-fetch from chain, bypassing cache
2144
- const fresh = await loadProject(146, 'eng', 5, true);
1750
+ const fresh = await loadProject(DEFAULT_CHAIN_ID, 'eng', 5, true);
2145
1751
  ```
2146
1752
 
2147
1753
  **Parameters:**
@@ -2152,7 +1758,7 @@ const fresh = await loadProject(146, 'eng', 5, true);
2152
1758
 
2153
1759
  **Returns:** `Promise<ProjectLocale>`
2154
1760
 
2155
- **Throws:** If cache miss and chain unreachable.
1761
+ **Throws:** If the chain can't be read and there is no cached copy (an expired copy is returned instead, unless `refresh` is set).
2156
1762
 
2157
1763
  ---
2158
1764
 
@@ -2163,7 +1769,7 @@ Loads all projects for a chain/language. Returns cached projects if chain is unr
2163
1769
  ```typescript
2164
1770
  import { loadProjects } from '@the_library/public/web3';
2165
1771
 
2166
- const projects = await loadProjects(146, 'eng');
1772
+ const projects = await loadProjects(DEFAULT_CHAIN_ID, 'eng');
2167
1773
  const validProjects = projects.filter(p => p !== null);
2168
1774
  ```
2169
1775
 
@@ -2175,21 +1781,21 @@ const validProjects = projects.filter(p => p !== null);
2175
1781
 
2176
1782
  ---
2177
1783
 
2178
- #### `projectLocale(project: Project, language: string, chainId: number): ProjectLocale`
1784
+ #### `projectLocale(project: Project, userLanguage: string, _chainId: number): ProjectLocale`
2179
1785
 
2180
- Helper to convert raw `Project` data to `ProjectLocale` with language selection and decimal normalization.
1786
+ Helper to convert raw `Project` data to `ProjectLocale`, picking the version in `userLanguage` (with fallback). Votes are whole numbers (1 vote = $1), so nothing is converted.
2181
1787
 
2182
1788
  ```typescript
2183
1789
  import { projectLocale } from '@the_library/public/web3';
2184
1790
 
2185
1791
  const raw = await client.projectManagerStorage.loadProject(5);
2186
- const locale = projectLocale(raw, 'eng', 146);
1792
+ const locale = projectLocale(raw, 'eng', DEFAULT_CHAIN_ID);
2187
1793
  ```
2188
1794
 
2189
1795
  **Parameters:**
2190
1796
  - `project: Project` — Raw contract data.
2191
- - `language: string` — Preferred language.
2192
- - `chainId: number` — For decimal normalization.
1797
+ - `userLanguage: string` — Preferred language.
1798
+ - `_chainId: number` — Unused; kept for existing callers.
2193
1799
 
2194
1800
  **Returns:** `ProjectLocale`
2195
1801
 
@@ -2197,53 +1803,24 @@ const locale = projectLocale(raw, 'eng', 146);
2197
1803
 
2198
1804
  #### Vue Composable: `useLoadProjects(chainId, language)`
2199
1805
 
2200
- ```typescript
2201
- import { useLoadProjects } from '@the_library/public/web3/vue';
2202
-
2203
- const chainId = ref(146);
2204
- const language = ref('eng');
2205
-
2206
- const { projects, isLoading, error, reload } = useLoadProjects(chainId, language);
2207
- // projects: Ref<Array<ProjectLocale | null>>
2208
- // isLoading: Ref<boolean>
2209
- // error: Ref<Error | null>
2210
- // reload: () => Promise<void>
2211
- ```
1806
+ See [3.10 Vue Composables](#useloadprojectschainid-mayberefnumber-language-mayberefstring-useloadprojectsreturn).
2212
1807
 
2213
1808
  ---
2214
1809
 
2215
1810
  ### 3.9 Chain & Network Configuration
2216
1811
 
2217
- #### `EVM_CHAINS` (constant)
1812
+ Networks are defined in `@the_library/network-config`, not in this package; import from it directly. Never hard-code a chain ID: take it from there.
2218
1813
 
2219
1814
  ```typescript
2220
- import { EVM_CHAINS } from '@the_library/public/web3';
1815
+ import { EVM_CHAINS, getChain, DEFAULT_CHAIN_ID, DEFAULT_STAGING_CHAIN_ID, CANONICAL } from '@the_library/network-config';
2221
1816
 
2222
- // EVM_CHAINS = {
2223
- // 146: { name: 'Sonic', ... },
2224
- // 14601: { name: 'Sonic Blaze', ... },
2225
- // 84532: { name: 'Base Sepolia', ... },
2226
- // }
1817
+ EVM_CHAINS; // Record<chainId, EvmChainConfig>: every defined network
1818
+ DEFAULT_CHAIN_ID; // the production chain (CANONICAL.production)
1819
+ DEFAULT_STAGING_CHAIN_ID; // the staging chain, a testnet (CANONICAL.staging)
1820
+ getChain(DEFAULT_CHAIN_ID).name; // throws for an unknown chain ID
2227
1821
  ```
2228
1822
 
2229
- ---
2230
-
2231
- #### `DEFAULT_CHAIN_ID` (constant)
2232
-
2233
- The default chain for new users (usually production mainnet, e.g., `146`).
2234
-
2235
- ---
2236
-
2237
- #### `getChain(chainId: number): EvmChainConfig`
2238
-
2239
- Gets full metadata for a chain. Throws if `chainId` is not registered.
2240
-
2241
- ```typescript
2242
- import { getChain } from '@the_library/public/web3';
2243
-
2244
- const chain = getChain(146);
2245
- console.log(`${chain.name} (${chain.symbol})`);
2246
- ```
1823
+ The `EvmChainConfig` shape is listed under [Chain Metadata](#chain-metadata). To turn one into a viem `Chain`, use `toViemChain(config)` from `@the_library/public/web3` (or `@the_library/public/web3/network`).
2247
1824
 
2248
1825
  ---
2249
1826
 
@@ -2251,16 +1828,12 @@ console.log(`${chain.name} (${chain.symbol})`);
2251
1828
 
2252
1829
  #### `bootstrapWebSDK(options?: BootstrapOptions): Promise<BootstrapResult>`
2253
1830
 
2254
- Single app-wide initialization: registers connectors, loads persisted session, hydrates accounts.
1831
+ Single app-wide initialization: registers the wallet connectors, loads the persisted session and hydrates the accounts.
2255
1832
 
2256
1833
  ```typescript
2257
1834
  import { bootstrapWebSDK, BootstrapResultKey } from '@the_library/public/web3/vue';
2258
1835
 
2259
- const result = await bootstrapWebSDK({
2260
- withTest: true,
2261
- environment: 'production',
2262
- chainFilter: [146, 14601], // optional: limit to these chains
2263
- });
1836
+ const result = await bootstrapWebSDK({ environment: 'production' });
2264
1837
 
2265
1838
  await result.ready; // wait for account discovery
2266
1839
 
@@ -2269,20 +1842,21 @@ app.provide(BootstrapResultKey, result);
2269
1842
  ```
2270
1843
 
2271
1844
  **Parameters:**
2272
- - `options.withTest?: boolean` — Include test chains (Base Sepolia).
2273
- - `options.environment?: 'production' | 'staging'` — Contract environment.
2274
- - `options.chainFilter?: number[]` — Optional: only bootstrap these chains.
1845
+ - `options.environment?: 'production' | 'staging'` — which deployment to use.
1846
+ - `options.withTest?: boolean` — include testnets. Default `true`; `false` keeps only mainnets.
1847
+ - `options.chainFilter?: number[]` — bootstrap exactly these chains instead (takes precedence over `withTest`).
2275
1848
 
2276
1849
  **Returns:** `Promise<BootstrapResult>`
2277
1850
 
2278
1851
  **Type: `BootstrapResult`**
2279
1852
  ```typescript
2280
1853
  interface BootstrapResult {
2281
- ready: Promise<void>;
1854
+ primaryAccount: Web3SessionAccount | null;
2282
1855
  chains: EvmChainConfig[];
1856
+ connectorsByChain: Record<number, ChainConnectors>; // { chain, installed, installable }
2283
1857
  installedConnectors: WalletConnector[];
2284
1858
  installableConnectors: WalletConnector[];
2285
- primaryAccount: Web3SessionAccount | null;
1859
+ ready: Promise<void>;
2286
1860
  }
2287
1861
  ```
2288
1862
 
@@ -2290,7 +1864,7 @@ interface BootstrapResult {
2290
1864
 
2291
1865
  #### `useBootstrap(): BootstrapResult`
2292
1866
 
2293
- Retrieves the app-wide bootstrap result (must have been provided via `app.provide(BootstrapResultKey, result)`).
1867
+ Returns the `BootstrapResult` provided at the app root with `app.provide(BootstrapResultKey, result)`.
2294
1868
 
2295
1869
  ```typescript
2296
1870
  import { useBootstrap } from '@the_library/public/web3/vue';
@@ -2300,7 +1874,7 @@ const { chains, installedConnectors, primaryAccount } = useBootstrap();
2300
1874
 
2301
1875
  ---
2302
1876
 
2303
- #### `useWeb3Session()`
1877
+ #### `useWeb3Session(): UseWeb3SessionReturn`
2304
1878
 
2305
1879
  Reactive wrapper over `sessionStore`.
2306
1880
 
@@ -2308,62 +1882,122 @@ Reactive wrapper over `sessionStore`.
2308
1882
  import { useWeb3Session } from '@the_library/public/web3/vue';
2309
1883
 
2310
1884
  const {
2311
- accounts, // Ref<Web3SessionAccount[]>
2312
- registeredAccounts, // Ref<Web3SessionAccount[]> filtered by hasAccount=true
2313
- activeAccount, // Ref<Web3SessionAccount | null>
2314
- isConnecting, // Ref<boolean>
2315
- setActive, // (account: Web3SessionAccount) => void
1885
+ accounts, // ComputedRef<Web3SessionAccount[]>
1886
+ registeredAccounts, // ComputedRef<Web3SessionAccount[]>, only hasAccount = true
1887
+ activeAccount, // ComputedRef<Web3SessionAccount | null>
1888
+ isConnecting, // Ref<boolean>
1889
+ setActive, // (account) => void
1890
+ discoverAccounts, // (connector, { forceRefresh? }) => Promise<Web3SessionAccount[]>
1891
+ syncAccount, // (address, chainId, wallet, forceRefresh?) => Promise<Web3SessionAccount>
1892
+ accountsForChain, // (chainId) => ComputedRef<Web3SessionAccount[]>
1893
+ registeredAccountsForChain, // (chainId) => ComputedRef<Web3SessionAccount[]>
2316
1894
  } = useWeb3Session();
2317
1895
  ```
2318
1896
 
1897
+ It also exposes the save counters: `lastSyncCounters(address, chainId)`, `setLastSavedPatchIndex(…)` and `setLastDownloadedSaveIndex(…)`.
1898
+
1899
+ ---
1900
+
1901
+ #### `useChain(chainId: MaybeRef<number>): UseChainReturn`
1902
+
1903
+ See [Chain Metadata → `useChain`](#vue-composable-usechainchainid).
1904
+
2319
1905
  ---
2320
1906
 
2321
- #### `useChain(chainId: number | Ref<number>)`
1907
+ #### `useLoadProjects(chainId: MaybeRef<number>, language: MaybeRef<string>): UseLoadProjectsReturn`
2322
1908
 
2323
1909
  ```typescript
2324
- import { useChain } from '@the_library/public/web3/vue';
1910
+ import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
1911
+ import { useLoadProjects } from '@the_library/public/web3/vue';
2325
1912
 
2326
- const { chain, client, usdPrice, refreshPrice, isMainnet, chainRouteQuery } = useChain(146);
2327
- // chain: Ref<EvmChainConfig>
2328
- // client: Ref<ReadOnlyDomainAPI>
2329
- // usdPrice: Ref<number>
2330
- // refreshPrice: () => Promise<void>
2331
- // isMainnet: boolean
2332
- // chainRouteQuery: { chainId: '146' }
1913
+ const { projects, isLoading, error, reload } = useLoadProjects(DEFAULT_CHAIN_ID, ref('eng'));
1914
+ // projects: Ref<Array<ProjectLocale | null>>, isLoading: Ref<boolean>, error: Ref<Error | null>, reload(): Promise<void>
2333
1915
  ```
2334
1916
 
2335
1917
  ---
2336
1918
 
2337
- #### `useLoadProjects(chainId: number | Ref<number>, language: string | Ref<string>)`
1919
+ #### `useSubscriptionSync(userId: number, chainId: number, environment: 'production' | 'staging')`
1920
+
1921
+ The state of a user's subscription sync: the stored cursor and how many patches the last sync applied (for a toast or badge). The sync itself runs on its own (after saves, at boot and on account discovery) and merges new patches into the models; this composable only reports it. `chainId` and `environment` are required because the same `userId` has a separate cursor on each chain and environment.
2338
1922
 
2339
1923
  ```typescript
2340
- import { useLoadProjects } from '@the_library/public/web3/vue';
1924
+ import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
1925
+ import { useSubscriptionSync } from '@the_library/public/web3/vue';
2341
1926
 
2342
- const { projects, isLoading, error, reload } = useLoadProjects(
2343
- ref(146),
2344
- ref('eng')
2345
- );
1927
+ const { cursor, appliedCount, isLoading, refresh } = useSubscriptionSync(userId, DEFAULT_CHAIN_ID, 'production');
1928
+ // cursor: Ref<SyncCursor | null>, appliedCount: Ref<number>, isLoading: Ref<boolean>, refresh(): Promise<void>
2346
1929
  ```
2347
1930
 
2348
1931
  ---
2349
1932
 
2350
- #### `useSubscriptionSync(userId: number | Ref<number>)`
1933
+ #### `mutationBus`, `registerHandler`, `bootstrapMutationHandlers`
1934
+
1935
+ Tracks the chain writes in flight, for optimistic UI and the transaction toasts.
1936
+
1937
+ - `mutationBus.dispatch(mutation, commit, label): Promise<boolean>` — applies the optimistic update registered for `mutation.type`, runs `commit` (which calls `onAccepted()` once the wallet has signed), and resolves `true` on success. On failure (including `commit` returning `false`) it rolls the update back, records an entry in `mutationBus.errors` (`reason: 'rejected'` when the user declined) and resolves `false`.
1938
+ - `mutationBus.pending`, `.errors`, `.successes` — reactive lists for the UI; `reportError(label, reason?, detail?)` / `reportSuccess(label, detail?)` add entries for writes made outside `dispatch`, and `clearError(id)` / `clearSuccess(id)` remove them.
1939
+ - `ChainMutation` is one of `retire`, `vote`, `save`, `subscribe`, `unsubscribe`, each with `wallet`, `chainId` and `account`.
1940
+ - `registerHandler(type, { applyOptimistic, rollback })` — the optimistic update for one mutation type. `bootstrapMutationHandlers()` registers the built-in ones (`vote`, `retire`).
1941
+
1942
+ ---
1943
+
1944
+ ### 3.11 Auth API (`@the_library/public/web3/auth`)
1945
+
1946
+ Signed messages shared by the browser and the Lambdas, so signer and verifier always build the same message. No wallet or network code: plain functions over viem's hashing and signature recovery.
1947
+
1948
+ #### Batch authorization (bulk account creation)
1949
+
1950
+ An admin signs (EIP-191 `personal_sign`) a message committing to the environment, batch ID, exact email list, `forceSendEmail` flag and signing time; the Lambda rebuilds the message, recovers the signer and checks it against its admin list.
2351
1951
 
2352
1952
  ```typescript
2353
- import { useSubscriptionSync } from '@the_library/public/web3/vue';
1953
+ import { createBatchSigningMessage, verifyBatchAuthorization, parseAuthorizedAdmins } from '@the_library/public/web3/auth';
1954
+
1955
+ const payload = { environment: 'staging', batchId, emails, forceSendEmail: false, issuedAt: new Date().toISOString() };
1956
+ const signature = await walletClient.signMessage({ account, message: createBatchSigningMessage(payload) });
1957
+
1958
+ // on the server
1959
+ const result = await verifyBatchAuthorization({ payload, signature, authorizedAdmins: parseAuthorizedAdmins(process.env.AUTHORIZED_ADMINS) });
1960
+ if (result.ok) { /* result.signer, result.emails (normalized) */ } else { /* result.reason */ }
1961
+ ```
2354
1962
 
2355
- const { cursor, patches, isLoading, refresh } = useSubscriptionSync(userId);
1963
+ - `verifyBatchAuthorization({ payload, signature, authorizedAdmins, claimedSigner?, now? })` → `{ ok: true, signer, emails } | { ok: false, reason }`, where `reason` is one of `invalid_batch_id`, `invalid_emails`, `invalid_issued_at`, `expired`, `issued_in_future`, `malformed_signature`, `signer_mismatch`, `unauthorized_signer`.
1964
+ - Signatures older than `BATCH_AUTHORIZATION_MAX_AGE_MS` (10 minutes) are rejected; `issuedAt` may be up to `BATCH_AUTHORIZATION_MAX_CLOCK_SKEW_MS` (2 minutes) in the future.
1965
+ - `normalizeBatchEmails(emails)` (lowercased, trimmed, de-duplicated, sorted) and `hashBatchEmails(emails)` (keccak256 of that list) are the building blocks.
1966
+
1967
+ #### Deletion consent (email-confirmed account deletion)
1968
+
1969
+ The Lambda signs, as `AccountManager`'s oracle, a consent to retire one account, and delivers it only by email; the link proves control of the inbox, and `AccountManager.unregisterWithConsent` refuses to retire an account without it. The signed hash is `keccak256(abi.encodePacked("EmailConsent", account, chainId, accountManager, expiry))`.
1970
+
1971
+ ```typescript
1972
+ import { signDeletionConsent, encodeDeletionConsentToken, buildConfirmDeleteLink, DELETION_CONSENT_TTL_SECONDS } from '@the_library/public/web3/auth';
1973
+
1974
+ // Lambda
1975
+ const consent = { account, chainId, accountManager, expiry: Math.floor(Date.now() / 1000) + DELETION_CONSENT_TTL_SECONDS };
1976
+ const signed = await signDeletionConsent(consent, oracleAccount.signMessage);
1977
+ const link = buildConfirmDeleteLink(encodeDeletionConsentToken(signed), frontendUrl);
1978
+ ```
1979
+
1980
+ ```typescript
1981
+ import { decodeDeletionConsentToken, verifyDeletionConsent } from '@the_library/public/web3/auth';
1982
+
1983
+ // Browser, before sending the transaction (the contract repeats every check)
1984
+ const consent = decodeDeletionConsentToken(token); // shape only, or null
1985
+ const check = consent && await verifyDeletionConsent(consent, { chainId, accountManager, oracleSigner });
2356
1986
  ```
2357
1987
 
1988
+ - `verifyDeletionConsent` fails with `malformed`, `expired`, `wrong_chain`, `wrong_account_manager` or `signer_mismatch`. Take `oracleSigner` and `accountManager` from the chain or registry, never from the link.
1989
+ - `DELETION_CONSENT_TTL_SECONDS` is 24 hours; `DELETION_CONSENT_PURPOSE` is `'EmailConsent'`; `deletionConsentMessageHash(consent)` returns the hash above.
1990
+
2358
1991
  ---
2359
1992
 
2360
1993
  ## 4. Branding API (`@the_library/public/branding`)
2361
1994
 
2362
- Static, framework-agnostic branding and contact data shared across apps.
1995
+ Static, framework-agnostic branding and contact data shared across the project's apps.
2363
1996
 
2364
1997
  ```typescript
2365
1998
  import {
2366
- globalBranding,
1999
+ globalBranding, // { founder, builder, socialMedia, contactFormUrl }
2000
+ founder, builder, socialMedia,
2367
2001
  worldBiblioTechBranding,
2368
2002
  dsafeLegalBranding,
2369
2003
  worldBibliotekBranding,
@@ -2371,33 +2005,25 @@ import {
2371
2005
 
2372
2006
  import type { AppBranding, FounderInfo, BuilderInfo, SocialMediaUrls } from '@the_library/public/branding';
2373
2007
 
2374
- // Access global founder/contact data
2375
- console.log(globalBranding.founder.email); // 'data@datapond.earth'
2376
- console.log(globalBranding.founder.name);
2377
- console.log(globalBranding.builder.name);
2378
- console.log(globalBranding.socialMedia.x); // 'https://x.com/realDataPond'
2008
+ globalBranding.founder.email; // 'data@datapond.earth'
2009
+ globalBranding.builder.name; // 'Pond Enterprise'
2010
+ globalBranding.socialMedia.x; // 'https://x.com/realDataPond'
2379
2011
 
2380
- // Access app-specific branding
2381
- console.log(worldBiblioTechBranding.appName); // 'World Biblio Tech'
2382
- console.log(worldBiblioTechBranding.url);
2383
- console.log(worldBiblioTechBranding.images.logo); // asset path
2012
+ worldBiblioTechBranding.appName; // 'World Biblio Tech'
2013
+ worldBiblioTechBranding.images.logo; // '@the_library/public/branding/assets/icon_transparent.png'
2384
2014
  ```
2385
2015
 
2016
+ Image paths point into the package's `branding/assets/` folder, so a bundler resolves them as `@the_library/public/branding/assets/<file>`.
2017
+
2386
2018
  **Type: `AppBranding`**
2387
2019
  ```typescript
2388
2020
  interface AppBranding {
2389
2021
  appName: string;
2390
- url: string;
2391
- description?: string;
2392
- images: {
2393
- logo: string; // relative path (e.g., '@the_library/public/branding/assets/...')
2394
- favicon?: string;
2395
- banner?: string;
2396
- };
2397
- support?: {
2398
- email?: string;
2399
- url?: string;
2400
- };
2022
+ appUrl: string;
2023
+ comingSoonUrl?: string;
2024
+ supportEmail?: string;
2025
+ contactFormUrl?: string;
2026
+ images: { logo?: string; favicon?: string };
2401
2027
  }
2402
2028
  ```
2403
2029
 
@@ -2406,7 +2032,9 @@ interface AppBranding {
2406
2032
  interface FounderInfo {
2407
2033
  name: string;
2408
2034
  email: string;
2409
- title?: string;
2035
+ whatsapp: string;
2036
+ linkedIn: string;
2037
+ photoPath: string;
2410
2038
  }
2411
2039
  ```
2412
2040
 
@@ -2414,17 +2042,23 @@ interface FounderInfo {
2414
2042
  ```typescript
2415
2043
  interface BuilderInfo {
2416
2044
  name: string;
2417
- title?: string;
2045
+ url: string;
2046
+ email: string;
2047
+ whatsapp: string;
2048
+ linkedIn: string;
2049
+ logoPath: string;
2418
2050
  }
2419
2051
  ```
2420
2052
 
2421
2053
  **Type: `SocialMediaUrls`**
2422
2054
  ```typescript
2423
2055
  interface SocialMediaUrls {
2424
- x?: string; // Twitter/X
2425
- github?: string;
2426
- linkedin?: string;
2427
- // ... other platforms
2056
+ facebook: string;
2057
+ instagram: string;
2058
+ bluesky: string;
2059
+ linkedin: string;
2060
+ x: string;
2061
+ kickstarter: string;
2428
2062
  }
2429
2063
  ```
2430
2064
 
@@ -2435,15 +2069,15 @@ interface SocialMediaUrls {
2435
2069
  ### Download Types
2436
2070
  - `FileState` — download/error state for one file
2437
2071
  - `RenderState` — page images and book metadata
2438
- - `BookData` — cached book title, page count, etc.
2439
- - `DownloadRecord` — progress (loaded/total bytes)
2072
+ - `BookData` — page dimensions of a parsed PDF
2073
+ - `DownloadProgressDetail`, `DownloadDoneDetail` — payloads of the download window events
2440
2074
  - `PruningState` — LRU cache eviction activity
2441
2075
  - `InitOptions` — downloader setup parameters
2076
+ - `SwStatus`, `SwGateOptions` (`/downloader/sw`) — service worker status and registration options
2442
2077
 
2443
2078
  ### Models Types
2444
2079
  - `ModelDefinition` — schema for a model type
2445
- - `LevelData` — user's reading level and corpus progress
2446
- - `BootData` — result of `bootstrapModelsV2()`
2080
+ - `LevelState` — a user's level: `{ level, progress, points }`
2447
2081
  - `TrashStore` — global trash index API (`list`/`count`/`usernames`/`subscribe`)
2448
2082
  - `StatSummary` — per-record visits/downloads/rating/shares/parts
2449
2083
  - `ActionTypes` — local analytics op discriminator (`Visit`, `Download`, `Rate`, ...)
@@ -2454,7 +2088,7 @@ interface SocialMediaUrls {
2454
2088
  - `Web3SessionAccount` — wallet state in session store
2455
2089
  - `SessionState` — `{ active, known }`
2456
2090
  - `ReadOnlyDomainAPI` — read client interface
2457
- - `EvmChainConfig` — chain metadata
2091
+ - `EvmChainConfig` — chain metadata (from `@the_library/network-config`)
2458
2092
  - `ProjectLocale` — project with selected language
2459
2093
  - `Project` — raw contract data
2460
2094
  - `ProjectVersion` — project version metadata