@serfab/quereus-plugin-sereus 1.6.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/README.md +15 -5
- package/dist/cluster-size.d.ts +74 -51
- package/dist/cluster-size.js +74 -51
- package/dist/cluster-size.js.map +1 -1
- package/dist/compose-strand.d.ts +13 -0
- package/dist/compose-strand.js +35 -10
- package/dist/compose-strand.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/parse-config.d.ts +33 -2
- package/dist/parse-config.js +123 -46
- package/dist/parse-config.js.map +1 -1
- package/dist/plugin-browser.d.ts +3 -3
- package/dist/plugin-browser.js +27 -27
- package/dist/plugin-browser.js.map +4 -4
- package/dist/plugin.js +3 -7
- package/dist/plugin.js.map +1 -1
- package/package.json +36 -16
- package/src/cluster-size.ts +74 -51
- package/src/compose-strand.ts +37 -10
- package/src/index.ts +1 -0
- package/src/parse-config.ts +189 -70
- package/src/plugin-browser.ts +32 -29
- package/src/plugin.ts +35 -39
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
|
-
//
|
|
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
|
-
|
|
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
|
}
|
package/dist/plugin.js.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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.
|
|
71
|
-
"@optimystic/db-p2p": "^1.
|
|
72
|
-
"@optimystic/db-p2p-storage-fs": "^1.
|
|
73
|
-
"@optimystic/db-p2p-storage-web": "^1.
|
|
74
|
-
"@optimystic/quereus-plugin-crypto": "^1.
|
|
75
|
-
"@optimystic/quereus-plugin-optimystic": "^1.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
159
|
+
"label": "Storage path",
|
|
160
|
+
"type": "string",
|
|
161
|
+
"help": "Node only"
|
|
142
162
|
}
|
|
143
163
|
]
|
|
144
164
|
}
|
package/src/cluster-size.ts
CHANGED
|
@@ -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
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* At
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
*
|
|
41
|
-
*
|
|
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
|
|
45
|
-
* instead of 1 s. A joining machine's first sync runs several
|
|
46
|
-
* that binds — and the first-sync band measured at
|
|
47
|
-
* to fit, is recorded once, on
|
|
48
|
-
* `strand-first-sync-gate.ts`. At 5000
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
|
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
|
|
79
|
-
*
|
|
80
|
-
* scenario measured above — `DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS`
|
|
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 =
|
|
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,
|
|
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`),
|
|
463
|
-
*
|
|
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
|
package/src/compose-strand.ts
CHANGED
|
@@ -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
|
|
252
|
-
// strand comes up through cadre-core, which does thread
|
|
253
|
-
// the plugin's own connect/e2e route where
|
|
254
|
-
//
|
|
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
|
|
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
|