@the_library/public 2.0.1 → 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.
- package/LICENSE +684 -0
- package/README.md +735 -1101
- package/THIRD_PARTY_NOTICES +489 -0
- package/assets/branding/pond-enterprise-logo.svg +3 -14
- package/dist/branding/index.js +6 -1
- package/dist/branding/index.js.map +1 -1
- package/dist/{chunk-3XUKOVEF.js → chunk-34UDKDMR.js} +7 -2
- package/dist/chunk-34UDKDMR.js.map +1 -0
- package/dist/{chunk-7G3BMGV2.js → chunk-3ALQKSIN.js} +11 -1
- package/dist/chunk-3ALQKSIN.js.map +1 -0
- package/dist/{chunk-PV3X54EU.js → chunk-AADFZOUD.js} +6 -1
- package/dist/chunk-AADFZOUD.js.map +1 -0
- package/dist/{chunk-FN52YV7L.js → chunk-DCTN7BWB.js} +6 -1
- package/dist/chunk-DCTN7BWB.js.map +1 -0
- package/dist/{chunk-GWQNULP4.js → chunk-HZZP2YTW.js} +7 -2
- package/dist/chunk-HZZP2YTW.js.map +1 -0
- package/dist/chunk-OKIGFADW.js +7 -0
- package/dist/chunk-OKIGFADW.js.map +1 -0
- package/dist/{chunk-D4DXGA2W.js → chunk-QL7HY3IK.js} +7 -2
- package/dist/chunk-QL7HY3IK.js.map +1 -0
- package/dist/{chunk-46FQHZNM.js → chunk-SB7IRHJD.js} +6 -1
- package/dist/chunk-SB7IRHJD.js.map +1 -0
- package/dist/{chunk-OELHBR4F.js → chunk-TQW56CCL.js} +7 -2
- package/dist/chunk-TQW56CCL.js.map +1 -0
- package/dist/{chunk-SD3INWZF.js → chunk-YRTDYXAD.js} +6 -1
- package/dist/chunk-YRTDYXAD.js.map +1 -0
- package/dist/downloader/dl2-sw.js +6 -1
- package/dist/downloader/dl2-sw.js.map +1 -1
- package/dist/downloader/file-worker-v2.js +5 -0
- package/dist/downloader/file-worker-v2.js.map +1 -1
- package/dist/downloader/index.js +6 -1
- package/dist/downloader/index.js.map +1 -1
- package/dist/downloader/mu-worker-v2.js +5 -0
- package/dist/downloader/mu-worker-v2.js.map +1 -1
- package/dist/downloader/react.js +6 -1
- package/dist/downloader/react.js.map +1 -1
- package/dist/downloader/solid.js +6 -1
- package/dist/downloader/solid.js.map +1 -1
- package/dist/downloader/sw.js +6 -1
- package/dist/downloader/sw.js.map +1 -1
- package/dist/downloader/vue.js +6 -1
- package/dist/downloader/vue.js.map +1 -1
- package/dist/global/public.global.js +35 -25
- package/dist/global/public.manifest.json +0 -1
- package/dist/models/astro.js +6 -1
- package/dist/models/astro.js.map +1 -1
- package/dist/models/index.js +6 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/models.js +6 -1
- package/dist/models/patch-worker.js +10 -0
- package/dist/models/patch-worker.js.map +1 -1
- package/dist/models/react.js +6 -1
- package/dist/models/react.js.map +1 -1
- package/dist/models/solid.js +6 -1
- package/dist/models/solid.js.map +1 -1
- package/dist/models/vue.js +6 -1
- package/dist/models/vue.js.map +1 -1
- package/dist/web3/auth.js +5 -0
- package/dist/web3/auth.js.map +1 -1
- package/dist/web3/index.js +6 -1
- package/dist/web3/index.js.map +1 -1
- package/dist/web3/network.js +6 -1
- package/dist/web3/registry.js +6 -1
- package/dist/web3/vue.js +6 -1
- package/dist/web3/vue.js.map +1 -1
- package/package.json +6 -5
- package/dist/chunk-3XUKOVEF.js.map +0 -1
- package/dist/chunk-46FQHZNM.js.map +0 -1
- package/dist/chunk-7G3BMGV2.js.map +0 -1
- package/dist/chunk-D4DXGA2W.js.map +0 -1
- package/dist/chunk-EMA2QJTW.js +0 -2
- package/dist/chunk-EMA2QJTW.js.map +0 -1
- package/dist/chunk-FN52YV7L.js.map +0 -1
- package/dist/chunk-GWQNULP4.js.map +0 -1
- package/dist/chunk-OELHBR4F.js.map +0 -1
- package/dist/chunk-PV3X54EU.js.map +0 -1
- package/dist/chunk-SD3INWZF.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,760 +1,559 @@
|
|
|
1
|
-
# @the_library/public
|
|
1
|
+
# @the_library/public
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
+
## Install
|
|
55
16
|
|
|
56
|
-
```
|
|
57
|
-
|
|
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
|
-
|
|
68
|
-
- `
|
|
69
|
-
-
|
|
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
|
-
|
|
25
|
+
## Entry points
|
|
84
26
|
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
import { GetPdf, GetObjectUrl } from '@the_library/public/downloader';
|
|
53
|
+
This SDK is the engineering foundation of:
|
|
106
54
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
- **identifiers:** ISBN, ISSN, ISNI, ORCID;
|
|
67
|
+
- **classifications:** Dewey, Library of Congress, UDC, Chinese, BBK, Thai and Colon.
|
|
129
68
|
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
+
### Identity: an account, not a person
|
|
157
93
|
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
import { EnsurePdf } from '@the_library/public/downloader';
|
|
99
|
+
### Free to use
|
|
162
100
|
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
- `txId: string` — Arweave TX ID of the PDF.
|
|
105
|
+
### Built to last
|
|
172
106
|
|
|
173
|
-
**
|
|
174
|
-
|
|
175
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
116
|
+
Each recipe is type-checked against the published package.
|
|
199
117
|
|
|
200
|
-
|
|
118
|
+
### Open a book
|
|
201
119
|
|
|
202
|
-
|
|
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 {
|
|
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
|
-
|
|
126
|
+
preloadWorkersV2(); // start the workers early
|
|
227
127
|
|
|
228
|
-
|
|
229
|
-
|
|
128
|
+
const book = await Book.LoadAsync(bookId); // bookId: bigint
|
|
129
|
+
img.src = await GetObjectUrl(await GetImage(book.previewTxId));
|
|
230
130
|
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
//
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
+
const title = (await Book.LoadAsync(bookId)).getTitle('eng');
|
|
160
|
+
```
|
|
263
161
|
|
|
264
|
-
|
|
162
|
+
### Read the chain
|
|
265
163
|
|
|
266
164
|
```typescript
|
|
267
|
-
import {
|
|
165
|
+
import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
|
|
166
|
+
import { getReadClient } from '@the_library/public/web3';
|
|
268
167
|
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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 {
|
|
176
|
+
import { createApp } from 'vue';
|
|
177
|
+
import { bootstrapWebSDK, BootstrapResultKey } from '@the_library/public/web3/vue';
|
|
288
178
|
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
296
|
-
- `txId: string` — Arweave TX ID.
|
|
189
|
+
### Save a reader's changes
|
|
297
190
|
|
|
298
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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 {
|
|
207
|
+
import { registerDownloaderSW } from '@the_library/public/downloader/sw';
|
|
318
208
|
|
|
319
|
-
const
|
|
320
|
-
|
|
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
|
-
|
|
215
|
+
## Table of Contents
|
|
330
216
|
|
|
331
|
-
|
|
217
|
+
- [Why it is designed this way](#why-it-is-designed-this-way)
|
|
218
|
+
- [How-to guides](#how-to-guides)
|
|
332
219
|
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
343
|
-
-
|
|
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
|
-
|
|
253
|
+
4. [Branding API](#4-branding-api-the_librarypublicbranding)
|
|
346
254
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#### `
|
|
271
|
+
#### `InitDownloaderV2(opts?: InitOptions): Promise<void>`
|
|
379
272
|
|
|
380
|
-
|
|
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 {
|
|
276
|
+
import { InitDownloaderV2 } from '@the_library/public/downloader';
|
|
384
277
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
- `
|
|
408
|
-
|
|
409
|
-
- `
|
|
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
|
-
#### `
|
|
294
|
+
#### `preloadWorkersV2(): void`
|
|
437
295
|
|
|
438
|
-
|
|
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 {
|
|
299
|
+
import { preloadWorkersV2 } from '@the_library/public/downloader';
|
|
442
300
|
|
|
443
|
-
|
|
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
|
-
|
|
306
|
+
### File Operations
|
|
455
307
|
|
|
456
|
-
|
|
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 {
|
|
313
|
+
import { GetPdf, GetObjectUrl } from '@the_library/public/downloader';
|
|
460
314
|
|
|
461
|
-
await
|
|
315
|
+
const response = await GetPdf(txId);
|
|
316
|
+
const url = await GetObjectUrl(response); // for <iframe> or <embed>
|
|
462
317
|
```
|
|
463
318
|
|
|
464
|
-
**
|
|
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
|
-
#### `
|
|
323
|
+
#### `GetImage(txId: string): Promise<Response>`
|
|
472
324
|
|
|
473
|
-
|
|
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 {
|
|
328
|
+
import { GetImage } from '@the_library/public/downloader';
|
|
477
329
|
|
|
478
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
343
|
+
const page2 = await EnsurePageImage(txId, 2);
|
|
344
|
+
```
|
|
522
345
|
|
|
523
|
-
|
|
346
|
+
---
|
|
524
347
|
|
|
525
|
-
#### `
|
|
348
|
+
#### `EnsurePdf(txId: string): Promise<void>`
|
|
526
349
|
|
|
527
|
-
|
|
350
|
+
Downloads and caches the PDF without returning it, e.g. to prefetch the next book.
|
|
528
351
|
|
|
529
|
-
|
|
352
|
+
---
|
|
530
353
|
|
|
531
|
-
|
|
354
|
+
#### `EnsurePageCount(txId: string): Promise<number>`
|
|
532
355
|
|
|
533
|
-
|
|
356
|
+
Returns the PDF's page count, downloading and parsing the PDF if needed.
|
|
534
357
|
|
|
535
|
-
|
|
358
|
+
---
|
|
536
359
|
|
|
537
|
-
#### `
|
|
360
|
+
#### `DownloadPdfToDisk(txId: string, filename: string): Promise<void>`
|
|
538
361
|
|
|
539
|
-
|
|
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
|
-
|
|
364
|
+
```typescript
|
|
365
|
+
import { DownloadPdfToDisk } from '@the_library/public/downloader';
|
|
542
366
|
|
|
543
|
-
|
|
367
|
+
await DownloadPdfToDisk(txId, 'My Book Title');
|
|
368
|
+
```
|
|
544
369
|
|
|
545
370
|
---
|
|
546
371
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
#### `pdfKey(txId: string): string`
|
|
372
|
+
#### `GetObjectUrl(resp: Response): Promise<string>` and `revokeObjectUrl(url: string): void`
|
|
550
373
|
|
|
551
|
-
|
|
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
|
|
377
|
+
const url = await GetObjectUrl(await GetPdf(txId));
|
|
378
|
+
// … later
|
|
379
|
+
revokeObjectUrl(url);
|
|
555
380
|
```
|
|
556
381
|
|
|
557
|
-
|
|
382
|
+
---
|
|
558
383
|
|
|
559
|
-
|
|
384
|
+
#### `SetPinnedPdfs(txIds: string[]): void`
|
|
560
385
|
|
|
561
|
-
|
|
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
|
-
|
|
388
|
+
---
|
|
564
389
|
|
|
565
|
-
|
|
566
|
-
const key = pageKey(txId, 5); // e.g., "arweave_page_5_{txId}"
|
|
567
|
-
```
|
|
390
|
+
#### `storageEstimate(): Promise<StorageEstimate>`
|
|
568
391
|
|
|
569
|
-
|
|
392
|
+
The browser's own storage figures (`navigator.storage.estimate()`): `usage` and `quota` in bytes.
|
|
570
393
|
|
|
571
394
|
```typescript
|
|
572
|
-
import {
|
|
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
|
-
|
|
584
|
-
|
|
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
|
-
|
|
403
|
+
#### `DownloadQuotaExceededError`
|
|
595
404
|
|
|
596
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
418
|
+
---
|
|
620
419
|
|
|
621
|
-
|
|
622
|
-
import {
|
|
623
|
-
useFileStatus,
|
|
624
|
-
useRenderStatus,
|
|
625
|
-
useGlobalProgress,
|
|
626
|
-
usePruning
|
|
627
|
-
} from '@the_library/public/downloader/react';
|
|
420
|
+
### Metadata & Storage
|
|
628
421
|
|
|
629
|
-
|
|
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
|
-
|
|
424
|
+
The page dimensions recorded once the PDF has been parsed, or `undefined` before that.
|
|
637
425
|
|
|
638
426
|
```typescript
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
###
|
|
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
|
-
#### `
|
|
438
|
+
#### `downloadState`
|
|
660
439
|
|
|
661
|
-
|
|
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 {
|
|
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
|
-
|
|
445
|
+
const unsubscribe = downloadState.subscribe((changedKey) => { /* re-read what you show */ });
|
|
673
446
|
|
|
674
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
468
|
+
### Keys & Naming
|
|
690
469
|
|
|
691
|
-
|
|
470
|
+
The cache names and keys, for code that reads Cache Storage directly (the service worker does):
|
|
692
471
|
|
|
693
472
|
```typescript
|
|
694
|
-
import {
|
|
473
|
+
import { CACHES, CACHE_VERSION, pdfKey, previewKey, pageKey } from '@the_library/public/downloader';
|
|
695
474
|
|
|
696
|
-
|
|
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
|
-
|
|
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
|
-
|
|
486
|
+
### Framework Adapters (`/downloader/{vue,react,solid}`)
|
|
706
487
|
|
|
707
|
-
|
|
488
|
+
The same four hooks in each framework, wrapping `downloadState`:
|
|
708
489
|
|
|
709
490
|
```typescript
|
|
710
|
-
import {
|
|
491
|
+
import { useFileStatus, useRenderStatus, useGlobalProgress, usePruning } from '@the_library/public/downloader/vue';
|
|
711
492
|
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
517
|
+
import { registerDownloaderSW } from '@the_library/public/downloader/sw';
|
|
728
518
|
|
|
729
|
-
await
|
|
730
|
-
console.
|
|
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
|
-
|
|
523
|
+
```typescript
|
|
524
|
+
interface SwGateOptions { allowedOrigins?: string[]; enableInDev?: boolean }
|
|
734
525
|
|
|
735
|
-
|
|
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
|
-
|
|
538
|
+
The current status, without registering anything.
|
|
740
539
|
|
|
741
|
-
|
|
540
|
+
#### `unregisterDownloaderSW(opts?: { clearCaches?: boolean; reload?: boolean }): Promise<boolean>`
|
|
742
541
|
|
|
743
|
-
|
|
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
|
-
|
|
747
|
-
```
|
|
544
|
+
#### `clearDownloaderCaches(): Promise<void>`
|
|
748
545
|
|
|
749
|
-
|
|
546
|
+
Deletes every downloader cache (all Cache Storage entries named `dl2-…`). IndexedDB records are kept.
|
|
547
|
+
|
|
548
|
+
#### `nukeDownloaderStorage(): Promise<void>`
|
|
750
549
|
|
|
751
|
-
|
|
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
|
|
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
|
|
596
|
+
#### `Registry` (singleton)
|
|
798
597
|
|
|
799
|
-
Holds
|
|
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
|
-
|
|
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(
|
|
815
|
-
|
|
816
|
-
- `Registry.
|
|
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;
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
650
|
+
#### `getRecord(objectType: number, id: bigint, creatorUsername?: string): any`
|
|
855
651
|
|
|
856
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
665
|
+
Puts a record object into the cache (replacing one with the same `id`).
|
|
894
666
|
|
|
895
667
|
---
|
|
896
668
|
|
|
897
|
-
#### `removeRecord(
|
|
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
|
-
|
|
671
|
+
Drops a record from the cache.
|
|
912
672
|
|
|
913
673
|
---
|
|
914
674
|
|
|
915
|
-
#### `hasRecord(
|
|
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
|
-
|
|
677
|
+
Whether the record is in the cache right now.
|
|
932
678
|
|
|
933
679
|
---
|
|
934
680
|
|
|
935
|
-
#### `pinRecord(
|
|
681
|
+
#### `pinRecord(objectType: number, id: bigint, creatorUsername?: string, reason?: string): void`
|
|
936
682
|
|
|
937
|
-
|
|
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
|
-
|
|
943
|
-
|
|
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(
|
|
695
|
+
#### `unpinRecord(objectType: number, id: bigint, creatorUsername?: string, reason?: string): void`
|
|
955
696
|
|
|
956
|
-
|
|
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(
|
|
701
|
+
#### `addReactivitySystem(sys: ReactivitySystem): () => void`
|
|
974
702
|
|
|
975
|
-
|
|
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: (
|
|
982
|
-
trigger: (
|
|
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(
|
|
716
|
+
#### `setReactivitySystem(sys: ReactivitySystem | null): void`
|
|
989
717
|
|
|
990
|
-
Replaces
|
|
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
|
-
|
|
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,
|
|
1000
|
-
// equivalently: from '@the_library/public/models/models'
|
|
727
|
+
import { Book, NewBookWithId, ModelTypes } from '@the_library/public/models/models';
|
|
1001
728
|
|
|
1002
|
-
//
|
|
1003
|
-
const book =
|
|
1004
|
-
book.
|
|
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
|
-
//
|
|
1008
|
-
|
|
733
|
+
// Synchronous access when it's known to be cached
|
|
734
|
+
if (Book.Has(bookId)) Book.Load(bookId);
|
|
1009
735
|
|
|
1010
|
-
//
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
//
|
|
1016
|
-
const
|
|
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
|
|
1022
|
-
|
|
1023
|
-
- `NewBookWithId(id: string, creatorUsername: string): Book` — Factory to create a new Book record in memory.
|
|
745
|
+
**Key members:**
|
|
1024
746
|
|
|
1025
|
-
- `Book.
|
|
1026
|
-
|
|
1027
|
-
- `
|
|
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`
|
|
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[]):
|
|
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[]):
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
-
|
|
976
|
+
#### `bootstrapModelsV2(opts: SessionBootstrapOptions): Promise<void>`
|
|
1250
977
|
|
|
1251
|
-
|
|
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: '
|
|
1256
|
-
models: [{ type: Book.type, create: NewBookWithId }],
|
|
1257
|
-
localMode: '/corpus_v2',
|
|
1258
|
-
onBoot: (data) =>
|
|
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
|
-
|
|
1263
|
-
- `userLang:
|
|
1264
|
-
- `models: ModelFactory[]` —
|
|
1265
|
-
- `
|
|
1266
|
-
- `
|
|
1267
|
-
- `
|
|
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
|
-
|
|
1003
|
+
#### `resetModelsV2Session(): void`
|
|
1270
1004
|
|
|
1271
|
-
|
|
1005
|
+
Clears the session and the cached level state, for logout and tests/HMR.
|
|
1272
1006
|
|
|
1273
|
-
|
|
1007
|
+
#### `clearLocalPatches()`
|
|
1274
1008
|
|
|
1275
|
-
|
|
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
|
-
|
|
1278
|
-
import { useLevel } from '@the_library/public/models/vue';
|
|
1011
|
+
#### `loadSearch(lang?: Lang): Promise<void>`
|
|
1279
1012
|
|
|
1280
|
-
|
|
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
|
-
|
|
1285
|
-
- `username?: string` — Username. Defaults to the currently-logged-in user.
|
|
1015
|
+
---
|
|
1286
1016
|
|
|
1287
|
-
|
|
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
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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
|
-
|
|
1030
|
+
#### `useDraft(model)`
|
|
1302
1031
|
|
|
1303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1046
|
+
#### `useTrash(objectType: number, usernames?: string[])`
|
|
1336
1047
|
|
|
1337
|
-
|
|
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); //
|
|
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
|
-
|
|
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
|
-
####
|
|
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
|
-
|
|
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
|
|
1073
|
+
Low-level functions behind `bootstrapModelsV2` and saving; apps rarely call them:
|
|
1468
1074
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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(
|
|
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
|
|
1526
|
-
userId
|
|
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,
|
|
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...',
|
|
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 (
|
|
1559
|
-
- `
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
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
|
|
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
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
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(
|
|
1429
|
+
const { chain, client, usdPrice, isPriceLoading, refreshPrice, isMainnet, isDeployed, chainRouteQuery } = useChain(DEFAULT_CHAIN_ID);
|
|
1823
1430
|
// chain: EvmChainConfig
|
|
1824
|
-
// client: ReadOnlyDomainAPI
|
|
1825
|
-
// usdPrice: Ref<number> —
|
|
1826
|
-
//
|
|
1827
|
-
//
|
|
1828
|
-
//
|
|
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
|
-
|
|
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(
|
|
1465
|
+
- `addressLoader.subscribe(listener: (chainId: string | number, status: LoadingStatus) => void): () => void` — Listen for load progress.
|
|
1858
1466
|
|
|
1859
|
-
- `addressLoader.getAddress(chainId: number,
|
|
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,
|
|
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
|
|
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(
|
|
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<
|
|
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 {
|
|
1530
|
+
import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
|
|
1531
|
+
import { cursorStore, cursorKey } from '@the_library/public/web3';
|
|
1922
1532
|
|
|
1923
|
-
|
|
1924
|
-
const
|
|
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(
|
|
1539
|
+
await cursorStore.set(key, result.cursor);
|
|
1928
1540
|
```
|
|
1929
1541
|
|
|
1930
1542
|
**Methods:**
|
|
1931
1543
|
|
|
1932
|
-
- `cursorStore.get(
|
|
1544
|
+
- `cursorStore.get(key: string): Promise<SyncCursor | undefined>` — Retrieve the persisted cursor, or `undefined` if never synced.
|
|
1933
1545
|
|
|
1934
|
-
- `cursorStore.set(
|
|
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
|
-
|
|
1550
|
+
---
|
|
1939
1551
|
|
|
1940
|
-
|
|
1941
|
-
import { useSubscriptionSync } from '@the_library/public/web3/vue';
|
|
1552
|
+
#### Vue Composable: `useSubscriptionSync(userId, chainId, environment)`
|
|
1942
1553
|
|
|
1943
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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?,
|
|
1988
|
-
if (e.event ===
|
|
1989
|
-
if (e.event ===
|
|
1990
|
-
if (e.event ===
|
|
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,
|
|
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: {
|
|
2000
|
-
//
|
|
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,
|
|
2005
|
-
//
|
|
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
|
|
2018
|
-
- `
|
|
2019
|
-
- `
|
|
2020
|
-
- `
|
|
2021
|
-
- `
|
|
2022
|
-
- `
|
|
2023
|
-
- `
|
|
2024
|
-
|
|
2025
|
-
|
|
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
|
-
**`
|
|
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.
|
|
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(
|
|
2116
|
-
if (hasPrivateKey(
|
|
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. **
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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,
|
|
1784
|
+
#### `projectLocale(project: Project, userLanguage: string, _chainId: number): ProjectLocale`
|
|
2179
1785
|
|
|
2180
|
-
Helper to convert raw `Project` data to `ProjectLocale` with
|
|
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',
|
|
1792
|
+
const locale = projectLocale(raw, 'eng', DEFAULT_CHAIN_ID);
|
|
2187
1793
|
```
|
|
2188
1794
|
|
|
2189
1795
|
**Parameters:**
|
|
2190
1796
|
- `project: Project` — Raw contract data.
|
|
2191
|
-
- `
|
|
2192
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
1815
|
+
import { EVM_CHAINS, getChain, DEFAULT_CHAIN_ID, DEFAULT_STAGING_CHAIN_ID, CANONICAL } from '@the_library/network-config';
|
|
2221
1816
|
|
|
2222
|
-
//
|
|
2223
|
-
//
|
|
2224
|
-
//
|
|
2225
|
-
//
|
|
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
|
|
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.
|
|
2273
|
-
- `options.
|
|
2274
|
-
- `options.chainFilter?: number[]` —
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
2312
|
-
registeredAccounts,
|
|
2313
|
-
activeAccount,
|
|
2314
|
-
isConnecting,
|
|
2315
|
-
setActive,
|
|
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
|
-
#### `
|
|
1907
|
+
#### `useLoadProjects(chainId: MaybeRef<number>, language: MaybeRef<string>): UseLoadProjectsReturn`
|
|
2322
1908
|
|
|
2323
1909
|
```typescript
|
|
2324
|
-
import {
|
|
1910
|
+
import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
|
|
1911
|
+
import { useLoadProjects } from '@the_library/public/web3/vue';
|
|
2325
1912
|
|
|
2326
|
-
const {
|
|
2327
|
-
//
|
|
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
|
-
#### `
|
|
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 {
|
|
1924
|
+
import { DEFAULT_CHAIN_ID } from '@the_library/network-config';
|
|
1925
|
+
import { useSubscriptionSync } from '@the_library/public/web3/vue';
|
|
2341
1926
|
|
|
2342
|
-
const {
|
|
2343
|
-
|
|
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
|
-
#### `
|
|
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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
2375
|
-
|
|
2376
|
-
|
|
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
|
-
//
|
|
2381
|
-
|
|
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
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
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` —
|
|
2439
|
-
- `
|
|
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
|
-
- `
|
|
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
|