@serfab/quereus-plugin-sereus 1.18.0 → 1.19.1
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 +5 -0
- package/dist/app-schema.d.ts +45 -0
- package/dist/app-schema.js +147 -0
- package/dist/app-schema.js.map +1 -0
- package/dist/compose-strand.d.ts +15 -19
- package/dist/compose-strand.js +75 -51
- package/dist/compose-strand.js.map +1 -1
- package/dist/connect-browser.d.ts +2 -0
- package/dist/connect-browser.js +10 -3
- package/dist/connect-browser.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin-browser.js +40 -40
- package/dist/plugin-browser.js.map +4 -4
- package/dist/strand-schema.d.ts +1 -1
- package/dist/strand-schema.js +8 -8
- package/dist/types.d.ts +49 -1
- package/package.json +8 -8
- package/src/app-schema.ts +172 -0
- package/src/compose-strand.ts +93 -56
- package/src/connect-browser.ts +10 -3
- package/src/index.ts +30 -18
- package/src/strand-schema.ts +8 -8
- package/src/types.ts +49 -1
package/src/compose-strand.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
2
|
import type { Database, VTablePluginInfo, FunctionPluginInfo, CollationPluginInfo } from '@quereus/quereus';
|
|
3
3
|
import optimysticPlugin from '@optimystic/quereus-plugin-optimystic/plugin';
|
|
4
|
-
import type { SqlCommitDurabilityListener, CommitFullyHeldListener } from '@optimystic/quereus-plugin-optimystic/plugin';
|
|
4
|
+
import type { SqlCommitDurabilityListener, CommitFullyHeldListener, OfflineConfig, OfflineMode } from '@optimystic/quereus-plugin-optimystic/plugin';
|
|
5
5
|
import type { Libp2p } from '@libp2p/interface';
|
|
6
6
|
import type { IRepo } from '@optimystic/db-core';
|
|
7
|
-
import type { IRawStorage, NodeOptions } from '@optimystic/db-p2p';
|
|
8
|
-
import type {
|
|
7
|
+
import type { IKVStore, IRawStorage, NodeOptions } from '@optimystic/db-p2p';
|
|
8
|
+
import type {
|
|
9
|
+
StrandConnectionOptions, SereusPluginResult, StrandTransactor, Libp2pNodeWithRepo,
|
|
10
|
+
StrandOfflineOptions, StrandOfflineController,
|
|
11
|
+
} from './types.js';
|
|
12
|
+
import { applyAppSchemaAfterQueue, type AppSchemaApply } from './app-schema.js';
|
|
9
13
|
import { STRAND_SCHEMA } from './strand-schema.js';
|
|
10
14
|
import { resolveStrandClusterSize, STRAND_CLUSTER_POLICY } from './cluster-size.js';
|
|
11
15
|
import { wrapStorageWithCache, disposeStorageCache } from './cached-storage.js';
|
|
@@ -17,6 +21,18 @@ const timing = debug('sereus:plugin:strand:timing');
|
|
|
17
21
|
/** The schemas whose tables a strand founder gives a collection: strand membership, then the sApp's. */
|
|
18
22
|
const FOUNDED_SCHEMAS = ['Strand', 'App'] as const;
|
|
19
23
|
|
|
24
|
+
/** The offline controller of each live strand composition composed with `offline`, by its `Database`. */
|
|
25
|
+
const offlineControllers = new WeakMap<Database, StrandOfflineController>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The offline controller of the live strand composition over `db`, or `undefined` when it was
|
|
29
|
+
* composed without `offline`, has shut down, or `db` is no strand composition. For Sereus's own
|
|
30
|
+
* writers, which are handed a bare `Database` and must see whether its queue holds transactions.
|
|
31
|
+
*/
|
|
32
|
+
export function offlineControllerOf(db: Database): StrandOfflineController | undefined {
|
|
33
|
+
return offlineControllers.get(db);
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
/**
|
|
21
37
|
* Minimal interface for the CollectionFactory returned by the optimystic plugin.
|
|
22
38
|
* We only need the methods this composition actually uses.
|
|
@@ -30,17 +46,22 @@ interface CollectionFactory {
|
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
/**
|
|
33
|
-
* Release a composition's hold on the optimystic plugin. The
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* database
|
|
49
|
+
* Release a composition's hold on the optimystic plugin. The offline queue goes first: its drain
|
|
50
|
+
* keeps timers and may have a submission in flight, which reports its commit through the bridge's
|
|
51
|
+
* follower, so the bridge must outlive it (the plugin's own `dispose()` orders them the same way).
|
|
52
|
+
* A queue `hydrate` restored before a failed composition is stopped here too. Then the transaction
|
|
53
|
+
* bridge: its durability follower subscribes to the node's `StorageRepo`, and cadre-core keeps a
|
|
54
|
+
* strand's node across a database rebuild, so a bridge never disposed stays subscribed once per
|
|
55
|
+
* rebuild. Then the collection factory: a node the composition created is stopped with it; an
|
|
56
|
+
* injected node is left running, because it belongs to the caller — cadre-core keeps a strand's
|
|
57
|
+
* node up across a failed database build and retries the build on it.
|
|
39
58
|
*
|
|
40
|
-
* Disposes the bridge itself rather than calling the plugin's `dispose()`, which would
|
|
41
|
-
* factory a second time on the created-node path (`shutdown()` ends with the same
|
|
59
|
+
* Disposes the queue and bridge itself rather than calling the plugin's `dispose()`, which would
|
|
60
|
+
* release the factory a second time on the created-node path (`shutdown()` ends with the same
|
|
61
|
+
* `dispose()`).
|
|
42
62
|
*/
|
|
43
63
|
async function releaseOptimysticPlugin(plugin: OptimysticPluginResult, createdNode: Libp2p | null): Promise<void> {
|
|
64
|
+
await plugin.offline.dispose();
|
|
44
65
|
// NOTE: the bridge's follower re-subscribes at the start of any transaction, so a write begun on
|
|
45
66
|
// this `Database` after release (before its owner closes it) subscribes again and nothing
|
|
46
67
|
// releases it. cadre-core stops its own writers before releasing; if an app write in flight at
|
|
@@ -84,6 +105,8 @@ interface OptimysticPluginResult {
|
|
|
84
105
|
* {@link createEmptyCollections}).
|
|
85
106
|
*/
|
|
86
107
|
txnBridge: { dispose(): void } & CollectionRegistrySource;
|
|
108
|
+
/** The offline queue, constructed on every registration; idle until something is queued. */
|
|
109
|
+
offline: OfflineMode;
|
|
87
110
|
[key: string]: unknown;
|
|
88
111
|
}
|
|
89
112
|
|
|
@@ -170,6 +193,13 @@ export interface StrandPlatform {
|
|
|
170
193
|
* default to IndexedDB so a reload survives.
|
|
171
194
|
*/
|
|
172
195
|
resolveStorage?(ctx: ResolveStorageContext): Promise<IRawStorage | undefined> | IRawStorage | undefined;
|
|
196
|
+
/**
|
|
197
|
+
* The store to journal offline transactions in when `options.offline` names no `journal`.
|
|
198
|
+
* Called after {@link resolveStorage}, only when `options.offline` is given. Optional — when
|
|
199
|
+
* omitted, or when it answers `undefined`, the queue lives in memory (Node). The browser answers
|
|
200
|
+
* a store over the IndexedDB database it defaulted the strand's storage to.
|
|
201
|
+
*/
|
|
202
|
+
defaultOfflineJournal?(): IKVStore | undefined;
|
|
173
203
|
/**
|
|
174
204
|
* Create a libp2p node. Called only when no node is injected and the
|
|
175
205
|
* transactor needs one. Node creates a TCP node via `@optimystic/db-p2p`;
|
|
@@ -264,6 +294,7 @@ export async function composeStrand(
|
|
|
264
294
|
networkName,
|
|
265
295
|
enableCache,
|
|
266
296
|
storage,
|
|
297
|
+
offline: options.offline && strandOfflineConfig(strandId, options.offline, platform),
|
|
267
298
|
});
|
|
268
299
|
} catch (err) {
|
|
269
300
|
await releaseStorageCache();
|
|
@@ -271,6 +302,13 @@ export async function composeStrand(
|
|
|
271
302
|
}
|
|
272
303
|
|
|
273
304
|
const { collectionFactory } = pluginResult;
|
|
305
|
+
const offline: StrandOfflineController | undefined = options.offline && pluginResult.offline;
|
|
306
|
+
if (offline) {
|
|
307
|
+
offlineControllers.set(db, offline);
|
|
308
|
+
}
|
|
309
|
+
// Aborted by `shutdown`, so a deferred sApp schema apply still waiting settles `cancelled`.
|
|
310
|
+
const released = new AbortController();
|
|
311
|
+
let appSchemaApply: AppSchemaApply | undefined;
|
|
274
312
|
|
|
275
313
|
let createdNode: Libp2p | null = null;
|
|
276
314
|
let hydrated: { tables: number; indexes: number } | undefined;
|
|
@@ -346,6 +384,13 @@ export async function composeStrand(
|
|
|
346
384
|
// nothing here writes membership rows. Founder bootstrap (Header, founding
|
|
347
385
|
// Manager/Member, invite/peer flows) is owned by the lifecycle ticket
|
|
348
386
|
// `strand-membership-lifecycle-population`.
|
|
387
|
+
//
|
|
388
|
+
// NOTE: unlike step 7, this apply is not deferred behind the offline queue. Every `Strand`
|
|
389
|
+
// table is tagged `"optimystic.offline" = 'network'`, so the only queued transactions that
|
|
390
|
+
// write one are network-acknowledged commits waiting behind app transactions, and the only
|
|
391
|
+
// change this schema has made since is a tag, which the queue does not refuse. If a
|
|
392
|
+
// later strand schema changes how a table's rows are stored (an index, a column), this apply
|
|
393
|
+
// can be refused by `QueuedEntriesBlockSchemaChangeError` the same way and needs step 7's path.
|
|
349
394
|
log('Applying Strand membership schema for strand %s', strandId);
|
|
350
395
|
await db.exec(`
|
|
351
396
|
declare schema Strand {
|
|
@@ -371,13 +416,17 @@ export async function composeStrand(
|
|
|
371
416
|
// differ: once one has run, any later step that fails leaves the schema partially
|
|
372
417
|
// migrated instead of restored. No schema in this repo evolves that way, so this
|
|
373
418
|
// is a note for whoever first ships a migration that does.
|
|
419
|
+
//
|
|
420
|
+
// The one exception: an apply refused by queued offline transactions does not fail the
|
|
421
|
+
// connect. The strand comes up on the `App` schema it had, and the apply is retried as the
|
|
422
|
+
// queue drains (see `applyAppSchemaAfterQueue`).
|
|
374
423
|
if (schema) {
|
|
375
424
|
log('Applying sApp schema for strand %s', strandId);
|
|
376
|
-
await
|
|
377
|
-
log('sApp schema applied');
|
|
425
|
+
appSchemaApply = await applyAppSchemaAfterQueue(db, schema, pluginResult.offline, released.signal);
|
|
378
426
|
}
|
|
379
427
|
} catch (err) {
|
|
380
428
|
// Clean up resources if setup fails after partial initialization.
|
|
429
|
+
offlineControllers.delete(db);
|
|
381
430
|
await releaseOptimysticPlugin(pluginResult, createdNode);
|
|
382
431
|
await releaseStorageCache();
|
|
383
432
|
throw err;
|
|
@@ -395,8 +444,15 @@ export async function composeStrand(
|
|
|
395
444
|
onCommitDurability: listener => pluginResult.onCommitDurability(listener),
|
|
396
445
|
onCommitFullyHeld: listener => pluginResult.onCommitFullyHeld(listener),
|
|
397
446
|
createEmptyCollections: except => createEmptyCollections(db, pluginResult.txnBridge, FOUNDED_SCHEMAS, except),
|
|
447
|
+
...(offline && { offline }),
|
|
448
|
+
...appSchemaApply,
|
|
398
449
|
async shutdown() {
|
|
399
450
|
log('Shutting down strand connection %s', strandId);
|
|
451
|
+
// NOTE: a deferred sApp schema apply already running is not awaited: it runs on against the
|
|
452
|
+
// released plugin, Quereus unwinds it when it fails, and `appSchema` settles `cancelled`. If
|
|
453
|
+
// that ever leaves a catalog write landing after close, await `appSchema` here first.
|
|
454
|
+
released.abort();
|
|
455
|
+
offlineControllers.delete(db);
|
|
400
456
|
await releaseOptimysticPlugin(pluginResult, createdNode);
|
|
401
457
|
await releaseStorageCache();
|
|
402
458
|
log('Strand connection %s shut down', strandId);
|
|
@@ -405,50 +461,23 @@ export async function composeStrand(
|
|
|
405
461
|
}
|
|
406
462
|
|
|
407
463
|
/**
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
* emits nothing. The one site that applies an `App` schema: `composeStrand` calls it at
|
|
411
|
-
* bring-up, and cadre-core's `StrandDatabase.attachAppSchema` calls it on a live storage
|
|
412
|
-
* replica when an app claims the strand. The database must already be composed (optimystic
|
|
413
|
-
* set as the default vtab), or the tables land in memory instead of the strand.
|
|
414
|
-
*
|
|
415
|
-
* Refuses a schema holding an item the Quereus parser skipped: it keeps any item whose leading
|
|
416
|
-
* keyword it does not model (`create unique index …`, a misspelled `tabel`, `domain`) as an
|
|
417
|
-
* opaque placeholder that apply ignores, so the app would otherwise run without that item.
|
|
418
|
-
*
|
|
419
|
-
* Refuses a schema holding a `seed` item: the apply runs on every node of the strand at every
|
|
420
|
-
* connect, so seed inserts would put a network write on each bring-up and two nodes inserting
|
|
421
|
-
* the same key at once would collide. Rows an app needs at birth are the founding app's to write.
|
|
464
|
+
* The Optimystic plugin's `offline` block for this strand: the caller's options, the platform's
|
|
465
|
+
* journal when the caller named none, and the journal label, which the plugin refuses without one.
|
|
422
466
|
*/
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
`);
|
|
429
|
-
assertNoIgnoredItems(db);
|
|
430
|
-
assertNoSeedItems(db);
|
|
431
|
-
await db.exec('apply schema App;');
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function declaredAppItems(db: Database) {
|
|
435
|
-
return db.declaredSchemaManager.getDeclaredSchema('App')?.items ?? [];
|
|
467
|
+
function strandOfflineConfig(strandId: string, offline: StrandOfflineOptions, platform: StrandPlatform): OfflineConfig {
|
|
468
|
+
const journal = offline.journal ?? platform.defaultOfflineJournal?.();
|
|
469
|
+
return journal === undefined
|
|
470
|
+
? { ...offline }
|
|
471
|
+
: { ...offline, journal, journalLabel: strandJournalLabel(strandId) };
|
|
436
472
|
}
|
|
437
473
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function assertNoSeedItems(db: Database): void {
|
|
447
|
-
// Quereus refuses a second seed for one table at declare, so these names are distinct.
|
|
448
|
-
const tables = declaredAppItems(db).flatMap(item => item.type === 'declaredSeed' ? [item.tableName] : []);
|
|
449
|
-
if (tables.length > 0) {
|
|
450
|
-
throw new Error(`sApp schema has seed item(s) for table(s) ${tables.join(', ')}; Sereus does not apply seed rows to a strand — insert the rows an app needs at birth from the app when it founds the strand`);
|
|
451
|
-
}
|
|
474
|
+
/**
|
|
475
|
+
* The offline journal label for a strand. A label is `[a-z0-9_-]` and a strand id `[a-z0-9._-]`, so
|
|
476
|
+
* `_` becomes `__` and `.` becomes `_d`: no two strand ids share a label, and two strands journaling
|
|
477
|
+
* into one store keep their records apart.
|
|
478
|
+
*/
|
|
479
|
+
function strandJournalLabel(strandId: string): string {
|
|
480
|
+
return `strand-${strandId.replaceAll('_', '__').replaceAll('.', '_d')}`;
|
|
452
481
|
}
|
|
453
482
|
|
|
454
483
|
/**
|
|
@@ -464,6 +493,7 @@ async function registerStrandPlugins(
|
|
|
464
493
|
networkName: string;
|
|
465
494
|
enableCache: boolean;
|
|
466
495
|
storage: IRawStorage | undefined;
|
|
496
|
+
offline: OfflineConfig | undefined;
|
|
467
497
|
},
|
|
468
498
|
): Promise<OptimysticPluginResult> {
|
|
469
499
|
await platform.registerCrypto(db);
|
|
@@ -481,9 +511,16 @@ async function registerStrandPlugins(
|
|
|
481
511
|
const storage = strand.storage;
|
|
482
512
|
pluginConfig.rawStorageFactory = () => storage;
|
|
483
513
|
}
|
|
514
|
+
// NOTE: `networkCommitTimeoutMs` is left at upstream's default (five minutes); nothing was
|
|
515
|
+
// measured to choose another. A network-acknowledged commit made while transactions are queued
|
|
516
|
+
// (any `Strand` write, all of them tagged 'network') waits that long behind them holding
|
|
517
|
+
// Quereus's execution lock. If that ever shows up as a frozen app, lower it for strands here.
|
|
518
|
+
if (strand.offline) {
|
|
519
|
+
pluginConfig.offline = strand.offline;
|
|
520
|
+
}
|
|
484
521
|
// The plugin's published signature is `Record<string, SqlValue>` but it also reads
|
|
485
|
-
// `rawStorageFactory` (a function reference)
|
|
486
|
-
// rather than widen the public type.
|
|
522
|
+
// `rawStorageFactory` (a function reference) and `offline` (an object) from the same
|
|
523
|
+
// map. Cast through unknown rather than widen the public type.
|
|
487
524
|
const pluginResult = optimysticPlugin(
|
|
488
525
|
db,
|
|
489
526
|
pluginConfig as unknown as Parameters<typeof optimysticPlugin>[1],
|
package/src/connect-browser.ts
CHANGED
|
@@ -3,7 +3,7 @@ import cryptoPlugin from '@optimystic/quereus-plugin-crypto/plugin';
|
|
|
3
3
|
import { createLibp2pNode } from '@optimystic/db-p2p/rn';
|
|
4
4
|
import { webSockets } from '@libp2p/websockets';
|
|
5
5
|
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
|
|
6
|
-
import { IndexedDBRawStorage, openOptimysticWebDb } from '@optimystic/db-p2p-storage-web';
|
|
6
|
+
import { IndexedDBKVStore, IndexedDBRawStorage, openOptimysticWebDb, type OptimysticWebDBHandle } from '@optimystic/db-p2p-storage-web';
|
|
7
7
|
import type { Libp2p } from '@libp2p/interface';
|
|
8
8
|
import type { StrandConnectionOptions, SereusPluginResult } from './types.js';
|
|
9
9
|
import { composeStrand, applyRegistrations } from './compose-strand.js';
|
|
@@ -23,6 +23,8 @@ interface CryptoPluginResult {
|
|
|
23
23
|
* - crypto registered inline (no runtime `@quereus/quereus` import, so the
|
|
24
24
|
* bundle does not duplicate the host's instance),
|
|
25
25
|
* - storage defaulting to `IndexedDBRawStorage` keyed by `sereus-strand-<id>`,
|
|
26
|
+
* - with `offline` given and no `journal`, the offline queue journaled into that same
|
|
27
|
+
* IndexedDB database (its `kv` object store) when the storage was defaulted,
|
|
26
28
|
* - a node created via the TCP-free `@optimystic/db-p2p/rn` entry with explicit
|
|
27
29
|
* `webSockets()` + `circuitRelayTransport()` transports.
|
|
28
30
|
*/
|
|
@@ -30,6 +32,8 @@ export async function connectToStrandBrowser(
|
|
|
30
32
|
db: Database,
|
|
31
33
|
options: StrandConnectionOptions,
|
|
32
34
|
): Promise<SereusPluginResult> {
|
|
35
|
+
// The IndexedDB database the storage was defaulted to, if it was; the offline journal shares it.
|
|
36
|
+
let defaultedDb: OptimysticWebDBHandle | undefined;
|
|
33
37
|
return composeStrand(db, options, {
|
|
34
38
|
registerCrypto(database) {
|
|
35
39
|
applyRegistrations(database, cryptoPlugin(database, {}) as unknown as CryptoPluginResult);
|
|
@@ -39,8 +43,11 @@ export async function connectToStrandBrowser(
|
|
|
39
43
|
// Browsers always default to IndexedDB so a reload survives; the
|
|
40
44
|
// unit-test fake transactor needs no storage.
|
|
41
45
|
if (resolvedTransactor === 'test') return undefined;
|
|
42
|
-
|
|
43
|
-
return new IndexedDBRawStorage(
|
|
46
|
+
defaultedDb = await openOptimysticWebDb(`sereus-strand-${strandId}`);
|
|
47
|
+
return new IndexedDBRawStorage(defaultedDb);
|
|
48
|
+
},
|
|
49
|
+
defaultOfflineJournal() {
|
|
50
|
+
return defaultedDb && new IndexedDBKVStore(defaultedDb);
|
|
44
51
|
},
|
|
45
52
|
async createNode({ networkName, bootstrapNodes, fretProfile, clusterSize, clusterPolicy, storage }) {
|
|
46
53
|
return createLibp2pNode({
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
|
-
export { connectToStrand } from './connect.js';
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export
|
|
18
|
-
export type {
|
|
1
|
+
export { connectToStrand } from './connect.js';
|
|
2
|
+
export { offlineControllerOf } from './compose-strand.js';
|
|
3
|
+
export { applyAppSchema } from './app-schema.js';
|
|
4
|
+
export { createEmptyCollections, type CollectionRegistrySource } from './empty-collections.js';
|
|
5
|
+
export {
|
|
6
|
+
MIN_CLUSTER_SIZE,
|
|
7
|
+
COHORT_READ_DEADLINE_MS,
|
|
8
|
+
CONTROL_REPLICATION_BREADTH,
|
|
9
|
+
CONTROL_CLUSTER_POLICY,
|
|
10
|
+
DEFAULT_STRAND_CLUSTER_SIZE,
|
|
11
|
+
STRAND_CLUSTER_POLICY,
|
|
12
|
+
resolveStrandClusterSize,
|
|
13
|
+
resolveRepairYardstick,
|
|
14
|
+
controlClusterPolicy,
|
|
15
|
+
strandClusterPolicy
|
|
16
|
+
} from './cluster-size.js';
|
|
17
|
+
export { wrapStorageWithCache, disposeStorageCache } from './cached-storage.js';
|
|
18
|
+
export type {
|
|
19
|
+
StrandConnectionOptions, SereusPluginResult, StrandTransactor, StrandOfflineOptions, StrandOfflineController, AppSchemaOutcome,
|
|
20
|
+
} from './types.js';
|
|
21
|
+
export type { SqlCommitDurability, CommitFullyHeld } from '@optimystic/quereus-plugin-optimystic/plugin';
|
|
22
|
+
export {
|
|
23
|
+
OfflineCommitTimeoutError,
|
|
24
|
+
QueuedEntriesBlockSchemaChangeError,
|
|
25
|
+
EntryNotDiscardableError,
|
|
26
|
+
UnknownEntryError,
|
|
27
|
+
QueueDisposedError,
|
|
28
|
+
OfflineJournalError,
|
|
29
|
+
} from '@optimystic/quereus-plugin-optimystic/plugin';
|
|
30
|
+
export type { QueuedTransactionStatus, OfflineModeState, CommitOutlook } from '@optimystic/quereus-plugin-optimystic/plugin';
|
package/src/strand-schema.ts
CHANGED
|
@@ -56,7 +56,7 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
56
56
|
|
|
57
57
|
primary key (/* empty - singleton */),
|
|
58
58
|
constraint InsertOnly check on update, delete (false), -- One-time insert only for now - TODO: revisit for versioning
|
|
59
|
-
);
|
|
59
|
+
) with tags ("optimystic.offline" = 'network');
|
|
60
60
|
|
|
61
61
|
-- An invitation to join a strand as a member
|
|
62
62
|
table Invite (
|
|
@@ -77,7 +77,7 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
77
77
|
-- and must also prove invite private key held by issuing manager
|
|
78
78
|
and verify(digest(new.Key || '|' || coalesce(new.Expiration, '')), context.InviteSignature, new.Key, 'ed25519')
|
|
79
79
|
)
|
|
80
|
-
) with context (ManagerKey text null, ManagerSignature text null, InviteSignature text null);
|
|
80
|
+
) with tags ("optimystic.offline" = 'network') with context (ManagerKey text null, ManagerSignature text null, InviteSignature text null);
|
|
81
81
|
|
|
82
82
|
-- Invite [InviteKey] has been used to add [MemberKey] as a member
|
|
83
83
|
-- No StampId here or on Invite: both tables are insert-only and their primary keys are
|
|
@@ -126,7 +126,7 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
126
126
|
constraint NotSealed check on insert (
|
|
127
127
|
exists (select 1 from Manager)
|
|
128
128
|
)
|
|
129
|
-
) with context (InviteSignature text null, Now datetime null);
|
|
129
|
+
) with tags ("optimystic.offline" = 'network') with context (InviteSignature text null, Now datetime null);
|
|
130
130
|
|
|
131
131
|
-- A manager has cancelled invitation [InviteKey]: it may never be consumed.
|
|
132
132
|
--
|
|
@@ -175,7 +175,7 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
175
175
|
and verify(digest('Strand.CancelledInvite', 'cancel', new.InviteKey),
|
|
176
176
|
context.ManagerSignature, A.MemberKey, 'ed25519'))
|
|
177
177
|
)
|
|
178
|
-
) with context (ManagerKey text, ManagerSignature text);
|
|
178
|
+
) with tags ("optimystic.offline" = 'network') with context (ManagerKey text, ManagerSignature text);
|
|
179
179
|
|
|
180
180
|
-- A party in the closed strand network
|
|
181
181
|
table Member (
|
|
@@ -292,7 +292,7 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
292
292
|
or (new.Key is null
|
|
293
293
|
and verify(digest('Strand.Member', 'leave', old.Key, old.StampId), context.MemberSignature, old.Key, 'ed25519'))
|
|
294
294
|
),
|
|
295
|
-
) with context (ManagerKey text null, ManagerSignature text null, MemberSignature text null);
|
|
295
|
+
) with tags ("optimystic.offline" = 'network') with context (ManagerKey text null, ManagerSignature text null, MemberSignature text null);
|
|
296
296
|
|
|
297
297
|
-- A member-associated peer (node)
|
|
298
298
|
--
|
|
@@ -356,7 +356,7 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
356
356
|
and verify(digest('Strand.MemberPeer', 'manager-remove', old.MemberKey, old.PeerId, old.StampId), context.ManagerSignature, A.MemberKey, 'ed25519')
|
|
357
357
|
))
|
|
358
358
|
),
|
|
359
|
-
) with context (Signature text null, ManagerKey text null, ManagerSignature text null);
|
|
359
|
+
) with tags ("optimystic.offline" = 'network') with context (Signature text null, ManagerKey text null, ManagerSignature text null);
|
|
360
360
|
|
|
361
361
|
-- A manager is a member that can issue invites, authorize members, and rotate managers
|
|
362
362
|
table Manager (
|
|
@@ -518,7 +518,7 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
518
518
|
and A.MemberKey <> old.MemberKey
|
|
519
519
|
and verify(digest('Strand.Manager', 'remove', old.MemberKey, old.StampId), context.Signature, A.MemberKey, 'ed25519')))
|
|
520
520
|
)
|
|
521
|
-
) with context (ManagerKey text null, Signature text null);
|
|
521
|
+
) with tags ("optimystic.offline" = 'network') with context (ManagerKey text null, Signature text null);
|
|
522
522
|
|
|
523
523
|
-- Permanent tombstones for retired per-row authorization nonces (StampIds) — the
|
|
524
524
|
-- delete half of the retired-stamp anti-replay idiom on Member / Manager /
|
|
@@ -580,5 +580,5 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
580
580
|
where M.Key = context.MemberKey
|
|
581
581
|
and verify(digest('Strand.Revocation', 'retire', new.TableName, new.StampId), context.Signature, M.Key, 'ed25519'))
|
|
582
582
|
)
|
|
583
|
-
) with context (MemberKey text, Signature text);
|
|
583
|
+
) with tags ("optimystic.offline" = 'network') with context (MemberKey text, Signature text);
|
|
584
584
|
`;
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Libp2p } from '@libp2p/interface';
|
|
2
2
|
import type { IRepo } from '@optimystic/db-core';
|
|
3
3
|
import type { IRawStorage } from '@optimystic/db-p2p';
|
|
4
|
-
import type { SqlCommitDurabilityListener, CommitFullyHeldListener } from '@optimystic/quereus-plugin-optimystic/plugin';
|
|
4
|
+
import type { SqlCommitDurabilityListener, CommitFullyHeldListener, OfflineConfig, OfflineMode } from '@optimystic/quereus-plugin-optimystic/plugin';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Which Optimystic transactor a strand connection commits through — a choice of
|
|
@@ -73,8 +73,44 @@ export interface StrandConnectionOptions {
|
|
|
73
73
|
* one is injected. Unit tests only.
|
|
74
74
|
*/
|
|
75
75
|
transactor?: StrandTransactor;
|
|
76
|
+
/**
|
|
77
|
+
* Turn on Optimystic's offline queue for this connection (docs/strands.md → "Offline mode").
|
|
78
|
+
* Absent: no offline configuration reaches the plugin, and the result carries no
|
|
79
|
+
* {@link SereusPluginResult.offline}. Programmatic only — the SQL plugin config cannot carry it.
|
|
80
|
+
*/
|
|
81
|
+
offline?: StrandOfflineOptions;
|
|
76
82
|
}
|
|
77
83
|
|
|
84
|
+
/**
|
|
85
|
+
* The Optimystic plugin's `offline` registration block (its README § "Offline mode"), less
|
|
86
|
+
* `journalLabel`: the composition derives the label from the strand id (`strandJournalLabel`).
|
|
87
|
+
* Without a `journal` the queue lives in memory, except where a platform defaults one (the browser
|
|
88
|
+
* connect journals into the strand's IndexedDB database).
|
|
89
|
+
*/
|
|
90
|
+
export type StrandOfflineOptions = Omit<OfflineConfig, 'journalLabel'>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The application's handle on a strand connection's offline queue: the part of Optimystic's
|
|
94
|
+
* `OfflineMode` an app calls. The class's other public methods are the plugin's internals.
|
|
95
|
+
*/
|
|
96
|
+
export type StrandOfflineController = Pick<OfflineMode,
|
|
97
|
+
'enter' | 'exit' | 'isEntered' | 'whenDrained' | 'onStatus' | 'onQueueState' | 'queueState'
|
|
98
|
+
| 'entries' | 'status' | 'discard' | 'outcomes' | 'acknowledge'>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* How a connection's sApp schema apply ended (see {@link SereusPluginResult.appSchema}):
|
|
102
|
+
*
|
|
103
|
+
* - `applied` — the schema is in place. `deferred` when queued offline transactions refused the
|
|
104
|
+
* first apply and a later one, once they had settled, landed.
|
|
105
|
+
* - `failed` — a deferred retry failed for a reason other than the queue; the connection keeps the
|
|
106
|
+
* schema it had before.
|
|
107
|
+
* - `cancelled` — the connection shut down before a deferred retry landed.
|
|
108
|
+
*/
|
|
109
|
+
export type AppSchemaOutcome =
|
|
110
|
+
| { state: 'applied'; deferred?: true }
|
|
111
|
+
| { state: 'failed'; error: unknown }
|
|
112
|
+
| { state: 'cancelled' };
|
|
113
|
+
|
|
78
114
|
export interface SereusPluginResult {
|
|
79
115
|
vtables: [];
|
|
80
116
|
functions: [];
|
|
@@ -122,6 +158,18 @@ export interface SereusPluginResult {
|
|
|
122
158
|
* `sync()` commits whatever is staged in it, outside any SQL transaction.
|
|
123
159
|
*/
|
|
124
160
|
createEmptyCollections: (except?: readonly string[]) => Promise<number>;
|
|
161
|
+
/** The offline queue's controller. Present only when {@link StrandConnectionOptions.offline} was given. */
|
|
162
|
+
offline?: StrandOfflineController;
|
|
163
|
+
/**
|
|
164
|
+
* Present when {@link StrandConnectionOptions.schema} was given; never rejects. Resolves
|
|
165
|
+
* `{ state: 'applied' }` at once when the apply succeeded at connect. When queued offline
|
|
166
|
+
* transactions refused it, the connection comes up on the `App` schema it already had and
|
|
167
|
+
* retries each time the queue drains; this settles when a retry lands, fails otherwise, or
|
|
168
|
+
* {@link shutdown} runs first.
|
|
169
|
+
*/
|
|
170
|
+
appSchema?: Promise<AppSchemaOutcome>;
|
|
171
|
+
/** The queued transaction ids that refused the connect-time apply. Present only when it was deferred. */
|
|
172
|
+
appSchemaDeferredBy?: readonly string[];
|
|
125
173
|
/** Shuts down the libp2p node and collection factory. Call when done. */
|
|
126
174
|
shutdown: () => Promise<void>;
|
|
127
175
|
}
|