@serfab/quereus-plugin-sereus 0.8.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 ADDED
@@ -0,0 +1,360 @@
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
+ - **Networked-only via the loader.** The plugin-loader settings expose the
134
+ `network` transactor; offline/solo `bootstrap` mode is only reachable through
135
+ the programmatic `connectToStrand({ mode: 'bootstrap' })` API.
136
+ - **WebRTC** requires cross-origin isolation (COOP/COEP) and is tracked as a
137
+ follow-up; v1 is WebSockets + circuit-relay only.
138
+ - **Lifecycle.** Quoomb Web's loader keeps only the manifest and discards the
139
+ plugin's returned `shutdown()`, so the libp2p node is torn down with the
140
+ worker rather than on plugin unload.
141
+
142
+ ---
143
+
144
+ ## Hosting the browser bundle
145
+
146
+ The browser bundle is just an ESM file; it can be served from anywhere that
147
+ returns a JavaScript content type with appropriate CORS.
148
+
149
+ ### Published package on a CDN (recommended)
150
+
151
+ After `yarn pub:quereus-plugin-sereus` (see [Publishing](#publishing)), the npm
152
+ package is mirrored by the public CDNs, which serve the correct content type and
153
+ CORS automatically:
154
+
155
+ - jsDelivr: `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
156
+ - jsDelivr (pinned): `https://cdn.jsdelivr.net/npm/@serfab/quereus-plugin-sereus@0.7.1/dist/plugin-browser.js`
157
+ - unpkg: `https://unpkg.com/@serfab/quereus-plugin-sereus/dist/plugin-browser.js`
158
+
159
+ ### Self-hosted / same-origin
160
+
161
+ Copy `dist/plugin-browser.js` to any static host (or Quoomb Web's `public/`) and
162
+ reference it by URL. Same-origin hosting sidesteps CORS entirely.
163
+
164
+ ### Loading from GitHub
165
+
166
+ Loading the bundle straight from a GitHub repo has a caveat: **`dist/` is
167
+ gitignored**, and `raw.githubusercontent.com` serves files as `text/plain` with
168
+ `X-Content-Type-Options: nosniff`, so the browser refuses to `import()` it as a
169
+ module. Two routes that *do* work:
170
+
171
+ 1. **jsDelivr's GitHub endpoint**, which serves repo files with the correct MIME
172
+ type and CORS — but only files that exist at the ref. Since `dist/` is not
173
+ committed, this requires the built bundle to be present at the ref you point
174
+ at (e.g. a CI step that commits `plugin-browser.js` to a `dist`/`gh-pages`
175
+ branch or a tag):
176
+
177
+ ```
178
+ https://cdn.jsdelivr.net/gh/gotchoices/sereus@<tag-or-branch>/packages/quereus-plugin-sereus/dist/plugin-browser.js
179
+ ```
180
+
181
+ 2. **GitHub Pages**: publish `plugin-browser.js` to a Pages site, which serves it
182
+ with a JavaScript content type and CORS.
183
+
184
+ For most cases the npm-backed CDN URL above is simpler and is the recommended
185
+ GitHub-free hosting path. (Note: GitHub *Release assets* are served as
186
+ `application/octet-stream` and cannot be imported as ESM.)
187
+
188
+ ---
189
+
190
+ ## Programmatic API
191
+
192
+ ```typescript
193
+ import { Database } from '@quereus/quereus';
194
+ import { connectToStrand } from '@serfab/quereus-plugin-sereus';
195
+
196
+ const db = new Database();
197
+ const strand = await connectToStrand(db, {
198
+ strandId: '550e8400-e29b-41d4-a716-446655440000',
199
+ schema: 'table Message (Id integer primary key, Content text not null)',
200
+ bootstrapNodes: ['/ip4/1.2.3.4/tcp/9100/p2p/QmPeerId'],
201
+ });
202
+
203
+ for await (const row of db.eval('select * from App.Message')) {
204
+ console.log(row);
205
+ }
206
+
207
+ await strand.shutdown();
208
+ ```
209
+
210
+ ### Plugin-loader (Node)
211
+
212
+ The `./plugin` export is compatible with Quereus plugin-loader:
213
+
214
+ ```typescript
215
+ import { Database, registerPlugin } from '@quereus/quereus';
216
+ import sereusPlugin from '@serfab/quereus-plugin-sereus/plugin';
217
+
218
+ const db = new Database();
219
+ const result = await registerPlugin(db, sereusPlugin, {
220
+ strand_id: '550e8400-e29b-41d4-a716-446655440000',
221
+ schema: 'table Message (Id integer primary key, Content text not null)',
222
+ bootstrap_nodes: '/ip4/1.2.3.4/tcp/9100/p2p/QmPeerId',
223
+ });
224
+
225
+ await result.shutdown();
226
+ ```
227
+
228
+ ### Bootstrap mode (solo node with persistent storage)
229
+
230
+ For a solo node (e.g. first-launch sApp init, single-host dev) that should apply
231
+ schema and accept DML without peer round trips, set `mode: 'bootstrap'` and pass
232
+ a persistent `IRawStorage`. The same storage instance is wired into both the
233
+ libp2p data path and the optimystic plugin's local transactor, so writes persist
234
+ across restart.
235
+
236
+ ```typescript
237
+ import { FileRawStorage } from '@optimystic/db-p2p-storage-fs';
238
+
239
+ const storage = new FileRawStorage('./data/my-strand');
240
+ const strand = await connectToStrand(db, {
241
+ strandId: 'abc',
242
+ mode: 'bootstrap',
243
+ storage,
244
+ schema: 'table Msg (Id integer primary key, Body text not null)',
245
+ });
246
+ ```
247
+
248
+ The plugin treats `storage` as borrowed: `shutdown()` releases the libp2p node
249
+ and collection factory but does **not** close the storage.
250
+
251
+ ### With an injected node
252
+
253
+ When integrating with an existing CadreNode or other libp2p host:
254
+
255
+ ```typescript
256
+ const strand = await connectToStrand(db, {
257
+ strandId: 'abc',
258
+ libp2pNode: existingNode,
259
+ coordinatedRepo: existingRepo,
260
+ });
261
+ ```
262
+
263
+ The plugin uses the injected node instead of creating one, and will not stop it
264
+ on shutdown.
265
+
266
+ ---
267
+
268
+ ## Configuration
269
+
270
+ ### `StrandConnectionOptions` (programmatic API)
271
+
272
+ | Option | Type | Default | Description |
273
+ |---|---|---|---|
274
+ | `strandId` | string | *required* | UUID of the strand to connect to |
275
+ | `bootstrapNodes` | string[] | `[]` | Bootstrap multiaddrs for peer discovery |
276
+ | `schema` | string | — | sApp schema DDL to apply |
277
+ | `sAppId` | string | `'unknown'` | sApp author public key |
278
+ | `sAppVersion` | string | `'1.0.0'` | sApp version |
279
+ | `port` | number | `0` | libp2p listening port (0 = random) |
280
+ | `enableCache` | boolean | `true` | Enable optimystic caching |
281
+ | `fretProfile` | `'edge' \| 'core'` | `'edge'` | FRET profile |
282
+ | `libp2pNode` | Libp2p | — | Inject an existing libp2p node |
283
+ | `coordinatedRepo` | IRepo | — | Required when `libp2pNode` is provided |
284
+ | `mode` | `'bootstrap' \| 'networked'` | `'networked'` | `'bootstrap'` routes through the local transactor (no peer round trips); `'networked'` uses the network transactor |
285
+ | `storage` | IRawStorage | — | Persistent raw storage. Borrowed — not closed on `shutdown()` |
286
+
287
+ ### Plugin settings (plugin-loader / Quoomb)
288
+
289
+ | Setting | Type | Default | Description |
290
+ |---|---|---|---|
291
+ | `strand_id` | string | *required* | UUID of the strand |
292
+ | `bootstrap_nodes` | string | `''` | Comma-separated bootstrap multiaddrs |
293
+ | `schema` | string | — | sApp schema DDL |
294
+ | `sapp_id` | string | `'unknown'` | sApp author public key |
295
+ | `sapp_version` | string | `'1.0.0'` | sApp version |
296
+ | `port` | number | `0` | libp2p listening port |
297
+ | `enable_cache` | boolean | `true` | Enable caching |
298
+ | `fret_profile` | string | `'edge'` | FRET profile (`'edge'` or `'core'`) |
299
+
300
+ **Bootstrap multiaddrs.** Browsers can only dial transports reachable from an
301
+ `https://` page. Use `/wss` (or `/dns/.../wss`, or a relay-fronted multiaddr).
302
+ Plain `/tcp/.../ws` over HTTP won't dial from a TLS origin, and raw TCP is
303
+ unavailable in the browser.
304
+
305
+ ## Provided functions and modules
306
+
307
+ Registered automatically from the composed plugins:
308
+
309
+ - **Virtual table module**: `optimystic` (set as default vtab)
310
+ - **Functions**: `StampId()`, `digest()`, `sign()`, `verify()`, `randomBytes()`
311
+
312
+ ---
313
+
314
+ ## Development
315
+
316
+ ```bash
317
+ yarn build # tsc + esbuild (emits dist/plugin.js and dist/plugin-browser.js)
318
+ yarn test # Run tests (unit + e2e projects, plus browser-bundle smoke tests)
319
+ yarn test:e2e # Run only the e2e project (real libp2p + FileRawStorage)
320
+ yarn dev:test # Watch mode
321
+ ```
322
+
323
+ `build` runs `tsc -p tsconfig.build.json` (emits `dist/plugin.js`,
324
+ `dist/plugin-browser.js` declarations, and other entries) and then
325
+ `node scripts/build-browser.mjs` (overwrites `dist/plugin-browser.js` with the
326
+ bundled artifact and its sourcemap, and prints raw + gzipped size).
327
+
328
+ The browser-bundle smoke tests (`test/browser-bundle.spec.ts` and
329
+ `test/browser-shape.spec.ts`) run under the `unit` project: they build the
330
+ bundle on demand if missing, then check that the artifact parses as ESM, has no
331
+ Node-only or TCP imports, stays under the size caps, and that its default export
332
+ reaches the IndexedDB layer in a jsdom + `fake-indexeddb` environment.
333
+
334
+ The `e2e` project covers two scenarios over real libp2p + `FileRawStorage`:
335
+
336
+ - `test/e2e/bootstrap.e2e.spec.ts` — solo-node bootstrap mode, including
337
+ cold-restart persistence across the shared storage directory.
338
+ - `test/e2e/networked.e2e.spec.ts` — two in-process peers exchanging strand data
339
+ through a `createLibp2pNode` mesh: cross-peer replication, bidirectional
340
+ convergence, and late-joiner catch-up.
341
+
342
+ ## Publishing
343
+
344
+ From the monorepo root:
345
+
346
+ ```bash
347
+ yarn pub:quereus-plugin-sereus # clean + build + npm publish --access public
348
+ ```
349
+
350
+ This is also included in the aggregate `yarn pub` release step. Publishing emits
351
+ both `dist/plugin.js` and the browser bundle `dist/plugin-browser.js` (both are
352
+ in the package `files`), so the CDN URLs under [Hosting the browser
353
+ bundle](#hosting-the-browser-bundle) resolve immediately after a successful
354
+ publish.
355
+
356
+ ## License
357
+
358
+ MIT
359
+ </content>
360
+ </invoke>
@@ -0,0 +1,84 @@
1
+ import type { Database } from '@quereus/quereus';
2
+ import type { Libp2p } from '@libp2p/interface';
3
+ import type { IRawStorage } from '@optimystic/db-p2p';
4
+ import type { StrandConnectionOptions, SereusPluginResult } from './types.js';
5
+ /** Registration shape shared by the crypto and optimystic plugin results. */
6
+ interface PluginRegistrations {
7
+ vtables?: Array<{
8
+ name: string;
9
+ module: unknown;
10
+ auxData: unknown;
11
+ }>;
12
+ functions?: Array<{
13
+ schema: unknown;
14
+ }>;
15
+ collations?: Array<{
16
+ name: string;
17
+ func: unknown;
18
+ normalizer?: unknown;
19
+ }>;
20
+ }
21
+ /**
22
+ * Inline equivalent of `@quereus/quereus`'s `registerPlugin` for plugins that
23
+ * only return functions/vtables/collations. Used directly for the optimystic
24
+ * result (so the composition can read `collectionFactory` off the same object),
25
+ * and by the browser crypto strategy to keep the browser bundle from pulling a
26
+ * duplicate `@quereus/quereus` next to the host's instance.
27
+ */
28
+ export declare function applyRegistrations(db: Database, result: PluginRegistrations): void;
29
+ /** Context passed to the platform's node-creation strategy. */
30
+ export interface CreateNodeContext {
31
+ networkName: string;
32
+ bootstrapNodes: string[];
33
+ fretProfile: 'edge' | 'core';
34
+ /** libp2p listening port (Node TCP). Browser transports ignore it. */
35
+ port: number;
36
+ /** Resolved persistent storage to back the node, if any. */
37
+ storage?: IRawStorage;
38
+ }
39
+ /** Context passed to the platform's storage-resolution strategy. */
40
+ export interface ResolveStorageContext {
41
+ strandId: string;
42
+ resolvedTransactor: string;
43
+ /** The storage the caller passed in `options.storage`, if any. */
44
+ requestedStorage?: IRawStorage;
45
+ }
46
+ /**
47
+ * Platform-specific seams the shared composition delegates to. Everything else
48
+ * (transactor resolution, plugin config, registration, default vtab, hydrate,
49
+ * schema apply, cleanup/shutdown) is identical across Node and browser and lives
50
+ * in {@link composeStrand}.
51
+ */
52
+ export interface StrandPlatform {
53
+ /**
54
+ * Register the crypto plugin against `db`. Node uses `@quereus/quereus`'s
55
+ * `registerPlugin`; the browser inlines `applyRegistrations` to avoid bundling
56
+ * a second `@quereus/quereus`.
57
+ */
58
+ registerCrypto(db: Database): void | Promise<void>;
59
+ /**
60
+ * Resolve the persistent storage to use. Optional — when omitted the caller's
61
+ * `options.storage` is used verbatim (Node). The browser supplies this to
62
+ * default to IndexedDB so a reload survives.
63
+ */
64
+ resolveStorage?(ctx: ResolveStorageContext): Promise<IRawStorage | undefined> | IRawStorage | undefined;
65
+ /**
66
+ * Create a libp2p node. Called only when no node is injected and the
67
+ * transactor needs one. Node creates a TCP node via `@optimystic/db-p2p`;
68
+ * the browser creates a WebSockets + circuit-relay node via the `/rn` entry.
69
+ * Returns the created node (its `coordinatedRepo` is read by the composition).
70
+ */
71
+ createNode(ctx: CreateNodeContext): Promise<Libp2p>;
72
+ }
73
+ /**
74
+ * Connect a Quereus `Database` to a Sereus strand. This is the single shared
75
+ * SQL-surface composition: `connectToStrand` (Node), `connectToStrandBrowser`
76
+ * (browser), and `cadre-core`'s `StrandDatabase` all flow through here, so the
77
+ * hydrate-before-apply fix and any future schema wiring land in one place.
78
+ *
79
+ * Steps: resolve transactor + storage, register crypto + optimystic plugins,
80
+ * acquire (inject or create) the libp2p node, set optimystic as the default
81
+ * vtab, hydrate the catalog, then apply the sApp schema.
82
+ */
83
+ export declare function composeStrand(db: Database, options: StrandConnectionOptions, platform: StrandPlatform): Promise<SereusPluginResult>;
84
+ export {};
@@ -0,0 +1,194 @@
1
+ import debug from 'debug';
2
+ import optimysticPlugin from '@optimystic/quereus-plugin-optimystic/plugin';
3
+ import { STRAND_SCHEMA } from './strand-schema.js';
4
+ const log = debug('sereus:plugin:strand');
5
+ const timing = debug('sereus:plugin:strand:timing');
6
+ /**
7
+ * Inline equivalent of `@quereus/quereus`'s `registerPlugin` for plugins that
8
+ * only return functions/vtables/collations. Used directly for the optimystic
9
+ * result (so the composition can read `collectionFactory` off the same object),
10
+ * and by the browser crypto strategy to keep the browser bundle from pulling a
11
+ * duplicate `@quereus/quereus` next to the host's instance.
12
+ */
13
+ export function applyRegistrations(db, result) {
14
+ for (const vtable of result.vtables ?? []) {
15
+ db.registerModule(vtable.name, vtable.module, vtable.auxData);
16
+ }
17
+ for (const func of result.functions ?? []) {
18
+ db.registerFunction(func.schema);
19
+ }
20
+ for (const collation of result.collations ?? []) {
21
+ db.registerCollation(collation.name, collation.func, collation.normalizer);
22
+ }
23
+ }
24
+ /**
25
+ * Connect a Quereus `Database` to a Sereus strand. This is the single shared
26
+ * SQL-surface composition: `connectToStrand` (Node), `connectToStrandBrowser`
27
+ * (browser), and `cadre-core`'s `StrandDatabase` all flow through here, so the
28
+ * hydrate-before-apply fix and any future schema wiring land in one place.
29
+ *
30
+ * Steps: resolve transactor + storage, register crypto + optimystic plugins,
31
+ * acquire (inject or create) the libp2p node, set optimystic as the default
32
+ * vtab, hydrate the catalog, then apply the sApp schema.
33
+ */
34
+ export async function composeStrand(db, options, platform) {
35
+ const { strandId, bootstrapNodes = [], schema, port = 0, enableCache = true, fretProfile = 'edge', mode, } = options;
36
+ // Resolve the transactor. `mode` is the public knob: bootstrap -> local,
37
+ // networked -> network. The legacy `transactor` override (used by unit
38
+ // tests with `'test'`) only applies when `mode` is unspecified.
39
+ let resolvedTransactor;
40
+ if (mode !== undefined) {
41
+ resolvedTransactor = mode === 'bootstrap' ? 'local' : 'network';
42
+ }
43
+ else if (options.transactor !== undefined) {
44
+ resolvedTransactor = options.transactor;
45
+ }
46
+ else {
47
+ resolvedTransactor = 'network';
48
+ }
49
+ const networkName = `strand-${strandId}`;
50
+ log('Connecting to strand %s (network: %s, mode=%s, transactor=%s)', strandId, networkName, mode ?? '(default)', resolvedTransactor);
51
+ // Resolve storage. Node passes `options.storage` through; the browser
52
+ // defaults to IndexedDB. The resolved instance feeds BOTH the plugin's
53
+ // bootstrap `rawStorageFactory` and node creation, so it must be decided up
54
+ // front (before pluginConfig is built).
55
+ const storage = platform.resolveStorage
56
+ ? await platform.resolveStorage({ strandId, resolvedTransactor, requestedStorage: options.storage })
57
+ : options.storage;
58
+ // 1. Register crypto plugin (digest, sign, verify, etc.)
59
+ await platform.registerCrypto(db);
60
+ log('Registered crypto plugin');
61
+ // 2. Register optimystic plugin with transactor defaults. In local mode with
62
+ // persistent storage, hand the same instance to the plugin so the local
63
+ // transactor persists DML on the host backend (not in-memory).
64
+ const pluginConfig = {
65
+ default_transactor: resolvedTransactor,
66
+ default_key_network: 'libp2p',
67
+ default_network_name: networkName,
68
+ enable_cache: enableCache,
69
+ };
70
+ if (resolvedTransactor === 'local' && storage) {
71
+ pluginConfig.rawStorageFactory = () => storage;
72
+ }
73
+ // The plugin's published signature is `Record<string, SqlValue>` but it
74
+ // also reads `rawStorageFactory` (a function reference) from the same map.
75
+ // Cast through unknown rather than widen the public type.
76
+ const pluginResult = optimysticPlugin(db, pluginConfig);
77
+ applyRegistrations(db, pluginResult);
78
+ log('Registered optimystic vtables and functions');
79
+ const { collectionFactory } = pluginResult;
80
+ let createdNode = null;
81
+ let hydrated;
82
+ try {
83
+ // 3. Acquire the libp2p node. Skip only when this is the unit-test fake
84
+ // transactor with no injected node — every real path needs a node.
85
+ if (resolvedTransactor !== 'test' || options.libp2pNode) {
86
+ let node;
87
+ let coordinatedRepo;
88
+ if (options.libp2pNode) {
89
+ node = options.libp2pNode;
90
+ if (!options.coordinatedRepo) {
91
+ throw new Error('coordinatedRepo is required when libp2pNode is provided');
92
+ }
93
+ coordinatedRepo = options.coordinatedRepo;
94
+ log('Using injected libp2p node');
95
+ }
96
+ else {
97
+ const created = await platform.createNode({ networkName, bootstrapNodes, fretProfile, port, storage });
98
+ createdNode = created;
99
+ node = created;
100
+ const repo = created.coordinatedRepo;
101
+ if (!repo) {
102
+ throw new Error('coordinatedRepo not available on created libp2p node');
103
+ }
104
+ coordinatedRepo = repo;
105
+ log('Created libp2p node (port: %d, fretProfile: %s, storage=%s)', port, fretProfile, !!storage);
106
+ }
107
+ collectionFactory.registerLibp2pNode(networkName, node, coordinatedRepo);
108
+ log('Registered libp2p node with collection factory');
109
+ }
110
+ // 4. Set optimystic as default vtab so `declare schema` tables use it.
111
+ db.setDefaultVtabName('optimystic');
112
+ db.setDefaultVtabArgs({
113
+ networkName,
114
+ transactor: resolvedTransactor,
115
+ keyNetwork: 'libp2p',
116
+ });
117
+ log('Set default vtab to optimystic (networkName=%s, transactor=%s)', networkName, resolvedTransactor);
118
+ // 5. Hydrate Quereus's catalog from persisted optimystic vtab schemas
119
+ // BEFORE applying the sApp schema. Without this, a warm restart diffs the
120
+ // wrapped DDL against an empty catalog and re-emits CREATE TABLE / CREATE
121
+ // INDEX for every persisted object — each round-tripping through optimystic
122
+ // storage (the measured ~160s warm-start regression). No-op on first launch.
123
+ const t0 = performance.now();
124
+ hydrated = await pluginResult.hydrate(db);
125
+ timing('[strand:%s] hydrate: %dms (tables=%d, indexes=%d)', strandId, Math.round(performance.now() - t0), hydrated.tables, hydrated.indexes);
126
+ log('Hydrated catalog for strand %s (tables=%d, indexes=%d)', strandId, hydrated.tables, hydrated.indexes);
127
+ // 6. Apply the strand membership/RBAC schema (`schemas/strand.qsql`,
128
+ // embedded as STRAND_SCHEMA) UNCONDITIONALLY — every strand has membership
129
+ // semantics whether or not an sApp schema is supplied. There are no
130
+ // cross-schema references between `Strand` and `App`, so order is
131
+ // irrelevant; apply `Strand` first for clarity. Because this runs AFTER
132
+ // hydrate, a warm restart diffs the membership DDL against the already-
133
+ // hydrated catalog and re-emits nothing (same fix as the sApp apply).
134
+ //
135
+ // This ticket only makes the tables present and their constraints active;
136
+ // nothing here writes membership rows. Founder bootstrap (Header, founding
137
+ // Authority/Member, invite/peer flows) is owned by the lifecycle ticket
138
+ // `strand-membership-lifecycle-population`.
139
+ log('Applying Strand membership schema for strand %s', strandId);
140
+ await db.exec(`
141
+ declare schema Strand {
142
+ ${STRAND_SCHEMA}
143
+ }
144
+ apply schema Strand;
145
+ `);
146
+ log('Strand membership schema applied');
147
+ // 7. Apply the sApp schema, if provided.
148
+ //
149
+ // SEAM: this and the Strand apply above are the single composition point
150
+ // where strand-level declarative DDL is applied. Keep new schema wiring
151
+ // here so it stays a one-location edit across Node, browser, and cadre-core.
152
+ if (schema) {
153
+ log('Applying sApp schema for strand %s', strandId);
154
+ await db.exec(`
155
+ declare schema App {
156
+ ${schema}
157
+ }
158
+ apply schema App;
159
+ `);
160
+ log('sApp schema applied');
161
+ }
162
+ }
163
+ catch (err) {
164
+ // Clean up resources if setup fails after partial initialization.
165
+ await collectionFactory.shutdown();
166
+ if (createdNode) {
167
+ await createdNode.stop();
168
+ }
169
+ throw err;
170
+ }
171
+ // 8. Return result with hydrate counts + shutdown handler. `shutdown` tears
172
+ // down the collection factory, and `collectionFactory.shutdown()` stops EVERY
173
+ // node registered with it — including an injected one (registered via
174
+ // `registerLibp2pNode`). So an injected node IS stopped on `shutdown`; the
175
+ // `createdNode` guard below only avoids a redundant second `stop()` on a node
176
+ // we created ourselves, it is not what spares an injected node. A caller that
177
+ // needs an injected node to outlive `shutdown` must re-create it (cadre-core's
178
+ // `StrandInstanceManager` instead re-stops it, which is idempotent).
179
+ return {
180
+ vtables: [],
181
+ functions: [],
182
+ collations: [],
183
+ hydrated,
184
+ async shutdown() {
185
+ log('Shutting down strand connection %s', strandId);
186
+ await collectionFactory.shutdown();
187
+ if (createdNode) {
188
+ await createdNode.stop();
189
+ }
190
+ log('Strand connection %s shut down', strandId);
191
+ },
192
+ };
193
+ }
194
+ //# sourceMappingURL=compose-strand.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose-strand.js","sourceRoot":"","sources":["../src/compose-strand.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { Database } from '@quereus/quereus';
2
+ import type { StrandConnectionOptions, SereusPluginResult } from './types.js';
3
+ /**
4
+ * Connect a Quereus Database to a Sereus strand from a browser/worker.
5
+ *
6
+ * Thin adapter over the shared {@link composeStrand} composition, supplying the
7
+ * browser platform seams:
8
+ * - crypto registered inline (no runtime `@quereus/quereus` import, so the
9
+ * bundle does not duplicate the host's instance),
10
+ * - storage defaulting to `IndexedDBRawStorage` keyed by `sereus-strand-<id>`,
11
+ * - a node created via the TCP-free `@optimystic/db-p2p/rn` entry with explicit
12
+ * `webSockets()` + `circuitRelayTransport()` transports.
13
+ */
14
+ export declare function connectToStrandBrowser(db: Database, options: StrandConnectionOptions): Promise<SereusPluginResult>;