@serfab/quereus-plugin-sereus 1.16.0 → 1.18.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 +390 -384
- package/dist/compose-strand.js +6 -1
- package/dist/compose-strand.js.map +1 -1
- package/dist/empty-collections.d.ts +28 -0
- package/dist/empty-collections.js +108 -0
- package/dist/empty-collections.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin-browser.js +25 -25
- package/dist/plugin-browser.js.map +4 -4
- package/dist/types.d.ts +14 -0
- package/package.json +8 -8
- package/src/compose-strand.ts +13 -3
- package/src/empty-collections.ts +139 -0
- package/src/index.ts +1 -0
- package/src/types.ts +135 -121
package/dist/types.d.ts
CHANGED
|
@@ -108,6 +108,20 @@ export interface SereusPluginResult {
|
|
|
108
108
|
* fires for a forgotten one. Passed through like {@link onCommitDurability}.
|
|
109
109
|
*/
|
|
110
110
|
onCommitFullyHeld: (listener: CommitFullyHeldListener) => () => void;
|
|
111
|
+
/**
|
|
112
|
+
* Commit an empty collection for every table declared in the `Strand` and `App` schemas that
|
|
113
|
+
* has none yet (main, index and unique-enforcement trees), and resolve to how many were
|
|
114
|
+
* committed. A founder calls it before its founding write, so a read of a table nobody has
|
|
115
|
+
* written is answered from local storage rather than by the cohort proving absence.
|
|
116
|
+
*
|
|
117
|
+
* `except` names qualified tables (`'Strand.Header'`) the caller is about to write, whose
|
|
118
|
+
* collections that write creates; every tree of those tables is left out. A name matching no
|
|
119
|
+
* declared table throws.
|
|
120
|
+
*
|
|
121
|
+
* Run it only while no other writer can reach this connection's `Database`: each collection's
|
|
122
|
+
* `sync()` commits whatever is staged in it, outside any SQL transaction.
|
|
123
|
+
*/
|
|
124
|
+
createEmptyCollections: (except?: readonly string[]) => Promise<number>;
|
|
111
125
|
/** Shuts down the libp2p node and collection factory. Call when done. */
|
|
112
126
|
shutdown: () => Promise<void>;
|
|
113
127
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serfab/quereus-plugin-sereus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Quereus plugin that connects to a Sereus strand for SQL access",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -68,13 +68,13 @@
|
|
|
68
68
|
"@libp2p/circuit-relay-v2": "^4.2.13",
|
|
69
69
|
"@libp2p/interface": "^3.3.0",
|
|
70
70
|
"@libp2p/websockets": "^10.1.21",
|
|
71
|
-
"@optimystic/db-core": "^1.
|
|
72
|
-
"@optimystic/db-p2p": "^1.
|
|
73
|
-
"@optimystic/db-p2p-storage-fs": "^1.
|
|
74
|
-
"@optimystic/db-p2p-storage-web": "^1.
|
|
75
|
-
"@optimystic/quereus-plugin-crypto": "^1.
|
|
76
|
-
"@optimystic/quereus-plugin-optimystic": "^1.
|
|
77
|
-
"@quereus/quereus": "^4.20.
|
|
71
|
+
"@optimystic/db-core": "^1.18.0",
|
|
72
|
+
"@optimystic/db-p2p": "^1.18.0",
|
|
73
|
+
"@optimystic/db-p2p-storage-fs": "^1.18.0",
|
|
74
|
+
"@optimystic/db-p2p-storage-web": "^1.18.0",
|
|
75
|
+
"@optimystic/quereus-plugin-crypto": "^1.18.0",
|
|
76
|
+
"@optimystic/quereus-plugin-optimystic": "^1.18.0",
|
|
77
|
+
"@quereus/quereus": "^4.20.3",
|
|
78
78
|
"debug": "^4.4.3"
|
|
79
79
|
},
|
|
80
80
|
"quereus": {
|
package/src/compose-strand.ts
CHANGED
|
@@ -9,10 +9,14 @@ import type { StrandConnectionOptions, SereusPluginResult, StrandTransactor, Lib
|
|
|
9
9
|
import { STRAND_SCHEMA } from './strand-schema.js';
|
|
10
10
|
import { resolveStrandClusterSize, STRAND_CLUSTER_POLICY } from './cluster-size.js';
|
|
11
11
|
import { wrapStorageWithCache, disposeStorageCache } from './cached-storage.js';
|
|
12
|
+
import { createEmptyCollections, type CollectionRegistrySource } from './empty-collections.js';
|
|
12
13
|
|
|
13
14
|
const log = debug('sereus:plugin:strand');
|
|
14
15
|
const timing = debug('sereus:plugin:strand:timing');
|
|
15
16
|
|
|
17
|
+
/** The schemas whose tables a strand founder gives a collection: strand membership, then the sApp's. */
|
|
18
|
+
const FOUNDED_SCHEMAS = ['Strand', 'App'] as const;
|
|
19
|
+
|
|
16
20
|
/**
|
|
17
21
|
* Minimal interface for the CollectionFactory returned by the optimystic plugin.
|
|
18
22
|
* We only need the methods this composition actually uses.
|
|
@@ -74,8 +78,12 @@ interface OptimysticPluginResult {
|
|
|
74
78
|
hydrate: (db: Database) => Promise<{ tables: number; indexes: number }>;
|
|
75
79
|
onCommitDurability: (listener: SqlCommitDurabilityListener) => () => void;
|
|
76
80
|
onCommitFullyHeld: (listener: CommitFullyHeldListener) => () => void;
|
|
77
|
-
/**
|
|
78
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Holds the durability follower's subscription to the node's `StorageRepo` (see
|
|
83
|
+
* {@link releaseOptimysticPlugin}) and the registry of every collection a table opened (see
|
|
84
|
+
* {@link createEmptyCollections}).
|
|
85
|
+
*/
|
|
86
|
+
txnBridge: { dispose(): void } & CollectionRegistrySource;
|
|
79
87
|
[key: string]: unknown;
|
|
80
88
|
}
|
|
81
89
|
|
|
@@ -375,7 +383,8 @@ export async function composeStrand(
|
|
|
375
383
|
throw err;
|
|
376
384
|
}
|
|
377
385
|
|
|
378
|
-
// 8. Return result with hydrate counts, the commit-durability listeners
|
|
386
|
+
// 8. Return result with hydrate counts, the commit-durability listeners, the founder's
|
|
387
|
+
// empty-collection step + shutdown handler.
|
|
379
388
|
// `shutdown` releases the plugin and stops the node only if this composition created it.
|
|
380
389
|
return {
|
|
381
390
|
vtables: [],
|
|
@@ -385,6 +394,7 @@ export async function composeStrand(
|
|
|
385
394
|
transactor: resolvedTransactor,
|
|
386
395
|
onCommitDurability: listener => pluginResult.onCommitDurability(listener),
|
|
387
396
|
onCommitFullyHeld: listener => pluginResult.onCommitFullyHeld(listener),
|
|
397
|
+
createEmptyCollections: except => createEmptyCollections(db, pluginResult.txnBridge, FOUNDED_SCHEMAS, except),
|
|
388
398
|
async shutdown() {
|
|
389
399
|
log('Shutting down strand connection %s', strandId);
|
|
390
400
|
await releaseOptimysticPlugin(pluginResult, createdNode);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import type { Database, TableSchema } from '@quereus/quereus';
|
|
3
|
+
import type { Collection, CollectionId } from '@optimystic/db-core';
|
|
4
|
+
import { defaultCollectionUri } from '@optimystic/quereus-plugin-optimystic';
|
|
5
|
+
|
|
6
|
+
const log = debug('sereus:plugin:empty-collections');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The part of the Optimystic plugin's transaction bridge this module reads: the live registry of
|
|
10
|
+
* every collection a table opened (its main tree, index trees and unique-enforcement trees),
|
|
11
|
+
* filled as each table initializes.
|
|
12
|
+
*/
|
|
13
|
+
export interface CollectionRegistrySource {
|
|
14
|
+
getCollectionRegistry(): Map<CollectionId, Collection<unknown>>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Commit an empty collection for every Optimystic table declared in `schemaNames` that has none
|
|
19
|
+
* yet, except the tables named in `except`, and resolve to how many collections were committed.
|
|
20
|
+
*
|
|
21
|
+
* Opening a table whose collection was never committed stages a fresh empty one in this node's
|
|
22
|
+
* tracker and writes nothing (`Collection.createOrOpen`'s invented branch), and a SQL commit
|
|
23
|
+
* flushes only the trees it wrote to. So the step opens every table, which registers each of its
|
|
24
|
+
* trees with the bridge, then syncs every registered collection still in that invented state.
|
|
25
|
+
*
|
|
26
|
+
* `except` holds qualified table names (`'Strand.Header'`) whose collections the caller is about
|
|
27
|
+
* to create by writing rows: an insert writes every tree of its table, so each of those trees is
|
|
28
|
+
* left out whether or not it was opened here. A name that matches no declared Optimystic table
|
|
29
|
+
* throws, so a renamed table cannot quietly fall back to committing everything.
|
|
30
|
+
*
|
|
31
|
+
* Commits outside any SQL transaction whatever is staged in those collections, so it is only
|
|
32
|
+
* sound while nothing else writes through `db`.
|
|
33
|
+
*/
|
|
34
|
+
export async function createEmptyCollections(
|
|
35
|
+
db: Database,
|
|
36
|
+
bridge: CollectionRegistrySource,
|
|
37
|
+
schemaNames: readonly string[],
|
|
38
|
+
except: readonly string[] = []
|
|
39
|
+
): Promise<number> {
|
|
40
|
+
const declared = optimysticTables(db, schemaNames);
|
|
41
|
+
const excluded = excludedTables(declared, except, schemaNames);
|
|
42
|
+
const tables = declared.filter(table => !excluded.includes(table));
|
|
43
|
+
await openTables(db, tables);
|
|
44
|
+
const registry = bridge.getCollectionRegistry();
|
|
45
|
+
assertMainCollectionsRegistered(registry, tables, schemaNames);
|
|
46
|
+
return await syncInventedCollections(registry, excluded.map(mainCollectionId));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Every Optimystic table the named schemas declare. A table reaches Optimystic either through its
|
|
51
|
+
* own `using` clause (an sApp's) or through the database's default vtab (the control tables), and
|
|
52
|
+
* both record `vtabModuleName`. None found means the filter no longer matches how those tables
|
|
53
|
+
* are backed, and carrying on would give no table a collection while appearing to work.
|
|
54
|
+
*/
|
|
55
|
+
function optimysticTables(db: Database, schemaNames: readonly string[]): TableSchema[] {
|
|
56
|
+
const tables = schemaNames.flatMap(name => {
|
|
57
|
+
const schema = db.schemaManager.getSchema(name);
|
|
58
|
+
return schema
|
|
59
|
+
? Array.from(schema.getAllTables()).filter(table => !table.isView && table.vtabModuleName === 'optimystic')
|
|
60
|
+
: [];
|
|
61
|
+
});
|
|
62
|
+
if (tables.length === 0) {
|
|
63
|
+
throw new Error(`Schemas ${schemaNames.join(', ')} declare no optimystic table; cannot give their tables an empty collection`);
|
|
64
|
+
}
|
|
65
|
+
return tables;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The declared tables `except` names, compared case-insensitively as SQL identifiers are. */
|
|
69
|
+
function excludedTables(declared: readonly TableSchema[], except: readonly string[], schemaNames: readonly string[]): TableSchema[] {
|
|
70
|
+
return except.map(name => {
|
|
71
|
+
const table = declared.find(candidate => `${candidate.schemaName}.${candidate.name}`.toLowerCase() === name.toLowerCase());
|
|
72
|
+
if (!table) {
|
|
73
|
+
throw new Error(`Cannot leave ${name} out of the empty collections: schemas ${schemaNames.join(', ')} declare no optimystic table by that name`);
|
|
74
|
+
}
|
|
75
|
+
return table;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A read initializes the table's vtab, which is what opens and registers its collections. */
|
|
80
|
+
async function openTables(db: Database, tables: readonly TableSchema[]): Promise<void> {
|
|
81
|
+
for (const table of tables) {
|
|
82
|
+
for await (const _row of db.eval(`select 1 from ${table.schemaName}."${table.name}" limit 1`)) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A declared table whose main collection is not registered means the plugin's open or
|
|
90
|
+
* registration path has changed under this module; founding on would leave that table with no
|
|
91
|
+
* collection while appearing to have given every table one.
|
|
92
|
+
*/
|
|
93
|
+
function assertMainCollectionsRegistered(
|
|
94
|
+
registry: ReadonlyMap<CollectionId, unknown>,
|
|
95
|
+
tables: readonly TableSchema[],
|
|
96
|
+
schemaNames: readonly string[]
|
|
97
|
+
): void {
|
|
98
|
+
const unregistered = tables.filter(table => !registry.has(mainCollectionId(table)));
|
|
99
|
+
if (unregistered.length > 0) {
|
|
100
|
+
const names = unregistered.map(table => `${table.schemaName}.${table.name} (${mainCollectionId(table)})`).join(', ');
|
|
101
|
+
throw new Error(`Opening the tables of ${schemaNames.join(', ')} registered no collection for ${names}; cannot give those tables an empty collection`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The collection id the Optimystic vtab stores `table` under: its URI without the `tree://` scheme. */
|
|
106
|
+
function mainCollectionId(table: TableSchema): CollectionId {
|
|
107
|
+
const explicitUri = table.vtabArgs?.['0'];
|
|
108
|
+
const uri = typeof explicitUri === 'string' && explicitUri !== '' ? explicitUri : defaultCollectionUri(table.schemaName, table.name);
|
|
109
|
+
return (uri.startsWith('tree://') ? uri.slice('tree://'.length) : uri) as CollectionId;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function syncInventedCollections(
|
|
113
|
+
registry: ReadonlyMap<CollectionId, Collection<unknown>>,
|
|
114
|
+
excludedMainIds: readonly CollectionId[]
|
|
115
|
+
): Promise<number> {
|
|
116
|
+
let committed = 0;
|
|
117
|
+
for (const [id, collection] of registry) {
|
|
118
|
+
if (belongsToExcludedTable(id, excludedMainIds)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// `committedRevision() === undefined` is the invented STATE, which its doc says is safe to branch on.
|
|
122
|
+
if (collection.committedRevision() === undefined && collection.hasUnsyncedChanges()) {
|
|
123
|
+
await collection.sync();
|
|
124
|
+
log('Committed empty collection %s', id);
|
|
125
|
+
committed++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return committed;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Whether `id` is one of an excluded table's trees: its main collection, or an index or
|
|
133
|
+
* unique-enforcement tree, which the Optimystic plugin keeps at `<main uri>/index/<name>`.
|
|
134
|
+
* Matched by id rather than by "opened here", because the caller's own reads of an excluded
|
|
135
|
+
* table registered it before this step ran.
|
|
136
|
+
*/
|
|
137
|
+
function belongsToExcludedTable(id: CollectionId, excludedMainIds: readonly CollectionId[]): boolean {
|
|
138
|
+
return excludedMainIds.some(mainId => id === mainId || id.startsWith(`${mainId}/index/`));
|
|
139
|
+
}
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -1,121 +1,135 @@
|
|
|
1
|
-
import type { Libp2p } from '@libp2p/interface';
|
|
2
|
-
import type { IRepo } from '@optimystic/db-core';
|
|
3
|
-
import type { IRawStorage } from '@optimystic/db-p2p';
|
|
4
|
-
import type { SqlCommitDurabilityListener, CommitFullyHeldListener } from '@optimystic/quereus-plugin-optimystic/plugin';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Which Optimystic transactor a strand connection commits through — a choice of
|
|
8
|
-
* storage engine, nothing more. See {@link StrandConnectionOptions.transactor}.
|
|
9
|
-
*
|
|
10
|
-
* Deliberately a closed union, and narrower than what Optimystic's
|
|
11
|
-
* `collection-factory.ts` accepts: that factory also switches on `'mesh-test'`
|
|
12
|
-
* (its production transactor stack over a one-node mock mesh) and falls through
|
|
13
|
-
* to a host-registered custom transactor for any other name. Neither is offered
|
|
14
|
-
* here — `composeStrand`'s node gate special-cases only `'test'`, so
|
|
15
|
-
* `'mesh-test'` would get a real libp2p node it does not want, and nothing in
|
|
16
|
-
* this repo registers a custom transactor. A closed union lets `parseConfig`
|
|
17
|
-
* reject a typo (`'locl'`) instead of handing it downstream; widen it (and the
|
|
18
|
-
* node gate with it) when one of those values is actually needed.
|
|
19
|
-
*/
|
|
20
|
-
export type StrandTransactor = 'local' | 'network' | 'test';
|
|
21
|
-
|
|
22
|
-
export interface StrandConnectionOptions {
|
|
23
|
-
/** UUID of the strand to connect to */
|
|
24
|
-
strandId: string;
|
|
25
|
-
/** Bootstrap multiaddrs for peer discovery */
|
|
26
|
-
bootstrapNodes?: string[];
|
|
27
|
-
/** sApp schema DDL to apply (optional - omit if schema already exists on strand) */
|
|
28
|
-
schema?: string;
|
|
29
|
-
/** sApp author public key */
|
|
30
|
-
sAppId?: string;
|
|
31
|
-
/** sApp version */
|
|
32
|
-
sAppVersion?: string;
|
|
33
|
-
/** libp2p listening port (default: 0 = random) */
|
|
34
|
-
port?: number;
|
|
35
|
-
/** Enable optimystic caching (default: true) */
|
|
36
|
-
enableCache?: boolean;
|
|
37
|
-
/** FRET profile (default: 'edge') */
|
|
38
|
-
fretProfile?: 'edge' | 'core';
|
|
39
|
-
/**
|
|
40
|
-
* Number of nodes Optimystic is told this strand's replication cluster should
|
|
41
|
-
* have. Ignored when {@link libp2pNode} is injected (the injected node was
|
|
42
|
-
* built with its own value). Every peer on the strand should use the same
|
|
43
|
-
* number — see `resolveStrandClusterSize`. Defaults to
|
|
44
|
-
* `DEFAULT_STRAND_CLUSTER_SIZE` (4 — the smallest breadth whose 0.75
|
|
45
|
-
* super-majority still commits with one holder offline).
|
|
46
|
-
*/
|
|
47
|
-
clusterSize?: number;
|
|
48
|
-
/** Inject an existing libp2p node instead of creating one */
|
|
49
|
-
libp2pNode?: Libp2p;
|
|
50
|
-
/** Required when libp2pNode is provided */
|
|
51
|
-
coordinatedRepo?: IRepo;
|
|
52
|
-
/**
|
|
53
|
-
* Persistent raw storage. When provided:
|
|
54
|
-
* - it is passed to `createLibp2pNode` as `storage` so the libp2p data path uses it,
|
|
55
|
-
* - with `transactor: 'local'` it is also handed to the optimystic plugin as
|
|
56
|
-
* `rawStorageFactory: () => storage` so the local transactor persists DML
|
|
57
|
-
* on the same instance (avoids cache divergence between the two consumers).
|
|
58
|
-
*
|
|
59
|
-
* The plugin treats `storage` as borrowed — it is NOT closed on `shutdown()`.
|
|
60
|
-
*/
|
|
61
|
-
storage?: IRawStorage;
|
|
62
|
-
/**
|
|
63
|
-
* Which Optimystic transactor writes and reads go through (default
|
|
64
|
-
* `'network'`):
|
|
65
|
-
*
|
|
66
|
-
* - `'network'` — transactions go through the strand's libp2p cohort. A node
|
|
67
|
-
* that is alone coordinates for itself, so this works with one peer or
|
|
68
|
-
* many; it is the only value an application should use.
|
|
69
|
-
* - `'local'` — transactions go straight to this process's raw storage, no
|
|
70
|
-
* peers consulted. For in-process tests and tooling; pair it with a
|
|
71
|
-
* persistent {@link storage} to survive restart.
|
|
72
|
-
* - `'test'` — Optimystic's in-memory fake. No libp2p node is created unless
|
|
73
|
-
* one is injected. Unit tests only.
|
|
74
|
-
*/
|
|
75
|
-
transactor?: StrandTransactor;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface SereusPluginResult {
|
|
79
|
-
vtables: [];
|
|
80
|
-
functions: [];
|
|
81
|
-
collations: [];
|
|
82
|
-
/**
|
|
83
|
-
* Catalog-hydration counts from the warm-restart hydrate that runs before
|
|
84
|
-
* `apply schema App;`. On a cold start (empty storage) both are 0; on a warm
|
|
85
|
-
* restart they report the persisted tables/indexes the catalog was primed
|
|
86
|
-
* with — so a test can assert hydration actually ran. `undefined` only if
|
|
87
|
-
* composition threw before hydrate.
|
|
88
|
-
*/
|
|
89
|
-
hydrated?: { tables: number; indexes: number };
|
|
90
|
-
/**
|
|
91
|
-
* The transactor this connection actually resolved to — the RESOLVED value,
|
|
92
|
-
* not the requested one, so a spec whose whole point is the arm it runs on can
|
|
93
|
-
* assert it rather than assume it. Equals `options.transactor` when one was
|
|
94
|
-
* given, `'network'` otherwise.
|
|
95
|
-
*/
|
|
96
|
-
transactor: StrandTransactor;
|
|
97
|
-
/**
|
|
98
|
-
* Be told, after every commit on this connection that wrote something, who holds what it
|
|
99
|
-
* wrote. Called once per such commit, synchronously, before the commit returns. The Optimystic
|
|
100
|
-
* plugin's listener of the same name, passed through; what the report means is in that
|
|
101
|
-
* plugin's README § "A commit's durability". Returns an idempotent unsubscribe.
|
|
102
|
-
*/
|
|
103
|
-
onCommitDurability: (listener: SqlCommitDurabilityListener) => () => void;
|
|
104
|
-
/**
|
|
105
|
-
* Be told once when a commit reported `followed` below full replication becomes held by
|
|
106
|
-
* every member of its cohort. Can fire inside the commit, right after its durability report.
|
|
107
|
-
* Followed commits live in memory: {@link shutdown} or a restart forgets them, and nothing
|
|
108
|
-
* fires for a forgotten one. Passed through like {@link onCommitDurability}.
|
|
109
|
-
*/
|
|
110
|
-
onCommitFullyHeld: (listener: CommitFullyHeldListener) => () => void;
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
1
|
+
import type { Libp2p } from '@libp2p/interface';
|
|
2
|
+
import type { IRepo } from '@optimystic/db-core';
|
|
3
|
+
import type { IRawStorage } from '@optimystic/db-p2p';
|
|
4
|
+
import type { SqlCommitDurabilityListener, CommitFullyHeldListener } from '@optimystic/quereus-plugin-optimystic/plugin';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Which Optimystic transactor a strand connection commits through — a choice of
|
|
8
|
+
* storage engine, nothing more. See {@link StrandConnectionOptions.transactor}.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately a closed union, and narrower than what Optimystic's
|
|
11
|
+
* `collection-factory.ts` accepts: that factory also switches on `'mesh-test'`
|
|
12
|
+
* (its production transactor stack over a one-node mock mesh) and falls through
|
|
13
|
+
* to a host-registered custom transactor for any other name. Neither is offered
|
|
14
|
+
* here — `composeStrand`'s node gate special-cases only `'test'`, so
|
|
15
|
+
* `'mesh-test'` would get a real libp2p node it does not want, and nothing in
|
|
16
|
+
* this repo registers a custom transactor. A closed union lets `parseConfig`
|
|
17
|
+
* reject a typo (`'locl'`) instead of handing it downstream; widen it (and the
|
|
18
|
+
* node gate with it) when one of those values is actually needed.
|
|
19
|
+
*/
|
|
20
|
+
export type StrandTransactor = 'local' | 'network' | 'test';
|
|
21
|
+
|
|
22
|
+
export interface StrandConnectionOptions {
|
|
23
|
+
/** UUID of the strand to connect to */
|
|
24
|
+
strandId: string;
|
|
25
|
+
/** Bootstrap multiaddrs for peer discovery */
|
|
26
|
+
bootstrapNodes?: string[];
|
|
27
|
+
/** sApp schema DDL to apply (optional - omit if schema already exists on strand) */
|
|
28
|
+
schema?: string;
|
|
29
|
+
/** sApp author public key */
|
|
30
|
+
sAppId?: string;
|
|
31
|
+
/** sApp version */
|
|
32
|
+
sAppVersion?: string;
|
|
33
|
+
/** libp2p listening port (default: 0 = random) */
|
|
34
|
+
port?: number;
|
|
35
|
+
/** Enable optimystic caching (default: true) */
|
|
36
|
+
enableCache?: boolean;
|
|
37
|
+
/** FRET profile (default: 'edge') */
|
|
38
|
+
fretProfile?: 'edge' | 'core';
|
|
39
|
+
/**
|
|
40
|
+
* Number of nodes Optimystic is told this strand's replication cluster should
|
|
41
|
+
* have. Ignored when {@link libp2pNode} is injected (the injected node was
|
|
42
|
+
* built with its own value). Every peer on the strand should use the same
|
|
43
|
+
* number — see `resolveStrandClusterSize`. Defaults to
|
|
44
|
+
* `DEFAULT_STRAND_CLUSTER_SIZE` (4 — the smallest breadth whose 0.75
|
|
45
|
+
* super-majority still commits with one holder offline).
|
|
46
|
+
*/
|
|
47
|
+
clusterSize?: number;
|
|
48
|
+
/** Inject an existing libp2p node instead of creating one */
|
|
49
|
+
libp2pNode?: Libp2p;
|
|
50
|
+
/** Required when libp2pNode is provided */
|
|
51
|
+
coordinatedRepo?: IRepo;
|
|
52
|
+
/**
|
|
53
|
+
* Persistent raw storage. When provided:
|
|
54
|
+
* - it is passed to `createLibp2pNode` as `storage` so the libp2p data path uses it,
|
|
55
|
+
* - with `transactor: 'local'` it is also handed to the optimystic plugin as
|
|
56
|
+
* `rawStorageFactory: () => storage` so the local transactor persists DML
|
|
57
|
+
* on the same instance (avoids cache divergence between the two consumers).
|
|
58
|
+
*
|
|
59
|
+
* The plugin treats `storage` as borrowed — it is NOT closed on `shutdown()`.
|
|
60
|
+
*/
|
|
61
|
+
storage?: IRawStorage;
|
|
62
|
+
/**
|
|
63
|
+
* Which Optimystic transactor writes and reads go through (default
|
|
64
|
+
* `'network'`):
|
|
65
|
+
*
|
|
66
|
+
* - `'network'` — transactions go through the strand's libp2p cohort. A node
|
|
67
|
+
* that is alone coordinates for itself, so this works with one peer or
|
|
68
|
+
* many; it is the only value an application should use.
|
|
69
|
+
* - `'local'` — transactions go straight to this process's raw storage, no
|
|
70
|
+
* peers consulted. For in-process tests and tooling; pair it with a
|
|
71
|
+
* persistent {@link storage} to survive restart.
|
|
72
|
+
* - `'test'` — Optimystic's in-memory fake. No libp2p node is created unless
|
|
73
|
+
* one is injected. Unit tests only.
|
|
74
|
+
*/
|
|
75
|
+
transactor?: StrandTransactor;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface SereusPluginResult {
|
|
79
|
+
vtables: [];
|
|
80
|
+
functions: [];
|
|
81
|
+
collations: [];
|
|
82
|
+
/**
|
|
83
|
+
* Catalog-hydration counts from the warm-restart hydrate that runs before
|
|
84
|
+
* `apply schema App;`. On a cold start (empty storage) both are 0; on a warm
|
|
85
|
+
* restart they report the persisted tables/indexes the catalog was primed
|
|
86
|
+
* with — so a test can assert hydration actually ran. `undefined` only if
|
|
87
|
+
* composition threw before hydrate.
|
|
88
|
+
*/
|
|
89
|
+
hydrated?: { tables: number; indexes: number };
|
|
90
|
+
/**
|
|
91
|
+
* The transactor this connection actually resolved to — the RESOLVED value,
|
|
92
|
+
* not the requested one, so a spec whose whole point is the arm it runs on can
|
|
93
|
+
* assert it rather than assume it. Equals `options.transactor` when one was
|
|
94
|
+
* given, `'network'` otherwise.
|
|
95
|
+
*/
|
|
96
|
+
transactor: StrandTransactor;
|
|
97
|
+
/**
|
|
98
|
+
* Be told, after every commit on this connection that wrote something, who holds what it
|
|
99
|
+
* wrote. Called once per such commit, synchronously, before the commit returns. The Optimystic
|
|
100
|
+
* plugin's listener of the same name, passed through; what the report means is in that
|
|
101
|
+
* plugin's README § "A commit's durability". Returns an idempotent unsubscribe.
|
|
102
|
+
*/
|
|
103
|
+
onCommitDurability: (listener: SqlCommitDurabilityListener) => () => void;
|
|
104
|
+
/**
|
|
105
|
+
* Be told once when a commit reported `followed` below full replication becomes held by
|
|
106
|
+
* every member of its cohort. Can fire inside the commit, right after its durability report.
|
|
107
|
+
* Followed commits live in memory: {@link shutdown} or a restart forgets them, and nothing
|
|
108
|
+
* fires for a forgotten one. Passed through like {@link onCommitDurability}.
|
|
109
|
+
*/
|
|
110
|
+
onCommitFullyHeld: (listener: CommitFullyHeldListener) => () => void;
|
|
111
|
+
/**
|
|
112
|
+
* Commit an empty collection for every table declared in the `Strand` and `App` schemas that
|
|
113
|
+
* has none yet (main, index and unique-enforcement trees), and resolve to how many were
|
|
114
|
+
* committed. A founder calls it before its founding write, so a read of a table nobody has
|
|
115
|
+
* written is answered from local storage rather than by the cohort proving absence.
|
|
116
|
+
*
|
|
117
|
+
* `except` names qualified tables (`'Strand.Header'`) the caller is about to write, whose
|
|
118
|
+
* collections that write creates; every tree of those tables is left out. A name matching no
|
|
119
|
+
* declared table throws.
|
|
120
|
+
*
|
|
121
|
+
* Run it only while no other writer can reach this connection's `Database`: each collection's
|
|
122
|
+
* `sync()` commits whatever is staged in it, outside any SQL transaction.
|
|
123
|
+
*/
|
|
124
|
+
createEmptyCollections: (except?: readonly string[]) => Promise<number>;
|
|
125
|
+
/** Shuts down the libp2p node and collection factory. Call when done. */
|
|
126
|
+
shutdown: () => Promise<void>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extended Libp2p node type with coordinatedRepo attached by createLibp2pNode.
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
133
|
+
export interface Libp2pNodeWithRepo extends Libp2p {
|
|
134
|
+
coordinatedRepo: IRepo;
|
|
135
|
+
}
|