@serfab/cadre-core 1.17.0 → 1.18.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 (144) 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 +1176 -564
  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 +14 -13
  35. package/dist/index.js +16 -13
  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 +89 -20
  56. package/dist/peer-join-backfill.js +265 -108
  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/strand-database.d.ts +23 -2
  65. package/dist/strand-database.js +26 -6
  66. package/dist/strand-database.js.map +1 -1
  67. package/dist/strand-first-sync-gate.d.ts +176 -89
  68. package/dist/strand-first-sync-gate.js +288 -122
  69. package/dist/strand-first-sync-gate.js.map +1 -1
  70. package/dist/strand-formation-deadlines.d.ts +9 -9
  71. package/dist/strand-formation-deadlines.js +9 -9
  72. package/dist/strand-formation-manager.d.ts +23 -17
  73. package/dist/strand-formation-manager.js +46 -44
  74. package/dist/strand-formation-manager.js.map +1 -1
  75. package/dist/strand-formation-protocol.d.ts +5 -8
  76. package/dist/strand-formation-protocol.js +101 -16
  77. package/dist/strand-formation-protocol.js.map +1 -1
  78. package/dist/strand-instance-manager.d.ts +26 -17
  79. package/dist/strand-instance-manager.js +119 -50
  80. package/dist/strand-instance-manager.js.map +1 -1
  81. package/dist/strand-membership-reconciler.d.ts +16 -13
  82. package/dist/strand-membership-reconciler.js +30 -22
  83. package/dist/strand-membership-reconciler.js.map +1 -1
  84. package/dist/strand-membership-writer.d.ts +40 -28
  85. package/dist/strand-membership-writer.js +212 -153
  86. package/dist/strand-membership-writer.js.map +1 -1
  87. package/dist/strand-move.js +5 -2
  88. package/dist/strand-move.js.map +1 -1
  89. package/dist/strand-revocation-enforcer.d.ts +134 -60
  90. package/dist/strand-revocation-enforcer.js +144 -71
  91. package/dist/strand-revocation-enforcer.js.map +1 -1
  92. package/dist/strand-solicitation.d.ts +50 -39
  93. package/dist/strand-solicitation.js +10 -3
  94. package/dist/strand-solicitation.js.map +1 -1
  95. package/dist/strand-watcher.d.ts +80 -13
  96. package/dist/strand-watcher.js +152 -48
  97. package/dist/strand-watcher.js.map +1 -1
  98. package/dist/table-change-watch.d.ts +63 -0
  99. package/dist/table-change-watch.js +209 -0
  100. package/dist/table-change-watch.js.map +1 -0
  101. package/dist/timeout-scheduler.d.ts +4 -3
  102. package/dist/timeout-scheduler.js +4 -3
  103. package/dist/timeout-scheduler.js.map +1 -1
  104. package/dist/types.d.ts +90 -49
  105. package/dist/types.js +4 -4
  106. package/dist/types.js.map +1 -1
  107. package/package.json +8 -8
  108. package/src/cadre-invite-protocol.ts +119 -31
  109. package/src/cadre-node.ts +1282 -584
  110. package/src/control-cohort.ts +86 -9
  111. package/src/control-database.ts +848 -403
  112. package/src/control-formation-recorder.ts +359 -353
  113. package/src/control-protocol-guard.ts +16 -2
  114. package/src/control-schema-version.ts +101 -51
  115. package/src/control-schema.ts +16 -16
  116. package/src/control-write-retry.ts +91 -19
  117. package/src/delegate-admission.ts +351 -351
  118. package/src/enrolled-machine-store.ts +3 -3
  119. package/src/formation-token.ts +63 -0
  120. package/src/index.ts +30 -17
  121. package/src/joined-strand-store.ts +69 -24
  122. package/src/link-budget.ts +9 -7
  123. package/src/member-redemption.ts +20 -13
  124. package/src/membership-view.ts +144 -0
  125. package/src/owner-grant-protocol.ts +33 -18
  126. package/src/peer-addr-book.ts +4 -3
  127. package/src/peer-join-backfill.ts +971 -761
  128. package/src/pending-join-runner.ts +76 -53
  129. package/src/seed-bootstrap.ts +72 -67
  130. package/src/strand-database.ts +352 -322
  131. package/src/strand-first-sync-gate.ts +334 -134
  132. package/src/strand-formation-deadlines.ts +227 -227
  133. package/src/strand-formation-manager.ts +731 -725
  134. package/src/strand-formation-protocol.ts +122 -24
  135. package/src/strand-instance-manager.ts +2092 -2026
  136. package/src/strand-membership-reconciler.ts +33 -25
  137. package/src/strand-membership-writer.ts +2093 -2019
  138. package/src/strand-move.ts +5 -2
  139. package/src/strand-revocation-enforcer.ts +740 -654
  140. package/src/strand-solicitation.ts +61 -34
  141. package/src/strand-watcher.ts +175 -56
  142. package/src/table-change-watch.ts +285 -0
  143. package/src/timeout-scheduler.ts +29 -28
  144. package/src/types.ts +89 -51
@@ -4,6 +4,7 @@ import { applyAppSchema, connectToStrand } from '@serfab/quereus-plugin-sereus';
4
4
  import { bootstrapFounderMembership } from './strand-membership-writer.js';
5
5
  import { strandMemberKeyPair } from './strand-member-key.js';
6
6
  import { StrandCommitRecord } from './strand-commit-durability.js';
7
+ import { TableWatchRegistry } from './table-change-watch.js';
7
8
  const log = debug('sereus:cadre:strand-db');
8
9
  const timing = debug('sereus:cadre:timing');
9
10
  /**
@@ -30,6 +31,8 @@ export class StrandDatabase {
30
31
  this.createEmptyCollections = null;
31
32
  this.initialized = false;
32
33
  this.unsubscribeCommits = null;
34
+ /** Subscriptions handed out by {@link watchTables}. */
35
+ this.tableWatches = new TableWatchRegistry();
33
36
  this.config = config;
34
37
  this.commits = new StrandCommitRecord(config.strandId, config);
35
38
  }
@@ -105,11 +108,13 @@ export class StrandDatabase {
105
108
  * therefore use to forge the founder's identity. A closed strand with no
106
109
  * `partyMemberPrivateKey` throws, because it could never seat a founding manager.
107
110
  * The shared key's public half IS passed, but only so the bootstrap can refuse a strand
108
- * founded under it before the split (`PreSplitStrandIdentityError`). Open strands derive
109
- * no keypair (Header only). Idempotent — see {@link bootstrapFounderMembership}.
111
+ * founded under it before the split (`PreSplitStrandIdentityError`). The strand node's
112
+ * peer id is passed under {@link StrandDatabaseConfig.bindFounderPeer}: the same node, so
113
+ * the same id, the membership reconciler binds. Open strands derive no keypair (Header
114
+ * only). Idempotent — see {@link bootstrapFounderMembership}.
110
115
  */
111
116
  async bootstrapFounder() {
112
- const { strandId, strandType, memberPrivateKey, partyMemberPrivateKey } = this.config;
117
+ const { strandId, strandType, memberPrivateKey, partyMemberPrivateKey, libp2pNode, bindFounderPeer } = this.config;
113
118
  const closed = strandType === 'c';
114
119
  await bootstrapFounderMembership(this.db, {
115
120
  strandId,
@@ -119,6 +124,7 @@ export class StrandDatabase {
119
124
  sharedMemberPublicKey: closed && memberPrivateKey
120
125
  ? strandMemberKeyPair(memberPrivateKey).publicKeyB64
121
126
  : undefined,
127
+ founderPeerId: closed && bindFounderPeer === true ? libp2pNode.peerId.toString() : undefined,
122
128
  createEmptyCollections: this.createEmptyCollections,
123
129
  });
124
130
  }
@@ -144,8 +150,8 @@ export class StrandDatabase {
144
150
  if (!partyMemberPrivateKey) {
145
151
  throw new Error(`Cannot found closed strand ${strandId}: this party has no StrandPartyKey for it. ` +
146
152
  'A closed strand needs a founding Member/Manager derived from the party\'s own ' +
147
- 'membership key (minted at publishStrand, or at the founder launch that follows a ' +
148
- 'publish interrupted before its mint) — the shared MemberPrivateKey deliberately ' +
153
+ 'membership key (minted when the strand is published, or at the founder launch for ' +
154
+ 'a closed row stored without one) — the shared MemberPrivateKey deliberately ' +
149
155
  'no longer derives anyone\'s identity.');
150
156
  }
151
157
  return strandMemberKeyPair(partyMemberPrivateKey);
@@ -193,6 +199,17 @@ export class StrandDatabase {
193
199
  this.ensureInitialized();
194
200
  return this.db;
195
201
  }
202
+ /**
203
+ * Subscribe `onChange` to changes in this strand's tables (`'Strand.Member'`, …): commits
204
+ * made here and changes replicated into this node's storage alike, on the single-flight
205
+ * schedule `table-change-watch.ts` describes. {@link close} disposes every subscription
206
+ * still live; a hibernated strand's database is closed and rebuilt on wake, so a consumer
207
+ * subscribes again on the new instance.
208
+ */
209
+ watchTables(tables, onChange, options) {
210
+ this.ensureInitialized();
211
+ return this.tableWatches.watch(this.db, tables, onChange, options);
212
+ }
196
213
  /**
197
214
  * The durability of a recent commit on this database, by its `StampId()` — the id a single
198
215
  * `insert … returning StampId() as Tx` statement returns. Read it after that statement
@@ -223,13 +240,16 @@ export class StrandDatabase {
223
240
  * failed part-way.
224
241
  */
225
242
  async close() {
226
- // First, so an event from the closing connection reaches no app handler.
243
+ // First, so an event from the closing connection reaches no app handler or table watch.
227
244
  this.unsubscribeCommits?.();
228
245
  this.unsubscribeCommits = null;
246
+ this.tableWatches.disposeAll();
229
247
  if (this.shutdownStrand) {
230
248
  await this.shutdownStrand();
231
249
  this.shutdownStrand = null;
232
250
  }
251
+ // Again: a subscription taken during the await above would keep its timers past close.
252
+ this.tableWatches.disposeAll();
233
253
  if (this.db) {
234
254
  void this.db.close();
235
255
  this.db = null;
@@ -1 +1 @@
1
- {"version":3,"file":"strand-database.js","sourceRoot":"","sources":["../src/strand-database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAKhF,OAAO,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAGnE,MAAM,GAAG,GAAG,KAAK,CAAC,wBAAwB,CAAC,CAAC;AAC5C,MAAM,MAAM,GAAG,KAAK,CAAC,qBAAqB,CAAC,CAAC;AA6C5C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,cAAc;IAUzB,YAAY,MAA4B;QAThC,OAAE,GAAoB,IAAI,CAAC;QAC3B,mBAAc,GAA0C,IAAI,CAAC;QAC7D,uBAAkB,GAA4C,IAAI,CAAC;QACnE,2BAAsB,GAAwD,IAAI,CAAC;QAEnF,gBAAW,GAAG,KAAK,CAAC;QAEpB,uBAAkB,GAAwB,IAAI,CAAC;QAGrD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,IAAI,kBAAkB,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACjE,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,UAAU;QACd,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,GAAG,CAAC,kDAAkD,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC9E,OAAO;QACT,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;QACjC,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QACnC,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YACjC,6EAA6E;YAC7E,wCAAwC;YACxC,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,CAAC;QACD,GAAG,CAAC,iDAAiD,EAAE,GAAG,EACxD,UAAU,CAAC,CAAC,CAAC,SAAS,UAAU,CAAC,EAAE,KAAK,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC;QAEpF,IAAI,CAAC,EAAE,GAAG,IAAI,QAAQ,EAAE,CAAC;QAEzB,gEAAgE;QAChE,8EAA8E;QAC9E,8BAA8B;QAC9B,0EAA0E;QAC1E,4EAA4E;QAC5E,6EAA6E;QAC7E,0EAA0E;QAC1E,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAC7B,EAAE;QACF,wFAAwF;QACxF,wFAAwF;QACxF,yFAAyF;QACzF,kEAAkE;QAClE,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,EAAE,EAAE;YAC5C,QAAQ,EAAE,GAAG;YACb,MAAM,EAAE,UAAU,EAAE,MAAM;YAC1B,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,UAAU;YAClC,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC,eAAe;YAC5C,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;QACH,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,QAAQ,CAAC;QACtC,IAAI,CAAC,kBAAkB,GAAG,MAAM,CAAC,UAAU,CAAC;QAC5C,IAAI,CAAC,sBAAsB,GAAG,MAAM,CAAC,sBAAsB,CAAC;QAC5D,4EAA4E;QAC5E,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,CAAC,sEAAsE,EAC3E,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,EACvC,MAAM,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC,EAAE,MAAM,CAAC,QAAQ,EAAE,OAAO,IAAI,CAAC,CAAC,CAAC;QAE/D,+EAA+E;QAC/E,yEAAyE;QACzE,iFAAiF;QACjF,2EAA2E;QAC3E,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YACjC,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAChC,CAAC;QAED,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,GAAG,CAAC,uDAAuD,EAAE,GAAG,CAAC,CAAC;IACpE,CAAC;IAEO,kBAAkB,CAAC,MAA0B;QACnD,MAAM,qBAAqB,GAAG,MAAM,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;QACrG,MAAM,eAAe,GAAG,MAAM,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1F,IAAI,CAAC,kBAAkB,GAAG,GAAG,EAAE;YAC7B,qBAAqB,EAAE,CAAC;YACxB,eAAe,EAAE,CAAC;QACpB,CAAC,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;OAWG;IACK,KAAK,CAAC,gBAAgB;QAC5B,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QACtF,MAAM,MAAM,GAAG,UAAU,KAAK,GAAG,CAAC;QAClC,MAAM,0BAA0B,CAAC,IAAI,CAAC,EAAG,EAAE;YACzC,QAAQ;YACR,IAAI,EAAE,UAAU;YAChB,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE;YACxB,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,QAAQ,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAC,SAAS;YAC/F,qBAAqB,EAAE,MAAM,IAAI,gBAAgB;gBAC/C,CAAC,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,CAAC,YAAY;gBACpD,CAAC,CAAC,SAAS;YACb,sBAAsB,EAAE,IAAI,CAAC,sBAAuB;SACrD,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACK,WAAW;QACjB,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QAC7C,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CACb,uBAAuB,QAAQ,sDAAsD;gBACrF,+EAA+E,CAChF,CAAC;QACJ,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC;IAED;;;;OAIG;IACK,oBAAoB,CAAC,QAAgB,EAAE,qBAAyC;QACtF,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CACb,8BAA8B,QAAQ,6CAA6C;gBACnF,gFAAgF;gBAChF,mFAAmF;gBACnF,kFAAkF;gBAClF,uCAAuC,CACxC,CAAC;QACJ,CAAC;QACD,OAAO,mBAAmB,CAAC,qBAAqB,CAAC,CAAC;IACpD,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,KAAK,CAAC,sBAAsB,CAAC,qBAA8B;;QACzD,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACzB,MAAA,IAAI,CAAC,MAAM,EAAC,qBAAqB,QAArB,qBAAqB,GAAK,qBAAqB,EAAC;QAC5D,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IAC7B,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,eAAe,CAAC,UAAsB;QAC1C,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACzB,MAAM,cAAc,CAAC,IAAI,CAAC,EAAG,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QAClD,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,UAAU,CAAC;QACpC,GAAG,CAAC,mDAAmD,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;IACpH,CAAC;IAED;;OAEG;IACH,WAAW;QACT,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC,EAAG,CAAC;IAClB,CAAC;IAED;;;;;;;OAOG;IACH,gBAAgB,CAAC,aAAqB;QACpC,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IACzC,CAAC;IAED;;;;;;OAMG;IACH,aAAa;QACX,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC,kBAAmB,CAAC;IAClC,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAK;QACT,yEAAyE;QACzE,IAAI,CAAC,kBAAkB,EAAE,EAAE,CAAC;QAC5B,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;QAC/B,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;YAC5B,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;QACD,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;YACZ,KAAK,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;YACrB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;QACjB,CAAC;QACD,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;QAC/B,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;QACzB,GAAG,CAAC,qCAAqC,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACnE,CAAC;IAEO,iBAAiB;QACvB,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,6BAA6B,IAAI,CAAC,MAAM,CAAC,QAAQ,4CAA4C,CAAC,CAAC;QACjH,CAAC;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"strand-database.js","sourceRoot":"","sources":["../src/strand-database.ts"],"names":[],"mappings":"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"}
@@ -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
@@ -63,11 +68,22 @@
63
68
  * it), a joiner's catalog will fork against the founder's — gate the schema apply on the
64
69
  * Header too, or have optimystic open the catalog read-only when a peer already holds it.
65
70
  */
66
- import type { Database } from '@quereus/quereus';
67
71
  import type { StrandDatabase } from './strand-database.js';
68
72
  import { type TimeoutScheduler } from './timeout-scheduler.js';
69
- /** How often a gated launch re-probes (`Strand.Header`, then every `App` table) while waiting for its first sync. */
70
- export declare const DEFAULT_STRAND_FIRST_SYNC_POLL_MS = 500;
73
+ /**
74
+ * How long a gated launch goes without probing when no data lands in the strand's store: the
75
+ * gate probes when data arrives, and this long after its last probe settled when none has. It
76
+ * covers a probe that failed for a reason no storage event follows — a consult that missed its
77
+ * deadline while the founder held the block, or a push that never comes.
78
+ *
79
+ * Why 2 s: arrivals wake the gate, so this timer is the fallback rather than the main path. At
80
+ * the supported 3 s link round trip one consult takes about 6 s (two round trips, per the 1.8.0
81
+ * measurements on {@link DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS}), so a 2 s idle gap after a failed
82
+ * probe adds at most a third of one probe round, and it is under 1% of that 360 s budget. Shorter
83
+ * brings back the cost of polling (the gate re-probed every 500 ms before it woke on arrivals);
84
+ * much longer (5 s) delays the case with no storage event by most of a probe round for nothing.
85
+ */
86
+ export declare const DEFAULT_STRAND_FIRST_SYNC_BACKSTOP_MS = 2000;
71
87
  /**
72
88
  * How long `CadreNode.addStrand` waits for a machine's first sync before rejecting with
73
89
  * {@link StrandAwaitingFirstSyncError}. It has to cover the slowest attach sereus carries,
@@ -98,6 +114,37 @@ export declare const DEFAULT_STRAND_FIRST_SYNC_POLL_MS = 500;
98
114
  * several of those — which is why both bands are recorded here, and why a change to either
99
115
  * number has to be weighed against the other.
100
116
  *
117
+ * AT THE SUPPORTED LINK ON `@optimystic/*` 1.17.0 (2026-10-21), 1 500 ms one-way, the same
118
+ * scenario, one run per arm, with the probe reading every `Strand` table as well as the `App`
119
+ * one (`debt-first-sync-gate-reads-every-strand-table`); the kept-store arm was not run:
120
+ * - Fresh join: writable at 28.7 s and NOT gated, as on 1.15.0.
121
+ * - Re-attach over an EMPTY store: writable at 105.8 and 105.9 s over two runs, the row A wrote
122
+ * while B was away readable at the same moment. B's first database build fails at 7.0 s (no
123
+ * seed peer has answered; `StrandInstanceManager.deferUnreachableJoin` retries it), B's strand
124
+ * node connects to A's at 15.1 s, the retried build's `connectToStrand` takes 21.2 s, and the
125
+ * launch's first probe then takes 72.6 s and passes with no read failing, so the gate never
126
+ * arms.
127
+ * - The same arm with the probe held to the Header and the `App` table (one run, otherwise the
128
+ * same build): that probe took 12.1 s, writable at 45.4 s, the row at 63.5 s. So the seven
129
+ * `Strand` reads cost about 60 s here, about 8.6 s each, against about 6 s for each of the
130
+ * baseline's two reads — more than the 42 s estimated from one 6 s consult per read.
131
+ *
132
+ * AT THE SUPPORTED LINK ON `@optimystic/*` 1.15.0 (2026-10-20), 1 500 ms one-way, the same
133
+ * scenario, one fresh-join run, with the one-row probes woken by data landing in the store
134
+ * (`first-sync-gate-probes-cheaply`; the full-count probes polled every 500 ms gave the same
135
+ * figures):
136
+ * - Fresh join: writable at 28.8 s, and NOT gated. The launch waits 27.3 s in
137
+ * `connectToStrand` for its seed peer (it hydrates the founder's catalog, 9 tables), and by
138
+ * then the store holds the strand: the first probe passed in 24 ms.
139
+ * - The strand case of `message-cost-accounting` (two relay-only parties on loopback) agrees:
140
+ * B's `addStrand` took 471 ms, with 13 `sync` streams to A, 4 block reads from the Header
141
+ * probe and 8 from the table probes. Its two tables are empty, so a full count would fetch
142
+ * the same blocks as a one-row read.
143
+ * - Neither arms the gate, so neither times the woken path. That was checked on
144
+ * `harness-topology`'s cross-party share (a joiner launched with no seed, dialed afterwards)
145
+ * with the backstop raised to 10 s: the founder's join backfill push opened the gate 2.1 s
146
+ * after it armed.
147
+ *
101
148
  * AT THE SUPPORTED LINK ON `@optimystic/*` 1.8.0 (2026-09-29), 1 500 ms one-way, the read
102
149
  * deadline derived from the declared link at 7 000 ms (`cohortReadDeadlineMs` in
103
150
  * `link-budget.ts`; the plugin's `COHORT_READ_DEADLINE_MS` is the same number), and cadre
@@ -154,19 +201,19 @@ export declare const DEFAULT_STRAND_FIRST_SYNC_POLL_MS = 500;
154
201
  * injector are unknown to us) opened the gate at about 150 s, after the previous 120 s budget
155
202
  * had already rejected; and a real Galaxy S7 joining fresh through relay.sereus.org took 178 s.
156
203
  *
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.
204
+ * 360 s clears the worst harness sample (105.9 s, a re-attach over an empty store at the
205
+ * supported link on 1.17.0) by about 3.4x, the reporter's re-attach by 2.4x and the device join
206
+ * by about 2x. Set by the rule every re-measure applies: keep it while the worst harness sample
207
+ * is at most 100 s, which holds a margin of at least 3x; otherwise raise it to 3x the worst
208
+ * sample, rounded up to the next whole minute. The 1.17.0 sample crossed 100 s, and 3x 105.9 s
209
+ * 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
210
+ * kept the 300 s before it. 240 s was rejected earlier: it clears the device join by only 1.35x,
211
+ * and that sample was not a re-attach. History: the original 30 s sat INSIDE the 1000 ms fresh
212
+ * band and refused about half of those joins while their sync was progressing normally; 120 s
213
+ * was sized from fresh joins only and refused the reporter's re-attach.
167
214
  *
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
215
+ * NOTE: accepted tradeoff — what 360 s costs. This wait is what an app's `addStrand` sits in
216
+ * before it is told "not yet", so a strand none of whose members is reachable at all takes six
170
217
  * minutes to report instead of two. The slow report was weighed against refusing attaches that
171
218
  * were working, and the refusal is the worse failure. The cost is bounded — the rejection is
172
219
  * retryable, the strand stays launched and keeps probing, and `strand:writable` fires the
@@ -175,7 +222,7 @@ export declare const DEFAULT_STRAND_FIRST_SYNC_POLL_MS = 500;
175
222
  * "no peer at all" could then be reported at once and this budget would only ever be spent on a
176
223
  * sync that is actually in progress — worth more the longer this budget gets.
177
224
  */
178
- export declare const DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS = 300000;
225
+ export declare const DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS = 360000;
179
226
  /** Embedder-facing tuning for the gate, threaded from `CadreNodeConfig.strandFirstSync`. */
180
227
  export interface StrandFirstSyncConfig {
181
228
  /**
@@ -184,8 +231,11 @@ export interface StrandFirstSyncConfig {
184
231
  * {@link DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS}.
185
232
  */
186
233
  timeoutMs?: number;
187
- /** Probe cadence while gated. Default {@link DEFAULT_STRAND_FIRST_SYNC_POLL_MS}. */
188
- pollIntervalMs?: number;
234
+ /**
235
+ * While gated, probe this long after the last probe settled when no data has landed in the
236
+ * strand's store. Default {@link DEFAULT_STRAND_FIRST_SYNC_BACKSTOP_MS}.
237
+ */
238
+ backstopMs?: number;
189
239
  }
190
240
  /**
191
241
  * Thrown by `CadreNode.addStrand` / `whenStrandWritable` when a machine has not received the
@@ -200,82 +250,119 @@ export declare class StrandAwaitingFirstSyncError extends Error {
200
250
  readonly waitedMs: number;
201
251
  constructor(strandId: string, waitedMs: number);
202
252
  }
203
- /**
204
- * Whether this machine holds the strand's `Strand.Header` row — the "has synced at least
205
- * once (or founded)" signal. A read, never a write: it cannot invent the collection. A
206
- * throwing read (an unreachable cohort mid-sync, a store still hydrating) reports `false`
207
- * with a log, since the caller's only response to either answer is "probe again".
208
- */
209
- export declare function strandHeaderHeld(db: Database, label: string): Promise<boolean>;
210
- /**
211
- * Whether a read of every `App` table settles on this machine — the "each app collection
212
- * has been fetched from the cohort, or is authoritatively absent" signal. Only a throwing
213
- * read (an unreachable cohort) reports `false`; an empty table is a settled read.
214
- */
215
- export declare function appTablesReadable(db: Database, label: string): Promise<boolean>;
216
- /**
217
- * The gate's whole probe: the Header is held AND every app table has been read once. The
218
- * app tables are read only once the Header is — a joiner with no peer would otherwise pay
219
- * one failing network read per table per probe for nothing.
220
- */
221
- export declare function strandFirstSyncComplete(db: Database, label: string): Promise<boolean>;
222
253
  export interface StrandFirstSyncGateDeps {
223
254
  /** Log tag naming which strand this gate holds (the strand id). */
224
255
  label: string;
225
- /** The initialized database being held back from the app until the Header is held. */
256
+ /** The initialized database being held back from the app until the first sync completes. */
226
257
  database: StrandDatabase;
227
258
  /**
228
- * Called exactly once, on the gate's own probe loop, when the first sync completes.
229
- * NOT called by {@link StrandFirstSyncGate.open} — a caller that force-opens the gate
230
- * (a founder bootstrap it just ran) publishes the database itself.
259
+ * Called exactly once, from the gate's own probe, when the first sync completes. NOT called
260
+ * by {@link StrandFirstSyncGate.open} — a caller that force-opens the gate (a founder
261
+ * bootstrap it just ran) publishes the database itself.
231
262
  */
232
263
  onHeaderHeld: () => void;
233
- /** Timer seam for the probe loop; omit for real (unref'd) timeouts. */
264
+ /**
265
+ * Subscribe `onChange` to every block landing in this strand's store — a push such as join
266
+ * backfill, a fetched block, a commit — and return the unsubscribe. `buildStrandDatabase`
267
+ * passes the strand node's `storageRepo.onAnyCollectionChange`. Omitted (test doubles, a node
268
+ * without it), the gate probes on its backstop alone.
269
+ */
270
+ subscribeStorageChanges?: (onChange: () => void) => () => void;
271
+ /** Timer seam for the probe schedule; omit for real (unref'd) timeouts. */
234
272
  scheduler?: TimeoutScheduler;
235
273
  }
236
274
  /**
237
- * Holds one launch's `StrandDatabase` until {@link strandFirstSyncComplete}. Probes are
238
- * sequential (the next is scheduled only after the previous read settles), so a slow
239
- * network read never stacks probes.
275
+ * Holds one launch's `StrandDatabase` until the first sync completes: the `Strand.Header` row is
276
+ * held and a read of every `Strand` and `App` table has settled. What a probe established is
277
+ * remembered for the gate's lifetime, so a later probe — the gate's own, after the caller's
278
+ * {@link probeOnce} — reads only what is still missing. A gate lives for one database build, so a
279
+ * hibernation wake or a deferred-join retry starts over, and over a store that holds everything
280
+ * every read answers locally.
281
+ *
282
+ * While armed it probes when data lands in the strand's store
283
+ * ({@link StrandFirstSyncGateDeps.subscribeStorageChanges}, coalesced over
284
+ * `TABLE_CHANGE_COALESCE_MS`), and {@link StrandFirstSyncConfig.backstopMs} after its last probe
285
+ * settled when nothing has. Probes run on `scheduleRuns`: one at a time, and data landing during
286
+ * one produces exactly one more.
287
+ *
288
+ * The wake source is the node's storage-wide change feed rather than a table watch
289
+ * (`StrandDatabase.watchTables`): the Optimystic plugin arms a table's replicated-change bridge
290
+ * only after the table initializes successfully, and the tables the gate waits on are the ones
291
+ * whose collection could not be fetched yet, so a table watch would not hear the push that
292
+ * delivers them. Each strand has its own libp2p node and store, so the feed hears this strand
293
+ * only.
240
294
  *
241
295
  * NOTE: a probe's reads dial the founder, through the relay when that is the only route, even
242
296
  * while the founder is unreachable: one run of `joiner-first-commit-over-slow-relay` logged 17
243
297
  * `hop connect denied` at the relay in about 10 s, each a few frames on the joiner's relay
244
- * connection. If that connection ever shows saturated while no strand member is connected, hold
245
- * the probe while the strand member redial (`strand-peer-redial.ts`) reports none connected.
298
+ * connection, when the gate re-probed every 500 ms and re-read every table. It now probes on
299
+ * arrivals and a 2 s backstop and skips what it holds, so expect fewer. If that connection ever
300
+ * shows saturated while no strand member is connected, hold the probe while the strand member
301
+ * redial (`strand-peer-redial.ts`) reports none connected.
246
302
  */
247
303
  export declare class StrandFirstSyncGate {
248
304
  private readonly deps;
249
305
  private readonly scheduler;
250
- private readonly pollIntervalMs;
251
- private timer;
306
+ private readonly backstopMs;
307
+ private readonly progress;
308
+ private probes;
309
+ private unsubscribeStorage;
310
+ private listening;
311
+ /**
312
+ * Data landed since the last probe started (or, before any probe, since {@link listen}), so one
313
+ * is already due and will read it.
314
+ */
315
+ private landedSinceProbe;
252
316
  private stopped;
253
317
  private opened;
254
- private probing;
255
318
  constructor(deps: StrandFirstSyncGateDeps, config?: StrandFirstSyncConfig);
256
319
  /** The database this gate is holding back. */
257
320
  get database(): StrandDatabase;
258
- /** True once the Header was seen (or the gate was force-opened); the loop is then done. */
321
+ /** True once the first sync completed (or the gate was force-opened); probing is then done. */
259
322
  get isOpen(): boolean;
260
- /** Arm the probe loop. The first probe runs after one interval — the caller already probed once. */
323
+ /** Whether a block landed since the last probe started, or since {@link listen} when none has. */
324
+ get heardDataSinceProbe(): boolean;
325
+ /**
326
+ * Subscribe to the storage change feed ahead of {@link start}, so a block landing before the
327
+ * gate is armed is heard: {@link heardDataSinceProbe} reports it, and `start` then probes at
328
+ * once. For a first join, which skips the caller's first probe unless a member's push already
329
+ * landed while the database was initializing.
330
+ */
331
+ listen(): void;
332
+ /**
333
+ * Arm the probe schedule. It probes when data lands, or one backstop later; and at once when
334
+ * data already landed since the last probe (or since {@link listen}). Without `listen`, data
335
+ * that landed between the caller's probe and this call raised no event the gate heard, so
336
+ * that first backstop is what catches it.
337
+ */
261
338
  start(): void;
262
339
  /**
263
- * Open without the probe loop: the caller is publishing the database itself — a launch
264
- * whose first probe passed (or a founder's), which never armed the loop, or a founder
265
- * bootstrap just run against the gated database. Stops the loop; `onHeaderHeld` is NOT
266
- * invoked.
340
+ * Open without probing: the caller is publishing the database itself — a launch whose first
341
+ * probe passed (or a founder's), which never armed the schedule, or a founder bootstrap just
342
+ * run against the gated database. Stops probing; `onHeaderHeld` is NOT invoked.
267
343
  */
268
344
  open(): void;
269
- /** Disarm the loop; a probe already in flight completes and is then ignored. */
345
+ /** Disarm: unsubscribe and drop the schedule; a probe already in flight completes and is then ignored. */
270
346
  stop(): void;
271
- private schedule;
272
- private probe;
273
347
  /**
274
- * One probe's answer, with a throw from OUTSIDE {@link strandFirstSyncComplete}'s own read
275
- * guards — the `App` schema lookup, or `getDatabase()` itself — reported as "not yet" like
276
- * any failing read. The loop is scheduled from `void this.probe()`, so an escaping rejection
277
- * would be unhandled AND leave nothing scheduled: the strand would stay gated for the whole
278
- * `timeoutMs` and never recover, which is the one failure this gate must not have.
348
+ * One probe's answer, reading only what this gate has not yet seen settled. A throw from
349
+ * OUTSIDE the probe's own read guards — a schema lookup, or `getDatabase()` itself — is
350
+ * reported as "not yet" like any failing read, so the caller's first probe cannot fail the
351
+ * launch over it and the gate's log says what happened.
352
+ */
353
+ probeOnce(): Promise<boolean>;
354
+ /**
355
+ * NOTE: a probe's own reads can land blocks (a fetched block, a read-repair), which fires this
356
+ * feed and schedules one follow-up probe. Each follow-up means a block newly arrived, so they
357
+ * stop when nothing new lands; if gated launches ever show probe storms in the logs, ignore
358
+ * events while a probe is in flight.
279
359
  */
280
- private probeHeld;
360
+ private subscribeStorage;
361
+ /**
362
+ * Called synchronously by the store after each block lands, so it only arms a timer. A push
363
+ * lands its blocks one by one; only the first since the last probe started arms one, since
364
+ * that probe has not started yet and will read the rest.
365
+ */
366
+ private onDataLanded;
367
+ private probe;
281
368
  }