@serfab/quereus-plugin-sereus 0.10.0 → 0.11.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/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
- * - in `bootstrap` mode it is also handed to the optimystic plugin as
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
- /** @internal Override transactor type. Used by unit tests with `'test'`. When set, takes precedence only if `mode` is not specified. */
52
- transactor?: string;
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.10.0",
3
+ "version": "0.11.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.22.0",
71
- "@optimystic/db-p2p": "^0.22.0",
72
- "@optimystic/db-p2p-storage-fs": "^0.22.0",
73
- "@optimystic/db-p2p-storage-web": "^0.22.0",
74
- "@optimystic/quereus-plugin-crypto": "^0.22.0",
75
- "@optimystic/quereus-plugin-optimystic": "^0.22.0",
76
- "@quereus/quereus": "^4.10.0",
70
+ "@optimystic/db-core": "^0.24.0",
71
+ "@optimystic/db-p2p": "^0.24.0",
72
+ "@optimystic/db-p2p-storage-fs": "^0.24.0",
73
+ "@optimystic/db-p2p-storage-web": "^0.24.0",
74
+ "@optimystic/quereus-plugin-crypto": "^0.24.0",
75
+ "@optimystic/quereus-plugin-optimystic": "^0.24.0",
76
+ "@quereus/quereus": "^4.14.0",
77
77
  "debug": "^4.4.3"
78
78
  },
79
79
  "quereus": {
@@ -0,0 +1,107 @@
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
+ * The memo dedupes *concurrent* seams; it is not what keeps a cache alive across a
35
+ * runtime rebuild. That is cadre-core's job, and it does it by OWNERSHIP: one
36
+ * resolved store per scope per runtime lifetime — a strand's store for the life of
37
+ * its instance (a hibernation quiesce → resume reuses the store already resolved,
38
+ * so the wake lands on a warm cache), the control store for the life of one
39
+ * `CadreNode.start()`. Because the store outlives the rebuild, so does its wrap,
40
+ * whether or not the embedder's provider returns a stable instance.
41
+ *
42
+ * When a scope's runtime stops for good, cadre-core calls {@link disposeStorageCache}
43
+ * — the release half of that ownership. Without it the retired wrapper's
44
+ * `CacheStoreHandle` would stay in the shared pool's registry for the process
45
+ * lifetime (`unregisterStore` is the only removal), one orphan per stop.
46
+ */
47
+
48
+ // Imported from the `/rn` entry — the platform-neutral surface (no Node-only
49
+ // transports) — because `composeStrand` reaches this module on the browser path
50
+ // too. Both db-p2p entrypoints re-export the same storage modules, so class
51
+ // identity (the `instanceof` checks below) is unaffected on Node.
52
+ import { CachedRawStorage, MemoryRawStorage, type IRawStorage } from '@optimystic/db-p2p/rn';
53
+
54
+ const wraps = new WeakMap<IRawStorage, CachedRawStorage>();
55
+
56
+ /**
57
+ * Reverse of `wraps`, so {@link disposeStorageCache} can evict the memo entry it
58
+ * cannot otherwise find — `wraps` is keyed by the inner instance, and dispose is
59
+ * handed the wrapper.
60
+ */
61
+ const inners = new WeakMap<CachedRawStorage, IRawStorage>();
62
+
63
+ /**
64
+ * The cached view of `storage`, memoized per instance. `label` names the store in
65
+ * the shared pool's `stats()` (e.g. `control`, or the strand id).
66
+ */
67
+ export function wrapStorageWithCache(storage: IRawStorage, label: string): IRawStorage {
68
+ if (storage instanceof MemoryRawStorage || storage instanceof CachedRawStorage) {
69
+ return storage;
70
+ }
71
+ let wrapped = wraps.get(storage);
72
+ if (!wrapped) {
73
+ wrapped = new CachedRawStorage(storage, undefined, label);
74
+ wraps.set(storage, wrapped);
75
+ inners.set(wrapped, storage);
76
+ }
77
+ return wrapped;
78
+ }
79
+
80
+ /**
81
+ * Release the cache wrapper {@link wrapStorageWithCache} returned: unregister it from
82
+ * the shared pool and forget the memo, so a later re-wrap of the same inner instance
83
+ * gets a live cache rather than this retired one. A disposed wrapper must never be
84
+ * handed out again — its pool store id is retired and never reused — and providers
85
+ * that return a stable instance per scope (the web reference app's `strand-storage.ts`,
86
+ * the integration harness's `captureRawStorage`) would get exactly that back on a
87
+ * relaunch if the memo were left in place.
88
+ *
89
+ * No-op for anything this module returned unwrapped (e.g. `MemoryRawStorage`), so
90
+ * callers need no `instanceof` test. Only the wrapper is released: the inner store the
91
+ * embedder returned is never closed — that handle stays the embedder's to manage.
92
+ */
93
+ export async function disposeStorageCache(storage: IRawStorage): Promise<void> {
94
+ if (!(storage instanceof CachedRawStorage)) {
95
+ return;
96
+ }
97
+ const inner = inners.get(storage);
98
+ if (inner) {
99
+ inners.delete(storage);
100
+ // Only evict the memo if it still points at THIS wrapper: a re-wrap after an
101
+ // earlier dispose has already installed a live successor we must not drop.
102
+ if (wraps.get(inner) === storage) {
103
+ wraps.delete(inner);
104
+ }
105
+ }
106
+ await storage.dispose();
107
+ }