@serfab/quereus-plugin-sereus 1.17.0 → 1.19.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/README.md CHANGED
@@ -1,388 +1,390 @@
1
- # @serfab/quereus-plugin-sereus
2
-
3
- A [Quereus](https://github.com/nicktobey/quereus) plugin that connects a Quereus
4
- database to a [Sereus](../../docs/architecture.md) strand for SQL access. It
5
- composes the [@optimystic/quereus-plugin-crypto](https://github.com/nicktobey/optimystic/tree/main/packages/quereus-plugin-crypto)
6
- and [@optimystic/quereus-plugin-optimystic](https://github.com/nicktobey/optimystic/tree/main/packages/quereus-plugin-optimystic)
7
- plugins, manages libp2p networking, and optionally applies a sApp schema.
8
-
9
- ## Features
10
-
11
- - **Strand connection**: Connect a Quereus database to a Sereus strand with a single call.
12
- - **Plugin composition**: Automatically registers the crypto and optimystic plugins.
13
- - **Node management**: Creates a libp2p node or accepts an injected one (e.g. from a CadreNode).
14
- - **Schema application**: Wraps DDL in `declare schema App { ... } apply schema App;`.
15
- - **Two runtimes**: A Node entry (`./plugin`) and a self-contained browser bundle
16
- (`./plugin-browser`) for Quoomb-web and other browser/worker hosts.
17
- - **Two entry points**: A plugin-loader-compatible default export and a
18
- programmatic `connectToStrand()` API.
19
-
20
- ## Artifacts
21
-
22
- The package ships two plugin artifacts, selected automatically by the consumer's
23
- environment via the `exports` map:
24
-
25
- | Artifact | Built by | Transports | Default storage | Use from |
26
- |---|---|---|---|---|
27
- | `dist/plugin.js` | `tsc` | TCP + WebSockets | in-memory / FS | Node (`./plugin`) |
28
- | `dist/plugin-browser.js` | `esbuild` | WebSockets + circuit-relay | IndexedDB | Browser/worker (`./plugin-browser`) |
29
-
30
- The browser bundle is a single self-contained, minified ESM file of roughly 1.9 MiB
31
- raw / 0.6 MiB gzipped (the exact measurement and its date live beside the size caps in
32
- `test/browser-bundle.spec.ts`; a second copy of the numbers here would go stale). A
33
- source map, `dist/plugin-browser.js.map`, ships beside it with the original sources
34
- embedded, so devtools still show readable code. It bundles only the TCP-free libp2p
35
- surface (`@optimystic/db-p2p/rn`) and does **not** import `@quereus/quereus` at
36
- runtime — registrations are applied against the host-supplied `db`, so it reuses the
37
- host's Quereus instance rather than instantiating a second one.
38
-
39
- ## Installation
40
-
41
- ```bash
42
- npm install @serfab/quereus-plugin-sereus
43
- # or
44
- yarn add @serfab/quereus-plugin-sereus
45
- ```
46
-
47
- ---
48
-
49
- ## Using it from Quoomb Web (step by step)
50
-
51
- [Quoomb Web](../../../quereus/packages/quoomb-web) loads plugins by URL: its
52
- worker calls `import(url)` and registers the module's default export against the
53
- live database. To connect Quoomb Web to a strand you need to (1) make the
54
- **browser bundle** reachable at an importable URL, and (2) give it a strand id
55
- and at least one **browser-dialable** bootstrap address.
56
-
57
- ### Prerequisites
58
-
59
- - A **strand id** (UUID) to connect to.
60
- - For networked use: a libp2p **bootstrap/relay reachable over secure WebSockets**
61
- (`/wss`) from an `https://` page. Browsers cannot dial raw TCP, and a plain
62
- `ws://`/`tcp` address will not dial from a TLS origin. WebSockets +
63
- circuit-relay are the only supported transports in v1.
64
- - The strand's **sApp schema** (optional — only needed to bootstrap a brand-new
65
- strand or to expose `App.*` tables before any peer has).
66
-
67
- ### Step 1 — Make the browser bundle importable
68
-
69
- Pick one of the hosting options below ([Hosting the browser
70
- bundle](#hosting-the-browser-bundle)). The quickest are:
71
-
72
- - **Published CDN URL** (recommended once published):
73
- `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
74
- - **Local dev**: copy `dist/plugin-browser.js` into Quoomb Web's `public/` folder
75
- so it is served same-origin at `http://localhost:5173/plugin-browser.js`.
76
-
77
- The URL must serve the file with `Content-Type: text/javascript` (or
78
- `application/javascript`) and CORS that permits the page origin.
79
-
80
- ### Step 2a — Load via the Plugins panel (manual)
81
-
82
- 1. Open Quoomb Web → **Plugins** panel.
83
- 2. Paste the bundle URL into the plugin URL field and install it.
84
- - The manual installer validates the URL and only accepts **`https://`** (or
85
- `file://`) ending in `.js`/`.mjs`. A same-origin `http://localhost` URL is
86
- **rejected here** — use Step 2b (config) for local-dev http, or serve
87
- Quoomb over https.
88
- 3. Open the plugin's config and set at least:
89
- - `strand_id` — the strand UUID.
90
- - `bootstrap_nodes` — comma-separated `/wss` multiaddrs.
91
- - `schema` — optional sApp DDL.
92
- 4. Run a query, e.g. `select * from App.Message`.
93
-
94
- ### Step 2b — Load via `quoomb.config.json` (autoload)
95
-
96
- The config/autoload path calls the loader directly and **bypasses the
97
- https-only check**, so it is the right choice for local `http://localhost`
98
- during development. In Quoomb Web, open the **Config** modal and import this
99
- JSON (or set it as `quoomb.config.json`):
100
-
101
- ```jsonc
102
- {
103
- "plugins": [
104
- {
105
- "source": "https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus/dist/plugin-browser.js",
106
- "config": {
107
- "strand_id": "550e8400-e29b-41d4-a716-446655440000",
108
- "bootstrap_nodes": "/dns4/relay.example/tcp/443/wss/p2p/Qm...",
109
- "schema": "table Message (Id integer primary key, Content text not null)"
110
- }
111
- }
112
- ],
113
- "autoload": true
114
- }
115
- ```
116
-
117
- With `autoload: true`, the plugin loads on session start and applies the schema.
118
-
119
- ### Step 3 — Query the strand
120
-
121
- Once loaded, the `optimystic` vtab is the default and `App.*` tables resolve
122
- against the strand:
123
-
124
- ```sql
125
- select * from App.Message;
126
- insert into App.Message (Id, Content) values (1, 'hello strand');
127
- ```
128
-
129
- ### Notes & limitations
130
-
131
- - **Storage.** When `storage` is not injected, the browser bundle opens a default
132
- `IndexedDBRawStorage` against an IndexedDB database named
133
- `sereus-strand-<strandId>`. State survives reload. The plugin treats the
134
- IndexedDB handle as borrowed and does **not** close it on `shutdown()` — its
135
- lifecycle is the page/worker.
136
- - **Network transactor by default.** Every connection commits through the
137
- strand's libp2p cohort unless it asks otherwise; the in-process `local`
138
- transactor is a testing/tooling option (`connectToStrand({ transactor:
139
- 'local' })`), not a browser deployment mode.
140
- - **WebRTC** requires cross-origin isolation (COOP/COEP) and is tracked as a
141
- follow-up; v1 is WebSockets + circuit-relay only.
142
- - **Lifecycle.** Quoomb Web's loader keeps only the manifest and discards the
143
- plugin's returned `shutdown()`, so the libp2p node is torn down with the
144
- worker rather than on plugin unload.
145
-
146
- ---
147
-
148
- ## Hosting the browser bundle
149
-
150
- The browser bundle is just an ESM file; it can be served from anywhere that
151
- returns a JavaScript content type with appropriate CORS.
152
-
153
- ### Published package on a CDN (recommended)
154
-
155
- After `yarn pub:quereus-plugin-sereus` (see [Publishing](#publishing)), the npm
156
- package is mirrored by the public CDNs, which serve the correct content type and
157
- CORS automatically:
158
-
159
- - jsDelivr: `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
160
- - jsDelivr (pinned): `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus@0.7.1/dist/plugin-browser.js`
161
- - unpkg: `https://unpkg.com/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
162
-
163
- ### Self-hosted / same-origin
164
-
165
- Copy `dist/plugin-browser.js` to any static host (or Quoomb Web's `public/`) and
166
- reference it by URL. Same-origin hosting sidesteps CORS entirely.
167
-
168
- ### Loading from GitHub
169
-
170
- Loading the bundle straight from a GitHub repo has a caveat: **`dist/` is
171
- gitignored**, and `raw.githubusercontent.com` serves files as `text/plain` with
172
- `X-Content-Type-Options: nosniff`, so the browser refuses to `import()` it as a
173
- module. Two routes that *do* work:
174
-
175
- 1. **jsDelivr's GitHub endpoint**, which serves repo files with the correct MIME
176
- type and CORS — but only files that exist at the ref. Since `dist/` is not
177
- committed, this requires the built bundle to be present at the ref you point
178
- at (e.g. a CI step that commits `plugin-browser.js` to a `dist`/`gh-pages`
179
- branch or a tag):
180
-
181
- ```
182
- https://cdn.jsdelivr.net/gh/gotchoices/sereus@<tag-or-branch>/packages/quereus-plugin-sereus/dist/plugin-browser.js
183
- ```
184
-
185
- 2. **GitHub Pages**: publish `plugin-browser.js` to a Pages site, which serves it
186
- with a JavaScript content type and CORS.
187
-
188
- For most cases the npm-backed CDN URL above is simpler and is the recommended
189
- GitHub-free hosting path. (Note: GitHub *Release assets* are served as
190
- `application/octet-stream` and cannot be imported as ESM.)
191
-
192
- ---
193
-
194
- ## Programmatic API
195
-
196
- ```typescript
197
- import { Database } from '@quereus/quereus';
198
- import { connectToStrand } from '@serfab/quereus-plugin-sereus';
199
-
200
- const db = new Database();
201
- const strand = await connectToStrand(db, {
202
- strandId: '550e8400-e29b-41d4-a716-446655440000',
203
- schema: 'table Message (Id integer primary key, Content text not null)',
204
- bootstrapNodes: ['/ip4/1.2.3.4/tcp/9100/p2p/QmPeerId'],
205
- });
206
-
207
- for await (const row of db.eval('select * from App.Message')) {
208
- console.log(row);
209
- }
210
-
211
- await strand.shutdown();
212
- ```
213
-
214
- ### Plugin-loader (Node)
215
-
216
- The `./plugin` export is compatible with Quereus plugin-loader:
217
-
218
- ```typescript
219
- import { Database, registerPlugin } from '@quereus/quereus';
220
- import sereusPlugin from '@serfab/quereus-plugin-sereus/plugin';
221
-
222
- const db = new Database();
223
- const result = await registerPlugin(db, sereusPlugin, {
224
- strand_id: '550e8400-e29b-41d4-a716-446655440000',
225
- schema: 'table Message (Id integer primary key, Content text not null)',
226
- bootstrap_nodes: '/ip4/1.2.3.4/tcp/9100/p2p/QmPeerId',
227
- });
228
-
229
- await result.shutdown();
230
- ```
231
-
232
- ### Local transactor (in-process, no cohort)
233
-
234
- `transactor: 'local'` sends every transaction straight to this process's raw
235
- storage — no peers are consulted at all. It is for in-process tests and tooling
236
- (this package's own e2e suites run on it); applications should stay on the
237
- default `network` transactor, which a lone node coordinates for itself.
238
-
239
- Pair it with a persistent `IRawStorage` to survive restart: the same storage
240
- instance is wired into both the libp2p data path and the optimystic plugin's
241
- local transactor.
242
-
243
- ```typescript
244
- import { FileRawStorage } from '@optimystic/db-p2p-storage-fs';
245
-
246
- const storage = new FileRawStorage('./data/my-strand');
247
- const strand = await connectToStrand(db, {
248
- strandId: 'abc',
249
- transactor: 'local',
250
- storage,
251
- schema: 'table Msg (Id integer primary key, Body text not null)',
252
- });
253
- ```
254
-
255
- The plugin treats `storage` as borrowed: `shutdown()` releases the libp2p node
256
- and collection factory but does **not** close the storage.
257
-
258
- ### With an injected node
259
-
260
- When integrating with an existing CadreNode or other libp2p host:
261
-
262
- ```typescript
263
- const strand = await connectToStrand(db, {
264
- strandId: 'abc',
265
- libp2pNode: existingNode,
266
- coordinatedRepo: existingRepo,
267
- });
268
- ```
269
-
270
- The plugin uses the injected node instead of creating one, and will not stop it
271
- on shutdown.
272
-
273
- ### A commit's durability
274
-
275
- The result's `onCommitDurability(listener)` is called after every commit that wrote something, before the commit returns, with who holds what it wrote; `onCommitFullyHeld(listener)` is called once when a commit reported `followed` below full replication becomes held by its whole cohort. Both pass through from `@optimystic/quereus-plugin-optimystic`, whose README explains the report ([A commit's durability](https://github.com/gotchoices/optimystic/blob/main/packages/quereus-plugin-optimystic/README.md#a-commits-durability--pluginoncommitdurabilitylistener)); the `SqlCommitDurability` and `CommitFullyHeld` types are re-exported here. A cadre app reads the same reports through `@serfab/cadre-core` ([strands.md → A commit's durability](../../docs/strands.md#a-commits-durability)).
276
-
277
- ### Founding a strand: `createEmptyCollections()`
278
-
279
- A table nobody has written has no collection, so no single machine can show it empty: every read of it asks the other members, and fails while they are offline. The result's `createEmptyCollections()` commits an empty collection for every table the `Strand` and `App` schemas declare that has none yet, and resolves to how many it committed. The founder calls it once, before writing `Strand.Header`, while no other machine can have written to the strand; cadre-core's founder bootstrap does this ([strands.md → Joining](../../docs/strands.md#joining-no-writes-before-the-first-sync)). Call it only while nothing else writes through the `Database`: it commits whatever is staged in those collections outside any SQL transaction.
280
-
281
- ---
282
-
283
- ## Configuration
284
-
285
- ### `StrandConnectionOptions` (programmatic API)
286
-
287
- | Option | Type | Default | Description |
288
- |---|---|---|---|
289
- | `strandId` | string | *required* | UUID of the strand to connect to |
290
- | `bootstrapNodes` | string[] | `[]` | Bootstrap multiaddrs for peer discovery |
291
- | `schema` | string | — | sApp schema DDL to apply, as the body of `declare schema App { ... }`. An item the Quereus parser would skip (`create unique index …`, a misspelled item keyword) is refused rather than ignored. Its tables may share names with the strand's own tables; they are stored separately ([Same-Named Tables](../../docs/strands.md#same-named-tables-in-strand-and-app)) |
292
- | `sAppId` | string | `'unknown'` | sApp author public key |
293
- | `sAppVersion` | string | `'1.0.0'` | sApp version |
294
- | `port` | number | `0` | libp2p listening port (0 = random) |
295
- | `enableCache` | boolean | `true` | Enable optimystic caching |
296
- | `fretProfile` | `'edge' \| 'core'` | `'edge'` | FRET profile |
297
- | `clusterSize` | number | `4` | Nodes the **strand's** replication cluster should have (the control network uses its own fixed `CONTROL_REPLICATION_BREADTH` and never reads this). 4 is the smallest breadth whose 0.75 super-majority (`ceil(4 × 0.75) = 3`) still commits with one holder offline; at 2 or 3 every holder must be awake for every write, and at 2 read repair cannot converge at all. Every peer on the strand should use the same value: a peer configured much higher derives a larger expected cohort and can reject a coordinator's smaller declared peer set as a downsize. Frozen when the libp2p node is created, so a change takes effect on the next restart. Must be an integer ≥ 2 (`MIN_CLUSTER_SIZE`, Optimystic's own minimum). Leaving it unset is *not* the same as passing 4 elsewhere — Optimystic's own fallback is 10, so this default is applied here. Ignored when `libp2pNode` is injected |
298
- | `libp2pNode` | Libp2p | — | Inject an existing libp2p node |
299
- | `coordinatedRepo` | IRepo | — | Required when `libp2pNode` is provided |
300
- | `transactor` | `'local' \| 'network' \| 'test'` | `'network'` | Storage engine transactions go through. `'network'`: the strand's libp2p cohort, which a lone node coordinates for itself — the only value an application should use. `'local'`: this process's raw storage directly, no peers consulted (in-process tests and tooling). `'test'`: Optimystic's in-memory fake; no libp2p node is created unless one is injected (unit tests) |
301
- | `storage` | IRawStorage | — | Persistent raw storage. Borrowed — not closed on `shutdown()` |
302
-
303
- ### Plugin settings (plugin-loader / Quoomb)
304
-
305
- | Setting | Type | Default | Description |
306
- |---|---|---|---|
307
- | `strand_id` | string | *required* | UUID of the strand |
308
- | `bootstrap_nodes` | string | `''` | Comma-separated bootstrap multiaddrs |
309
- | `schema` | string | — | sApp schema DDL |
310
- | `sapp_id` | string | `'unknown'` | sApp author public key |
311
- | `sapp_version` | string | `'1.0.0'` | sApp version |
312
- | `port` | number | `0` | libp2p listening port: an integer from 0 to 65535 (0 = random) |
313
- | `enable_cache` | boolean | `true` | Enable caching: `true`, `false`, `1` or `0` |
314
- | `fret_profile` | select | `'edge'` | FRET profile (`'edge'` or `'core'`) |
315
- | `transactor` | select | `'network'` | Storage engine (`'local'`, `'network'` or `'test'`) — see the `transactor` row above |
316
- | `storage_path` | string | — | Node entry only: directory for a persistent `FileRawStorage`. The browser entry rejects it (it stores in IndexedDB) |
317
-
318
- Settings are checked strictly at load. An unknown key (including the retired
319
- `mode`) or a value of the wrong type fails the load with one error that names
320
- every offending setting; nothing falls back to a default the user did not
321
- choose. Leaving a setting out, NULL, or `''` means "use the default" (`''`
322
- only for the string and select settings). A host that adds keys of its own to
323
- the plugin's settings map therefore fails to load the plugin. `port` and
324
- `enable_cache` must be a JSON number and boolean: a `${VAR}` reference in
325
- `quoomb.config.json` expands to a string, which they reject.
326
-
327
- **Bootstrap multiaddrs.** Browsers can only dial transports reachable from an
328
- `https://` page. Use `/wss` (or `/dns/.../wss`, or a relay-fronted multiaddr).
329
- Plain `/tcp/.../ws` over HTTP won't dial from a TLS origin, and raw TCP is
330
- unavailable in the browser.
331
-
332
- ## Provided functions and modules
333
-
334
- Registered automatically from the composed plugins:
335
-
336
- - **Virtual table module**: `optimystic` (set as default vtab)
337
- - **Functions**: `StampId()`, `digest()`, `sign()`, `verify()`, `randomBytes()`
338
-
339
- ---
340
-
341
- ## Development
342
-
343
- ```bash
344
- yarn build # tsc + esbuild (emits dist/plugin.js and dist/plugin-browser.js)
345
- yarn test # Run tests (unit + e2e projects, plus browser-bundle smoke tests)
346
- yarn test:e2e # Run only the e2e project (real libp2p + FileRawStorage)
347
- yarn dev:test # Watch mode
348
- ```
349
-
350
- `build` runs `tsc -p tsconfig.build.json` (emits `dist/plugin.js`,
351
- `dist/plugin-browser.js` declarations, and other entries) and then
352
- `node scripts/build-browser.mjs` (overwrites `dist/plugin-browser.js` with the
353
- bundled artifact and its sourcemap, and prints raw + gzipped size).
354
-
355
- The browser-bundle smoke tests (`test/browser-bundle.spec.ts` and
356
- `test/browser-shape.spec.ts`) run under the `unit` project: they build the
357
- bundle on demand if missing, then check that the artifact parses as ESM, has no
358
- Node-only or TCP imports, stays under the size caps, and that its default export
359
- reaches the IndexedDB layer in a jsdom + `fake-indexeddb` environment.
360
-
361
- The `e2e` project covers two scenarios over real libp2p + `FileRawStorage`:
362
-
363
- - `test/e2e/local-transactor.e2e.spec.ts` — a single in-process connection on the
364
- `local` transactor, including cold-restart persistence across the shared
365
- storage directory.
366
- - `test/e2e/networked.e2e.spec.ts` — two in-process peers exchanging strand data
367
- through a `createLibp2pNode` mesh: cross-peer replication, bidirectional
368
- convergence, and late-joiner catch-up.
369
-
370
- ## Publishing
371
-
372
- From the monorepo root:
373
-
374
- ```bash
375
- yarn pub:quereus-plugin-sereus # clean + build + npm publish --access public
376
- ```
377
-
378
- This is also included in the aggregate `yarn pub` release step. Publishing emits
379
- both `dist/plugin.js` and the browser bundle `dist/plugin-browser.js` (both are
380
- in the package `files`), so the CDN URLs under [Hosting the browser
381
- bundle](#hosting-the-browser-bundle) resolve immediately after a successful
382
- publish.
383
-
384
- ## License
385
-
386
- MIT
387
- </content>
388
- </invoke>
1
+ # @serfab/quereus-plugin-sereus
2
+
3
+ A [Quereus](https://github.com/nicktobey/quereus) plugin that connects a Quereus
4
+ database to a [Sereus](../../docs/architecture.md) strand for SQL access. It
5
+ composes the [@optimystic/quereus-plugin-crypto](https://github.com/nicktobey/optimystic/tree/main/packages/quereus-plugin-crypto)
6
+ and [@optimystic/quereus-plugin-optimystic](https://github.com/nicktobey/optimystic/tree/main/packages/quereus-plugin-optimystic)
7
+ plugins, manages libp2p networking, and optionally applies a sApp schema.
8
+
9
+ ## Features
10
+
11
+ - **Strand connection**: Connect a Quereus database to a Sereus strand with a single call.
12
+ - **Plugin composition**: Automatically registers the crypto and optimystic plugins.
13
+ - **Node management**: Creates a libp2p node or accepts an injected one (e.g. from a CadreNode).
14
+ - **Schema application**: Wraps DDL in `declare schema App { ... } apply schema App;`.
15
+ - **Two runtimes**: A Node entry (`./plugin`) and a self-contained browser bundle
16
+ (`./plugin-browser`) for Quoomb-web and other browser/worker hosts.
17
+ - **Two entry points**: A plugin-loader-compatible default export and a
18
+ programmatic `connectToStrand()` API.
19
+
20
+ ## Artifacts
21
+
22
+ The package ships two plugin artifacts, selected automatically by the consumer's
23
+ environment via the `exports` map:
24
+
25
+ | Artifact | Built by | Transports | Default storage | Use from |
26
+ |---|---|---|---|---|
27
+ | `dist/plugin.js` | `tsc` | TCP + WebSockets | in-memory / FS | Node (`./plugin`) |
28
+ | `dist/plugin-browser.js` | `esbuild` | WebSockets + circuit-relay | IndexedDB | Browser/worker (`./plugin-browser`) |
29
+
30
+ The browser bundle is a single self-contained, minified ESM file of roughly 1.9 MiB
31
+ raw / 0.6 MiB gzipped (the exact measurement and its date live beside the size caps in
32
+ `test/browser-bundle.spec.ts`; a second copy of the numbers here would go stale). A
33
+ source map, `dist/plugin-browser.js.map`, ships beside it with the original sources
34
+ embedded, so devtools still show readable code. It bundles only the TCP-free libp2p
35
+ surface (`@optimystic/db-p2p/rn`) and does **not** import `@quereus/quereus` at
36
+ runtime — registrations are applied against the host-supplied `db`, so it reuses the
37
+ host's Quereus instance rather than instantiating a second one.
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ npm install @serfab/quereus-plugin-sereus
43
+ # or
44
+ yarn add @serfab/quereus-plugin-sereus
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Using it from Quoomb Web (step by step)
50
+
51
+ [Quoomb Web](../../../quereus/packages/quoomb-web) loads plugins by URL: its
52
+ worker calls `import(url)` and registers the module's default export against the
53
+ live database. To connect Quoomb Web to a strand you need to (1) make the
54
+ **browser bundle** reachable at an importable URL, and (2) give it a strand id
55
+ and at least one **browser-dialable** bootstrap address.
56
+
57
+ ### Prerequisites
58
+
59
+ - A **strand id** (UUID) to connect to.
60
+ - For networked use: a libp2p **bootstrap/relay reachable over secure WebSockets**
61
+ (`/wss`) from an `https://` page. Browsers cannot dial raw TCP, and a plain
62
+ `ws://`/`tcp` address will not dial from a TLS origin. WebSockets +
63
+ circuit-relay are the only supported transports in v1.
64
+ - The strand's **sApp schema** (optional — only needed to bootstrap a brand-new
65
+ strand or to expose `App.*` tables before any peer has).
66
+
67
+ ### Step 1 — Make the browser bundle importable
68
+
69
+ Pick one of the hosting options below ([Hosting the browser
70
+ bundle](#hosting-the-browser-bundle)). The quickest are:
71
+
72
+ - **Published CDN URL** (recommended once published):
73
+ `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
74
+ - **Local dev**: copy `dist/plugin-browser.js` into Quoomb Web's `public/` folder
75
+ so it is served same-origin at `http://localhost:5173/plugin-browser.js`.
76
+
77
+ The URL must serve the file with `Content-Type: text/javascript` (or
78
+ `application/javascript`) and CORS that permits the page origin.
79
+
80
+ ### Step 2a — Load via the Plugins panel (manual)
81
+
82
+ 1. Open Quoomb Web → **Plugins** panel.
83
+ 2. Paste the bundle URL into the plugin URL field and install it.
84
+ - The manual installer validates the URL and only accepts **`https://`** (or
85
+ `file://`) ending in `.js`/`.mjs`. A same-origin `http://localhost` URL is
86
+ **rejected here** — use Step 2b (config) for local-dev http, or serve
87
+ Quoomb over https.
88
+ 3. Open the plugin's config and set at least:
89
+ - `strand_id` — the strand UUID.
90
+ - `bootstrap_nodes` — comma-separated `/wss` multiaddrs.
91
+ - `schema` — optional sApp DDL.
92
+ 4. Run a query, e.g. `select * from App.Message`.
93
+
94
+ ### Step 2b — Load via `quoomb.config.json` (autoload)
95
+
96
+ The config/autoload path calls the loader directly and **bypasses the
97
+ https-only check**, so it is the right choice for local `http://localhost`
98
+ during development. In Quoomb Web, open the **Config** modal and import this
99
+ JSON (or set it as `quoomb.config.json`):
100
+
101
+ ```jsonc
102
+ {
103
+ "plugins": [
104
+ {
105
+ "source": "https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus/dist/plugin-browser.js",
106
+ "config": {
107
+ "strand_id": "550e8400-e29b-41d4-a716-446655440000",
108
+ "bootstrap_nodes": "/dns4/relay.example/tcp/443/wss/p2p/Qm...",
109
+ "schema": "table Message (Id integer primary key, Content text not null)"
110
+ }
111
+ }
112
+ ],
113
+ "autoload": true
114
+ }
115
+ ```
116
+
117
+ With `autoload: true`, the plugin loads on session start and applies the schema.
118
+
119
+ ### Step 3 — Query the strand
120
+
121
+ Once loaded, the `optimystic` vtab is the default and `App.*` tables resolve
122
+ against the strand:
123
+
124
+ ```sql
125
+ select * from App.Message;
126
+ insert into App.Message (Id, Content) values (1, 'hello strand');
127
+ ```
128
+
129
+ ### Notes & limitations
130
+
131
+ - **Storage.** When `storage` is not injected, the browser bundle opens a default
132
+ `IndexedDBRawStorage` against an IndexedDB database named
133
+ `sereus-strand-<strandId>`. State survives reload. The plugin treats the
134
+ IndexedDB handle as borrowed and does **not** close it on `shutdown()` — its
135
+ lifecycle is the page/worker.
136
+ - **Network transactor by default.** Every connection commits through the
137
+ strand's libp2p cohort unless it asks otherwise; the in-process `local`
138
+ transactor is a testing/tooling option (`connectToStrand({ transactor:
139
+ 'local' })`), not a browser deployment mode.
140
+ - **WebRTC** requires cross-origin isolation (COOP/COEP) and is tracked as a
141
+ follow-up; v1 is WebSockets + circuit-relay only.
142
+ - **Lifecycle.** Quoomb Web's loader keeps only the manifest and discards the
143
+ plugin's returned `shutdown()`, so the libp2p node is torn down with the
144
+ worker rather than on plugin unload.
145
+
146
+ ---
147
+
148
+ ## Hosting the browser bundle
149
+
150
+ The browser bundle is just an ESM file; it can be served from anywhere that
151
+ returns a JavaScript content type with appropriate CORS.
152
+
153
+ ### Published package on a CDN (recommended)
154
+
155
+ After `yarn pub:quereus-plugin-sereus` (see [Publishing](#publishing)), the npm
156
+ package is mirrored by the public CDNs, which serve the correct content type and
157
+ CORS automatically:
158
+
159
+ - jsDelivr: `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
160
+ - jsDelivr (pinned): `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus@0.7.1/dist/plugin-browser.js`
161
+ - unpkg: `https://unpkg.com/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
162
+
163
+ ### Self-hosted / same-origin
164
+
165
+ Copy `dist/plugin-browser.js` to any static host (or Quoomb Web's `public/`) and
166
+ reference it by URL. Same-origin hosting sidesteps CORS entirely.
167
+
168
+ ### Loading from GitHub
169
+
170
+ Loading the bundle straight from a GitHub repo has a caveat: **`dist/` is
171
+ gitignored**, and `raw.githubusercontent.com` serves files as `text/plain` with
172
+ `X-Content-Type-Options: nosniff`, so the browser refuses to `import()` it as a
173
+ module. Two routes that *do* work:
174
+
175
+ 1. **jsDelivr's GitHub endpoint**, which serves repo files with the correct MIME
176
+ type and CORS — but only files that exist at the ref. Since `dist/` is not
177
+ committed, this requires the built bundle to be present at the ref you point
178
+ at (e.g. a CI step that commits `plugin-browser.js` to a `dist`/`gh-pages`
179
+ branch or a tag):
180
+
181
+ ```
182
+ https://cdn.jsdelivr.net/gh/gotchoices/sereus@<tag-or-branch>/packages/quereus-plugin-sereus/dist/plugin-browser.js
183
+ ```
184
+
185
+ 2. **GitHub Pages**: publish `plugin-browser.js` to a Pages site, which serves it
186
+ with a JavaScript content type and CORS.
187
+
188
+ For most cases the npm-backed CDN URL above is simpler and is the recommended
189
+ GitHub-free hosting path. (Note: GitHub *Release assets* are served as
190
+ `application/octet-stream` and cannot be imported as ESM.)
191
+
192
+ ---
193
+
194
+ ## Programmatic API
195
+
196
+ ```typescript
197
+ import { Database } from '@quereus/quereus';
198
+ import { connectToStrand } from '@serfab/quereus-plugin-sereus';
199
+
200
+ const db = new Database();
201
+ const strand = await connectToStrand(db, {
202
+ strandId: '550e8400-e29b-41d4-a716-446655440000',
203
+ schema: 'table Message (Id integer primary key, Content text not null)',
204
+ bootstrapNodes: ['/ip4/1.2.3.4/tcp/9100/p2p/QmPeerId'],
205
+ });
206
+
207
+ for await (const row of db.eval('select * from App.Message')) {
208
+ console.log(row);
209
+ }
210
+
211
+ await strand.shutdown();
212
+ ```
213
+
214
+ ### Plugin-loader (Node)
215
+
216
+ The `./plugin` export is compatible with Quereus plugin-loader:
217
+
218
+ ```typescript
219
+ import { Database, registerPlugin } from '@quereus/quereus';
220
+ import sereusPlugin from '@serfab/quereus-plugin-sereus/plugin';
221
+
222
+ const db = new Database();
223
+ const result = await registerPlugin(db, sereusPlugin, {
224
+ strand_id: '550e8400-e29b-41d4-a716-446655440000',
225
+ schema: 'table Message (Id integer primary key, Content text not null)',
226
+ bootstrap_nodes: '/ip4/1.2.3.4/tcp/9100/p2p/QmPeerId',
227
+ });
228
+
229
+ await result.shutdown();
230
+ ```
231
+
232
+ ### Local transactor (in-process, no cohort)
233
+
234
+ `transactor: 'local'` sends every transaction straight to this process's raw
235
+ storage — no peers are consulted at all. It is for in-process tests and tooling
236
+ (this package's own e2e suites run on it); applications should stay on the
237
+ default `network` transactor, which a lone node coordinates for itself.
238
+
239
+ Pair it with a persistent `IRawStorage` to survive restart: the same storage
240
+ instance is wired into both the libp2p data path and the optimystic plugin's
241
+ local transactor.
242
+
243
+ ```typescript
244
+ import { FileRawStorage } from '@optimystic/db-p2p-storage-fs';
245
+
246
+ const storage = new FileRawStorage('./data/my-strand');
247
+ const strand = await connectToStrand(db, {
248
+ strandId: 'abc',
249
+ transactor: 'local',
250
+ storage,
251
+ schema: 'table Msg (Id integer primary key, Body text not null)',
252
+ });
253
+ ```
254
+
255
+ The plugin treats `storage` as borrowed: `shutdown()` releases the libp2p node
256
+ and collection factory but does **not** close the storage.
257
+
258
+ ### With an injected node
259
+
260
+ When integrating with an existing CadreNode or other libp2p host:
261
+
262
+ ```typescript
263
+ const strand = await connectToStrand(db, {
264
+ strandId: 'abc',
265
+ libp2pNode: existingNode,
266
+ coordinatedRepo: existingRepo,
267
+ });
268
+ ```
269
+
270
+ The plugin uses the injected node instead of creating one, and will not stop it
271
+ on shutdown.
272
+
273
+ ### A commit's durability
274
+
275
+ The result's `onCommitDurability(listener)` is called after every commit that wrote something, before the commit returns, with who holds what it wrote; `onCommitFullyHeld(listener)` is called once when a commit reported `followed` below full replication becomes held by its whole cohort. Both pass through from `@optimystic/quereus-plugin-optimystic`, whose README explains the report ([A commit's durability](https://github.com/gotchoices/optimystic/blob/main/packages/quereus-plugin-optimystic/README.md#a-commits-durability--pluginoncommitdurabilitylistener)); the `SqlCommitDurability` and `CommitFullyHeld` types are re-exported here. A cadre app reads the same reports through `@serfab/cadre-core` ([strands.md → A commit's durability](../../docs/strands.md#a-commits-durability)).
276
+
277
+ ### Founding a strand: `createEmptyCollections()`
278
+
279
+ A table nobody has written has no collection, so no single machine can show it empty: every read of it asks the other members, and fails while they are offline. The result's `createEmptyCollections(except?)` commits an empty collection for every table the `Strand` and `App` schemas declare that has none yet, and resolves to how many it committed. `except` lists qualified tables (`'Strand.Header'`) the caller is about to write: their inserts create their collections, so every tree of those tables is left out, and a name matching no declared table throws. The founder calls it once, before its founding write, while no other machine can have written to the strand; cadre-core's founder bootstrap does this, leaving out the tables its founding transaction writes ([strands.md → Joining](../../docs/strands.md#joining-no-writes-before-the-first-sync)). Call it only while nothing else writes through the `Database`: it commits whatever is staged in those collections outside any SQL transaction.
280
+
281
+ The same step is exported on its own as `createEmptyCollections(db, bridge, schemaNames, except?)`, for a database composed outside this plugin: `bridge` is the Optimystic plugin's transaction bridge (`txnBridge` on its result), and the call throws when `schemaNames` declare no Optimystic table. cadre-core's control-database genesis calls it with `['CadreControl']` ([architecture.md → Control tables are network-backed](../../docs/architecture.md#control-tables-are-network-backed)).
282
+
283
+ ---
284
+
285
+ ## Configuration
286
+
287
+ ### `StrandConnectionOptions` (programmatic API)
288
+
289
+ | Option | Type | Default | Description |
290
+ |---|---|---|---|
291
+ | `strandId` | string | *required* | UUID of the strand to connect to |
292
+ | `bootstrapNodes` | string[] | `[]` | Bootstrap multiaddrs for peer discovery |
293
+ | `schema` | string | — | sApp schema DDL to apply, as the body of `declare schema App { ... }`. An item the Quereus parser would skip (`create unique index …`, a misspelled item keyword) is refused rather than ignored. Its tables may share names with the strand's own tables; they are stored separately ([Same-Named Tables](../../docs/strands.md#same-named-tables-in-strand-and-app)) |
294
+ | `sAppId` | string | `'unknown'` | sApp author public key |
295
+ | `sAppVersion` | string | `'1.0.0'` | sApp version |
296
+ | `port` | number | `0` | libp2p listening port (0 = random) |
297
+ | `enableCache` | boolean | `true` | Enable optimystic caching |
298
+ | `fretProfile` | `'edge' \| 'core'` | `'edge'` | FRET profile |
299
+ | `clusterSize` | number | `4` | Nodes the **strand's** replication cluster should have (the control network uses its own fixed `CONTROL_REPLICATION_BREADTH` and never reads this). 4 is the smallest breadth whose 0.75 super-majority (`ceil(4 × 0.75) = 3`) still commits with one holder offline; at 2 or 3 every holder must be awake for every write, and at 2 read repair cannot converge at all. Every peer on the strand should use the same value: a peer configured much higher derives a larger expected cohort and can reject a coordinator's smaller declared peer set as a downsize. Frozen when the libp2p node is created, so a change takes effect on the next restart. Must be an integer ≥ 2 (`MIN_CLUSTER_SIZE`, Optimystic's own minimum). Leaving it unset is *not* the same as passing 4 elsewhere — Optimystic's own fallback is 10, so this default is applied here. Ignored when `libp2pNode` is injected |
300
+ | `libp2pNode` | Libp2p | — | Inject an existing libp2p node |
301
+ | `coordinatedRepo` | IRepo | — | Required when `libp2pNode` is provided |
302
+ | `transactor` | `'local' \| 'network' \| 'test'` | `'network'` | Storage engine transactions go through. `'network'`: the strand's libp2p cohort, which a lone node coordinates for itself — the only value an application should use. `'local'`: this process's raw storage directly, no peers consulted (in-process tests and tooling). `'test'`: Optimystic's in-memory fake; no libp2p node is created unless one is injected (unit tests) |
303
+ | `storage` | IRawStorage | — | Persistent raw storage. Borrowed — not closed on `shutdown()` |
304
+
305
+ ### Plugin settings (plugin-loader / Quoomb)
306
+
307
+ | Setting | Type | Default | Description |
308
+ |---|---|---|---|
309
+ | `strand_id` | string | *required* | UUID of the strand |
310
+ | `bootstrap_nodes` | string | `''` | Comma-separated bootstrap multiaddrs |
311
+ | `schema` | string | — | sApp schema DDL |
312
+ | `sapp_id` | string | `'unknown'` | sApp author public key |
313
+ | `sapp_version` | string | `'1.0.0'` | sApp version |
314
+ | `port` | number | `0` | libp2p listening port: an integer from 0 to 65535 (0 = random) |
315
+ | `enable_cache` | boolean | `true` | Enable caching: `true`, `false`, `1` or `0` |
316
+ | `fret_profile` | select | `'edge'` | FRET profile (`'edge'` or `'core'`) |
317
+ | `transactor` | select | `'network'` | Storage engine (`'local'`, `'network'` or `'test'`) — see the `transactor` row above |
318
+ | `storage_path` | string | — | Node entry only: directory for a persistent `FileRawStorage`. The browser entry rejects it (it stores in IndexedDB) |
319
+
320
+ Settings are checked strictly at load. An unknown key (including the retired
321
+ `mode`) or a value of the wrong type fails the load with one error that names
322
+ every offending setting; nothing falls back to a default the user did not
323
+ choose. Leaving a setting out, NULL, or `''` means "use the default" (`''`
324
+ only for the string and select settings). A host that adds keys of its own to
325
+ the plugin's settings map therefore fails to load the plugin. `port` and
326
+ `enable_cache` must be a JSON number and boolean: a `${VAR}` reference in
327
+ `quoomb.config.json` expands to a string, which they reject.
328
+
329
+ **Bootstrap multiaddrs.** Browsers can only dial transports reachable from an
330
+ `https://` page. Use `/wss` (or `/dns/.../wss`, or a relay-fronted multiaddr).
331
+ Plain `/tcp/.../ws` over HTTP won't dial from a TLS origin, and raw TCP is
332
+ unavailable in the browser.
333
+
334
+ ## Provided functions and modules
335
+
336
+ Registered automatically from the composed plugins:
337
+
338
+ - **Virtual table module**: `optimystic` (set as default vtab)
339
+ - **Functions**: `StampId()`, `digest()`, `sign()`, `verify()`, `randomBytes()`
340
+
341
+ ---
342
+
343
+ ## Development
344
+
345
+ ```bash
346
+ yarn build # tsc + esbuild (emits dist/plugin.js and dist/plugin-browser.js)
347
+ yarn test # Run tests (unit + e2e projects, plus browser-bundle smoke tests)
348
+ yarn test:e2e # Run only the e2e project (real libp2p + FileRawStorage)
349
+ yarn dev:test # Watch mode
350
+ ```
351
+
352
+ `build` runs `tsc -p tsconfig.build.json` (emits `dist/plugin.js`,
353
+ `dist/plugin-browser.js` declarations, and other entries) and then
354
+ `node scripts/build-browser.mjs` (overwrites `dist/plugin-browser.js` with the
355
+ bundled artifact and its sourcemap, and prints raw + gzipped size).
356
+
357
+ The browser-bundle smoke tests (`test/browser-bundle.spec.ts` and
358
+ `test/browser-shape.spec.ts`) run under the `unit` project: they build the
359
+ bundle on demand if missing, then check that the artifact parses as ESM, has no
360
+ Node-only or TCP imports, stays under the size caps, and that its default export
361
+ reaches the IndexedDB layer in a jsdom + `fake-indexeddb` environment.
362
+
363
+ The `e2e` project covers two scenarios over real libp2p + `FileRawStorage`:
364
+
365
+ - `test/e2e/local-transactor.e2e.spec.ts` — a single in-process connection on the
366
+ `local` transactor, including cold-restart persistence across the shared
367
+ storage directory.
368
+ - `test/e2e/networked.e2e.spec.ts` — two in-process peers exchanging strand data
369
+ through a `createLibp2pNode` mesh: cross-peer replication, bidirectional
370
+ convergence, and late-joiner catch-up.
371
+
372
+ ## Publishing
373
+
374
+ From the monorepo root:
375
+
376
+ ```bash
377
+ yarn pub:quereus-plugin-sereus # clean + build + npm publish --access public
378
+ ```
379
+
380
+ This is also included in the aggregate `yarn pub` release step. Publishing emits
381
+ both `dist/plugin.js` and the browser bundle `dist/plugin-browser.js` (both are
382
+ in the package `files`), so the CDN URLs under [Hosting the browser
383
+ bundle](#hosting-the-browser-bundle) resolve immediately after a successful
384
+ publish.
385
+
386
+ ## License
387
+
388
+ MIT
389
+ </content>
390
+ </invoke>