@serfab/quereus-plugin-sereus 1.7.0 → 1.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/dist/plugin.js CHANGED
@@ -14,14 +14,10 @@ export { parseConfig };
14
14
  * Default export: Quereus plugin registration function.
15
15
  */
16
16
  export default async function register(db, config = {}) {
17
- const options = parseConfig(config);
18
- // `storage_path` is a Node-only loader convenience: a `Record<string, SqlValue>`
19
- // cannot carry an `IRawStorage`, so the path is resolved to a concrete
20
- // `FileRawStorage` here (not in the platform-agnostic `parseConfig`). The
21
- // dynamic import keeps `@optimystic/db-p2p-storage-fs` (and its `node:fs`
17
+ const { options, storagePath } = parseConfig(config);
18
+ // The dynamic import keeps `@optimystic/db-p2p-storage-fs` (and its `node:fs`
22
19
  // dependency) out of the browser/RN module graph.
23
- const storagePath = config.storage_path;
24
- if (typeof storagePath === 'string' && storagePath) {
20
+ if (storagePath) {
25
21
  const { FileRawStorage } = await import('@optimystic/db-p2p-storage-fs');
26
22
  options.storage = new FileRawStorage(storagePath);
27
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,OAAO,EAAE,WAAW,EAAE,CAAC;AAEvB;;GAEG;AACH,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,QAAQ,CACrC,EAAY,EACZ,SAAmC,EAAE;IAErC,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAEpC,iFAAiF;IACjF,uEAAuE;IACvE,0EAA0E;IAC1E,0EAA0E;IAC1E,kDAAkD;IAClD,MAAM,WAAW,GAAG,MAAM,CAAC,YAAY,CAAC;IACxC,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,EAAE,CAAC;QACpD,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,+BAA+B,CAAC,CAAC;QACzE,OAAO,CAAC,OAAO,GAAG,IAAI,cAAc,CAAC,WAAW,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,eAAe,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC"}
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,OAAO,EAAE,WAAW,EAAE,CAAC;AAEvB;;GAEG;AACH,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,QAAQ,CACrC,EAAY,EACZ,SAAmC,EAAE;IAErC,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAErD,8EAA8E;IAC9E,kDAAkD;IAClD,IAAI,WAAW,EAAE,CAAC;QACjB,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,+BAA+B,CAAC,CAAC;QACzE,OAAO,CAAC,OAAO,GAAG,IAAI,cAAc,CAAC,WAAW,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,eAAe,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serfab/quereus-plugin-sereus",
3
- "version": "1.7.0",
3
+ "version": "1.8.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",
@@ -59,6 +59,7 @@
59
59
  "esbuild": "^0.27.2",
60
60
  "fake-indexeddb": "^6.2.4",
61
61
  "jsdom": "^25.0.1",
62
+ "marked": "^18.0.14",
62
63
  "rimraf": "^6.1.2",
63
64
  "typescript": "^5.9.3",
64
65
  "vitest": "^4.0.17"
@@ -67,12 +68,12 @@
67
68
  "@libp2p/circuit-relay-v2": "^4.1.3",
68
69
  "@libp2p/interface": "^3.1.0",
69
70
  "@libp2p/websockets": "^10.1.3",
70
- "@optimystic/db-core": "^1.7.0",
71
- "@optimystic/db-p2p": "^1.7.0",
72
- "@optimystic/db-p2p-storage-fs": "^1.7.0",
73
- "@optimystic/db-p2p-storage-web": "^1.7.0",
74
- "@optimystic/quereus-plugin-crypto": "^1.7.0",
75
- "@optimystic/quereus-plugin-optimystic": "^1.7.0",
71
+ "@optimystic/db-core": "^1.8.0",
72
+ "@optimystic/db-p2p": "^1.8.0",
73
+ "@optimystic/db-p2p-storage-fs": "^1.8.0",
74
+ "@optimystic/db-p2p-storage-web": "^1.8.0",
75
+ "@optimystic/quereus-plugin-crypto": "^1.8.0",
76
+ "@optimystic/quereus-plugin-optimystic": "^1.8.0",
76
77
  "@quereus/quereus": "^4.20.0",
77
78
  "debug": "^4.4.3"
78
79
  },
@@ -92,53 +93,72 @@
92
93
  "settings": [
93
94
  {
94
95
  "key": "strand_id",
96
+ "label": "Strand ID",
95
97
  "type": "string"
96
98
  },
97
99
  {
98
100
  "key": "bootstrap_nodes",
101
+ "label": "Bootstrap nodes",
99
102
  "type": "string",
100
- "default": ""
103
+ "default": "",
104
+ "help": "comma-separated multiaddrs"
101
105
  },
102
106
  {
103
107
  "key": "schema",
108
+ "label": "sApp schema",
104
109
  "type": "string"
105
110
  },
106
111
  {
107
112
  "key": "sapp_id",
113
+ "label": "sApp ID",
108
114
  "type": "string",
109
115
  "default": "unknown"
110
116
  },
111
117
  {
112
118
  "key": "sapp_version",
119
+ "label": "sApp version",
113
120
  "type": "string",
114
121
  "default": "1.0.0"
115
122
  },
116
123
  {
117
124
  "key": "port",
125
+ "label": "Port",
118
126
  "type": "number",
119
- "default": 0
127
+ "default": 0,
128
+ "help": "0 = random"
120
129
  },
121
130
  {
122
131
  "key": "enable_cache",
132
+ "label": "Enable cache",
123
133
  "type": "boolean",
124
134
  "default": true
125
135
  },
126
136
  {
127
137
  "key": "fret_profile",
128
- "type": "string",
138
+ "label": "FRET profile",
139
+ "type": "select",
140
+ "options": [
141
+ "edge",
142
+ "core"
143
+ ],
129
144
  "default": "edge"
130
145
  },
131
- {
132
- "key": "mode",
133
- "type": "string"
134
- },
135
146
  {
136
147
  "key": "transactor",
137
- "type": "string"
148
+ "label": "Transactor",
149
+ "type": "select",
150
+ "options": [
151
+ "network",
152
+ "local",
153
+ "test"
154
+ ],
155
+ "help": "storage engine"
138
156
  },
139
157
  {
140
158
  "key": "storage_path",
141
- "type": "string"
159
+ "label": "Storage path",
160
+ "type": "string",
161
+ "help": "Node only"
142
162
  }
143
163
  ]
144
164
  }
@@ -16,18 +16,35 @@ export const MIN_CLUSTER_SIZE = 2;
16
16
  * 1000 ms, a LAN-shaped budget, and it deliberately left that default alone — choosing the
17
17
  * value for a deployment of phones is the embedder's job.
18
18
  *
19
- * **Why 5000.** Optimystic GitHub #22, reported by a sereus-based chat app: two parties that
20
- * reach each other only through a public circuit relay have a round trip near 1.8 s, and a
21
- * fresh stream (dial or reuse, protocol select, send, receive) cannot finish inside a second.
22
- * At a cohort of two there is exactly one peer to ask, so one late answer leaves the read with
23
- * nothing to corroborate against and it is declined. 5000 covers 1.8 s with room for the Noise
24
- * handshake, the relay hops and a phone's pure-JS crypto (cadre-core's
25
- * `NetworkConfig.noiseCrypto`), and it is the value Optimystic's own note advises for that
26
- * link. The whole-pass reconcile bound Optimystic derives from it (`max(5000, 5 x this)`)
27
- * becomes 25 s.
28
- *
29
- * **Measured** 2026-09-26, one Windows developer machine, two relay-only `CadreNode`s on one
30
- * shared loopback dedicated relay, the one-way outbound frame delay raised to 900 ms
19
+ * **Why 7000: it is a derivation, written out.** One read-path request opens a fresh protocol
20
+ * stream over a connection that is already open, which costs the protocol negotiation plus the
21
+ * request and its answer: two link round trips, `CIRCUIT_REQUEST_ROUND_TRIPS` in cadre-core's
22
+ * `link-budget.ts`. At the link round trip cadre declares by default,
23
+ * `DECLARED_LINK_ROUND_TRIP_MS` (3 500 ms, covering the 3-second relayed round trip sereus
24
+ * supports), that is 2 x 3 500 = 7 000 ms — `cohortReadDeadlineMs()` in that module. This
25
+ * constant must equal it, and `cadre-core/test/link-budget.spec.ts` pins the two equal; it is
26
+ * spelled as a number here only because this package cannot import cadre-core (the dependency
27
+ * runs the other way). A host that declares its own link (`NetworkConfig.linkRoundTripMs`)
28
+ * gets the derivation at that link instead of this constant, and a host that sets
29
+ * `NetworkConfig.cohortQueryTimeoutMs` gets exactly what it set. No admission allowance is
30
+ * added: a read runs over an open connection and opens none. The whole-pass reconcile bound
31
+ * Optimystic derives from it (`max(5000, 5 x this)`) becomes 35 s.
32
+ *
33
+ * **History: why 5000 before, and why it was not enough.** Optimystic GitHub #22, reported by
34
+ * a sereus-based chat app: two parties that reach each other only through a public circuit
35
+ * relay have a round trip near 1.8 s, and a fresh stream (dial or reuse, protocol select, send,
36
+ * receive) cannot finish inside a second. At a cohort of two there is exactly one peer to ask,
37
+ * so one late answer leaves the read with nothing to corroborate against and it is declined.
38
+ * 5000 was chosen to cover 1.8 s with room for the Noise handshake, the relay hops and a
39
+ * phone's pure-JS crypto (cadre-core's `NetworkConfig.noiseCrypto`), and it is the value
40
+ * Optimystic's own note advises for that link. But sereus supports a 3 s round trip, and two
41
+ * round trips of pure delay at that link is 6 s: above 5 000, so at the supported link every
42
+ * consult timed out and every read had nothing to corroborate against — the #22 failure again,
43
+ * one band up. The derivation ends that class: the next person to widen the link changes one
44
+ * declaration.
45
+ *
46
+ * **Measured at 5000**, 2026-09-26, one Windows developer machine, two relay-only `CadreNode`s
47
+ * on one shared loopback dedicated relay, the one-way outbound frame delay raised to 900 ms
31
48
  * (`pipelined` mode — see `docs/testing.md` → "Where measurements live") AFTER the strand had
32
49
  * formed and first synced, for a round trip near 1.8 s. A machine that had joined, gone away
33
50
  * while the other wrote, then re-attached over its own stale store and read the row written
@@ -36,53 +53,55 @@ export const MIN_CLUSTER_SIZE = 2;
36
53
  * `cluster-fetch:peers-silent { silent: 1, consulted: 2 }` followed by
37
54
  * `cluster-fetch:no-quorum`, retried about a second later until one lands; at 5000 the
38
55
  * corroboration succeeded instead (`cluster-fetch:local-current`). Both journeys COMPLETED at
39
- * either value, which is why no committed scenario gates this — 900 ms is not where it breaks,
40
- * and above it a run breaks on redialling instead. The recipe for re-measuring is in
41
- * `docs/testing.md` → "Where measurements live".
56
+ * either value, which is why no committed scenario gates this — 900 ms is not where it breaks.
57
+ * The recipe for re-measuring is in `docs/testing.md` → "Where measurements live".
58
+ *
59
+ * **Measured at 7000 against 5000**, 2026-09-29, same machine and shape, at 1 500 ms one-way
60
+ * (the supported 3 s round trip), fresh-join arm, one run each under
61
+ * `DEBUG=optimystic:db-p2p:coordinator-repo*`, on `@optimystic/*` 1.7.0: **39 declined reads at
62
+ * 5000, 32 at 7000**, both runs completing (writable at 70.2 s and 63.6 s). That difference
63
+ * was noise, not the deadline at work: in both runs every consult on the joiner ended
64
+ * 3.00-3.02 s after the one before it, the signature of Optimystic's then-fixed 3 000 ms
65
+ * request dial deadline, which also bounded the protocol negotiation on an ALREADY-OPEN
66
+ * connection, where the negotiation alone costs one link round trip — 3 s at this delay.
67
+ *
68
+ * **Measured at 7000 on 1.8.0**, the same day and shape, once a consult ran under this deadline
69
+ * alone and cadre-core stated its declared link to Optimystic: **no declined read** during the
70
+ * measured attach, over two fresh joins and three re-attaches, and every consult on the joiner
71
+ * completed 6.03-6.05 s after the one before it — the two round trips this deadline is derived
72
+ * from, with about 0.95 s to spare. The first-sync figures from those runs are on
73
+ * `DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS`.
42
74
  *
43
75
  * **What it costs**, in the terms Optimystic's own field doc uses: a peer that is truly gone
44
- * now holds a read of a block missing locally for up to 5 s before the read is declined
45
- * instead of 1 s. A joining machine's first sync runs several such consults, so it is the cost
46
- * that binds — and the first-sync band measured at BOTH deadlines, against the budget it has
47
- * to fit, is recorded once, on `DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS` in cadre-core's
48
- * `strand-first-sync-gate.ts`. At 5000 against the 30 s budget that preceded it, this change
49
- * would have turned a slow-but-working join into the very `StrandAwaitingFirstSyncError` the
50
- * report named. 3000 is the fallback if that margin ever goes — still 1.7x the reported round
51
- * trip, and a doomed consult costs 40% less.
52
- *
53
- * **What else it lengthens, which the first-sync budget is only the largest case of.** Every
54
- * cadre-core deadline that wraps an Optimystic read or commit was sized when one silent peer
55
- * cost 1 s and a whole reconcile pass cost 5 s; those are now 5 s and 25 s. Two consequences
56
- * are already true rather than hypothetical:
57
- *
58
- * - The inbound membership admission gate reads the control database uncached and FAILS OPEN
59
- * after 2 s (`ADMISSION_DECISION_TIMEOUT_MS`, cadre-core's
60
- * `membership-connection-gater.ts`), so a decision whose read consults a silent peer can no
61
- * longer settle inside that deadline at all: an unplaced peer is admitted at the connection
62
- * layer for the whole 5 s instead of roughly 1 s. It is admitted to nothing more than a
63
- * connection — the per-protocol stream gates still decide, and unplaced relay reservations
64
- * stay capped (`MAX_UNAUTHORIZED_RELAY_RESERVATIONS`) — which is why this is a widened cost
65
- * and not a hole. `backlog/debt-cadre-deadlines-sized-against-old-optimystic-bounds` owns
66
- * the audit of the rest.
67
- * - `CONTROL_READ_RETRY_BUDGET_MS` (1500 ms) now terminates the control-read retry loop after
68
- * the FIRST attempt whenever that attempt burned this deadline, so a read failing that way
69
- * gets no second presentation. Its own doc carries the coupling.
76
+ * now holds a read of a block missing locally for up to 7 s before the read is declined
77
+ * instead of 5 s (1 s at Optimystic's default). A joining machine's first sync runs several
78
+ * such consults, so it is the cost that binds — and the first-sync band measured at each
79
+ * deadline, against the budget it has to fit, is recorded once, on
80
+ * `DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS` in cadre-core's `strand-first-sync-gate.ts`. At 5000
81
+ * against the 30 s budget that once preceded it, that change alone would have turned a
82
+ * slow-but-working join into the very `StrandAwaitingFirstSyncError` the report named.
83
+ *
84
+ * **What else it lengthens.** Every cadre-core deadline that waits on an Optimystic read or
85
+ * commit waits on this one too; which of them cut the read off on purpose and which must
86
+ * outlast it is listed in `docs/cadre-consistency.md` → "Deadlines Over Optimystic's Reads and
87
+ * Commits".
70
88
  *
71
89
  * **One value for both networks, not two.** The reason to widen is the link, and a phone's
72
90
  * control node and its strand nodes share it; nothing about control traffic or strand traffic
73
91
  * argues for different numbers. A host that needs another value moves both at once —
74
- * cadre-core's `NetworkConfig.cohortQueryTimeoutMs`, threaded to {@link controlClusterPolicy}
75
- * and {@link strandClusterPolicy}.
92
+ * cadre-core's `NetworkConfig.cohortQueryTimeoutMs` or `NetworkConfig.linkRoundTripMs`,
93
+ * threaded to {@link controlClusterPolicy} and {@link strandClusterPolicy} through one helper
94
+ * (`declaredCohortReadDeadlineMs` in cadre-core's `link-budget.ts`).
76
95
  *
77
96
  * NOTE: this number and the first-sync budget are coupled; the margin between that budget and
78
- * the slowest first sync measured is stated on it. Re-measure the first-sync band before raising
79
- * this again, or when a deployment's first sync grows more collections than the two-table
80
- * scenario measured above — `DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS` carries the result and
81
- * `docs/testing.md` the recipe.
97
+ * the slowest first sync measured is stated on it. Re-measure the first-sync band before
98
+ * raising the declared link round trip, or when a deployment's first sync grows more
99
+ * collections than the two-table scenario measured above — `DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS`
100
+ * carries the result and `docs/testing.md` the recipe.
82
101
  * Nothing warns when the margin goes; the symptom is `StrandAwaitingFirstSyncError` on a join
83
102
  * that was progressing normally, which is the failure this pair of numbers exists to end.
84
103
  */
85
- export const COHORT_READ_DEADLINE_MS = 5000;
104
+ export const COHORT_READ_DEADLINE_MS = 7000;
86
105
 
87
106
  /**
88
107
  * How many nodes each block of the **control** (cadre membership) database is replicated
@@ -253,7 +272,9 @@ export const CONTROL_CLUSTER_POLICY = Object.freeze({
253
272
  * The control network derives one from the party's enrolled machines
254
273
  * ({@link controlClusterPolicy}), which is exact there because every enrolled machine runs the
255
274
  * control node. A strand has no equivalent count: it launches only on machines whose embedder
256
- * registered its sApp config, and reusing the party count over-declares — which is worse than
275
+ * registered its sApp config, plus storage-profile machines that host unclaimed strands as
276
+ * storage replicas (cadre-core's `CadreNodeConfig.hostUnclaimedStrands`) and whose strand filter
277
+ * admits it. Phones serve only what their app claims, so reusing the party count over-declares, which is worse than
257
278
  * declaring nothing, because at a yardstick of three or more the corroboration floor is pinned
258
279
  * at two peers and a strand that can field only one could then never repair at all. So every
259
280
  * strand runs {@link STRAND_CLUSTER_POLICY} unchanged and takes the single-voter exposure
@@ -459,8 +480,10 @@ export function resolveStrandClusterSize(configured?: number): number {
459
480
  * machine runs the control node by construction: there, enrolled IS serving
460
481
  * ({@link controlClusterPolicy}, fed by cadre-core's `enrolled-machine-store.ts`). A STRAND does
461
482
  * not — it launches only on machines whose embedder registered its sApp config
462
- * (`CadreNode.addStrand`), so a closed strand shared by two machines of a three-machine party is
463
- * served by two, and declaring three would be exactly the unsafe over-declaration above. No
483
+ * (`CadreNode.addStrand`), plus storage-profile machines hosting it as a storage replica
484
+ * (`CadreNodeConfig.hostUnclaimedStrands`) whose strand filter admits it. A closed strand
485
+ * claimed by two phones of a three-phone party is served by two, and declaring three would be
486
+ * exactly the unsafe over-declaration above. No
464
487
  * authenticated per-strand serving count exists yet, so strand nodes declare NOTHING and run the
465
488
  * frozen {@link STRAND_CLUSTER_POLICY} — keeping the known, upstream-tracked single-voter
466
489
  * exposure (`backlog/debt-read-repair-single-voter-corroboration`) rather than risking the
@@ -248,10 +248,11 @@ export async function composeStrand(
248
248
  } else {
249
249
  // NOTE: this path takes the frozen policy whole, so it takes COHORT_READ_DEADLINE_MS
250
250
  // with no way to override it — `StrandConnectionOptions` has no counterpart to
251
- // cadre-core's `NetworkConfig.cohortQueryTimeoutMs`. Fine today: every production
252
- // strand comes up through cadre-core, which does thread the field, and this path is
253
- // the plugin's own connect/e2e route where 5000 ms is the right answer anyway. If a
254
- // plugin embedder ever needs a different deadline,
251
+ // cadre-core's `NetworkConfig.cohortQueryTimeoutMs` or `linkRoundTripMs`. Fine
252
+ // today: every production strand comes up through cadre-core, which does thread
253
+ // both, and this path is the plugin's own connect/e2e route, where the constant —
254
+ // two link round trips at cadre's default declared link — is the right answer
255
+ // anyway. If a plugin embedder ever needs a different deadline,
255
256
  // `strandClusterPolicy(clusterSize, { cohortQueryTimeoutMs: <ms> })` is the call to
256
257
  // reach for here.
257
258
  const created = await platform.createNode({ networkName, bootstrapNodes, fretProfile, port, clusterSize, clusterPolicy: STRAND_CLUSTER_POLICY, storage });
@@ -328,12 +329,7 @@ export async function composeStrand(
328
329
  // is a note for whoever first ships a migration that does.
329
330
  if (schema) {
330
331
  log('Applying sApp schema for strand %s', strandId);
331
- await db.exec(`
332
- declare schema App {
333
- ${schema}
334
- }
335
- apply schema App;
336
- `);
332
+ await applyAppSchema(db, schema);
337
333
  log('sApp schema applied');
338
334
  }
339
335
  } catch (err) {
@@ -372,6 +368,37 @@ export async function composeStrand(
372
368
  };
373
369
  }
374
370
 
371
+ /**
372
+ * Step 7 of {@link composeStrand}: declare the sApp's schema as `App` and apply it — a
373
+ * declarative diff against the catalog, so re-applying a schema that is already in place
374
+ * emits nothing. The one site that applies an `App` schema: `composeStrand` calls it at
375
+ * bring-up, and cadre-core's `StrandDatabase.attachAppSchema` calls it on a live storage
376
+ * replica when an app claims the strand. The database must already be composed (optimystic
377
+ * set as the default vtab), or the tables land in memory instead of the strand.
378
+ *
379
+ * Refuses a schema holding an item the Quereus parser skipped: it keeps any item whose leading
380
+ * keyword it does not model (`create unique index …`, a misspelled `tabel`, `domain`) as an
381
+ * opaque placeholder that apply ignores, so the app would otherwise run without that item.
382
+ */
383
+ export async function applyAppSchema(db: Database, schema: string): Promise<void> {
384
+ await db.exec(`
385
+ declare schema App {
386
+ ${schema}
387
+ }
388
+ `);
389
+ assertNoIgnoredItems(db);
390
+ await db.exec('apply schema App;');
391
+ }
392
+
393
+ function assertNoIgnoredItems(db: Database): void {
394
+ const items = db.declaredSchemaManager.getDeclaredSchema('App')?.items ?? [];
395
+ const ignored = items.filter(item => item.type === 'declareIgnored').length;
396
+ if (ignored > 0) {
397
+ // Only a count: Quereus leaves an ignored item's source text empty.
398
+ throw new Error(`sApp schema has ${ignored} item(s) the parser does not recognize (a \`create …\` prefix or a misspelled item keyword such as \`tabel\`); items are \`table\`, \`index\`, \`unique index\`, \`view\`, \`materialized view\`, \`seed\` and \`assertion\``);
399
+ }
400
+ }
401
+
375
402
  /**
376
403
  * Steps 1-2 of {@link composeStrand}: register the crypto plugin, then the optimystic
377
404
  * plugin carrying this strand's transactor defaults. Split out so the composition can
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { connectToStrand } from './connect.js';
2
+ export { applyAppSchema } from './compose-strand.js';
2
3
  export {
3
4
  MIN_CLUSTER_SIZE,
4
5
  COHORT_READ_DEADLINE_MS,