@serfab/quereus-plugin-sereus 1.10.0 → 1.11.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,380 +1,380 @@
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
- ---
274
-
275
- ## Configuration
276
-
277
- ### `StrandConnectionOptions` (programmatic API)
278
-
279
- | Option | Type | Default | Description |
280
- |---|---|---|---|
281
- | `strandId` | string | *required* | UUID of the strand to connect to |
282
- | `bootstrapNodes` | string[] | `[]` | Bootstrap multiaddrs for peer discovery |
283
- | `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)) |
284
- | `sAppId` | string | `'unknown'` | sApp author public key |
285
- | `sAppVersion` | string | `'1.0.0'` | sApp version |
286
- | `port` | number | `0` | libp2p listening port (0 = random) |
287
- | `enableCache` | boolean | `true` | Enable optimystic caching |
288
- | `fretProfile` | `'edge' \| 'core'` | `'edge'` | FRET profile |
289
- | `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 |
290
- | `libp2pNode` | Libp2p | — | Inject an existing libp2p node |
291
- | `coordinatedRepo` | IRepo | — | Required when `libp2pNode` is provided |
292
- | `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) |
293
- | `storage` | IRawStorage | — | Persistent raw storage. Borrowed — not closed on `shutdown()` |
294
-
295
- ### Plugin settings (plugin-loader / Quoomb)
296
-
297
- | Setting | Type | Default | Description |
298
- |---|---|---|---|
299
- | `strand_id` | string | *required* | UUID of the strand |
300
- | `bootstrap_nodes` | string | `''` | Comma-separated bootstrap multiaddrs |
301
- | `schema` | string | — | sApp schema DDL |
302
- | `sapp_id` | string | `'unknown'` | sApp author public key |
303
- | `sapp_version` | string | `'1.0.0'` | sApp version |
304
- | `port` | number | `0` | libp2p listening port: an integer from 0 to 65535 (0 = random) |
305
- | `enable_cache` | boolean | `true` | Enable caching: `true`, `false`, `1` or `0` |
306
- | `fret_profile` | select | `'edge'` | FRET profile (`'edge'` or `'core'`) |
307
- | `transactor` | select | `'network'` | Storage engine (`'local'`, `'network'` or `'test'`) — see the `transactor` row above |
308
- | `storage_path` | string | — | Node entry only: directory for a persistent `FileRawStorage`. The browser entry rejects it (it stores in IndexedDB) |
309
-
310
- Settings are checked strictly at load. An unknown key (including the retired
311
- `mode`) or a value of the wrong type fails the load with one error that names
312
- every offending setting; nothing falls back to a default the user did not
313
- choose. Leaving a setting out, NULL, or `''` means "use the default" (`''`
314
- only for the string and select settings). A host that adds keys of its own to
315
- the plugin's settings map therefore fails to load the plugin. `port` and
316
- `enable_cache` must be a JSON number and boolean: a `${VAR}` reference in
317
- `quoomb.config.json` expands to a string, which they reject.
318
-
319
- **Bootstrap multiaddrs.** Browsers can only dial transports reachable from an
320
- `https://` page. Use `/wss` (or `/dns/.../wss`, or a relay-fronted multiaddr).
321
- Plain `/tcp/.../ws` over HTTP won't dial from a TLS origin, and raw TCP is
322
- unavailable in the browser.
323
-
324
- ## Provided functions and modules
325
-
326
- Registered automatically from the composed plugins:
327
-
328
- - **Virtual table module**: `optimystic` (set as default vtab)
329
- - **Functions**: `StampId()`, `digest()`, `sign()`, `verify()`, `randomBytes()`
330
-
331
- ---
332
-
333
- ## Development
334
-
335
- ```bash
336
- yarn build # tsc + esbuild (emits dist/plugin.js and dist/plugin-browser.js)
337
- yarn test # Run tests (unit + e2e projects, plus browser-bundle smoke tests)
338
- yarn test:e2e # Run only the e2e project (real libp2p + FileRawStorage)
339
- yarn dev:test # Watch mode
340
- ```
341
-
342
- `build` runs `tsc -p tsconfig.build.json` (emits `dist/plugin.js`,
343
- `dist/plugin-browser.js` declarations, and other entries) and then
344
- `node scripts/build-browser.mjs` (overwrites `dist/plugin-browser.js` with the
345
- bundled artifact and its sourcemap, and prints raw + gzipped size).
346
-
347
- The browser-bundle smoke tests (`test/browser-bundle.spec.ts` and
348
- `test/browser-shape.spec.ts`) run under the `unit` project: they build the
349
- bundle on demand if missing, then check that the artifact parses as ESM, has no
350
- Node-only or TCP imports, stays under the size caps, and that its default export
351
- reaches the IndexedDB layer in a jsdom + `fake-indexeddb` environment.
352
-
353
- The `e2e` project covers two scenarios over real libp2p + `FileRawStorage`:
354
-
355
- - `test/e2e/local-transactor.e2e.spec.ts` — a single in-process connection on the
356
- `local` transactor, including cold-restart persistence across the shared
357
- storage directory.
358
- - `test/e2e/networked.e2e.spec.ts` — two in-process peers exchanging strand data
359
- through a `createLibp2pNode` mesh: cross-peer replication, bidirectional
360
- convergence, and late-joiner catch-up.
361
-
362
- ## Publishing
363
-
364
- From the monorepo root:
365
-
366
- ```bash
367
- yarn pub:quereus-plugin-sereus # clean + build + npm publish --access public
368
- ```
369
-
370
- This is also included in the aggregate `yarn pub` release step. Publishing emits
371
- both `dist/plugin.js` and the browser bundle `dist/plugin-browser.js` (both are
372
- in the package `files`), so the CDN URLs under [Hosting the browser
373
- bundle](#hosting-the-browser-bundle) resolve immediately after a successful
374
- publish.
375
-
376
- ## License
377
-
378
- MIT
379
- </content>
380
- </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
+ ---
274
+
275
+ ## Configuration
276
+
277
+ ### `StrandConnectionOptions` (programmatic API)
278
+
279
+ | Option | Type | Default | Description |
280
+ |---|---|---|---|
281
+ | `strandId` | string | *required* | UUID of the strand to connect to |
282
+ | `bootstrapNodes` | string[] | `[]` | Bootstrap multiaddrs for peer discovery |
283
+ | `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)) |
284
+ | `sAppId` | string | `'unknown'` | sApp author public key |
285
+ | `sAppVersion` | string | `'1.0.0'` | sApp version |
286
+ | `port` | number | `0` | libp2p listening port (0 = random) |
287
+ | `enableCache` | boolean | `true` | Enable optimystic caching |
288
+ | `fretProfile` | `'edge' \| 'core'` | `'edge'` | FRET profile |
289
+ | `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 |
290
+ | `libp2pNode` | Libp2p | — | Inject an existing libp2p node |
291
+ | `coordinatedRepo` | IRepo | — | Required when `libp2pNode` is provided |
292
+ | `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) |
293
+ | `storage` | IRawStorage | — | Persistent raw storage. Borrowed — not closed on `shutdown()` |
294
+
295
+ ### Plugin settings (plugin-loader / Quoomb)
296
+
297
+ | Setting | Type | Default | Description |
298
+ |---|---|---|---|
299
+ | `strand_id` | string | *required* | UUID of the strand |
300
+ | `bootstrap_nodes` | string | `''` | Comma-separated bootstrap multiaddrs |
301
+ | `schema` | string | — | sApp schema DDL |
302
+ | `sapp_id` | string | `'unknown'` | sApp author public key |
303
+ | `sapp_version` | string | `'1.0.0'` | sApp version |
304
+ | `port` | number | `0` | libp2p listening port: an integer from 0 to 65535 (0 = random) |
305
+ | `enable_cache` | boolean | `true` | Enable caching: `true`, `false`, `1` or `0` |
306
+ | `fret_profile` | select | `'edge'` | FRET profile (`'edge'` or `'core'`) |
307
+ | `transactor` | select | `'network'` | Storage engine (`'local'`, `'network'` or `'test'`) — see the `transactor` row above |
308
+ | `storage_path` | string | — | Node entry only: directory for a persistent `FileRawStorage`. The browser entry rejects it (it stores in IndexedDB) |
309
+
310
+ Settings are checked strictly at load. An unknown key (including the retired
311
+ `mode`) or a value of the wrong type fails the load with one error that names
312
+ every offending setting; nothing falls back to a default the user did not
313
+ choose. Leaving a setting out, NULL, or `''` means "use the default" (`''`
314
+ only for the string and select settings). A host that adds keys of its own to
315
+ the plugin's settings map therefore fails to load the plugin. `port` and
316
+ `enable_cache` must be a JSON number and boolean: a `${VAR}` reference in
317
+ `quoomb.config.json` expands to a string, which they reject.
318
+
319
+ **Bootstrap multiaddrs.** Browsers can only dial transports reachable from an
320
+ `https://` page. Use `/wss` (or `/dns/.../wss`, or a relay-fronted multiaddr).
321
+ Plain `/tcp/.../ws` over HTTP won't dial from a TLS origin, and raw TCP is
322
+ unavailable in the browser.
323
+
324
+ ## Provided functions and modules
325
+
326
+ Registered automatically from the composed plugins:
327
+
328
+ - **Virtual table module**: `optimystic` (set as default vtab)
329
+ - **Functions**: `StampId()`, `digest()`, `sign()`, `verify()`, `randomBytes()`
330
+
331
+ ---
332
+
333
+ ## Development
334
+
335
+ ```bash
336
+ yarn build # tsc + esbuild (emits dist/plugin.js and dist/plugin-browser.js)
337
+ yarn test # Run tests (unit + e2e projects, plus browser-bundle smoke tests)
338
+ yarn test:e2e # Run only the e2e project (real libp2p + FileRawStorage)
339
+ yarn dev:test # Watch mode
340
+ ```
341
+
342
+ `build` runs `tsc -p tsconfig.build.json` (emits `dist/plugin.js`,
343
+ `dist/plugin-browser.js` declarations, and other entries) and then
344
+ `node scripts/build-browser.mjs` (overwrites `dist/plugin-browser.js` with the
345
+ bundled artifact and its sourcemap, and prints raw + gzipped size).
346
+
347
+ The browser-bundle smoke tests (`test/browser-bundle.spec.ts` and
348
+ `test/browser-shape.spec.ts`) run under the `unit` project: they build the
349
+ bundle on demand if missing, then check that the artifact parses as ESM, has no
350
+ Node-only or TCP imports, stays under the size caps, and that its default export
351
+ reaches the IndexedDB layer in a jsdom + `fake-indexeddb` environment.
352
+
353
+ The `e2e` project covers two scenarios over real libp2p + `FileRawStorage`:
354
+
355
+ - `test/e2e/local-transactor.e2e.spec.ts` — a single in-process connection on the
356
+ `local` transactor, including cold-restart persistence across the shared
357
+ storage directory.
358
+ - `test/e2e/networked.e2e.spec.ts` — two in-process peers exchanging strand data
359
+ through a `createLibp2pNode` mesh: cross-peer replication, bidirectional
360
+ convergence, and late-joiner catch-up.
361
+
362
+ ## Publishing
363
+
364
+ From the monorepo root:
365
+
366
+ ```bash
367
+ yarn pub:quereus-plugin-sereus # clean + build + npm publish --access public
368
+ ```
369
+
370
+ This is also included in the aggregate `yarn pub` release step. Publishing emits
371
+ both `dist/plugin.js` and the browser bundle `dist/plugin-browser.js` (both are
372
+ in the package `files`), so the CDN URLs under [Hosting the browser
373
+ bundle](#hosting-the-browser-bundle) resolve immediately after a successful
374
+ publish.
375
+
376
+ ## License
377
+
378
+ MIT
379
+ </content>
380
+ </invoke>