@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 +360 -0
- package/dist/compose-strand.d.ts +84 -0
- package/dist/compose-strand.js +194 -0
- package/dist/compose-strand.js.map +1 -0
- package/dist/connect-browser.d.ts +14 -0
- package/dist/connect-browser.js +45 -0
- package/dist/connect-browser.js.map +1 -0
- package/dist/connect.d.ts +10 -0
- package/dist/connect.js +31 -0
- package/dist/connect.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/parse-config.d.ts +7 -0
- package/dist/parse-config.js +47 -0
- package/dist/parse-config.js.map +1 -0
- package/dist/plugin-browser.d.ts +19 -0
- package/dist/plugin-browser.js +94664 -0
- package/dist/plugin-browser.js.map +7 -0
- package/dist/plugin.d.ts +17 -0
- package/dist/plugin.js +30 -0
- package/dist/plugin.js.map +1 -0
- package/dist/strand-schema.d.ts +30 -0
- package/dist/strand-schema.js +161 -0
- package/dist/strand-schema.js.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +145 -0
- package/src/compose-strand.ts +314 -0
- package/src/connect-browser.ts +56 -0
- package/src/connect.ts +37 -0
- package/src/index.ts +2 -0
- package/src/parse-config.ts +55 -0
- package/src/plugin-browser.ts +29 -0
- package/src/plugin.ts +39 -0
- package/src/strand-schema.ts +160 -0
- package/src/types.ts +69 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import type { Database } from '@quereus/quereus';
|
|
3
|
+
import optimysticPlugin from '@optimystic/quereus-plugin-optimystic/plugin';
|
|
4
|
+
import type { Libp2p } from '@libp2p/interface';
|
|
5
|
+
import type { IRepo } from '@optimystic/db-core';
|
|
6
|
+
import type { IRawStorage } from '@optimystic/db-p2p';
|
|
7
|
+
import type { StrandConnectionOptions, SereusPluginResult, Libp2pNodeWithRepo } from './types.js';
|
|
8
|
+
import { STRAND_SCHEMA } from './strand-schema.js';
|
|
9
|
+
|
|
10
|
+
const log = debug('sereus:plugin:strand');
|
|
11
|
+
const timing = debug('sereus:plugin:strand:timing');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Minimal interface for the CollectionFactory returned by the optimystic plugin.
|
|
15
|
+
* We only need the methods this composition actually uses.
|
|
16
|
+
*/
|
|
17
|
+
interface CollectionFactory {
|
|
18
|
+
registerLibp2pNode(networkName: string, node: Libp2p, coordinatedRepo: IRepo): void;
|
|
19
|
+
shutdown(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Result of registering the optimystic plugin. The published plugin signature
|
|
24
|
+
* does not surface `collectionFactory` / `hydrate`, so this is the local shim
|
|
25
|
+
* over what `@optimystic/quereus-plugin-optimystic/plugin` actually returns
|
|
26
|
+
* (see its `plugin.ts` — `hydrate` delegates to `optimysticModule.hydrateCatalog`).
|
|
27
|
+
*/
|
|
28
|
+
interface OptimysticPluginResult {
|
|
29
|
+
collectionFactory: CollectionFactory;
|
|
30
|
+
vtables: Array<{ name: string; module: unknown; auxData: unknown }>;
|
|
31
|
+
functions: Array<{ schema: unknown }>;
|
|
32
|
+
collations?: Array<{ name: string; func: unknown; normalizer?: unknown }>;
|
|
33
|
+
/**
|
|
34
|
+
* Hydrate Quereus's in-memory catalog from persisted optimystic vtab schemas.
|
|
35
|
+
* Must run BEFORE `apply schema App;` on a warm restart so the declarative
|
|
36
|
+
* diff sees the existing tables and skips re-emitting CREATE TABLE / CREATE
|
|
37
|
+
* INDEX for each persisted object. Idempotent; `{ tables: 0, indexes: 0 }`
|
|
38
|
+
* on cold start.
|
|
39
|
+
*/
|
|
40
|
+
hydrate: (db: Database) => Promise<{ tables: number; indexes: number }>;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Registration shape shared by the crypto and optimystic plugin results. */
|
|
45
|
+
interface PluginRegistrations {
|
|
46
|
+
vtables?: Array<{ name: string; module: unknown; auxData: unknown }>;
|
|
47
|
+
functions?: Array<{ schema: unknown }>;
|
|
48
|
+
collations?: Array<{ name: string; func: unknown; normalizer?: unknown }>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Inline equivalent of `@quereus/quereus`'s `registerPlugin` for plugins that
|
|
53
|
+
* only return functions/vtables/collations. Used directly for the optimystic
|
|
54
|
+
* result (so the composition can read `collectionFactory` off the same object),
|
|
55
|
+
* and by the browser crypto strategy to keep the browser bundle from pulling a
|
|
56
|
+
* duplicate `@quereus/quereus` next to the host's instance.
|
|
57
|
+
*/
|
|
58
|
+
export function applyRegistrations(db: Database, result: PluginRegistrations): void {
|
|
59
|
+
for (const vtable of result.vtables ?? []) {
|
|
60
|
+
db.registerModule(vtable.name, vtable.module as any, vtable.auxData);
|
|
61
|
+
}
|
|
62
|
+
for (const func of result.functions ?? []) {
|
|
63
|
+
db.registerFunction(func.schema as any);
|
|
64
|
+
}
|
|
65
|
+
for (const collation of result.collations ?? []) {
|
|
66
|
+
db.registerCollation(collation.name, collation.func as any, collation.normalizer as any);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Context passed to the platform's node-creation strategy. */
|
|
71
|
+
export interface CreateNodeContext {
|
|
72
|
+
networkName: string;
|
|
73
|
+
bootstrapNodes: string[];
|
|
74
|
+
fretProfile: 'edge' | 'core';
|
|
75
|
+
/** libp2p listening port (Node TCP). Browser transports ignore it. */
|
|
76
|
+
port: number;
|
|
77
|
+
/** Resolved persistent storage to back the node, if any. */
|
|
78
|
+
storage?: IRawStorage;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Context passed to the platform's storage-resolution strategy. */
|
|
82
|
+
export interface ResolveStorageContext {
|
|
83
|
+
strandId: string;
|
|
84
|
+
resolvedTransactor: string;
|
|
85
|
+
/** The storage the caller passed in `options.storage`, if any. */
|
|
86
|
+
requestedStorage?: IRawStorage;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Platform-specific seams the shared composition delegates to. Everything else
|
|
91
|
+
* (transactor resolution, plugin config, registration, default vtab, hydrate,
|
|
92
|
+
* schema apply, cleanup/shutdown) is identical across Node and browser and lives
|
|
93
|
+
* in {@link composeStrand}.
|
|
94
|
+
*/
|
|
95
|
+
export interface StrandPlatform {
|
|
96
|
+
/**
|
|
97
|
+
* Register the crypto plugin against `db`. Node uses `@quereus/quereus`'s
|
|
98
|
+
* `registerPlugin`; the browser inlines `applyRegistrations` to avoid bundling
|
|
99
|
+
* a second `@quereus/quereus`.
|
|
100
|
+
*/
|
|
101
|
+
registerCrypto(db: Database): void | Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the persistent storage to use. Optional — when omitted the caller's
|
|
104
|
+
* `options.storage` is used verbatim (Node). The browser supplies this to
|
|
105
|
+
* default to IndexedDB so a reload survives.
|
|
106
|
+
*/
|
|
107
|
+
resolveStorage?(ctx: ResolveStorageContext): Promise<IRawStorage | undefined> | IRawStorage | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Create a libp2p node. Called only when no node is injected and the
|
|
110
|
+
* transactor needs one. Node creates a TCP node via `@optimystic/db-p2p`;
|
|
111
|
+
* the browser creates a WebSockets + circuit-relay node via the `/rn` entry.
|
|
112
|
+
* Returns the created node (its `coordinatedRepo` is read by the composition).
|
|
113
|
+
*/
|
|
114
|
+
createNode(ctx: CreateNodeContext): Promise<Libp2p>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Connect a Quereus `Database` to a Sereus strand. This is the single shared
|
|
119
|
+
* SQL-surface composition: `connectToStrand` (Node), `connectToStrandBrowser`
|
|
120
|
+
* (browser), and `cadre-core`'s `StrandDatabase` all flow through here, so the
|
|
121
|
+
* hydrate-before-apply fix and any future schema wiring land in one place.
|
|
122
|
+
*
|
|
123
|
+
* Steps: resolve transactor + storage, register crypto + optimystic plugins,
|
|
124
|
+
* acquire (inject or create) the libp2p node, set optimystic as the default
|
|
125
|
+
* vtab, hydrate the catalog, then apply the sApp schema.
|
|
126
|
+
*/
|
|
127
|
+
export async function composeStrand(
|
|
128
|
+
db: Database,
|
|
129
|
+
options: StrandConnectionOptions,
|
|
130
|
+
platform: StrandPlatform,
|
|
131
|
+
): Promise<SereusPluginResult> {
|
|
132
|
+
const {
|
|
133
|
+
strandId,
|
|
134
|
+
bootstrapNodes = [],
|
|
135
|
+
schema,
|
|
136
|
+
port = 0,
|
|
137
|
+
enableCache = true,
|
|
138
|
+
fretProfile = 'edge',
|
|
139
|
+
mode,
|
|
140
|
+
} = options;
|
|
141
|
+
|
|
142
|
+
// Resolve the transactor. `mode` is the public knob: bootstrap -> local,
|
|
143
|
+
// networked -> network. The legacy `transactor` override (used by unit
|
|
144
|
+
// tests with `'test'`) only applies when `mode` is unspecified.
|
|
145
|
+
let resolvedTransactor: string;
|
|
146
|
+
if (mode !== undefined) {
|
|
147
|
+
resolvedTransactor = mode === 'bootstrap' ? 'local' : 'network';
|
|
148
|
+
} else if (options.transactor !== undefined) {
|
|
149
|
+
resolvedTransactor = options.transactor;
|
|
150
|
+
} else {
|
|
151
|
+
resolvedTransactor = 'network';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const networkName = `strand-${strandId}`;
|
|
155
|
+
log('Connecting to strand %s (network: %s, mode=%s, transactor=%s)',
|
|
156
|
+
strandId, networkName, mode ?? '(default)', resolvedTransactor);
|
|
157
|
+
|
|
158
|
+
// Resolve storage. Node passes `options.storage` through; the browser
|
|
159
|
+
// defaults to IndexedDB. The resolved instance feeds BOTH the plugin's
|
|
160
|
+
// bootstrap `rawStorageFactory` and node creation, so it must be decided up
|
|
161
|
+
// front (before pluginConfig is built).
|
|
162
|
+
const storage = platform.resolveStorage
|
|
163
|
+
? await platform.resolveStorage({ strandId, resolvedTransactor, requestedStorage: options.storage })
|
|
164
|
+
: options.storage;
|
|
165
|
+
|
|
166
|
+
// 1. Register crypto plugin (digest, sign, verify, etc.)
|
|
167
|
+
await platform.registerCrypto(db);
|
|
168
|
+
log('Registered crypto plugin');
|
|
169
|
+
|
|
170
|
+
// 2. Register optimystic plugin with transactor defaults. In local mode with
|
|
171
|
+
// persistent storage, hand the same instance to the plugin so the local
|
|
172
|
+
// transactor persists DML on the host backend (not in-memory).
|
|
173
|
+
const pluginConfig: Record<string, unknown> = {
|
|
174
|
+
default_transactor: resolvedTransactor,
|
|
175
|
+
default_key_network: 'libp2p',
|
|
176
|
+
default_network_name: networkName,
|
|
177
|
+
enable_cache: enableCache,
|
|
178
|
+
};
|
|
179
|
+
if (resolvedTransactor === 'local' && storage) {
|
|
180
|
+
pluginConfig.rawStorageFactory = () => storage;
|
|
181
|
+
}
|
|
182
|
+
// The plugin's published signature is `Record<string, SqlValue>` but it
|
|
183
|
+
// also reads `rawStorageFactory` (a function reference) from the same map.
|
|
184
|
+
// Cast through unknown rather than widen the public type.
|
|
185
|
+
const pluginResult = optimysticPlugin(
|
|
186
|
+
db,
|
|
187
|
+
pluginConfig as unknown as Parameters<typeof optimysticPlugin>[1],
|
|
188
|
+
) as OptimysticPluginResult;
|
|
189
|
+
applyRegistrations(db, pluginResult);
|
|
190
|
+
log('Registered optimystic vtables and functions');
|
|
191
|
+
|
|
192
|
+
const { collectionFactory } = pluginResult;
|
|
193
|
+
|
|
194
|
+
let createdNode: Libp2p | null = null;
|
|
195
|
+
let hydrated: { tables: number; indexes: number } | undefined;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
// 3. Acquire the libp2p node. Skip only when this is the unit-test fake
|
|
199
|
+
// transactor with no injected node — every real path needs a node.
|
|
200
|
+
if (resolvedTransactor !== 'test' || options.libp2pNode) {
|
|
201
|
+
let node: Libp2p;
|
|
202
|
+
let coordinatedRepo: IRepo;
|
|
203
|
+
|
|
204
|
+
if (options.libp2pNode) {
|
|
205
|
+
node = options.libp2pNode;
|
|
206
|
+
if (!options.coordinatedRepo) {
|
|
207
|
+
throw new Error('coordinatedRepo is required when libp2pNode is provided');
|
|
208
|
+
}
|
|
209
|
+
coordinatedRepo = options.coordinatedRepo;
|
|
210
|
+
log('Using injected libp2p node');
|
|
211
|
+
} else {
|
|
212
|
+
const created = await platform.createNode({ networkName, bootstrapNodes, fretProfile, port, storage });
|
|
213
|
+
createdNode = created;
|
|
214
|
+
node = created;
|
|
215
|
+
const repo = (created as Libp2pNodeWithRepo).coordinatedRepo;
|
|
216
|
+
if (!repo) {
|
|
217
|
+
throw new Error('coordinatedRepo not available on created libp2p node');
|
|
218
|
+
}
|
|
219
|
+
coordinatedRepo = repo;
|
|
220
|
+
log('Created libp2p node (port: %d, fretProfile: %s, storage=%s)', port, fretProfile, !!storage);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
collectionFactory.registerLibp2pNode(networkName, node, coordinatedRepo);
|
|
224
|
+
log('Registered libp2p node with collection factory');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 4. Set optimystic as default vtab so `declare schema` tables use it.
|
|
228
|
+
db.setDefaultVtabName('optimystic');
|
|
229
|
+
db.setDefaultVtabArgs({
|
|
230
|
+
networkName,
|
|
231
|
+
transactor: resolvedTransactor,
|
|
232
|
+
keyNetwork: 'libp2p',
|
|
233
|
+
});
|
|
234
|
+
log('Set default vtab to optimystic (networkName=%s, transactor=%s)', networkName, resolvedTransactor);
|
|
235
|
+
|
|
236
|
+
// 5. Hydrate Quereus's catalog from persisted optimystic vtab schemas
|
|
237
|
+
// BEFORE applying the sApp schema. Without this, a warm restart diffs the
|
|
238
|
+
// wrapped DDL against an empty catalog and re-emits CREATE TABLE / CREATE
|
|
239
|
+
// INDEX for every persisted object — each round-tripping through optimystic
|
|
240
|
+
// storage (the measured ~160s warm-start regression). No-op on first launch.
|
|
241
|
+
const t0 = performance.now();
|
|
242
|
+
hydrated = await pluginResult.hydrate(db);
|
|
243
|
+
timing('[strand:%s] hydrate: %dms (tables=%d, indexes=%d)',
|
|
244
|
+
strandId, Math.round(performance.now() - t0), hydrated.tables, hydrated.indexes);
|
|
245
|
+
log('Hydrated catalog for strand %s (tables=%d, indexes=%d)', strandId, hydrated.tables, hydrated.indexes);
|
|
246
|
+
|
|
247
|
+
// 6. Apply the strand membership/RBAC schema (`schemas/strand.qsql`,
|
|
248
|
+
// embedded as STRAND_SCHEMA) UNCONDITIONALLY — every strand has membership
|
|
249
|
+
// semantics whether or not an sApp schema is supplied. There are no
|
|
250
|
+
// cross-schema references between `Strand` and `App`, so order is
|
|
251
|
+
// irrelevant; apply `Strand` first for clarity. Because this runs AFTER
|
|
252
|
+
// hydrate, a warm restart diffs the membership DDL against the already-
|
|
253
|
+
// hydrated catalog and re-emits nothing (same fix as the sApp apply).
|
|
254
|
+
//
|
|
255
|
+
// This ticket only makes the tables present and their constraints active;
|
|
256
|
+
// nothing here writes membership rows. Founder bootstrap (Header, founding
|
|
257
|
+
// Authority/Member, invite/peer flows) is owned by the lifecycle ticket
|
|
258
|
+
// `strand-membership-lifecycle-population`.
|
|
259
|
+
log('Applying Strand membership schema for strand %s', strandId);
|
|
260
|
+
await db.exec(`
|
|
261
|
+
declare schema Strand {
|
|
262
|
+
${STRAND_SCHEMA}
|
|
263
|
+
}
|
|
264
|
+
apply schema Strand;
|
|
265
|
+
`);
|
|
266
|
+
log('Strand membership schema applied');
|
|
267
|
+
|
|
268
|
+
// 7. Apply the sApp schema, if provided.
|
|
269
|
+
//
|
|
270
|
+
// SEAM: this and the Strand apply above are the single composition point
|
|
271
|
+
// where strand-level declarative DDL is applied. Keep new schema wiring
|
|
272
|
+
// here so it stays a one-location edit across Node, browser, and cadre-core.
|
|
273
|
+
if (schema) {
|
|
274
|
+
log('Applying sApp schema for strand %s', strandId);
|
|
275
|
+
await db.exec(`
|
|
276
|
+
declare schema App {
|
|
277
|
+
${schema}
|
|
278
|
+
}
|
|
279
|
+
apply schema App;
|
|
280
|
+
`);
|
|
281
|
+
log('sApp schema applied');
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
// Clean up resources if setup fails after partial initialization.
|
|
285
|
+
await collectionFactory.shutdown();
|
|
286
|
+
if (createdNode) {
|
|
287
|
+
await createdNode.stop();
|
|
288
|
+
}
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 8. Return result with hydrate counts + shutdown handler. `shutdown` tears
|
|
293
|
+
// down the collection factory, and `collectionFactory.shutdown()` stops EVERY
|
|
294
|
+
// node registered with it — including an injected one (registered via
|
|
295
|
+
// `registerLibp2pNode`). So an injected node IS stopped on `shutdown`; the
|
|
296
|
+
// `createdNode` guard below only avoids a redundant second `stop()` on a node
|
|
297
|
+
// we created ourselves, it is not what spares an injected node. A caller that
|
|
298
|
+
// needs an injected node to outlive `shutdown` must re-create it (cadre-core's
|
|
299
|
+
// `StrandInstanceManager` instead re-stops it, which is idempotent).
|
|
300
|
+
return {
|
|
301
|
+
vtables: [],
|
|
302
|
+
functions: [],
|
|
303
|
+
collations: [],
|
|
304
|
+
hydrated,
|
|
305
|
+
async shutdown() {
|
|
306
|
+
log('Shutting down strand connection %s', strandId);
|
|
307
|
+
await collectionFactory.shutdown();
|
|
308
|
+
if (createdNode) {
|
|
309
|
+
await createdNode.stop();
|
|
310
|
+
}
|
|
311
|
+
log('Strand connection %s shut down', strandId);
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Database } from '@quereus/quereus';
|
|
2
|
+
import cryptoPlugin from '@optimystic/quereus-plugin-crypto/plugin';
|
|
3
|
+
import { createLibp2pNode } from '@optimystic/db-p2p/rn';
|
|
4
|
+
import { webSockets } from '@libp2p/websockets';
|
|
5
|
+
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
|
|
6
|
+
import { IndexedDBRawStorage, openOptimysticWebDb } from '@optimystic/db-p2p-storage-web';
|
|
7
|
+
import type { Libp2p } from '@libp2p/interface';
|
|
8
|
+
import type { StrandConnectionOptions, SereusPluginResult } from './types.js';
|
|
9
|
+
import { composeStrand, applyRegistrations } from './compose-strand.js';
|
|
10
|
+
|
|
11
|
+
/** The subset of the crypto plugin result we register inline in the browser. */
|
|
12
|
+
interface CryptoPluginResult {
|
|
13
|
+
vtables: Array<{ name: string; module: unknown; auxData: unknown }>;
|
|
14
|
+
functions: Array<{ schema: unknown }>;
|
|
15
|
+
collations: Array<{ name: string; func: unknown; normalizer?: unknown }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Connect a Quereus Database to a Sereus strand from a browser/worker.
|
|
20
|
+
*
|
|
21
|
+
* Thin adapter over the shared {@link composeStrand} composition, supplying the
|
|
22
|
+
* browser platform seams:
|
|
23
|
+
* - crypto registered inline (no runtime `@quereus/quereus` import, so the
|
|
24
|
+
* bundle does not duplicate the host's instance),
|
|
25
|
+
* - storage defaulting to `IndexedDBRawStorage` keyed by `sereus-strand-<id>`,
|
|
26
|
+
* - a node created via the TCP-free `@optimystic/db-p2p/rn` entry with explicit
|
|
27
|
+
* `webSockets()` + `circuitRelayTransport()` transports.
|
|
28
|
+
*/
|
|
29
|
+
export async function connectToStrandBrowser(
|
|
30
|
+
db: Database,
|
|
31
|
+
options: StrandConnectionOptions,
|
|
32
|
+
): Promise<SereusPluginResult> {
|
|
33
|
+
return composeStrand(db, options, {
|
|
34
|
+
registerCrypto(database) {
|
|
35
|
+
applyRegistrations(database, cryptoPlugin(database, {}) as CryptoPluginResult);
|
|
36
|
+
},
|
|
37
|
+
async resolveStorage({ strandId, resolvedTransactor, requestedStorage }) {
|
|
38
|
+
if (requestedStorage) return requestedStorage;
|
|
39
|
+
// Browsers always default to IndexedDB so a reload survives; the
|
|
40
|
+
// unit-test fake transactor needs no storage.
|
|
41
|
+
if (resolvedTransactor === 'test') return undefined;
|
|
42
|
+
const dbHandle = await openOptimysticWebDb(`sereus-strand-${strandId}`);
|
|
43
|
+
return new IndexedDBRawStorage(dbHandle);
|
|
44
|
+
},
|
|
45
|
+
async createNode({ networkName, bootstrapNodes, fretProfile, storage }) {
|
|
46
|
+
return createLibp2pNode({
|
|
47
|
+
transports: [webSockets(), circuitRelayTransport()],
|
|
48
|
+
listenAddrs: [],
|
|
49
|
+
bootstrapNodes,
|
|
50
|
+
networkName,
|
|
51
|
+
fretProfile,
|
|
52
|
+
...(storage && { storage }),
|
|
53
|
+
}) as Promise<Libp2p>;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
package/src/connect.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Database } from '@quereus/quereus';
|
|
2
|
+
import { registerPlugin } from '@quereus/quereus';
|
|
3
|
+
import cryptoPlugin from '@optimystic/quereus-plugin-crypto/plugin';
|
|
4
|
+
import type { Libp2p } from '@libp2p/interface';
|
|
5
|
+
import type { StrandConnectionOptions, SereusPluginResult } from './types.js';
|
|
6
|
+
import { composeStrand } from './compose-strand.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Connect a Quereus Database to a Sereus strand (Node).
|
|
10
|
+
*
|
|
11
|
+
* Thin adapter over the shared {@link composeStrand} composition, supplying the
|
|
12
|
+
* Node platform seams: crypto via `@quereus/quereus`'s `registerPlugin`, and a
|
|
13
|
+
* TCP libp2p node created (only when not injected) via `@optimystic/db-p2p`.
|
|
14
|
+
*/
|
|
15
|
+
export async function connectToStrand(
|
|
16
|
+
db: Database,
|
|
17
|
+
options: StrandConnectionOptions,
|
|
18
|
+
): Promise<SereusPluginResult> {
|
|
19
|
+
return composeStrand(db, options, {
|
|
20
|
+
async registerCrypto(database) {
|
|
21
|
+
await registerPlugin(database, cryptoPlugin);
|
|
22
|
+
},
|
|
23
|
+
async createNode({ networkName, bootstrapNodes, fretProfile, port, storage }) {
|
|
24
|
+
// Dynamically import to keep the module cross-platform friendly: this
|
|
25
|
+
// pulls the Node-only `@optimystic/db-p2p` (TCP) entry, and is only
|
|
26
|
+
// reached when actually creating a node (never for injected nodes).
|
|
27
|
+
const { createLibp2pNode } = await import('@optimystic/db-p2p');
|
|
28
|
+
return createLibp2pNode({
|
|
29
|
+
port,
|
|
30
|
+
bootstrapNodes,
|
|
31
|
+
networkName,
|
|
32
|
+
fretProfile,
|
|
33
|
+
...(storage && { storage }),
|
|
34
|
+
}) as Promise<Libp2p>;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { SqlValue } from '@quereus/quereus';
|
|
2
|
+
import type { StrandConnectionOptions } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse the plugin-loader SqlValue config into typed StrandConnectionOptions.
|
|
6
|
+
* Shared by the Node (`plugin.ts`) and browser (`plugin-browser.ts`) entries.
|
|
7
|
+
*/
|
|
8
|
+
export function parseConfig(config: Record<string, SqlValue>): StrandConnectionOptions {
|
|
9
|
+
const strandId = config.strand_id;
|
|
10
|
+
if (typeof strandId !== 'string' || !strandId) {
|
|
11
|
+
throw new Error('quereus-plugin-sereus: strand_id is required');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const bootstrapNodesRaw = config.bootstrap_nodes;
|
|
15
|
+
const bootstrapNodes = typeof bootstrapNodesRaw === 'string' && bootstrapNodesRaw
|
|
16
|
+
? bootstrapNodesRaw.split(',').map(s => s.trim()).filter(Boolean)
|
|
17
|
+
: [];
|
|
18
|
+
|
|
19
|
+
const schema = typeof config.schema === 'string' ? config.schema : undefined;
|
|
20
|
+
const sAppId = typeof config.sapp_id === 'string' ? config.sapp_id : 'unknown';
|
|
21
|
+
const sAppVersion = typeof config.sapp_version === 'string' ? config.sapp_version : '1.0.0';
|
|
22
|
+
const port = typeof config.port === 'number' ? config.port : 0;
|
|
23
|
+
const enableCache = config.enable_cache !== false && config.enable_cache !== 0;
|
|
24
|
+
const fretProfile = config.fret_profile === 'core' ? 'core' as const : 'edge' as const;
|
|
25
|
+
|
|
26
|
+
// Lifecycle mode: selects bootstrap (local transactor) vs networked. Only the
|
|
27
|
+
// two known values are honored; anything else falls through to the default.
|
|
28
|
+
const mode = config.mode === 'bootstrap' || config.mode === 'networked'
|
|
29
|
+
? config.mode
|
|
30
|
+
: undefined;
|
|
31
|
+
|
|
32
|
+
// Internal transactor override (e.g. `'test'`). Applies only when `mode` is
|
|
33
|
+
// unset — see `StrandConnectionOptions.transactor`.
|
|
34
|
+
const transactor = typeof config.transactor === 'string' && config.transactor
|
|
35
|
+
? config.transactor
|
|
36
|
+
: undefined;
|
|
37
|
+
|
|
38
|
+
// NOTE: persistent storage cannot ride a `Record<string, SqlValue>` — an
|
|
39
|
+
// `IRawStorage` is not an `SqlValue`. The Node loader (`plugin.ts`) reads the
|
|
40
|
+
// platform-only `storage_path` key directly and resolves it to a concrete
|
|
41
|
+
// `FileRawStorage`; it is intentionally NOT parsed into the typed options here.
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
strandId,
|
|
45
|
+
bootstrapNodes,
|
|
46
|
+
schema,
|
|
47
|
+
sAppId,
|
|
48
|
+
sAppVersion,
|
|
49
|
+
port,
|
|
50
|
+
enableCache,
|
|
51
|
+
fretProfile,
|
|
52
|
+
...(mode && { mode }),
|
|
53
|
+
...(transactor && { transactor }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quereus plugin entry point for Sereus strand connections (browser/worker).
|
|
3
|
+
*
|
|
4
|
+
* This is the ESM-bundled counterpart to `./plugin`. Load via Quoomb-web's
|
|
5
|
+
* plugin URL field or `plugins[].source` in `quoomb.config.json`. Defaults
|
|
6
|
+
* storage to IndexedDB and uses WebSockets + circuit-relay transports.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Database, SqlValue } from '@quereus/quereus';
|
|
10
|
+
import { connectToStrandBrowser } from './connect-browser.js';
|
|
11
|
+
import { parseConfig } from './parse-config.js';
|
|
12
|
+
import type { SereusPluginResult } from './types.js';
|
|
13
|
+
|
|
14
|
+
export { parseConfig };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default export: Quereus plugin registration function (browser).
|
|
18
|
+
*
|
|
19
|
+
* Note: the `storage_path` setting is Node-only (it resolves to a filesystem
|
|
20
|
+
* `FileRawStorage` in `./plugin`). The browser entry ignores it and defaults
|
|
21
|
+
* storage to IndexedDB via `connectToStrandBrowser`.
|
|
22
|
+
*/
|
|
23
|
+
export default async function register(
|
|
24
|
+
db: Database,
|
|
25
|
+
config: Record<string, SqlValue> = {},
|
|
26
|
+
): Promise<SereusPluginResult> {
|
|
27
|
+
const options = parseConfig(config);
|
|
28
|
+
return connectToStrandBrowser(db, options);
|
|
29
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quereus plugin entry point for Sereus strand connections (Node).
|
|
3
|
+
*
|
|
4
|
+
* Loaded via Quoomb's `.plugin install` or config file, or via `registerPlugin()`.
|
|
5
|
+
* Parses SqlValue config and delegates to `connectToStrand`.
|
|
6
|
+
*
|
|
7
|
+
* Browser/Worker consumers should load `./plugin-browser` instead, which uses
|
|
8
|
+
* the TCP-free libp2p entry and IndexedDB-backed default storage.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Database, SqlValue } from '@quereus/quereus';
|
|
12
|
+
import { connectToStrand } from './connect.js';
|
|
13
|
+
import { parseConfig } from './parse-config.js';
|
|
14
|
+
import type { SereusPluginResult } from './types.js';
|
|
15
|
+
|
|
16
|
+
export { parseConfig };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default export: Quereus plugin registration function.
|
|
20
|
+
*/
|
|
21
|
+
export default async function register(
|
|
22
|
+
db: Database,
|
|
23
|
+
config: Record<string, SqlValue> = {},
|
|
24
|
+
): Promise<SereusPluginResult> {
|
|
25
|
+
const options = parseConfig(config);
|
|
26
|
+
|
|
27
|
+
// `storage_path` is a Node-only loader convenience: a `Record<string, SqlValue>`
|
|
28
|
+
// cannot carry an `IRawStorage`, so the path is resolved to a concrete
|
|
29
|
+
// `FileRawStorage` here (not in the platform-agnostic `parseConfig`). The
|
|
30
|
+
// dynamic import keeps `@optimystic/db-p2p-storage-fs` (and its `node:fs`
|
|
31
|
+
// dependency) out of the browser/RN module graph.
|
|
32
|
+
const storagePath = config.storage_path;
|
|
33
|
+
if (typeof storagePath === 'string' && storagePath) {
|
|
34
|
+
const { FileRawStorage } = await import('@optimystic/db-p2p-storage-fs');
|
|
35
|
+
options.storage = new FileRawStorage(storagePath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return connectToStrand(db, options);
|
|
39
|
+
}
|