@serfab/quereus-plugin-sereus 0.12.0 → 1.0.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,367 +1,367 @@
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 ESM file (~2.5 MiB raw, ~550 KiB
31
- gzipped). It bundles only the TCP-free libp2p surface (`@optimystic/db-p2p/rn`)
32
- and does **not** import `@quereus/quereus` at runtime — registrations are applied
33
- against the host-supplied `db`, so it reuses the host's Quereus instance rather
34
- than instantiating a second one.
35
-
36
- ## Installation
37
-
38
- ```bash
39
- npm install @serfab/quereus-plugin-sereus
40
- # or
41
- yarn add @serfab/quereus-plugin-sereus
42
- ```
43
-
44
- ---
45
-
46
- ## Using it from Quoomb Web (step by step)
47
-
48
- [Quoomb Web](../../../quereus/packages/quoomb-web) loads plugins by URL: its
49
- worker calls `import(url)` and registers the module's default export against the
50
- live database. To connect Quoomb Web to a strand you need to (1) make the
51
- **browser bundle** reachable at an importable URL, and (2) give it a strand id
52
- and at least one **browser-dialable** bootstrap address.
53
-
54
- ### Prerequisites
55
-
56
- - A **strand id** (UUID) to connect to.
57
- - For networked use: a libp2p **bootstrap/relay reachable over secure WebSockets**
58
- (`/wss`) from an `https://` page. Browsers cannot dial raw TCP, and a plain
59
- `ws://`/`tcp` address will not dial from a TLS origin. WebSockets +
60
- circuit-relay are the only supported transports in v1.
61
- - The strand's **sApp schema** (optional — only needed to bootstrap a brand-new
62
- strand or to expose `App.*` tables before any peer has).
63
-
64
- ### Step 1 — Make the browser bundle importable
65
-
66
- Pick one of the hosting options below ([Hosting the browser
67
- bundle](#hosting-the-browser-bundle)). The quickest are:
68
-
69
- - **Published CDN URL** (recommended once published):
70
- `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
71
- - **Local dev**: copy `dist/plugin-browser.js` into Quoomb Web's `public/` folder
72
- so it is served same-origin at `http://localhost:5173/plugin-browser.js`.
73
-
74
- The URL must serve the file with `Content-Type: text/javascript` (or
75
- `application/javascript`) and CORS that permits the page origin.
76
-
77
- ### Step 2a — Load via the Plugins panel (manual)
78
-
79
- 1. Open Quoomb Web → **Plugins** panel.
80
- 2. Paste the bundle URL into the plugin URL field and install it.
81
- - The manual installer validates the URL and only accepts **`https://`** (or
82
- `file://`) ending in `.js`/`.mjs`. A same-origin `http://localhost` URL is
83
- **rejected here** — use Step 2b (config) for local-dev http, or serve
84
- Quoomb over https.
85
- 3. Open the plugin's config and set at least:
86
- - `strand_id` — the strand UUID.
87
- - `bootstrap_nodes` — comma-separated `/wss` multiaddrs.
88
- - `schema` — optional sApp DDL.
89
- 4. Run a query, e.g. `select * from App.Message`.
90
-
91
- ### Step 2b — Load via `quoomb.config.json` (autoload)
92
-
93
- The config/autoload path calls the loader directly and **bypasses the
94
- https-only check**, so it is the right choice for local `http://localhost`
95
- during development. In Quoomb Web, open the **Config** modal and import this
96
- JSON (or set it as `quoomb.config.json`):
97
-
98
- ```jsonc
99
- {
100
- "plugins": [
101
- {
102
- "source": "https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus/dist/plugin-browser.js",
103
- "config": {
104
- "strand_id": "550e8400-e29b-41d4-a716-446655440000",
105
- "bootstrap_nodes": "/dns4/relay.example/tcp/443/wss/p2p/Qm...",
106
- "schema": "table Message (Id integer primary key, Content text not null)"
107
- }
108
- }
109
- ],
110
- "autoload": true
111
- }
112
- ```
113
-
114
- With `autoload: true`, the plugin loads on session start and applies the schema.
115
-
116
- ### Step 3 — Query the strand
117
-
118
- Once loaded, the `optimystic` vtab is the default and `App.*` tables resolve
119
- against the strand:
120
-
121
- ```sql
122
- select * from App.Message;
123
- insert into App.Message (Id, Content) values (1, 'hello strand');
124
- ```
125
-
126
- ### Notes & limitations
127
-
128
- - **Storage.** When `storage` is not injected, the browser bundle opens a default
129
- `IndexedDBRawStorage` against an IndexedDB database named
130
- `sereus-strand-<strandId>`. State survives reload. The plugin treats the
131
- IndexedDB handle as borrowed and does **not** close it on `shutdown()` — its
132
- lifecycle is the page/worker.
133
- - **Network transactor by default.** Every connection commits through the
134
- strand's libp2p cohort unless it asks otherwise; the in-process `local`
135
- transactor is a testing/tooling option (`connectToStrand({ transactor:
136
- 'local' })`), not a browser deployment mode.
137
- - **WebRTC** requires cross-origin isolation (COOP/COEP) and is tracked as a
138
- follow-up; v1 is WebSockets + circuit-relay only.
139
- - **Lifecycle.** Quoomb Web's loader keeps only the manifest and discards the
140
- plugin's returned `shutdown()`, so the libp2p node is torn down with the
141
- worker rather than on plugin unload.
142
-
143
- ---
144
-
145
- ## Hosting the browser bundle
146
-
147
- The browser bundle is just an ESM file; it can be served from anywhere that
148
- returns a JavaScript content type with appropriate CORS.
149
-
150
- ### Published package on a CDN (recommended)
151
-
152
- After `yarn pub:quereus-plugin-sereus` (see [Publishing](#publishing)), the npm
153
- package is mirrored by the public CDNs, which serve the correct content type and
154
- CORS automatically:
155
-
156
- - jsDelivr: `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
157
- - jsDelivr (pinned): `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus@0.7.1/dist/plugin-browser.js`
158
- - unpkg: `https://unpkg.com/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
159
-
160
- ### Self-hosted / same-origin
161
-
162
- Copy `dist/plugin-browser.js` to any static host (or Quoomb Web's `public/`) and
163
- reference it by URL. Same-origin hosting sidesteps CORS entirely.
164
-
165
- ### Loading from GitHub
166
-
167
- Loading the bundle straight from a GitHub repo has a caveat: **`dist/` is
168
- gitignored**, and `raw.githubusercontent.com` serves files as `text/plain` with
169
- `X-Content-Type-Options: nosniff`, so the browser refuses to `import()` it as a
170
- module. Two routes that *do* work:
171
-
172
- 1. **jsDelivr's GitHub endpoint**, which serves repo files with the correct MIME
173
- type and CORS — but only files that exist at the ref. Since `dist/` is not
174
- committed, this requires the built bundle to be present at the ref you point
175
- at (e.g. a CI step that commits `plugin-browser.js` to a `dist`/`gh-pages`
176
- branch or a tag):
177
-
178
- ```
179
- https://cdn.jsdelivr.net/gh/gotchoices/sereus@<tag-or-branch>/packages/quereus-plugin-sereus/dist/plugin-browser.js
180
- ```
181
-
182
- 2. **GitHub Pages**: publish `plugin-browser.js` to a Pages site, which serves it
183
- with a JavaScript content type and CORS.
184
-
185
- For most cases the npm-backed CDN URL above is simpler and is the recommended
186
- GitHub-free hosting path. (Note: GitHub *Release assets* are served as
187
- `application/octet-stream` and cannot be imported as ESM.)
188
-
189
- ---
190
-
191
- ## Programmatic API
192
-
193
- ```typescript
194
- import { Database } from '@quereus/quereus';
195
- import { connectToStrand } from '@serfab/quereus-plugin-sereus';
196
-
197
- const db = new Database();
198
- const strand = await connectToStrand(db, {
199
- strandId: '550e8400-e29b-41d4-a716-446655440000',
200
- schema: 'table Message (Id integer primary key, Content text not null)',
201
- bootstrapNodes: ['/ip4/1.2.3.4/tcp/9100/p2p/QmPeerId'],
202
- });
203
-
204
- for await (const row of db.eval('select * from App.Message')) {
205
- console.log(row);
206
- }
207
-
208
- await strand.shutdown();
209
- ```
210
-
211
- ### Plugin-loader (Node)
212
-
213
- The `./plugin` export is compatible with Quereus plugin-loader:
214
-
215
- ```typescript
216
- import { Database, registerPlugin } from '@quereus/quereus';
217
- import sereusPlugin from '@serfab/quereus-plugin-sereus/plugin';
218
-
219
- const db = new Database();
220
- const result = await registerPlugin(db, sereusPlugin, {
221
- strand_id: '550e8400-e29b-41d4-a716-446655440000',
222
- schema: 'table Message (Id integer primary key, Content text not null)',
223
- bootstrap_nodes: '/ip4/1.2.3.4/tcp/9100/p2p/QmPeerId',
224
- });
225
-
226
- await result.shutdown();
227
- ```
228
-
229
- ### Local transactor (in-process, no cohort)
230
-
231
- `transactor: 'local'` sends every transaction straight to this process's raw
232
- storage — no peers are consulted at all. It is for in-process tests and tooling
233
- (this package's own e2e suites run on it); applications should stay on the
234
- default `network` transactor, which a lone node coordinates for itself.
235
-
236
- Pair it with a persistent `IRawStorage` to survive restart: the same storage
237
- instance is wired into both the libp2p data path and the optimystic plugin's
238
- local transactor.
239
-
240
- ```typescript
241
- import { FileRawStorage } from '@optimystic/db-p2p-storage-fs';
242
-
243
- const storage = new FileRawStorage('./data/my-strand');
244
- const strand = await connectToStrand(db, {
245
- strandId: 'abc',
246
- transactor: 'local',
247
- storage,
248
- schema: 'table Msg (Id integer primary key, Body text not null)',
249
- });
250
- ```
251
-
252
- The plugin treats `storage` as borrowed: `shutdown()` releases the libp2p node
253
- and collection factory but does **not** close the storage.
254
-
255
- ### With an injected node
256
-
257
- When integrating with an existing CadreNode or other libp2p host:
258
-
259
- ```typescript
260
- const strand = await connectToStrand(db, {
261
- strandId: 'abc',
262
- libp2pNode: existingNode,
263
- coordinatedRepo: existingRepo,
264
- });
265
- ```
266
-
267
- The plugin uses the injected node instead of creating one, and will not stop it
268
- on shutdown.
269
-
270
- ---
271
-
272
- ## Configuration
273
-
274
- ### `StrandConnectionOptions` (programmatic API)
275
-
276
- | Option | Type | Default | Description |
277
- |---|---|---|---|
278
- | `strandId` | string | *required* | UUID of the strand to connect to |
279
- | `bootstrapNodes` | string[] | `[]` | Bootstrap multiaddrs for peer discovery |
280
- | `schema` | string | — | sApp schema DDL to apply |
281
- | `sAppId` | string | `'unknown'` | sApp author public key |
282
- | `sAppVersion` | string | `'1.0.0'` | sApp version |
283
- | `port` | number | `0` | libp2p listening port (0 = random) |
284
- | `enableCache` | boolean | `true` | Enable optimystic caching |
285
- | `fretProfile` | `'edge' \| 'core'` | `'edge'` | FRET profile |
286
- | `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 |
287
- | `libp2pNode` | Libp2p | — | Inject an existing libp2p node |
288
- | `coordinatedRepo` | IRepo | — | Required when `libp2pNode` is provided |
289
- | `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) |
290
- | `storage` | IRawStorage | — | Persistent raw storage. Borrowed — not closed on `shutdown()` |
291
-
292
- ### Plugin settings (plugin-loader / Quoomb)
293
-
294
- | Setting | Type | Default | Description |
295
- |---|---|---|---|
296
- | `strand_id` | string | *required* | UUID of the strand |
297
- | `bootstrap_nodes` | string | `''` | Comma-separated bootstrap multiaddrs |
298
- | `schema` | string | — | sApp schema DDL |
299
- | `sapp_id` | string | `'unknown'` | sApp author public key |
300
- | `sapp_version` | string | `'1.0.0'` | sApp version |
301
- | `port` | number | `0` | libp2p listening port |
302
- | `enable_cache` | boolean | `true` | Enable caching |
303
- | `fret_profile` | string | `'edge'` | FRET profile (`'edge'` or `'core'`) |
304
- | `transactor` | string | `'network'` | Storage engine (`'local'`, `'network'` or `'test'`) — see the `transactor` row above. An unrecognised value is rejected at load |
305
-
306
- **Bootstrap multiaddrs.** Browsers can only dial transports reachable from an
307
- `https://` page. Use `/wss` (or `/dns/.../wss`, or a relay-fronted multiaddr).
308
- Plain `/tcp/.../ws` over HTTP won't dial from a TLS origin, and raw TCP is
309
- unavailable in the browser.
310
-
311
- ## Provided functions and modules
312
-
313
- Registered automatically from the composed plugins:
314
-
315
- - **Virtual table module**: `optimystic` (set as default vtab)
316
- - **Functions**: `StampId()`, `digest()`, `sign()`, `verify()`, `randomBytes()`
317
-
318
- ---
319
-
320
- ## Development
321
-
322
- ```bash
323
- yarn build # tsc + esbuild (emits dist/plugin.js and dist/plugin-browser.js)
324
- yarn test # Run tests (unit + e2e projects, plus browser-bundle smoke tests)
325
- yarn test:e2e # Run only the e2e project (real libp2p + FileRawStorage)
326
- yarn dev:test # Watch mode
327
- ```
328
-
329
- `build` runs `tsc -p tsconfig.build.json` (emits `dist/plugin.js`,
330
- `dist/plugin-browser.js` declarations, and other entries) and then
331
- `node scripts/build-browser.mjs` (overwrites `dist/plugin-browser.js` with the
332
- bundled artifact and its sourcemap, and prints raw + gzipped size).
333
-
334
- The browser-bundle smoke tests (`test/browser-bundle.spec.ts` and
335
- `test/browser-shape.spec.ts`) run under the `unit` project: they build the
336
- bundle on demand if missing, then check that the artifact parses as ESM, has no
337
- Node-only or TCP imports, stays under the size caps, and that its default export
338
- reaches the IndexedDB layer in a jsdom + `fake-indexeddb` environment.
339
-
340
- The `e2e` project covers two scenarios over real libp2p + `FileRawStorage`:
341
-
342
- - `test/e2e/local-transactor.e2e.spec.ts` — a single in-process connection on the
343
- `local` transactor, including cold-restart persistence across the shared
344
- storage directory.
345
- - `test/e2e/networked.e2e.spec.ts` — two in-process peers exchanging strand data
346
- through a `createLibp2pNode` mesh: cross-peer replication, bidirectional
347
- convergence, and late-joiner catch-up.
348
-
349
- ## Publishing
350
-
351
- From the monorepo root:
352
-
353
- ```bash
354
- yarn pub:quereus-plugin-sereus # clean + build + npm publish --access public
355
- ```
356
-
357
- This is also included in the aggregate `yarn pub` release step. Publishing emits
358
- both `dist/plugin.js` and the browser bundle `dist/plugin-browser.js` (both are
359
- in the package `files`), so the CDN URLs under [Hosting the browser
360
- bundle](#hosting-the-browser-bundle) resolve immediately after a successful
361
- publish.
362
-
363
- ## License
364
-
365
- MIT
366
- </content>
367
- </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 ESM file (~2.5 MiB raw, ~550 KiB
31
+ gzipped). It bundles only the TCP-free libp2p surface (`@optimystic/db-p2p/rn`)
32
+ and does **not** import `@quereus/quereus` at runtime — registrations are applied
33
+ against the host-supplied `db`, so it reuses the host's Quereus instance rather
34
+ than instantiating a second one.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ npm install @serfab/quereus-plugin-sereus
40
+ # or
41
+ yarn add @serfab/quereus-plugin-sereus
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Using it from Quoomb Web (step by step)
47
+
48
+ [Quoomb Web](../../../quereus/packages/quoomb-web) loads plugins by URL: its
49
+ worker calls `import(url)` and registers the module's default export against the
50
+ live database. To connect Quoomb Web to a strand you need to (1) make the
51
+ **browser bundle** reachable at an importable URL, and (2) give it a strand id
52
+ and at least one **browser-dialable** bootstrap address.
53
+
54
+ ### Prerequisites
55
+
56
+ - A **strand id** (UUID) to connect to.
57
+ - For networked use: a libp2p **bootstrap/relay reachable over secure WebSockets**
58
+ (`/wss`) from an `https://` page. Browsers cannot dial raw TCP, and a plain
59
+ `ws://`/`tcp` address will not dial from a TLS origin. WebSockets +
60
+ circuit-relay are the only supported transports in v1.
61
+ - The strand's **sApp schema** (optional — only needed to bootstrap a brand-new
62
+ strand or to expose `App.*` tables before any peer has).
63
+
64
+ ### Step 1 — Make the browser bundle importable
65
+
66
+ Pick one of the hosting options below ([Hosting the browser
67
+ bundle](#hosting-the-browser-bundle)). The quickest are:
68
+
69
+ - **Published CDN URL** (recommended once published):
70
+ `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
71
+ - **Local dev**: copy `dist/plugin-browser.js` into Quoomb Web's `public/` folder
72
+ so it is served same-origin at `http://localhost:5173/plugin-browser.js`.
73
+
74
+ The URL must serve the file with `Content-Type: text/javascript` (or
75
+ `application/javascript`) and CORS that permits the page origin.
76
+
77
+ ### Step 2a — Load via the Plugins panel (manual)
78
+
79
+ 1. Open Quoomb Web → **Plugins** panel.
80
+ 2. Paste the bundle URL into the plugin URL field and install it.
81
+ - The manual installer validates the URL and only accepts **`https://`** (or
82
+ `file://`) ending in `.js`/`.mjs`. A same-origin `http://localhost` URL is
83
+ **rejected here** — use Step 2b (config) for local-dev http, or serve
84
+ Quoomb over https.
85
+ 3. Open the plugin's config and set at least:
86
+ - `strand_id` — the strand UUID.
87
+ - `bootstrap_nodes` — comma-separated `/wss` multiaddrs.
88
+ - `schema` — optional sApp DDL.
89
+ 4. Run a query, e.g. `select * from App.Message`.
90
+
91
+ ### Step 2b — Load via `quoomb.config.json` (autoload)
92
+
93
+ The config/autoload path calls the loader directly and **bypasses the
94
+ https-only check**, so it is the right choice for local `http://localhost`
95
+ during development. In Quoomb Web, open the **Config** modal and import this
96
+ JSON (or set it as `quoomb.config.json`):
97
+
98
+ ```jsonc
99
+ {
100
+ "plugins": [
101
+ {
102
+ "source": "https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus/dist/plugin-browser.js",
103
+ "config": {
104
+ "strand_id": "550e8400-e29b-41d4-a716-446655440000",
105
+ "bootstrap_nodes": "/dns4/relay.example/tcp/443/wss/p2p/Qm...",
106
+ "schema": "table Message (Id integer primary key, Content text not null)"
107
+ }
108
+ }
109
+ ],
110
+ "autoload": true
111
+ }
112
+ ```
113
+
114
+ With `autoload: true`, the plugin loads on session start and applies the schema.
115
+
116
+ ### Step 3 — Query the strand
117
+
118
+ Once loaded, the `optimystic` vtab is the default and `App.*` tables resolve
119
+ against the strand:
120
+
121
+ ```sql
122
+ select * from App.Message;
123
+ insert into App.Message (Id, Content) values (1, 'hello strand');
124
+ ```
125
+
126
+ ### Notes & limitations
127
+
128
+ - **Storage.** When `storage` is not injected, the browser bundle opens a default
129
+ `IndexedDBRawStorage` against an IndexedDB database named
130
+ `sereus-strand-<strandId>`. State survives reload. The plugin treats the
131
+ IndexedDB handle as borrowed and does **not** close it on `shutdown()` — its
132
+ lifecycle is the page/worker.
133
+ - **Network transactor by default.** Every connection commits through the
134
+ strand's libp2p cohort unless it asks otherwise; the in-process `local`
135
+ transactor is a testing/tooling option (`connectToStrand({ transactor:
136
+ 'local' })`), not a browser deployment mode.
137
+ - **WebRTC** requires cross-origin isolation (COOP/COEP) and is tracked as a
138
+ follow-up; v1 is WebSockets + circuit-relay only.
139
+ - **Lifecycle.** Quoomb Web's loader keeps only the manifest and discards the
140
+ plugin's returned `shutdown()`, so the libp2p node is torn down with the
141
+ worker rather than on plugin unload.
142
+
143
+ ---
144
+
145
+ ## Hosting the browser bundle
146
+
147
+ The browser bundle is just an ESM file; it can be served from anywhere that
148
+ returns a JavaScript content type with appropriate CORS.
149
+
150
+ ### Published package on a CDN (recommended)
151
+
152
+ After `yarn pub:quereus-plugin-sereus` (see [Publishing](#publishing)), the npm
153
+ package is mirrored by the public CDNs, which serve the correct content type and
154
+ CORS automatically:
155
+
156
+ - jsDelivr: `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
157
+ - jsDelivr (pinned): `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus@0.7.1/dist/plugin-browser.js`
158
+ - unpkg: `https://unpkg.com/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
159
+
160
+ ### Self-hosted / same-origin
161
+
162
+ Copy `dist/plugin-browser.js` to any static host (or Quoomb Web's `public/`) and
163
+ reference it by URL. Same-origin hosting sidesteps CORS entirely.
164
+
165
+ ### Loading from GitHub
166
+
167
+ Loading the bundle straight from a GitHub repo has a caveat: **`dist/` is
168
+ gitignored**, and `raw.githubusercontent.com` serves files as `text/plain` with
169
+ `X-Content-Type-Options: nosniff`, so the browser refuses to `import()` it as a
170
+ module. Two routes that *do* work:
171
+
172
+ 1. **jsDelivr's GitHub endpoint**, which serves repo files with the correct MIME
173
+ type and CORS — but only files that exist at the ref. Since `dist/` is not
174
+ committed, this requires the built bundle to be present at the ref you point
175
+ at (e.g. a CI step that commits `plugin-browser.js` to a `dist`/`gh-pages`
176
+ branch or a tag):
177
+
178
+ ```
179
+ https://cdn.jsdelivr.net/gh/gotchoices/sereus@<tag-or-branch>/packages/quereus-plugin-sereus/dist/plugin-browser.js
180
+ ```
181
+
182
+ 2. **GitHub Pages**: publish `plugin-browser.js` to a Pages site, which serves it
183
+ with a JavaScript content type and CORS.
184
+
185
+ For most cases the npm-backed CDN URL above is simpler and is the recommended
186
+ GitHub-free hosting path. (Note: GitHub *Release assets* are served as
187
+ `application/octet-stream` and cannot be imported as ESM.)
188
+
189
+ ---
190
+
191
+ ## Programmatic API
192
+
193
+ ```typescript
194
+ import { Database } from '@quereus/quereus';
195
+ import { connectToStrand } from '@serfab/quereus-plugin-sereus';
196
+
197
+ const db = new Database();
198
+ const strand = await connectToStrand(db, {
199
+ strandId: '550e8400-e29b-41d4-a716-446655440000',
200
+ schema: 'table Message (Id integer primary key, Content text not null)',
201
+ bootstrapNodes: ['/ip4/1.2.3.4/tcp/9100/p2p/QmPeerId'],
202
+ });
203
+
204
+ for await (const row of db.eval('select * from App.Message')) {
205
+ console.log(row);
206
+ }
207
+
208
+ await strand.shutdown();
209
+ ```
210
+
211
+ ### Plugin-loader (Node)
212
+
213
+ The `./plugin` export is compatible with Quereus plugin-loader:
214
+
215
+ ```typescript
216
+ import { Database, registerPlugin } from '@quereus/quereus';
217
+ import sereusPlugin from '@serfab/quereus-plugin-sereus/plugin';
218
+
219
+ const db = new Database();
220
+ const result = await registerPlugin(db, sereusPlugin, {
221
+ strand_id: '550e8400-e29b-41d4-a716-446655440000',
222
+ schema: 'table Message (Id integer primary key, Content text not null)',
223
+ bootstrap_nodes: '/ip4/1.2.3.4/tcp/9100/p2p/QmPeerId',
224
+ });
225
+
226
+ await result.shutdown();
227
+ ```
228
+
229
+ ### Local transactor (in-process, no cohort)
230
+
231
+ `transactor: 'local'` sends every transaction straight to this process's raw
232
+ storage — no peers are consulted at all. It is for in-process tests and tooling
233
+ (this package's own e2e suites run on it); applications should stay on the
234
+ default `network` transactor, which a lone node coordinates for itself.
235
+
236
+ Pair it with a persistent `IRawStorage` to survive restart: the same storage
237
+ instance is wired into both the libp2p data path and the optimystic plugin's
238
+ local transactor.
239
+
240
+ ```typescript
241
+ import { FileRawStorage } from '@optimystic/db-p2p-storage-fs';
242
+
243
+ const storage = new FileRawStorage('./data/my-strand');
244
+ const strand = await connectToStrand(db, {
245
+ strandId: 'abc',
246
+ transactor: 'local',
247
+ storage,
248
+ schema: 'table Msg (Id integer primary key, Body text not null)',
249
+ });
250
+ ```
251
+
252
+ The plugin treats `storage` as borrowed: `shutdown()` releases the libp2p node
253
+ and collection factory but does **not** close the storage.
254
+
255
+ ### With an injected node
256
+
257
+ When integrating with an existing CadreNode or other libp2p host:
258
+
259
+ ```typescript
260
+ const strand = await connectToStrand(db, {
261
+ strandId: 'abc',
262
+ libp2pNode: existingNode,
263
+ coordinatedRepo: existingRepo,
264
+ });
265
+ ```
266
+
267
+ The plugin uses the injected node instead of creating one, and will not stop it
268
+ on shutdown.
269
+
270
+ ---
271
+
272
+ ## Configuration
273
+
274
+ ### `StrandConnectionOptions` (programmatic API)
275
+
276
+ | Option | Type | Default | Description |
277
+ |---|---|---|---|
278
+ | `strandId` | string | *required* | UUID of the strand to connect to |
279
+ | `bootstrapNodes` | string[] | `[]` | Bootstrap multiaddrs for peer discovery |
280
+ | `schema` | string | — | sApp schema DDL to apply, as the body of `declare schema App { ... }`. 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)) |
281
+ | `sAppId` | string | `'unknown'` | sApp author public key |
282
+ | `sAppVersion` | string | `'1.0.0'` | sApp version |
283
+ | `port` | number | `0` | libp2p listening port (0 = random) |
284
+ | `enableCache` | boolean | `true` | Enable optimystic caching |
285
+ | `fretProfile` | `'edge' \| 'core'` | `'edge'` | FRET profile |
286
+ | `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 |
287
+ | `libp2pNode` | Libp2p | — | Inject an existing libp2p node |
288
+ | `coordinatedRepo` | IRepo | — | Required when `libp2pNode` is provided |
289
+ | `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) |
290
+ | `storage` | IRawStorage | — | Persistent raw storage. Borrowed — not closed on `shutdown()` |
291
+
292
+ ### Plugin settings (plugin-loader / Quoomb)
293
+
294
+ | Setting | Type | Default | Description |
295
+ |---|---|---|---|
296
+ | `strand_id` | string | *required* | UUID of the strand |
297
+ | `bootstrap_nodes` | string | `''` | Comma-separated bootstrap multiaddrs |
298
+ | `schema` | string | — | sApp schema DDL |
299
+ | `sapp_id` | string | `'unknown'` | sApp author public key |
300
+ | `sapp_version` | string | `'1.0.0'` | sApp version |
301
+ | `port` | number | `0` | libp2p listening port |
302
+ | `enable_cache` | boolean | `true` | Enable caching |
303
+ | `fret_profile` | string | `'edge'` | FRET profile (`'edge'` or `'core'`) |
304
+ | `transactor` | string | `'network'` | Storage engine (`'local'`, `'network'` or `'test'`) — see the `transactor` row above. An unrecognised value is rejected at load |
305
+
306
+ **Bootstrap multiaddrs.** Browsers can only dial transports reachable from an
307
+ `https://` page. Use `/wss` (or `/dns/.../wss`, or a relay-fronted multiaddr).
308
+ Plain `/tcp/.../ws` over HTTP won't dial from a TLS origin, and raw TCP is
309
+ unavailable in the browser.
310
+
311
+ ## Provided functions and modules
312
+
313
+ Registered automatically from the composed plugins:
314
+
315
+ - **Virtual table module**: `optimystic` (set as default vtab)
316
+ - **Functions**: `StampId()`, `digest()`, `sign()`, `verify()`, `randomBytes()`
317
+
318
+ ---
319
+
320
+ ## Development
321
+
322
+ ```bash
323
+ yarn build # tsc + esbuild (emits dist/plugin.js and dist/plugin-browser.js)
324
+ yarn test # Run tests (unit + e2e projects, plus browser-bundle smoke tests)
325
+ yarn test:e2e # Run only the e2e project (real libp2p + FileRawStorage)
326
+ yarn dev:test # Watch mode
327
+ ```
328
+
329
+ `build` runs `tsc -p tsconfig.build.json` (emits `dist/plugin.js`,
330
+ `dist/plugin-browser.js` declarations, and other entries) and then
331
+ `node scripts/build-browser.mjs` (overwrites `dist/plugin-browser.js` with the
332
+ bundled artifact and its sourcemap, and prints raw + gzipped size).
333
+
334
+ The browser-bundle smoke tests (`test/browser-bundle.spec.ts` and
335
+ `test/browser-shape.spec.ts`) run under the `unit` project: they build the
336
+ bundle on demand if missing, then check that the artifact parses as ESM, has no
337
+ Node-only or TCP imports, stays under the size caps, and that its default export
338
+ reaches the IndexedDB layer in a jsdom + `fake-indexeddb` environment.
339
+
340
+ The `e2e` project covers two scenarios over real libp2p + `FileRawStorage`:
341
+
342
+ - `test/e2e/local-transactor.e2e.spec.ts` — a single in-process connection on the
343
+ `local` transactor, including cold-restart persistence across the shared
344
+ storage directory.
345
+ - `test/e2e/networked.e2e.spec.ts` — two in-process peers exchanging strand data
346
+ through a `createLibp2pNode` mesh: cross-peer replication, bidirectional
347
+ convergence, and late-joiner catch-up.
348
+
349
+ ## Publishing
350
+
351
+ From the monorepo root:
352
+
353
+ ```bash
354
+ yarn pub:quereus-plugin-sereus # clean + build + npm publish --access public
355
+ ```
356
+
357
+ This is also included in the aggregate `yarn pub` release step. Publishing emits
358
+ both `dist/plugin.js` and the browser bundle `dist/plugin-browser.js` (both are
359
+ in the package `files`), so the CDN URLs under [Hosting the browser
360
+ bundle](#hosting-the-browser-bundle) resolve immediately after a successful
361
+ publish.
362
+
363
+ ## License
364
+
365
+ MIT
366
+ </content>
367
+ </invoke>