@serfab/cadre-core 1.17.0 → 1.19.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.
Files changed (148) hide show
  1. package/README.md +3 -2
  2. package/dist/cadre-invite-protocol.d.ts +35 -13
  3. package/dist/cadre-invite-protocol.js +90 -25
  4. package/dist/cadre-invite-protocol.js.map +1 -1
  5. package/dist/cadre-node.d.ts +586 -255
  6. package/dist/cadre-node.js +1182 -566
  7. package/dist/cadre-node.js.map +1 -1
  8. package/dist/control-cohort.d.ts +66 -9
  9. package/dist/control-cohort.js +67 -9
  10. package/dist/control-cohort.js.map +1 -1
  11. package/dist/control-database.d.ts +451 -193
  12. package/dist/control-database.js +701 -359
  13. package/dist/control-database.js.map +1 -1
  14. package/dist/control-formation-recorder.d.ts +46 -43
  15. package/dist/control-formation-recorder.js +77 -78
  16. package/dist/control-formation-recorder.js.map +1 -1
  17. package/dist/control-protocol-guard.d.ts +6 -0
  18. package/dist/control-protocol-guard.js +14 -2
  19. package/dist/control-protocol-guard.js.map +1 -1
  20. package/dist/control-schema-version.d.ts +48 -31
  21. package/dist/control-schema-version.js +67 -45
  22. package/dist/control-schema-version.js.map +1 -1
  23. package/dist/control-schema.d.ts +8 -8
  24. package/dist/control-schema.js +16 -16
  25. package/dist/control-schema.js.map +1 -1
  26. package/dist/control-write-retry.d.ts +42 -7
  27. package/dist/control-write-retry.js +87 -19
  28. package/dist/control-write-retry.js.map +1 -1
  29. package/dist/enrolled-machine-store.d.ts +3 -3
  30. package/dist/enrolled-machine-store.js +3 -3
  31. package/dist/formation-token.d.ts +48 -0
  32. package/dist/formation-token.js +26 -0
  33. package/dist/formation-token.js.map +1 -0
  34. package/dist/index.d.ts +15 -14
  35. package/dist/index.js +17 -14
  36. package/dist/index.js.map +1 -1
  37. package/dist/joined-strand-store.d.ts +26 -9
  38. package/dist/joined-strand-store.js +63 -24
  39. package/dist/joined-strand-store.js.map +1 -1
  40. package/dist/link-budget.d.ts +9 -7
  41. package/dist/link-budget.js +9 -7
  42. package/dist/link-budget.js.map +1 -1
  43. package/dist/member-redemption.d.ts +15 -9
  44. package/dist/member-redemption.js +13 -8
  45. package/dist/member-redemption.js.map +1 -1
  46. package/dist/membership-view.d.ts +91 -0
  47. package/dist/membership-view.js +110 -0
  48. package/dist/membership-view.js.map +1 -0
  49. package/dist/owner-grant-protocol.d.ts +11 -7
  50. package/dist/owner-grant-protocol.js +31 -17
  51. package/dist/owner-grant-protocol.js.map +1 -1
  52. package/dist/peer-addr-book.d.ts +10 -1
  53. package/dist/peer-addr-book.js +4 -3
  54. package/dist/peer-addr-book.js.map +1 -1
  55. package/dist/peer-join-backfill.d.ts +96 -30
  56. package/dist/peer-join-backfill.js +272 -128
  57. package/dist/peer-join-backfill.js.map +1 -1
  58. package/dist/pending-join-runner.d.ts +48 -18
  59. package/dist/pending-join-runner.js +74 -56
  60. package/dist/pending-join-runner.js.map +1 -1
  61. package/dist/seed-bootstrap.d.ts +47 -40
  62. package/dist/seed-bootstrap.js +66 -61
  63. package/dist/seed-bootstrap.js.map +1 -1
  64. package/dist/self-address-update.d.ts +13 -0
  65. package/dist/self-address-update.js +19 -0
  66. package/dist/self-address-update.js.map +1 -0
  67. package/dist/strand-database.d.ts +23 -2
  68. package/dist/strand-database.js +26 -6
  69. package/dist/strand-database.js.map +1 -1
  70. package/dist/strand-first-sync-gate.d.ts +176 -89
  71. package/dist/strand-first-sync-gate.js +288 -122
  72. package/dist/strand-first-sync-gate.js.map +1 -1
  73. package/dist/strand-formation-deadlines.d.ts +9 -9
  74. package/dist/strand-formation-deadlines.js +9 -9
  75. package/dist/strand-formation-manager.d.ts +23 -17
  76. package/dist/strand-formation-manager.js +46 -44
  77. package/dist/strand-formation-manager.js.map +1 -1
  78. package/dist/strand-formation-protocol.d.ts +5 -8
  79. package/dist/strand-formation-protocol.js +101 -16
  80. package/dist/strand-formation-protocol.js.map +1 -1
  81. package/dist/strand-instance-manager.d.ts +26 -17
  82. package/dist/strand-instance-manager.js +119 -50
  83. package/dist/strand-instance-manager.js.map +1 -1
  84. package/dist/strand-membership-reconciler.d.ts +16 -13
  85. package/dist/strand-membership-reconciler.js +28 -22
  86. package/dist/strand-membership-reconciler.js.map +1 -1
  87. package/dist/strand-membership-writer.d.ts +40 -28
  88. package/dist/strand-membership-writer.js +212 -153
  89. package/dist/strand-membership-writer.js.map +1 -1
  90. package/dist/strand-move.js +5 -2
  91. package/dist/strand-move.js.map +1 -1
  92. package/dist/strand-revocation-enforcer.d.ts +134 -60
  93. package/dist/strand-revocation-enforcer.js +144 -71
  94. package/dist/strand-revocation-enforcer.js.map +1 -1
  95. package/dist/strand-solicitation.d.ts +50 -39
  96. package/dist/strand-solicitation.js +10 -3
  97. package/dist/strand-solicitation.js.map +1 -1
  98. package/dist/strand-watcher.d.ts +80 -13
  99. package/dist/strand-watcher.js +152 -48
  100. package/dist/strand-watcher.js.map +1 -1
  101. package/dist/table-change-watch.d.ts +63 -0
  102. package/dist/table-change-watch.js +209 -0
  103. package/dist/table-change-watch.js.map +1 -0
  104. package/dist/timeout-scheduler.d.ts +4 -3
  105. package/dist/timeout-scheduler.js +4 -3
  106. package/dist/timeout-scheduler.js.map +1 -1
  107. package/dist/types.d.ts +90 -49
  108. package/dist/types.js +4 -4
  109. package/dist/types.js.map +1 -1
  110. package/package.json +8 -8
  111. package/src/cadre-invite-protocol.ts +119 -31
  112. package/src/cadre-node.ts +1289 -588
  113. package/src/control-cohort.ts +86 -9
  114. package/src/control-database.ts +848 -403
  115. package/src/control-formation-recorder.ts +359 -353
  116. package/src/control-protocol-guard.ts +16 -2
  117. package/src/control-schema-version.ts +101 -51
  118. package/src/control-schema.ts +16 -16
  119. package/src/control-write-retry.ts +91 -19
  120. package/src/delegate-admission.ts +351 -351
  121. package/src/enrolled-machine-store.ts +3 -3
  122. package/src/formation-token.ts +63 -0
  123. package/src/index.ts +30 -18
  124. package/src/joined-strand-store.ts +69 -24
  125. package/src/link-budget.ts +9 -7
  126. package/src/member-redemption.ts +20 -13
  127. package/src/membership-view.ts +144 -0
  128. package/src/owner-grant-protocol.ts +33 -18
  129. package/src/peer-addr-book.ts +4 -3
  130. package/src/peer-join-backfill.ts +961 -761
  131. package/src/pending-join-runner.ts +76 -53
  132. package/src/seed-bootstrap.ts +72 -67
  133. package/src/self-address-update.ts +19 -0
  134. package/src/strand-database.ts +352 -322
  135. package/src/strand-first-sync-gate.ts +334 -134
  136. package/src/strand-formation-deadlines.ts +227 -227
  137. package/src/strand-formation-manager.ts +731 -725
  138. package/src/strand-formation-protocol.ts +122 -24
  139. package/src/strand-instance-manager.ts +2092 -2026
  140. package/src/strand-membership-reconciler.ts +31 -25
  141. package/src/strand-membership-writer.ts +2093 -2019
  142. package/src/strand-move.ts +5 -2
  143. package/src/strand-revocation-enforcer.ts +740 -654
  144. package/src/strand-solicitation.ts +61 -34
  145. package/src/strand-watcher.ts +175 -56
  146. package/src/table-change-watch.ts +285 -0
  147. package/src/timeout-scheduler.ts +29 -28
  148. package/src/types.ts +89 -51
@@ -11,16 +11,16 @@
11
11
  * founder from a joiner; sereus can, because the launch knows whether it is founding.
12
12
  *
13
13
  * The invariant this module enforces: **a machine that has never held this strand's
14
- * `Strand.Header` row, and has not read every `App` table once, must not commit to it.**
15
- * The founder writes the Header in its bootstrap; every other machine can only receive it
16
- * from a peer, so "no local Header" means "never synced". The gate therefore holds the
14
+ * `Strand.Header` row, and has not read every `Strand` and `App` table once, must not commit
15
+ * to it.** The founder writes the Header in its bootstrap; every other machine can only receive
16
+ * it from a peer, so "no local Header" means "never synced". The gate therefore holds the
17
17
  * freshly initialized `StrandDatabase` back from the app — `StrandInstance.database` stays
18
- * unset and the instance reports `'syncing'` — and probes on a cadence until the Header is
19
- * readable AND a read of each app table settles, at which point the database is published
20
- * and the instance goes `'active'`. A machine whose store already holds the Header and every
21
- * app table (a founder, and normally a restart or a hibernation resume) passes the probe on
22
- * the first try and is never gated, so offline-first writes on a machine that has synced
23
- * before keep working.
18
+ * unset and the instance reports `'syncing'` — and probes, whenever data lands in the strand's
19
+ * store and on a backstop timer, until the Header is readable AND a read of each other table
20
+ * settles, at which point the database is published and the instance goes `'active'`. A
21
+ * machine whose store already holds the Header and every other table (a founder, and normally
22
+ * a restart or a hibernation resume) passes the probe on the first try and is never gated, so
23
+ * offline-first writes on a machine that has synced before keep working.
24
24
  *
25
25
  * "Has synced before" is decided by what the local store actually holds, not by whether this
26
26
  * machine once read the strand: a machine that left soon after its first sync can hold less
@@ -29,23 +29,28 @@
29
29
  * it kept, without the `Strand.Header` collection it had read before it left. Gating it is
30
30
  * right: writing into a collection this machine does not hold would fork it.
31
31
  *
32
- * Why the app tables too: the Header is ONE collection. It reaches a joiner ahead of the
33
- * founder's app-table collections (peer-join backfill and pull-on-read deliver those
34
- * separately), and a write to a table whose collection this machine has not yet fetched
35
- * invents one exactly as a lone joiner did. Measured on the reference chat schema
36
- * (2026-09-16, direct connections, Header-only probe): the joiner's participant + message
37
- * written the instant `addStrand` resolved diverged in 3 of 4 runs. Reading each table once
38
- * pulls its collection while the founder is reachable, so the write that follows appends.
32
+ * Why the other tables too: the Header is ONE collection. It reaches a joiner ahead of the
33
+ * founder's other collections (peer-join backfill and pull-on-read deliver those separately),
34
+ * and a write to a table whose collection this machine has not yet fetched invents one exactly
35
+ * as a lone joiner did. Measured on the reference chat schema (2026-09-16, direct connections,
36
+ * Header-only probe): the joiner's participant + message written the instant `addStrand`
37
+ * resolved diverged in 3 of 4 runs. The same holds for the `Strand` tables, which reach a joiner
38
+ * by the same backfill: the membership loop writes `ConsumedInvite`, `Member` and `MemberPeer`
39
+ * about a second after publication, and a manager writes `Invite`, `CancelledInvite`, `Manager`
40
+ * and `Revocation`. Reading each table once pulls its collection while the founder is
41
+ * reachable, so the write that follows appends.
39
42
  *
40
- * Every probe is a read (`Strand.Header`, then `select count(1)` on each `App` table). Reads
41
- * never invent a collection (Optimystic's `Collection.open` resolves undefined on an
42
- * authoritatively absent header, and throws on an unreachable one), so probing is safe to
43
- * repeat and a throw is just "not yet". Every table declared at founding has a collection
44
- * from the start (the founder bootstrap commits them all before the Header), so a probe read
45
- * fetches it; a table a later schema version adds reads as absent until its first write
46
- * creates the collection — see `docs/strands.md` ("Joining") for that residual.
43
+ * Every probe read is `select 1 from <table> limit 1` (`Strand.Header`, then each other `Strand`
44
+ * table, then each `App` table, skipping those already seen settled): opening the table fetches
45
+ * its collection header, and one row walks the root to the first leaf. Reads never commit a
46
+ * collection (one ruled authoritatively absent is staged empty in memory only, and an
47
+ * unreachable one throws), so probing is safe to repeat and a throw is just "not yet". Every
48
+ * table declared at founding has a collection from the start (the founder bootstrap commits the
49
+ * rest empty before its founding transaction writes the Header and the membership rows), so a
50
+ * probe read fetches it; a table a later schema version adds reads as absent until its first
51
+ * write creates the collection — see `docs/strands.md` ("Joining") for that residual.
47
52
  *
48
- * Owned by `StrandInstanceManager`: created in `buildStrandRuntime` for every launch, armed
53
+ * Owned by `StrandInstanceManager`: created in `buildStrandDatabase` for every database build, armed
49
54
  * only for a non-founder launch whose first probe fails, stopped and dropped in
50
55
  * `releaseRuntime` (so a quiesce → resume rebuild re-probes over the same store), and opened
51
56
  * by `publishDatabase` whenever the database is published without its loop — a launch that
@@ -64,10 +69,23 @@
64
69
  * Header too, or have optimystic open the catalog read-only when a peer already holds it.
65
70
  */
66
71
  import debug from 'debug';
72
+ import { scheduleRuns, TABLE_CHANGE_COALESCE_MS } from './table-change-watch.js';
67
73
  import { defaultTimeoutScheduler } from './timeout-scheduler.js';
68
74
  const log = debug('sereus:cadre:strand-first-sync');
69
- /** How often a gated launch re-probes (`Strand.Header`, then every `App` table) while waiting for its first sync. */
70
- export const DEFAULT_STRAND_FIRST_SYNC_POLL_MS = 500;
75
+ /**
76
+ * How long a gated launch goes without probing when no data lands in the strand's store: the
77
+ * gate probes when data arrives, and this long after its last probe settled when none has. It
78
+ * covers a probe that failed for a reason no storage event follows — a consult that missed its
79
+ * deadline while the founder held the block, or a push that never comes.
80
+ *
81
+ * Why 2 s: arrivals wake the gate, so this timer is the fallback rather than the main path. At
82
+ * the supported 3 s link round trip one consult takes about 6 s (two round trips, per the 1.8.0
83
+ * measurements on {@link DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS}), so a 2 s idle gap after a failed
84
+ * probe adds at most a third of one probe round, and it is under 1% of that 360 s budget. Shorter
85
+ * brings back the cost of polling (the gate re-probed every 500 ms before it woke on arrivals);
86
+ * much longer (5 s) delays the case with no storage event by most of a probe round for nothing.
87
+ */
88
+ export const DEFAULT_STRAND_FIRST_SYNC_BACKSTOP_MS = 2000;
71
89
  /**
72
90
  * How long `CadreNode.addStrand` waits for a machine's first sync before rejecting with
73
91
  * {@link StrandAwaitingFirstSyncError}. It has to cover the slowest attach sereus carries,
@@ -98,6 +116,37 @@ export const DEFAULT_STRAND_FIRST_SYNC_POLL_MS = 500;
98
116
  * several of those — which is why both bands are recorded here, and why a change to either
99
117
  * number has to be weighed against the other.
100
118
  *
119
+ * AT THE SUPPORTED LINK ON `@optimystic/*` 1.17.0 (2026-10-21), 1 500 ms one-way, the same
120
+ * scenario, one run per arm, with the probe reading every `Strand` table as well as the `App`
121
+ * one (`debt-first-sync-gate-reads-every-strand-table`); the kept-store arm was not run:
122
+ * - Fresh join: writable at 28.7 s and NOT gated, as on 1.15.0.
123
+ * - Re-attach over an EMPTY store: writable at 105.8 and 105.9 s over two runs, the row A wrote
124
+ * while B was away readable at the same moment. B's first database build fails at 7.0 s (no
125
+ * seed peer has answered; `StrandInstanceManager.deferUnreachableJoin` retries it), B's strand
126
+ * node connects to A's at 15.1 s, the retried build's `connectToStrand` takes 21.2 s, and the
127
+ * launch's first probe then takes 72.6 s and passes with no read failing, so the gate never
128
+ * arms.
129
+ * - The same arm with the probe held to the Header and the `App` table (one run, otherwise the
130
+ * same build): that probe took 12.1 s, writable at 45.4 s, the row at 63.5 s. So the seven
131
+ * `Strand` reads cost about 60 s here, about 8.6 s each, against about 6 s for each of the
132
+ * baseline's two reads — more than the 42 s estimated from one 6 s consult per read.
133
+ *
134
+ * AT THE SUPPORTED LINK ON `@optimystic/*` 1.15.0 (2026-10-20), 1 500 ms one-way, the same
135
+ * scenario, one fresh-join run, with the one-row probes woken by data landing in the store
136
+ * (`first-sync-gate-probes-cheaply`; the full-count probes polled every 500 ms gave the same
137
+ * figures):
138
+ * - Fresh join: writable at 28.8 s, and NOT gated. The launch waits 27.3 s in
139
+ * `connectToStrand` for its seed peer (it hydrates the founder's catalog, 9 tables), and by
140
+ * then the store holds the strand: the first probe passed in 24 ms.
141
+ * - The strand case of `message-cost-accounting` (two relay-only parties on loopback) agrees:
142
+ * B's `addStrand` took 471 ms, with 13 `sync` streams to A, 4 block reads from the Header
143
+ * probe and 8 from the table probes. Its two tables are empty, so a full count would fetch
144
+ * the same blocks as a one-row read.
145
+ * - Neither arms the gate, so neither times the woken path. That was checked on
146
+ * `harness-topology`'s cross-party share (a joiner launched with no seed, dialed afterwards)
147
+ * with the backstop raised to 10 s: the founder's join backfill push opened the gate 2.1 s
148
+ * after it armed.
149
+ *
101
150
  * AT THE SUPPORTED LINK ON `@optimystic/*` 1.8.0 (2026-09-29), 1 500 ms one-way, the read
102
151
  * deadline derived from the declared link at 7 000 ms (`cohortReadDeadlineMs` in
103
152
  * `link-budget.ts`; the plugin's `COHORT_READ_DEADLINE_MS` is the same number), and cadre
@@ -154,19 +203,19 @@ export const DEFAULT_STRAND_FIRST_SYNC_POLL_MS = 500;
154
203
  * injector are unknown to us) opened the gate at about 150 s, after the previous 120 s budget
155
204
  * had already rejected; and a real Galaxy S7 joining fresh through relay.sereus.org took 178 s.
156
205
  *
157
- * 300 s clears the worst harness sample (82 s, a fresh join at the supported link on 1.7.0; the
158
- * worst on 1.8.0 is 64 s) by about 3.6x, the reporter's re-attach by 2x and the device join by
159
- * about 1.7x. Kept at 300 s on 2026-09-29, and again after the 1.8.0 re-measure the same day,
160
- * by the rule that re-measure applied and the next one should: keep it while the
161
- * worst harness sample is at most 100 s, which holds a margin of at least 3x; otherwise raise
162
- * it to 3x the worst sample, rounded up to the next whole minute. 240 s was rejected earlier:
163
- * it clears the device join by only 1.35x, and that sample was not a re-attach. History: the
164
- * original 30 s sat INSIDE the 1000 ms fresh band and refused about half of those joins while
165
- * their sync was progressing normally; 120 s was sized from fresh joins only and refused the
166
- * reporter's re-attach.
206
+ * 360 s clears the worst harness sample (105.9 s, a re-attach over an empty store at the
207
+ * supported link on 1.17.0) by about 3.4x, the reporter's re-attach by 2.4x and the device join
208
+ * by about 2x. Set by the rule every re-measure applies: keep it while the worst harness sample
209
+ * is at most 100 s, which holds a margin of at least 3x; otherwise raise it to 3x the worst
210
+ * sample, rounded up to the next whole minute. The 1.17.0 sample crossed 100 s, and 3x 105.9 s
211
+ * is 318 s, so 360 s (2026-10-21); the 1.7.0 and 1.8.0 re-measures (worst 82 s and 64 s) had
212
+ * kept the 300 s before it. 240 s was rejected earlier: it clears the device join by only 1.35x,
213
+ * and that sample was not a re-attach. History: the original 30 s sat INSIDE the 1000 ms fresh
214
+ * band and refused about half of those joins while their sync was progressing normally; 120 s
215
+ * was sized from fresh joins only and refused the reporter's re-attach.
167
216
  *
168
- * NOTE: accepted tradeoff — what 300 s costs. This wait is what an app's `addStrand` sits in
169
- * before it is told "not yet", so a strand none of whose members is reachable at all takes five
217
+ * NOTE: accepted tradeoff — what 360 s costs. This wait is what an app's `addStrand` sits in
218
+ * before it is told "not yet", so a strand none of whose members is reachable at all takes six
170
219
  * minutes to report instead of two. The slow report was weighed against refusing attaches that
171
220
  * were working, and the refusal is the worse failure. The cost is bounded — the rejection is
172
221
  * retryable, the strand stays launched and keeps probing, and `strand:writable` fires the
@@ -176,7 +225,7 @@ export const DEFAULT_STRAND_FIRST_SYNC_POLL_MS = 500;
176
225
  * sync that is actually in progress — worth more the longer this budget gets.
177
226
  */
178
227
  // eslint-disable-next-line no-restricted-syntax -- measured, not derived: the bands above, from integration-tests' strand-reattach-first-sync-measure.integration.ts; a whole first sync has no round-trip count
179
- export const DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS = 300000;
228
+ export const DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS = 360000;
180
229
  /**
181
230
  * Thrown by `CadreNode.addStrand` / `whenStrandWritable` when a machine has not received the
182
231
  * strand's data from another member within the wait budget — because no other member is
@@ -197,16 +246,24 @@ export class StrandAwaitingFirstSyncError extends Error {
197
246
  this.waitedMs = waitedMs;
198
247
  }
199
248
  }
249
+ const STRAND_HEADER = { schema: 'Strand', name: 'Header' };
250
+ function qualifiedName(table) {
251
+ return `${table.schema}.${table.name}`;
252
+ }
253
+ /** The read `openTables` in `quereus-plugin-sereus/src/empty-collections.ts` opens a collection with. */
254
+ function probeSql(table) {
255
+ return `select 1 from ${table.schema}."${table.name}" limit 1`;
256
+ }
200
257
  /**
201
- * Whether this machine holds the strand's `Strand.Header` row — the "has synced at least
202
- * once (or founded)" signal. A read, never a write: it cannot invent the collection. A
203
- * throwing read (an unreachable cohort mid-sync, a store still hydrating) reports `false`
204
- * with a log, since the caller's only response to either answer is "probe again".
258
+ * Whether this machine holds the strand's `Strand.Header` row — the "has synced at least once
259
+ * (or founded)" signal: held when the read returns a row. A throwing read (an unreachable cohort
260
+ * mid-sync, a store still hydrating) reports `false` with a log, since the caller's only
261
+ * response to either answer is "probe again".
205
262
  */
206
- export async function strandHeaderHeld(db, label) {
263
+ async function headerHeld(db, label) {
207
264
  try {
208
- for await (const row of db.eval('select count(1) as Count from Strand.Header')) {
209
- return (row.Count ?? 0) > 0;
265
+ for await (const _row of db.eval(probeSql(STRAND_HEADER))) {
266
+ return true;
210
267
  }
211
268
  return false;
212
269
  }
@@ -215,78 +272,175 @@ export async function strandHeaderHeld(db, label) {
215
272
  return false;
216
273
  }
217
274
  }
218
- /** The tables the sApp declared in the `App` schema, by name; views excluded. */
219
- function appTableNames(db) {
220
- const app = db.schemaManager.getSchema('App');
221
- return app ? Array.from(app.getAllTables()).filter((table) => !table.isView).map((table) => table.name) : [];
275
+ /**
276
+ * Whether a read of `table` settles on this machine — its collection has been fetched from the
277
+ * cohort, or is authoritatively absent. Only a throwing read (an unreachable cohort, a block not
278
+ * yet arrived) reports `false`; an empty table is a settled read, because the founder commits
279
+ * an empty collection for every declared table.
280
+ */
281
+ async function tableReadable(db, table, label) {
282
+ try {
283
+ for await (const _row of db.eval(probeSql(table))) {
284
+ break;
285
+ }
286
+ return true;
287
+ }
288
+ catch (error) {
289
+ log('[%s] %s probe failed (treated as not yet synced): %s', label, qualifiedName(table), error instanceof Error ? error.message : String(error));
290
+ return false;
291
+ }
292
+ }
293
+ /** The tables `schemaName` declares, views excluded; none when the schema is absent. */
294
+ function schemaTables(db, schemaName) {
295
+ const schema = db.schemaManager.getSchema(schemaName);
296
+ return schema
297
+ ? Array.from(schema.getAllTables()).filter((table) => !table.isView).map((table) => ({ schema: schemaName, name: table.name }))
298
+ : [];
222
299
  }
223
300
  /**
224
- * Whether a read of every `App` table settles on this machine — the "each app collection
225
- * has been fetched from the cohort, or is authoritatively absent" signal. Only a throwing
226
- * read (an unreachable cohort) reports `false`; an empty table is a settled read.
301
+ * Every table this machine may write, in probe order: the membership tables in `Strand` (a fixed
302
+ * set every strand has) and then the sApp's in `App`. The Header is left out, compared
303
+ * case-insensitively as SQL identifiers are, because it is read first and alone, and its read
304
+ * means something else (held = a row came back).
227
305
  */
228
- export async function appTablesReadable(db, label) {
229
- for (const table of appTableNames(db)) {
230
- try {
231
- for await (const _row of db.eval(`select count(1) as Count from App."${table}"`)) {
232
- break;
233
- }
306
+ function writableTables(db) {
307
+ const header = STRAND_HEADER.name.toLowerCase();
308
+ return [
309
+ ...schemaTables(db, STRAND_HEADER.schema).filter((table) => table.name.toLowerCase() !== header),
310
+ ...schemaTables(db, 'App')
311
+ ];
312
+ }
313
+ /**
314
+ * The gate's whole probe: the Header is held AND a read of every other `Strand` table and every
315
+ * `App` table has settled. Reads only what `progress` does not record yet, and records what it
316
+ * establishes.
317
+ *
318
+ * The Header is read first and alone: a joiner with no peer would otherwise pay one failing
319
+ * network read per table for nothing. The tables are then read one after another, stopping at
320
+ * the first that fails — the next probe starts there, and while the cohort is unreachable every
321
+ * read of a block this machine lacks fails the same way, each after a consult.
322
+ *
323
+ * NOTE: the reads are serialized (Quereus's default), not `readConcurrency: 'committed'`, though
324
+ * committed reads would let the table reads overlap. The Optimystic plugin's committed read
325
+ * serves the tree the table already holds and never refreshes it from the network, and schema
326
+ * apply initializes every table when the joiner has no catalog to hydrate, over an empty
327
+ * collection staged in memory when no peer could answer. A committed probe of such a table reads
328
+ * that empty collection as settled and opens the gate onto a fork. A serialized read refreshes
329
+ * the tree first (`Collection.update`), which adopts the founder's log once a peer holds it.
330
+ * Revisit if the plugin's committed read ever refreshes a tree it has never synced, or the gate
331
+ * can ask Optimystic directly whether a collection is held.
332
+ */
333
+ async function strandFirstSyncComplete(db, label, progress) {
334
+ if (!progress.header) {
335
+ if (!await headerHeld(db, label)) {
336
+ return false;
234
337
  }
235
- catch (error) {
236
- log('[%s] App.%s probe failed (treated as not yet synced): %s', label, table, error instanceof Error ? error.message : String(error));
338
+ progress.header = true;
339
+ }
340
+ for (const table of writableTables(db)) {
341
+ const name = qualifiedName(table);
342
+ if (progress.tables.has(name)) {
343
+ continue;
344
+ }
345
+ if (!await tableReadable(db, table, label)) {
237
346
  return false;
238
347
  }
348
+ progress.tables.add(name);
239
349
  }
240
350
  return true;
241
351
  }
242
352
  /**
243
- * The gate's whole probe: the Header is held AND every app table has been read once. The
244
- * app tables are read only once the Header is — a joiner with no peer would otherwise pay
245
- * one failing network read per table per probe for nothing.
246
- */
247
- export async function strandFirstSyncComplete(db, label) {
248
- return await strandHeaderHeld(db, label) && await appTablesReadable(db, label);
249
- }
250
- /**
251
- * Holds one launch's `StrandDatabase` until {@link strandFirstSyncComplete}. Probes are
252
- * sequential (the next is scheduled only after the previous read settles), so a slow
253
- * network read never stacks probes.
353
+ * Holds one launch's `StrandDatabase` until the first sync completes: the `Strand.Header` row is
354
+ * held and a read of every `Strand` and `App` table has settled. What a probe established is
355
+ * remembered for the gate's lifetime, so a later probe — the gate's own, after the caller's
356
+ * {@link probeOnce} — reads only what is still missing. A gate lives for one database build, so a
357
+ * hibernation wake or a deferred-join retry starts over, and over a store that holds everything
358
+ * every read answers locally.
359
+ *
360
+ * While armed it probes when data lands in the strand's store
361
+ * ({@link StrandFirstSyncGateDeps.subscribeStorageChanges}, coalesced over
362
+ * `TABLE_CHANGE_COALESCE_MS`), and {@link StrandFirstSyncConfig.backstopMs} after its last probe
363
+ * settled when nothing has. Probes run on `scheduleRuns`: one at a time, and data landing during
364
+ * one produces exactly one more.
365
+ *
366
+ * The wake source is the node's storage-wide change feed rather than a table watch
367
+ * (`StrandDatabase.watchTables`): the Optimystic plugin arms a table's replicated-change bridge
368
+ * only after the table initializes successfully, and the tables the gate waits on are the ones
369
+ * whose collection could not be fetched yet, so a table watch would not hear the push that
370
+ * delivers them. Each strand has its own libp2p node and store, so the feed hears this strand
371
+ * only.
254
372
  *
255
373
  * NOTE: a probe's reads dial the founder, through the relay when that is the only route, even
256
374
  * while the founder is unreachable: one run of `joiner-first-commit-over-slow-relay` logged 17
257
375
  * `hop connect denied` at the relay in about 10 s, each a few frames on the joiner's relay
258
- * connection. If that connection ever shows saturated while no strand member is connected, hold
259
- * the probe while the strand member redial (`strand-peer-redial.ts`) reports none connected.
376
+ * connection, when the gate re-probed every 500 ms and re-read every table. It now probes on
377
+ * arrivals and a 2 s backstop and skips what it holds, so expect fewer. If that connection ever
378
+ * shows saturated while no strand member is connected, hold the probe while the strand member
379
+ * redial (`strand-peer-redial.ts`) reports none connected.
260
380
  */
261
381
  export class StrandFirstSyncGate {
262
382
  constructor(deps, config) {
263
383
  this.deps = deps;
384
+ this.progress = { header: false, tables: new Set() };
385
+ this.listening = false;
386
+ /**
387
+ * Data landed since the last probe started (or, before any probe, since {@link listen}), so one
388
+ * is already due and will read it.
389
+ */
390
+ this.landedSinceProbe = false;
264
391
  this.stopped = false;
265
392
  this.opened = false;
266
- this.probing = false;
267
393
  this.scheduler = deps.scheduler ?? defaultTimeoutScheduler;
268
- this.pollIntervalMs = config?.pollIntervalMs ?? DEFAULT_STRAND_FIRST_SYNC_POLL_MS;
394
+ this.backstopMs = config?.backstopMs ?? DEFAULT_STRAND_FIRST_SYNC_BACKSTOP_MS;
269
395
  }
270
396
  /** The database this gate is holding back. */
271
397
  get database() {
272
398
  return this.deps.database;
273
399
  }
274
- /** True once the Header was seen (or the gate was force-opened); the loop is then done. */
400
+ /** True once the first sync completed (or the gate was force-opened); probing is then done. */
275
401
  get isOpen() {
276
402
  return this.opened;
277
403
  }
278
- /** Arm the probe loop. The first probe runs after one interval — the caller already probed once. */
404
+ /** Whether a block landed since the last probe started, or since {@link listen} when none has. */
405
+ get heardDataSinceProbe() {
406
+ return this.landedSinceProbe;
407
+ }
408
+ /**
409
+ * Subscribe to the storage change feed ahead of {@link start}, so a block landing before the
410
+ * gate is armed is heard: {@link heardDataSinceProbe} reports it, and `start` then probes at
411
+ * once. For a first join, which skips the caller's first probe unless a member's push already
412
+ * landed while the database was initializing.
413
+ */
414
+ listen() {
415
+ if (this.stopped || this.opened || this.listening)
416
+ return;
417
+ this.listening = true;
418
+ this.unsubscribeStorage = this.subscribeStorage();
419
+ }
420
+ /**
421
+ * Arm the probe schedule. It probes when data lands, or one backstop later; and at once when
422
+ * data already landed since the last probe (or since {@link listen}). Without `listen`, data
423
+ * that landed between the caller's probe and this call raised no event the gate heard, so
424
+ * that first backstop is what catches it.
425
+ */
279
426
  start() {
280
- if (this.stopped || this.opened || this.timer !== undefined)
427
+ if (this.stopped || this.opened || this.probes !== undefined)
281
428
  return;
282
- this.schedule();
283
- log('[%s] first-sync gate armed: no Strand.Header held yet; probing every %dms', this.deps.label, this.pollIntervalMs);
429
+ this.probes = scheduleRuns(() => this.probe(), {
430
+ label: `first-sync:${this.deps.label}`,
431
+ backstopMs: this.backstopMs,
432
+ scheduler: this.scheduler,
433
+ });
434
+ this.listen();
435
+ if (this.landedSinceProbe) {
436
+ this.probes.trigger(TABLE_CHANGE_COALESCE_MS);
437
+ }
438
+ log('[%s] first-sync gate armed: probing when data lands, and %dms after the last probe otherwise', this.deps.label, this.backstopMs);
284
439
  }
285
440
  /**
286
- * Open without the probe loop: the caller is publishing the database itself — a launch
287
- * whose first probe passed (or a founder's), which never armed the loop, or a founder
288
- * bootstrap just run against the gated database. Stops the loop; `onHeaderHeld` is NOT
289
- * invoked.
441
+ * Open without probing: the caller is publishing the database itself — a launch whose first
442
+ * probe passed (or a founder's), which never armed the schedule, or a founder bootstrap just
443
+ * run against the gated database. Stops probing; `onHeaderHeld` is NOT invoked.
290
444
  */
291
445
  open() {
292
446
  if (this.opened)
@@ -295,55 +449,67 @@ export class StrandFirstSyncGate {
295
449
  this.stop();
296
450
  log('[%s] database published by the caller; first-sync gate not needed or force-opened', this.deps.label);
297
451
  }
298
- /** Disarm the loop; a probe already in flight completes and is then ignored. */
452
+ /** Disarm: unsubscribe and drop the schedule; a probe already in flight completes and is then ignored. */
299
453
  stop() {
300
454
  this.stopped = true;
301
- if (this.timer !== undefined) {
302
- this.scheduler.clearTimeout(this.timer);
303
- this.timer = undefined;
304
- }
305
- }
306
- schedule() {
307
- this.timer = this.scheduler.setTimeout(() => {
308
- this.timer = undefined;
309
- void this.probe();
310
- }, this.pollIntervalMs);
311
- }
312
- async probe() {
313
- if (this.stopped || this.opened || this.probing)
314
- return;
315
- this.probing = true;
316
- const held = await this.probeHeld();
317
- // Re-check after the await: a release or a force-open may have landed mid-read.
318
- if (this.stopped || this.opened)
319
- return;
320
- if (!held) {
321
- this.schedule();
322
- return;
323
- }
324
- this.opened = true;
325
- this.stop();
326
- log('[%s] first-sync gate opened: Strand.Header and every App table read from a peer', this.deps.label);
327
- this.deps.onHeaderHeld();
455
+ this.unsubscribeStorage?.();
456
+ this.unsubscribeStorage = undefined;
457
+ this.probes?.dispose();
458
+ this.probes = undefined;
328
459
  }
329
460
  /**
330
- * One probe's answer, with a throw from OUTSIDE {@link strandFirstSyncComplete}'s own read
331
- * guards — the `App` schema lookup, or `getDatabase()` itself — reported as "not yet" like
332
- * any failing read. The loop is scheduled from `void this.probe()`, so an escaping rejection
333
- * would be unhandled AND leave nothing scheduled: the strand would stay gated for the whole
334
- * `timeoutMs` and never recover, which is the one failure this gate must not have.
461
+ * One probe's answer, reading only what this gate has not yet seen settled. A throw from
462
+ * OUTSIDE the probe's own read guards — a schema lookup, or `getDatabase()` itself — is
463
+ * reported as "not yet" like any failing read, so the caller's first probe cannot fail the
464
+ * launch over it and the gate's log says what happened.
335
465
  */
336
- async probeHeld() {
466
+ async probeOnce() {
467
+ this.landedSinceProbe = false;
337
468
  try {
338
- return await strandFirstSyncComplete(this.deps.database.getDatabase(), this.deps.label);
469
+ return await strandFirstSyncComplete(this.deps.database.getDatabase(), this.deps.label, this.progress);
339
470
  }
340
471
  catch (error) {
341
472
  log('[%s] first-sync probe threw (treated as not yet synced): %s', this.deps.label, error instanceof Error ? error.message : String(error));
342
473
  return false;
343
474
  }
344
- finally {
345
- this.probing = false;
475
+ }
476
+ /**
477
+ * NOTE: a probe's own reads can land blocks (a fetched block, a read-repair), which fires this
478
+ * feed and schedules one follow-up probe. Each follow-up means a block newly arrived, so they
479
+ * stop when nothing new lands; if gated launches ever show probe storms in the logs, ignore
480
+ * events while a probe is in flight.
481
+ */
482
+ subscribeStorage() {
483
+ const subscribe = this.deps.subscribeStorageChanges;
484
+ if (!subscribe) {
485
+ log('[%s] no storage change feed on this node; probing every %dms only', this.deps.label, this.backstopMs);
486
+ return undefined;
346
487
  }
488
+ return subscribe(() => this.onDataLanded());
489
+ }
490
+ /**
491
+ * Called synchronously by the store after each block lands, so it only arms a timer. A push
492
+ * lands its blocks one by one; only the first since the last probe started arms one, since
493
+ * that probe has not started yet and will read the rest.
494
+ */
495
+ onDataLanded() {
496
+ if (this.landedSinceProbe)
497
+ return;
498
+ this.landedSinceProbe = true;
499
+ this.probes?.trigger(TABLE_CHANGE_COALESCE_MS);
500
+ }
501
+ async probe() {
502
+ if (this.stopped || this.opened)
503
+ return;
504
+ log('[%s] first-sync probe (%s)', this.deps.label, this.landedSinceProbe ? 'data landed' : 'backstop');
505
+ const held = await this.probeOnce();
506
+ // Re-check after the await: a release or a force-open may have landed mid-read.
507
+ if (this.stopped || this.opened || !held)
508
+ return;
509
+ this.opened = true;
510
+ this.stop();
511
+ log('[%s] first-sync gate opened: Strand.Header, every Strand table and every App table read from a peer', this.deps.label);
512
+ this.deps.onHeaderHeld();
347
513
  }
348
514
  }
349
515
  //# sourceMappingURL=strand-first-sync-gate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"strand-first-sync-gate.js","sourceRoot":"","sources":["../src/strand-first-sync-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,uBAAuB,EAAyB,MAAM,wBAAwB,CAAC;AAExF,MAAM,GAAG,GAAG,KAAK,CAAC,gCAAgC,CAAC,CAAC;AAEpD,qHAAqH;AACrH,MAAM,CAAC,MAAM,iCAAiC,GAAG,GAAG,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0GG;AACH,iNAAiN;AACjN,MAAM,CAAC,MAAM,oCAAoC,GAAG,MAAO,CAAC;AAc5D;;;;;;;GAOG;AACH,MAAM,OAAO,4BAA6B,SAAQ,KAAK;IAIrD,YAAY,QAAgB,EAAE,QAAgB;QAC5C,KAAK,CACH,UAAU,QAAQ,4EAA4E;YAC9F,+BAA+B,QAAQ,oDAAoD;YAC3F,6FAA6F;YAC7F,2FAA2F;YAC3F,kCAAkC,CACnC,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAC;QAC3C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,EAAY,EAAE,KAAa;IAChE,IAAI,CAAC;QACH,IAAI,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,6CAA6C,CAAC,EAAE,CAAC;YAC/E,OAAO,CAAE,GAAG,CAAC,KAAgB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,wDAAwD,EAAE,KAAK,EACjE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1D,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,SAAS,aAAa,CAAC,EAAY;IACjC,MAAM,GAAG,GAAG,EAAE,CAAC,aAAa,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC9C,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC/G,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,EAAY,EAAE,KAAa;IACjE,KAAK,MAAM,KAAK,IAAI,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC;QACtC,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC,IAAI,CAAC,sCAAsC,KAAK,GAAG,CAAC,EAAE,CAAC;gBACjF,MAAM;YACR,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,GAAG,CAAC,0DAA0D,EAAE,KAAK,EAAE,KAAK,EAC1E,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAC1D,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,EAAY,EAAE,KAAa;IACvE,OAAO,MAAM,gBAAgB,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,MAAM,iBAAiB,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;AACjF,CAAC;AAiBD;;;;;;;;;;GAUG;AACH,MAAM,OAAO,mBAAmB;IAQ9B,YAA6B,IAA6B,EAAE,MAA8B;QAA7D,SAAI,GAAJ,IAAI,CAAyB;QAJlD,YAAO,GAAG,KAAK,CAAC;QAChB,WAAM,GAAG,KAAK,CAAC;QACf,YAAO,GAAG,KAAK,CAAC;QAGtB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,uBAAuB,CAAC;QAC3D,IAAI,CAAC,cAAc,GAAG,MAAM,EAAE,cAAc,IAAI,iCAAiC,CAAC;IACpF,CAAC;IAED,8CAA8C;IAC9C,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;IAC5B,CAAC;IAED,2FAA2F;IAC3F,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,oGAAoG;IACpG,KAAK;QACH,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO;QACpE,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,GAAG,CAAC,2EAA2E,EAC7E,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;IAC1C,CAAC;IAED;;;;;OAKG;IACH,IAAI;QACF,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,GAAG,CAAC,mFAAmF,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5G,CAAC;IAED,gFAAgF;IAChF,IAAI;QACF,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACxC,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;QACzB,CAAC;IACH,CAAC;IAEO,QAAQ;QACd,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,GAAG,EAAE;YAC1C,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;YACvB,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;IAC1B,CAAC;IAEO,KAAK,CAAC,KAAK;QACjB,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QACxD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QACpC,gFAAgF;QAChF,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QACxC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,GAAG,CAAC,iFAAiF,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;IAC3B,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,SAAS;QACrB,IAAI,CAAC;YACH,OAAO,MAAM,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1F,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,GAAG,CAAC,6DAA6D,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAChF,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAC1D,OAAO,KAAK,CAAC;QACf,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACvB,CAAC;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"strand-first-sync-gate.js","sourceRoot":"","sources":["../src/strand-first-sync-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,YAAY,EAAE,wBAAwB,EAAyB,MAAM,yBAAyB,CAAC;AACxG,OAAO,EAAE,uBAAuB,EAAyB,MAAM,wBAAwB,CAAC;AAExF,MAAM,GAAG,GAAG,KAAK,CAAC,gCAAgC,CAAC,CAAC;AAEpD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qCAAqC,GAAG,IAAK,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyIG;AACH,iNAAiN;AACjN,MAAM,CAAC,MAAM,oCAAoC,GAAG,MAAO,CAAC;AAiB5D;;;;;;;GAOG;AACH,MAAM,OAAO,4BAA6B,SAAQ,KAAK;IAIrD,YAAY,QAAgB,EAAE,QAAgB;QAC5C,KAAK,CACH,UAAU,QAAQ,4EAA4E;YAC9F,+BAA+B,QAAQ,oDAAoD;YAC3F,6FAA6F;YAC7F,2FAA2F;YAC3F,kCAAkC,CACnC,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAC;QAC3C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AAmBD,MAAM,aAAa,GAAgB,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAExE,SAAS,aAAa,CAAC,KAAkB;IACvC,OAAO,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;AACzC,CAAC;AAED,yGAAyG;AACzG,SAAS,QAAQ,CAAC,KAAkB;IAClC,OAAO,iBAAiB,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,IAAI,WAAW,CAAC;AACjE,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,UAAU,CAAC,EAAY,EAAE,KAAa;IACnD,IAAI,CAAC;QACH,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC;YAC1D,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,wDAAwD,EAAE,KAAK,EACjE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1D,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,aAAa,CAAC,EAAY,EAAE,KAAkB,EAAE,KAAa;IAC1E,IAAI,CAAC;QACH,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YAClD,MAAM;QACR,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,sDAAsD,EAAE,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,EACrF,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1D,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,wFAAwF;AACxF,SAAS,YAAY,CAAC,EAAY,EAAE,UAAkB;IACpD,MAAM,MAAM,GAAG,EAAE,CAAC,aAAa,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IACtD,OAAO,MAAM;QACX,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/H,CAAC,CAAC,EAAE,CAAC;AACT,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CAAC,EAAY;IAClC,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;IAChD,OAAO;QACL,GAAG,YAAY,CAAC,EAAE,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;QAChG,GAAG,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC;KAC3B,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,KAAK,UAAU,uBAAuB,CAAC,EAAY,EAAE,KAAa,EAAE,QAA2B;IAC7F,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,UAAU,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,KAAK,CAAC;QACf,CAAC;QACD,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC;IACzB,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,cAAc,CAAC,EAAE,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,SAAS;QACX,CAAC;QACD,IAAI,CAAC,MAAM,aAAa,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC;YAC3C,OAAO,KAAK,CAAC;QACf,CAAC;QACD,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAwBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,OAAO,mBAAmB;IAe9B,YAA6B,IAA6B,EAAE,MAA8B;QAA7D,SAAI,GAAJ,IAAI,CAAyB;QAZzC,aAAQ,GAAsB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;QAG5E,cAAS,GAAG,KAAK,CAAC;QAC1B;;;WAGG;QACK,qBAAgB,GAAG,KAAK,CAAC;QACzB,YAAO,GAAG,KAAK,CAAC;QAChB,WAAM,GAAG,KAAK,CAAC;QAGrB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,uBAAuB,CAAC;QAC3D,IAAI,CAAC,UAAU,GAAG,MAAM,EAAE,UAAU,IAAI,qCAAqC,CAAC;IAChF,CAAC;IAED,8CAA8C;IAC9C,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;IAC5B,CAAC;IAED,+FAA+F;IAC/F,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,kGAAkG;IAClG,IAAI,mBAAmB;QACrB,OAAO,IAAI,CAAC,gBAAgB,CAAC;IAC/B,CAAC;IAED;;;;;OAKG;IACH,MAAM;QACJ,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC1D,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;IACpD,CAAC;IAED;;;;;OAKG;IACH,KAAK;QACH,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;YAAE,OAAO;QACrE,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE;YAC7C,KAAK,EAAE,cAAc,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE;YACtC,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC1B,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC;QAChD,CAAC;QACD,GAAG,CAAC,8FAA8F,EAChG,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IACtC,CAAC;IAED;;;;OAIG;IACH,IAAI;QACF,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,GAAG,CAAC,mFAAmF,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5G,CAAC;IAED,0GAA0G;IAC1G,IAAI;QACF,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC,kBAAkB,EAAE,EAAE,CAAC;QAC5B,IAAI,CAAC,kBAAkB,GAAG,SAAS,CAAC;QACpC,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;IAC1B,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,SAAS;QACb,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC;QAC9B,IAAI,CAAC;YACH,OAAO,MAAM,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzG,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,GAAG,CAAC,6DAA6D,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAChF,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAC1D,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACK,gBAAgB;QACtB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,uBAAuB,CAAC;QACpD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,GAAG,CAAC,mEAAmE,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;YAC3G,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,SAAS,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACK,YAAY;QAClB,IAAI,IAAI,CAAC,gBAAgB;YAAE,OAAO;QAClC,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,wBAAwB,CAAC,CAAC;IACjD,CAAC;IAEO,KAAK,CAAC,KAAK;QACjB,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QACxC,GAAG,CAAC,4BAA4B,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACvG,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QACpC,gFAAgF;QAChF,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI;YAAE,OAAO;QACjD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,GAAG,CAAC,qGAAqG,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5H,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;IAC3B,CAAC;CACF"}
@@ -64,15 +64,15 @@ export interface FormationDeadlines {
64
64
  * trip, budgeted as a request over an open stream (2) for the same headroom as every other
65
65
  * one-exchange read in `link-budget.ts`.
66
66
  * - **validation** and the reads inside **provisionWork** are budgeted at two consulting
67
- * reads each, not one per read the code issues. What actually runs: validation is three
68
- * control reads (`isTokenValid` reads the invite; `isTokenUsed` reads it again and counts
69
- * its usages) plus whatever the disclosure validator does; the bound provisioning path
70
- * (`StrandFormationManager.provisionAsResponder`) reads the invite and the strand row to
71
- * resolve the host strand, the invite again, the approver's `ValidationKey` and the seat
72
- * count around the outside approval hook (an HTTP call, so its own
73
- * `DEFAULT_APPROVAL_TIMEOUT_MS` is link-independent and added flat), the strand row and its
74
- * `StrandPartyKey` to issue the joiner's membership invite, and the host strand's stamp
75
- * before the `FormationUsage` write; then one strand-database commit issues the invite.
67
+ * reads each, not one per read the code issues. What actually runs: validation is two
68
+ * control reads (`readToken` reads the invite and, for a metered unexpired one, counts its
69
+ * usages) plus whatever the disclosure validator does; the bound provisioning path
70
+ * (`StrandFormationManager.provisionAsResponder`) acts on that read, reads the strand row
71
+ * with its stamp to resolve the host strand, the approver's `ValidationKey` and the seat
72
+ * count around the outside approval hook when the invite names one (an HTTP call, so its
73
+ * own `DEFAULT_APPROVAL_TIMEOUT_MS` is link-independent and added flat), and the host
74
+ * strand's `StrandPartyKey` to issue the joiner's membership invite; then one
75
+ * strand-database commit issues the invite.
76
76
  * Every one of those is a read of this party's own control database, which in steady state
77
77
  * touches held blocks and consults nobody (`cadre-consistency.md` → "Deadlines Over
78
78
  * Optimystic's Reads and Commits"), so `R` is charged as headroom for the ones that do