@serfab/quereus-plugin-sereus 0.10.0 → 0.12.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 +367 -361
- package/dist/cached-storage.d.ts +95 -0
- package/dist/cached-storage.js +156 -0
- package/dist/cached-storage.js.map +1 -0
- package/dist/compose-strand.d.ts +16 -3
- package/dist/compose-strand.js +96 -50
- package/dist/compose-strand.js.map +1 -1
- package/dist/connect-browser.js +2 -1
- package/dist/connect-browser.js.map +1 -1
- package/dist/connect.js +2 -1
- package/dist/connect.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/parse-config.js +23 -11
- package/dist/parse-config.js.map +1 -1
- package/dist/plugin-browser.js +77326 -74471
- package/dist/plugin-browser.js.map +4 -4
- package/dist/strand-schema.d.ts +1 -1
- package/dist/strand-schema.js +544 -503
- package/dist/strand-schema.js.map +1 -1
- package/dist/types.d.ts +37 -10
- package/package.json +8 -8
- package/src/cached-storage.ts +178 -0
- package/src/cluster-size.ts +248 -248
- package/src/compose-strand.ts +396 -325
- package/src/connect-browser.ts +2 -1
- package/src/connect.ts +2 -1
- package/src/index.ts +2 -1
- package/src/parse-config.ts +70 -55
- package/src/plugin-browser.ts +29 -29
- package/src/plugin.ts +39 -39
- package/src/strand-schema.ts +574 -533
- package/src/types.ts +106 -78
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"strand-schema.js","sourceRoot":"","sources":["../src/strand-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG
|
|
1
|
+
{"version":3,"file":"strand-schema.js","sourceRoot":"","sources":["../src/strand-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgiB5B,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* Which Optimystic transactor a strand connection commits through — a choice of
|
|
6
|
+
* storage engine, nothing more. See {@link StrandConnectionOptions.transactor}.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately a closed union, and narrower than what Optimystic's
|
|
9
|
+
* `collection-factory.ts` accepts: that factory also switches on `'mesh-test'`
|
|
10
|
+
* (its production transactor stack over a one-node mock mesh) and falls through
|
|
11
|
+
* to a host-registered custom transactor for any other name. Neither is offered
|
|
12
|
+
* here — `composeStrand`'s node gate special-cases only `'test'`, so
|
|
13
|
+
* `'mesh-test'` would get a real libp2p node it does not want, and nothing in
|
|
14
|
+
* this repo registers a custom transactor. A closed union lets `parseConfig`
|
|
15
|
+
* reject a typo (`'locl'`) instead of handing it downstream; widen it (and the
|
|
16
|
+
* node gate with it) when one of those values is actually needed.
|
|
17
|
+
*/
|
|
18
|
+
export type StrandTransactor = 'local' | 'network' | 'test';
|
|
4
19
|
export interface StrandConnectionOptions {
|
|
5
20
|
/** UUID of the strand to connect to */
|
|
6
21
|
strandId: string;
|
|
@@ -31,25 +46,30 @@ export interface StrandConnectionOptions {
|
|
|
31
46
|
libp2pNode?: Libp2p;
|
|
32
47
|
/** Required when libp2pNode is provided */
|
|
33
48
|
coordinatedRepo?: IRepo;
|
|
34
|
-
/**
|
|
35
|
-
* Lifecycle mode. `'networked'` (default) uses the network transactor and
|
|
36
|
-
* is appropriate for multi-peer participation. `'bootstrap'` switches to a
|
|
37
|
-
* local transactor so a solo node can apply schema and accept DML with no
|
|
38
|
-
* peer round trips; pair it with a persistent `storage` to survive restart.
|
|
39
|
-
*/
|
|
40
|
-
mode?: 'bootstrap' | 'networked';
|
|
41
49
|
/**
|
|
42
50
|
* Persistent raw storage. When provided:
|
|
43
51
|
* - it is passed to `createLibp2pNode` as `storage` so the libp2p data path uses it,
|
|
44
|
-
* -
|
|
52
|
+
* - with `transactor: 'local'` it is also handed to the optimystic plugin as
|
|
45
53
|
* `rawStorageFactory: () => storage` so the local transactor persists DML
|
|
46
54
|
* on the same instance (avoids cache divergence between the two consumers).
|
|
47
55
|
*
|
|
48
56
|
* The plugin treats `storage` as borrowed — it is NOT closed on `shutdown()`.
|
|
49
57
|
*/
|
|
50
58
|
storage?: IRawStorage;
|
|
51
|
-
/**
|
|
52
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Which Optimystic transactor writes and reads go through (default
|
|
61
|
+
* `'network'`):
|
|
62
|
+
*
|
|
63
|
+
* - `'network'` — transactions go through the strand's libp2p cohort. A node
|
|
64
|
+
* that is alone coordinates for itself, so this works with one peer or
|
|
65
|
+
* many; it is the only value an application should use.
|
|
66
|
+
* - `'local'` — transactions go straight to this process's raw storage, no
|
|
67
|
+
* peers consulted. For in-process tests and tooling; pair it with a
|
|
68
|
+
* persistent {@link storage} to survive restart.
|
|
69
|
+
* - `'test'` — Optimystic's in-memory fake. No libp2p node is created unless
|
|
70
|
+
* one is injected. Unit tests only.
|
|
71
|
+
*/
|
|
72
|
+
transactor?: StrandTransactor;
|
|
53
73
|
}
|
|
54
74
|
export interface SereusPluginResult {
|
|
55
75
|
vtables: [];
|
|
@@ -66,6 +86,13 @@ export interface SereusPluginResult {
|
|
|
66
86
|
tables: number;
|
|
67
87
|
indexes: number;
|
|
68
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* The transactor this connection actually resolved to — the RESOLVED value,
|
|
91
|
+
* not the requested one, so a spec whose whole point is the arm it runs on can
|
|
92
|
+
* assert it rather than assume it. Equals `options.transactor` when one was
|
|
93
|
+
* given, `'network'` otherwise.
|
|
94
|
+
*/
|
|
95
|
+
transactor: StrandTransactor;
|
|
69
96
|
/** Shuts down the libp2p node and collection factory. Call when done. */
|
|
70
97
|
shutdown: () => Promise<void>;
|
|
71
98
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serfab/quereus-plugin-sereus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -67,13 +67,13 @@
|
|
|
67
67
|
"@libp2p/circuit-relay-v2": "^4.1.3",
|
|
68
68
|
"@libp2p/interface": "^3.1.0",
|
|
69
69
|
"@libp2p/websockets": "^10.1.3",
|
|
70
|
-
"@optimystic/db-core": "^0.
|
|
71
|
-
"@optimystic/db-p2p": "^0.
|
|
72
|
-
"@optimystic/db-p2p-storage-fs": "^0.
|
|
73
|
-
"@optimystic/db-p2p-storage-web": "^0.
|
|
74
|
-
"@optimystic/quereus-plugin-crypto": "^0.
|
|
75
|
-
"@optimystic/quereus-plugin-optimystic": "^0.
|
|
76
|
-
"@quereus/quereus": "^4.
|
|
70
|
+
"@optimystic/db-core": "^0.27.0",
|
|
71
|
+
"@optimystic/db-p2p": "^0.27.0",
|
|
72
|
+
"@optimystic/db-p2p-storage-fs": "^0.27.0",
|
|
73
|
+
"@optimystic/db-p2p-storage-web": "^0.27.0",
|
|
74
|
+
"@optimystic/quereus-plugin-crypto": "^0.27.0",
|
|
75
|
+
"@optimystic/quereus-plugin-optimystic": "^0.27.0",
|
|
76
|
+
"@quereus/quereus": "^4.18.0",
|
|
77
77
|
"debug": "^4.4.3"
|
|
78
78
|
},
|
|
79
79
|
"quereus": {
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap an embedder-supplied `IRawStorage` in `@optimystic/db-p2p`'s write-through
|
|
3
|
+
* raw-storage cache (`CachedRawStorage`) before handing it to a node or transactor.
|
|
4
|
+
*
|
|
5
|
+
* Why: bringing a control database or strand up is raw-storage-operation bound —
|
|
6
|
+
* uncached, a cold start re-reads the same handful of blocks ~2000 times (see the
|
|
7
|
+
* budget history in cadre-core's `test/control-start-storage-op-budget.spec.ts` and
|
|
8
|
+
* the sereus tickets `complete/optimystic-block-read-amplification-on-control-start`
|
|
9
|
+
* + `complete/optimystic-schema-catalog-reread-per-write-blows-storage-budgets`).
|
|
10
|
+
* Upstream ships the remedy as an opt-in wrapper — adoption is deliberately the
|
|
11
|
+
* consumer's choice — and this module is where Sereus opts in. It lives in this
|
|
12
|
+
* package (not cadre-core) for the same reason `cluster-size.ts` does: both the SQL
|
|
13
|
+
* plugin's own connectors (`composeStrand`, including the browser/IndexedDB path,
|
|
14
|
+
* where per-operation latency is highest) and cadre-core's control/strand seams
|
|
15
|
+
* need the identical wrap, and two copies could disagree.
|
|
16
|
+
*
|
|
17
|
+
* Soundness rests on the cache's single-process-owner invariant (upstream
|
|
18
|
+
* docs/storage.md §6): every write to the backend must funnel through the wrapped
|
|
19
|
+
* instance. Two rules uphold that:
|
|
20
|
+
*
|
|
21
|
+
* - **One wrap per inner instance** (WeakMap memo): the same `IRawStorage` reaches
|
|
22
|
+
* this function from several seams at once — cadre-core's node wiring and the
|
|
23
|
+
* strand backfill share the instance the node was built with, and the SQL
|
|
24
|
+
* plugin's `composeStrand` re-wraps whatever it is handed. Two live caches over
|
|
25
|
+
* one backend would each miss the other's writes; the memo makes the wrap
|
|
26
|
+
* idempotent instead — a caller wrapping an already-wrapped or already-seen
|
|
27
|
+
* instance gets the same object back.
|
|
28
|
+
* - **A Sereus process owns its data stores** — no second process writes the same
|
|
29
|
+
* backend, which is the invariant's other half.
|
|
30
|
+
*
|
|
31
|
+
* `MemoryRawStorage` is returned unwrapped on upstream's own guidance: the backend
|
|
32
|
+
* is already in-memory, so the cache adds bookkeeping with nothing to save.
|
|
33
|
+
*
|
|
34
|
+
* **{@link wrapStorageWithCache} and {@link disposeStorageCache} are a matched pair
|
|
35
|
+
* with a holder count between them.** Every wrap that hands back a cache this module
|
|
36
|
+
* created takes one claim on it; every dispose of that cache releases one. The cache
|
|
37
|
+
* is emptied, unregistered from the shared pool, and forgotten only when the LAST
|
|
38
|
+
* claim goes. That is what makes overlapping scopes safe: `CadreNodeConfig.storage.provider`
|
|
39
|
+
* may be a single `IRawStorage` shared by the control database and every workspace,
|
|
40
|
+
* and without a count the first scope to stop would retire the cache the others are
|
|
41
|
+
* still reading through — then the next scope to start, finding the memo empty, would
|
|
42
|
+
* build a SECOND cache over the same backend, and each would serve its own stale view
|
|
43
|
+
* (including remembered "this block does not exist" answers). It is also what lets
|
|
44
|
+
* `composeStrand` release its own claim unconditionally on `shutdown()` without having
|
|
45
|
+
* to guess whether it or cadre-core created the wrapper.
|
|
46
|
+
*
|
|
47
|
+
* The memo dedupes *concurrent* seams; it is not what keeps a cache alive across a
|
|
48
|
+
* runtime rebuild. That is cadre-core's job, and it does it by OWNERSHIP: one
|
|
49
|
+
* resolved store per scope per runtime lifetime — a strand's store for the life of
|
|
50
|
+
* its instance (a hibernation quiesce → resume reuses the store already resolved,
|
|
51
|
+
* so the wake lands on a warm cache), the control store for the life of one
|
|
52
|
+
* `CadreNode.start()`. Because the store outlives the rebuild, so does its wrap,
|
|
53
|
+
* whether or not the embedder's provider returns a stable instance.
|
|
54
|
+
*
|
|
55
|
+
* When a scope's runtime stops for good, it calls {@link disposeStorageCache} — the
|
|
56
|
+
* release half of that ownership. Without it the retired wrapper's `CacheStoreHandle`
|
|
57
|
+
* would stay in the shared pool's registry for the process lifetime (`unregisterStore`
|
|
58
|
+
* is the only removal), one orphan per stop.
|
|
59
|
+
*
|
|
60
|
+
* NOTE: upstream ships an equivalent counted wrap of its own — `withReadCache`, which
|
|
61
|
+
* hands back a `ReadCacheLease` per caller and additionally dedupes by
|
|
62
|
+
* `getStoreIdentity()`, so two DISTINCT instances over one directory share one cache.
|
|
63
|
+
* This module keeps its own count instead, because its public surface releases by
|
|
64
|
+
* WRAPPER (`disposeStorageCache(storage)`) rather than by lease, and `withReadCache`
|
|
65
|
+
* returns no lease on its already-cached pass-through — which would leave
|
|
66
|
+
* `composeStrand`'s unconditional release unable to tell its own claim from cadre-core's.
|
|
67
|
+
* Adopting the lease API would mean changing that surface at all three call sites; worth
|
|
68
|
+
* doing if Sereus ever needs the identity-keyed dedupe (two `FileRawStorage` over one
|
|
69
|
+
* path), and not before.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
// Imported from the `/rn` entry — the platform-neutral surface (no Node-only
|
|
73
|
+
// transports) — because `composeStrand` reaches this module on the browser path
|
|
74
|
+
// too. Both db-p2p entrypoints re-export the same storage modules, so class
|
|
75
|
+
// identity (the `instanceof` checks below) is unaffected on Node.
|
|
76
|
+
import { CachedRawStorage, MemoryRawStorage, type IRawStorage } from '@optimystic/db-p2p/rn';
|
|
77
|
+
|
|
78
|
+
const wraps = new WeakMap<IRawStorage, CachedRawStorage>();
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* One live cache this module created, and the number of holders claiming it.
|
|
82
|
+
*
|
|
83
|
+
* `inner` is the reverse of `wraps`, so {@link disposeStorageCache} can evict the memo
|
|
84
|
+
* entry it cannot otherwise find — `wraps` is keyed by the inner instance, and dispose
|
|
85
|
+
* is handed the wrapper.
|
|
86
|
+
*
|
|
87
|
+
* Presence in this map is also how the module answers "is this cache mine to retire?":
|
|
88
|
+
* an entry exists from construction until the last holder releases, so a cache the
|
|
89
|
+
* embedder built itself — and one whose last holder already released it — has none, and
|
|
90
|
+
* both take no claim and are never disposed here.
|
|
91
|
+
*/
|
|
92
|
+
type CacheClaim = {
|
|
93
|
+
readonly inner: IRawStorage;
|
|
94
|
+
holders: number;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const claims = new WeakMap<CachedRawStorage, CacheClaim>();
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Count one more holder of `wrapped`, if this module created it. A cache built outside
|
|
101
|
+
* this module stays its creator's to release, so it is left uncounted and
|
|
102
|
+
* {@link disposeStorageCache} no-ops on it.
|
|
103
|
+
*/
|
|
104
|
+
function addHolder(wrapped: CachedRawStorage): void {
|
|
105
|
+
const claim = claims.get(wrapped);
|
|
106
|
+
if (claim) {
|
|
107
|
+
claim.holders += 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The cached view of `storage`, memoized per instance, with one holder claim taken for
|
|
113
|
+
* this caller — release it with {@link disposeStorageCache} when this caller's scope
|
|
114
|
+
* ends. `label` names the store in the shared pool's `stats()` (e.g. `control`, or the
|
|
115
|
+
* strand id); on a memo hit the first caller's label sticks, since a second cache over
|
|
116
|
+
* one backend is the failure that matters and a stale pool label is not.
|
|
117
|
+
*/
|
|
118
|
+
export function wrapStorageWithCache(storage: IRawStorage, label: string): IRawStorage {
|
|
119
|
+
if (storage instanceof MemoryRawStorage) {
|
|
120
|
+
return storage;
|
|
121
|
+
}
|
|
122
|
+
if (storage instanceof CachedRawStorage) {
|
|
123
|
+
// Already the wrapper this module handed out: cadre-core wraps at its own seams
|
|
124
|
+
// and passes the wrapper down to `composeStrand`, which wraps again. Count the
|
|
125
|
+
// new holder rather than stacking a second cache over one backend.
|
|
126
|
+
// NOTE: a RETIRED wrapper (last holder already released) is also returned as-is,
|
|
127
|
+
// uncounted — the caller gets a dead cache with no error. No call site can reach
|
|
128
|
+
// that today: both retire sites drop their reference in the same block that
|
|
129
|
+
// releases. If a caller ever holds a wrapper across its own release, make this
|
|
130
|
+
// branch throw on an absent claim rather than hand back the corpse.
|
|
131
|
+
addHolder(storage);
|
|
132
|
+
return storage;
|
|
133
|
+
}
|
|
134
|
+
const memoized = wraps.get(storage);
|
|
135
|
+
if (memoized) {
|
|
136
|
+
addHolder(memoized);
|
|
137
|
+
return memoized;
|
|
138
|
+
}
|
|
139
|
+
const wrapped = new CachedRawStorage(storage, undefined, label);
|
|
140
|
+
wraps.set(storage, wrapped);
|
|
141
|
+
claims.set(wrapped, { inner: storage, holders: 1 });
|
|
142
|
+
return wrapped;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Release this caller's claim on the cache wrapper {@link wrapStorageWithCache} returned.
|
|
147
|
+
* The wrapper is emptied, unregistered from the shared pool, and dropped from the memo
|
|
148
|
+
* only when the LAST holder releases — so a scope stopping never retires a cache another
|
|
149
|
+
* live scope is still reading through, and a still-held wrapper is never replaced by a
|
|
150
|
+
* competing second cache over the same backend.
|
|
151
|
+
*
|
|
152
|
+
* A wrapper with no live claim is left alone: that covers both a cache the embedder built
|
|
153
|
+
* itself (theirs to dispose) and a late second dispose of an already-retired wrapper,
|
|
154
|
+
* whose successor in the memo must not be dropped.
|
|
155
|
+
*
|
|
156
|
+
* No-op for anything this module returned unwrapped (e.g. `MemoryRawStorage`), so callers
|
|
157
|
+
* need no `instanceof` test. Only the wrapper is released: the inner store the embedder
|
|
158
|
+
* returned is never closed — that handle stays the embedder's to manage.
|
|
159
|
+
*/
|
|
160
|
+
export async function disposeStorageCache(storage: IRawStorage): Promise<void> {
|
|
161
|
+
if (!(storage instanceof CachedRawStorage)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const claim = claims.get(storage);
|
|
165
|
+
if (!claim) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Decrement, and on zero retire the bookkeeping, all in this SYNCHRONOUS prefix —
|
|
169
|
+
// before the await below. A wrap of the same inner instance interleaved between the
|
|
170
|
+
// two would otherwise find a memo entry pointing at a dying cache.
|
|
171
|
+
claim.holders -= 1;
|
|
172
|
+
if (claim.holders > 0) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
claims.delete(storage);
|
|
176
|
+
wraps.delete(claim.inner);
|
|
177
|
+
await storage.dispose();
|
|
178
|
+
}
|