@serfab/cadre-core 1.13.0 → 1.15.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 (230) hide show
  1. package/README.md +293 -274
  2. package/dist/cadre-invite-protocol.d.ts +383 -0
  3. package/dist/cadre-invite-protocol.js +774 -0
  4. package/dist/cadre-invite-protocol.js.map +1 -0
  5. package/dist/cadre-join-request.d.ts +42 -0
  6. package/dist/cadre-join-request.js +144 -0
  7. package/dist/cadre-join-request.js.map +1 -0
  8. package/dist/cadre-node.d.ts +849 -210
  9. package/dist/cadre-node.js +1574 -374
  10. package/dist/cadre-node.js.map +1 -1
  11. package/dist/claim-proof.d.ts +45 -0
  12. package/dist/claim-proof.js +132 -0
  13. package/dist/claim-proof.js.map +1 -0
  14. package/dist/control-authorization.d.ts +80 -19
  15. package/dist/control-authorization.js +55 -13
  16. package/dist/control-authorization.js.map +1 -1
  17. package/dist/control-cohort.js +1 -1
  18. package/dist/control-cohort.js.map +1 -1
  19. package/dist/control-database.d.ts +605 -108
  20. package/dist/control-database.js +1299 -280
  21. package/dist/control-database.js.map +1 -1
  22. package/dist/control-protocol-guard.d.ts +142 -0
  23. package/dist/control-protocol-guard.js +293 -0
  24. package/dist/control-protocol-guard.js.map +1 -0
  25. package/dist/control-schema.d.ts +1 -1
  26. package/dist/control-schema.js +817 -1067
  27. package/dist/control-schema.js.map +1 -1
  28. package/dist/control-stream.d.ts +4 -0
  29. package/dist/control-stream.js +22 -7
  30. package/dist/control-stream.js.map +1 -1
  31. package/dist/delegate-admission.d.ts +1 -1
  32. package/dist/delegate-admission.js +1 -1
  33. package/dist/device-token.d.ts +11 -9
  34. package/dist/device-token.js +14 -12
  35. package/dist/device-token.js.map +1 -1
  36. package/dist/ed25519-key.d.ts +22 -9
  37. package/dist/ed25519-key.js +47 -10
  38. package/dist/ed25519-key.js.map +1 -1
  39. package/dist/fetch-impl.d.ts +12 -0
  40. package/dist/fetch-impl.js +23 -0
  41. package/dist/fetch-impl.js.map +1 -0
  42. package/dist/file-durable-slot.js +2 -1
  43. package/dist/file-durable-slot.js.map +1 -1
  44. package/dist/file-safe-name.d.ts +25 -0
  45. package/dist/file-safe-name.js +29 -0
  46. package/dist/file-safe-name.js.map +1 -0
  47. package/dist/formation-approval.js +5 -11
  48. package/dist/formation-approval.js.map +1 -1
  49. package/dist/fs-atomic.d.ts +0 -19
  50. package/dist/fs-atomic.js +3 -24
  51. package/dist/fs-atomic.js.map +1 -1
  52. package/dist/index.d.ts +22 -10
  53. package/dist/index.js +54 -11
  54. package/dist/index.js.map +1 -1
  55. package/dist/invitation-bootstrap.d.ts +50 -12
  56. package/dist/invitation-bootstrap.js +76 -13
  57. package/dist/invitation-bootstrap.js.map +1 -1
  58. package/dist/joined-strand-store.d.ts +7 -0
  59. package/dist/joined-strand-store.js +9 -0
  60. package/dist/joined-strand-store.js.map +1 -1
  61. package/dist/key-store-file.js +2 -1
  62. package/dist/key-store-file.js.map +1 -1
  63. package/dist/link-budget.d.ts +25 -33
  64. package/dist/link-budget.js +25 -35
  65. package/dist/link-budget.js.map +1 -1
  66. package/dist/machine-removal.d.ts +78 -0
  67. package/dist/machine-removal.js +166 -0
  68. package/dist/machine-removal.js.map +1 -0
  69. package/dist/membership-connection-gater.d.ts +191 -151
  70. package/dist/membership-connection-gater.js +236 -173
  71. package/dist/membership-connection-gater.js.map +1 -1
  72. package/dist/node-claim-payload.d.ts +74 -0
  73. package/dist/node-claim-payload.js +224 -0
  74. package/dist/node-claim-payload.js.map +1 -0
  75. package/dist/node-default-transports.d.ts +27 -0
  76. package/dist/node-default-transports.js +35 -0
  77. package/dist/node-default-transports.js.map +1 -0
  78. package/dist/node-transports.d.ts +30 -0
  79. package/dist/node-transports.js +40 -0
  80. package/dist/node-transports.js.map +1 -0
  81. package/dist/owner-anchor-sync.d.ts +22 -0
  82. package/dist/owner-anchor-sync.js +143 -0
  83. package/dist/owner-anchor-sync.js.map +1 -0
  84. package/dist/peer-authorization.d.ts +113 -18
  85. package/dist/peer-authorization.js +225 -29
  86. package/dist/peer-authorization.js.map +1 -1
  87. package/dist/peer-dial.d.ts +143 -27
  88. package/dist/peer-dial.js +276 -31
  89. package/dist/peer-dial.js.map +1 -1
  90. package/dist/peer-join-backfill.d.ts +26 -2
  91. package/dist/peer-join-backfill.js +60 -12
  92. package/dist/peer-join-backfill.js.map +1 -1
  93. package/dist/peer-record.d.ts +17 -52
  94. package/dist/peer-record.js +20 -58
  95. package/dist/peer-record.js.map +1 -1
  96. package/dist/pending-join-runner.d.ts +66 -58
  97. package/dist/pending-join-runner.js +136 -145
  98. package/dist/pending-join-runner.js.map +1 -1
  99. package/dist/primary-lan-address.d.ts +8 -0
  100. package/dist/primary-lan-address.js +46 -0
  101. package/dist/primary-lan-address.js.map +1 -0
  102. package/dist/provider/client.d.ts +131 -0
  103. package/dist/provider/client.js +226 -0
  104. package/dist/provider/client.js.map +1 -0
  105. package/dist/provider/descriptor.d.ts +41 -0
  106. package/dist/provider/descriptor.js +103 -0
  107. package/dist/provider/descriptor.js.map +1 -0
  108. package/dist/provider/index.d.ts +9 -0
  109. package/dist/provider/index.js +10 -0
  110. package/dist/provider/index.js.map +1 -0
  111. package/dist/provider/oauth.d.ts +125 -0
  112. package/dist/provider/oauth.js +199 -0
  113. package/dist/provider/oauth.js.map +1 -0
  114. package/dist/provider/rent-node.d.ts +101 -0
  115. package/dist/provider/rent-node.js +220 -0
  116. package/dist/provider/rent-node.js.map +1 -0
  117. package/dist/push-fanout.d.ts +8 -2
  118. package/dist/push-fanout.js +8 -2
  119. package/dist/push-fanout.js.map +1 -1
  120. package/dist/relay-addrs.d.ts +9 -4
  121. package/dist/relay-addrs.js +9 -4
  122. package/dist/relay-addrs.js.map +1 -1
  123. package/dist/relay-reservation.d.ts +51 -16
  124. package/dist/relay-reservation.js +107 -24
  125. package/dist/relay-reservation.js.map +1 -1
  126. package/dist/seed-bootstrap.d.ts +165 -123
  127. package/dist/seed-bootstrap.js +313 -275
  128. package/dist/seed-bootstrap.js.map +1 -1
  129. package/dist/seed-trust-policy.d.ts +98 -37
  130. package/dist/seed-trust-policy.js +188 -2
  131. package/dist/seed-trust-policy.js.map +1 -1
  132. package/dist/strand-addr-protocol.d.ts +24 -26
  133. package/dist/strand-addr-protocol.js +19 -26
  134. package/dist/strand-addr-protocol.js.map +1 -1
  135. package/dist/strand-commit-durability.d.ts +54 -0
  136. package/dist/strand-commit-durability.js +74 -0
  137. package/dist/strand-commit-durability.js.map +1 -0
  138. package/dist/strand-database.d.ts +23 -8
  139. package/dist/strand-database.js +41 -13
  140. package/dist/strand-database.js.map +1 -1
  141. package/dist/strand-first-sync-gate.d.ts +7 -1
  142. package/dist/strand-first-sync-gate.js +7 -1
  143. package/dist/strand-first-sync-gate.js.map +1 -1
  144. package/dist/strand-formation-protocol.js +5 -2
  145. package/dist/strand-formation-protocol.js.map +1 -1
  146. package/dist/strand-fret-addrs.d.ts +19 -1
  147. package/dist/strand-fret-addrs.js +27 -9
  148. package/dist/strand-fret-addrs.js.map +1 -1
  149. package/dist/strand-instance-manager.d.ts +81 -19
  150. package/dist/strand-instance-manager.js +216 -105
  151. package/dist/strand-instance-manager.js.map +1 -1
  152. package/dist/strand-move.d.ts +124 -0
  153. package/dist/strand-move.js +438 -0
  154. package/dist/strand-move.js.map +1 -0
  155. package/dist/strand-network-config.js +7 -6
  156. package/dist/strand-network-config.js.map +1 -1
  157. package/dist/strand-peer-redial.d.ts +113 -0
  158. package/dist/strand-peer-redial.js +236 -0
  159. package/dist/strand-peer-redial.js.map +1 -0
  160. package/dist/strand-revocation-enforcer.d.ts +11 -11
  161. package/dist/strand-revocation-enforcer.js +11 -11
  162. package/dist/strand-wake-protocol.d.ts +13 -16
  163. package/dist/strand-wake-protocol.js +23 -27
  164. package/dist/strand-wake-protocol.js.map +1 -1
  165. package/dist/trusted-owner-store.d.ts +35 -5
  166. package/dist/trusted-owner-store.js +50 -8
  167. package/dist/trusted-owner-store.js.map +1 -1
  168. package/dist/types.d.ts +512 -89
  169. package/dist/types.js +10 -0
  170. package/dist/types.js.map +1 -1
  171. package/package.json +19 -10
  172. package/src/cadre-invite-protocol.ts +1052 -0
  173. package/src/cadre-join-request.ts +167 -0
  174. package/src/cadre-node.ts +1703 -386
  175. package/src/claim-proof.ts +147 -0
  176. package/src/control-authorization.ts +203 -117
  177. package/src/control-cohort.ts +1 -1
  178. package/src/control-database.ts +4663 -3453
  179. package/src/control-protocol-guard.ts +348 -0
  180. package/src/control-schema.ts +828 -1078
  181. package/src/control-stream.ts +26 -7
  182. package/src/delegate-admission.ts +1 -1
  183. package/src/device-token.ts +14 -10
  184. package/src/ed25519-key.ts +47 -10
  185. package/src/fetch-impl.ts +23 -0
  186. package/src/file-durable-slot.ts +64 -63
  187. package/src/file-safe-name.ts +32 -0
  188. package/src/formation-approval.ts +604 -610
  189. package/src/fs-atomic.ts +92 -117
  190. package/src/index.ts +789 -648
  191. package/src/invitation-bootstrap.ts +91 -13
  192. package/src/joined-strand-store.ts +508 -498
  193. package/src/key-store-file.ts +142 -141
  194. package/src/link-budget.ts +25 -36
  195. package/src/machine-removal.ts +216 -0
  196. package/src/membership-connection-gater.ts +259 -191
  197. package/src/node-claim-payload.ts +259 -0
  198. package/src/node-default-transports.ts +38 -0
  199. package/src/node-transports.ts +49 -0
  200. package/src/owner-anchor-sync.ts +171 -0
  201. package/src/peer-authorization.ts +258 -29
  202. package/src/peer-dial.ts +604 -315
  203. package/src/peer-join-backfill.ts +62 -11
  204. package/src/peer-record.ts +159 -197
  205. package/src/pending-join-runner.ts +729 -735
  206. package/src/primary-lan-address.ts +47 -0
  207. package/src/provider/client.ts +326 -0
  208. package/src/provider/descriptor.ts +129 -0
  209. package/src/provider/index.ts +59 -0
  210. package/src/provider/oauth.ts +285 -0
  211. package/src/provider/rent-node.ts +275 -0
  212. package/src/push-fanout.ts +8 -2
  213. package/src/relay-addrs.ts +9 -4
  214. package/src/relay-reservation.ts +1211 -1121
  215. package/src/seed-bootstrap.ts +1502 -1448
  216. package/src/seed-trust-policy.ts +353 -140
  217. package/src/strand-addr-protocol.ts +26 -35
  218. package/src/strand-commit-durability.ts +116 -0
  219. package/src/strand-database.ts +46 -14
  220. package/src/strand-first-sync-gate.ts +7 -1
  221. package/src/strand-formation-protocol.ts +1039 -1036
  222. package/src/strand-fret-addrs.ts +179 -145
  223. package/src/strand-instance-manager.ts +2026 -1883
  224. package/src/strand-move.ts +585 -0
  225. package/src/strand-network-config.ts +7 -6
  226. package/src/strand-peer-redial.ts +307 -0
  227. package/src/strand-revocation-enforcer.ts +11 -11
  228. package/src/strand-wake-protocol.ts +23 -30
  229. package/src/trusted-owner-store.ts +242 -171
  230. package/src/types.ts +2687 -2242
@@ -0,0 +1,116 @@
1
+ import debug from 'debug';
2
+ import { durabilityRank } from '@optimystic/db-core';
3
+ import type { WriteDurability } from '@optimystic/db-core';
4
+ import type { CommitFullyHeld, SqlCommitDurability } from '@serfab/quereus-plugin-sereus';
5
+
6
+ const log = debug('sereus:cadre:strand-commit-durability');
7
+
8
+ /** What one commit on a strand wrote, and whether it survives this machine stopping for good. */
9
+ export interface StrandCommitDurability {
10
+ readonly strandId: string;
11
+ /** `StampId()` inside the committed transaction. */
12
+ readonly transactionId: string;
13
+ /**
14
+ * Another member holds every block the commit wrote: the overall answer is `majority` or `full`
15
+ * and no block was abandoned (`torn`). False for `local`, `unrouted`, an unknown overall, or a
16
+ * torn commit. Becomes true once `strand:commit-held` fires for the commit.
17
+ */
18
+ readonly heldElsewhere: boolean;
19
+ /**
20
+ * When {@link heldElsewhere} is false: `strand:commit-held` will fire for this commit if it
21
+ * reaches the others while this database stays open. False means nothing will say so.
22
+ */
23
+ readonly followed: boolean;
24
+ /** The plugin's report, for per-table detail. */
25
+ readonly report: SqlCommitDurability;
26
+ }
27
+
28
+ /** A commit that was held only below full replication is now held by every member of its cohort. */
29
+ export interface StrandCommitHeld {
30
+ readonly strandId: string;
31
+ readonly transactionId: string;
32
+ }
33
+
34
+ /** Where a strand database forwards each commit's durability and each held commit. */
35
+ export interface StrandCommitListeners {
36
+ onCommitDurability?: (commit: StrandCommitDurability) => void;
37
+ onCommitHeld?: (event: StrandCommitHeld) => void;
38
+ }
39
+
40
+ // NOTE: if an app reads a commit's durability later than 256 commits after it, raise this or let
41
+ // the app pass the id up front.
42
+ const RECENT_COMMIT_LIMIT = 256;
43
+
44
+ /**
45
+ * `majority` counts: a strict majority of a cohort of at least two is at least two machines, so
46
+ * the write survives this one stopping. A torn commit does not, though it can succeed as `majority`
47
+ * (Optimystic clamps a torn `full` to it): its abandoned blocks are held by nobody. The one place
48
+ * this rule lives; apps read the field.
49
+ */
50
+ function isHeldElsewhere(overall: WriteDurability | undefined): boolean {
51
+ return overall !== undefined
52
+ && durabilityRank(overall.quorum) >= durabilityRank('majority')
53
+ && (overall.torn?.length ?? 0) === 0;
54
+ }
55
+
56
+ /**
57
+ * The durability of the most recent commits on one strand database, by transaction id. The
58
+ * plugin reports a commit — and a `covered` commit is held — before the statement that made it
59
+ * resolves, so an app that learns its id from `returning StampId()` reads the answer from here
60
+ * afterwards. Each entry is frozen and replaced when the commit becomes held, so a value read
61
+ * from {@link get} never changes under its reader.
62
+ */
63
+ export class StrandCommitRecord {
64
+ private readonly commits = new Map<string, StrandCommitDurability>();
65
+
66
+ constructor(
67
+ private readonly strandId: string,
68
+ private readonly listeners: StrandCommitListeners
69
+ ) {}
70
+
71
+ get(transactionId: string): StrandCommitDurability | undefined {
72
+ return this.commits.get(transactionId);
73
+ }
74
+
75
+ /** A plugin `onCommitDurability` report: record it, then forward it. */
76
+ reportCommit(report: SqlCommitDurability): void {
77
+ const commit = Object.freeze({
78
+ strandId: this.strandId,
79
+ transactionId: report.transactionId,
80
+ heldElsewhere: isHeldElsewhere(report.durability.overall),
81
+ followed: report.followed,
82
+ report
83
+ });
84
+ this.remember(commit);
85
+ this.forward('durability', report.transactionId, () => this.listeners.onCommitDurability?.(commit));
86
+ }
87
+
88
+ /** A plugin `onCommitFullyHeld` event: mark the commit held, then forward it — also for one no longer recorded. */
89
+ reportHeld({ transactionId }: CommitFullyHeld): void {
90
+ const recorded = this.commits.get(transactionId);
91
+ if (recorded) {
92
+ this.commits.set(transactionId, Object.freeze({ ...recorded, heldElsewhere: true }));
93
+ }
94
+ this.forward('held', transactionId, () => this.listeners.onCommitHeld?.({ strandId: this.strandId, transactionId }));
95
+ }
96
+
97
+ private remember(commit: StrandCommitDurability): void {
98
+ this.commits.set(commit.transactionId, commit);
99
+ if (this.commits.size > RECENT_COMMIT_LIMIT) {
100
+ const oldest = this.commits.keys().next().value!;
101
+ this.commits.delete(oldest);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Run an app's handler isolated: it runs inside the commit, and a throw reaching the plugin
107
+ * would be logged there as the plugin's own listener failing.
108
+ */
109
+ private forward(kind: string, transactionId: string, deliver: () => void): void {
110
+ try {
111
+ deliver();
112
+ } catch (error) {
113
+ log('strand %s: %s handler threw for transaction %s: %O', this.strandId, kind, transactionId, error);
114
+ }
115
+ }
116
+ }
@@ -7,11 +7,13 @@ import type { IRepo } from '@optimystic/db-core';
7
7
  import type { SAppConfig } from './types.js';
8
8
  import { bootstrapFounderMembership } from './strand-membership-writer.js';
9
9
  import { strandMemberKeyPair } from './strand-member-key.js';
10
+ import { StrandCommitRecord } from './strand-commit-durability.js';
11
+ import type { StrandCommitDurability, StrandCommitListeners } from './strand-commit-durability.js';
10
12
 
11
13
  const log = debug('sereus:cadre:strand-db');
12
14
  const timing = debug('sereus:cadre:timing');
13
15
 
14
- export interface StrandDatabaseConfig {
16
+ export interface StrandDatabaseConfig extends StrandCommitListeners {
15
17
  /** The strand ID */
16
18
  strandId: string;
17
19
  /**
@@ -63,10 +65,12 @@ export interface StrandDatabaseConfig {
63
65
  * but delegates the actual SQL-surface composition — plugin registration, node
64
66
  * wiring, catalog hydration, schema apply — to `connectToStrand` from
65
67
  * `@serfab/quereus-plugin-sereus`, the single shared composition. The libp2p
66
- * node is injected here, so `connectToStrand` never *creates* the node;
67
- * `StrandInstanceManager` owns the node lifecycle. (The strand connection's
68
- * `shutdown` still stops the injected node via the collection factory, so the
69
- * manager's own `node.stop()` is an idempotent second stop — see `close()`.)
68
+ * node is injected here, so `connectToStrand` neither creates nor stops it:
69
+ * `StrandInstanceManager` owns the node lifecycle, and a failed or closed
70
+ * database leaves the node running.
71
+ *
72
+ * One instance is one attempt: a failed {@link initialize} is closed and a new
73
+ * instance built for the next attempt.
70
74
  */
71
75
  export class StrandDatabase {
72
76
  private db: Database | null = null;
@@ -74,9 +78,12 @@ export class StrandDatabase {
74
78
  private resolvedTransactor: SereusPluginResult['transactor'] | null = null;
75
79
  private readonly config: StrandDatabaseConfig;
76
80
  private initialized = false;
81
+ private readonly commits: StrandCommitRecord;
82
+ private unsubscribeCommits: (() => void) | null = null;
77
83
 
78
84
  constructor(config: StrandDatabaseConfig) {
79
85
  this.config = config;
86
+ this.commits = new StrandCommitRecord(config.strandId, config);
80
87
  }
81
88
 
82
89
  /**
@@ -103,10 +110,8 @@ export class StrandDatabase {
103
110
  this.db = new Database();
104
111
 
105
112
  // Delegate to the shared composition. The node is injected, so:
106
- // - `connectToStrand` never creates a node (its `createdNode` stays null);
107
- // `StrandInstanceManager` owns the node lifecycle. Its `shutdown` does
108
- // still stop the injected node via the collection factory, so the
109
- // manager's later `node.stop()` is an idempotent second stop;
113
+ // - `connectToStrand` never creates or stops a node; `StrandInstanceManager`
114
+ // owns the node lifecycle;
110
115
  // - no `storage` is passed: the plugin consumes it only to build a local
111
116
  // transactor's raw-storage factory or to create a node it was not given,
112
117
  // and cadre-core always injects the node and runs the network transactor.
@@ -126,14 +131,16 @@ export class StrandDatabase {
126
131
  });
127
132
  this.shutdownStrand = result.shutdown;
128
133
  this.resolvedTransactor = result.transactor;
134
+ // Before the founder bootstrap, so its commits are reported like any other.
135
+ this.subscribeToCommits(result);
129
136
  timing('[strandDb:%s] connectToStrand: %dms (hydrated tables=%d, indexes=%d)',
130
137
  sid, Math.round(performance.now() - t0),
131
138
  result.hydrated?.tables ?? 0, result.hydrated?.indexes ?? 0);
132
139
 
133
140
  // Founder-only: write the one-time membership bootstrap now that the schema is
134
141
  // applied (connectToStrand has returned). A throw here propagates out of
135
- // initialize() so buildStrandRuntime's rollback tears the half-built strand
136
- // down rather than leaking a node. Joiners skip this — their rows arrive via sync.
142
+ // initialize(), and the caller closes this half-built database (close() runs the
143
+ // composition's shutdown). Joiners skip this — their rows arrive via sync.
137
144
  if (this.config.founder === true) {
138
145
  await this.bootstrapFounder();
139
146
  }
@@ -142,6 +149,15 @@ export class StrandDatabase {
142
149
  log('StrandDatabase for strand %s initialized successfully', sid);
143
150
  }
144
151
 
152
+ private subscribeToCommits(result: SereusPluginResult): void {
153
+ const unsubscribeDurability = result.onCommitDurability(report => this.commits.reportCommit(report));
154
+ const unsubscribeHeld = result.onCommitFullyHeld(event => this.commits.reportHeld(event));
155
+ this.unsubscribeCommits = () => {
156
+ unsubscribeDurability();
157
+ unsubscribeHeld();
158
+ };
159
+ }
160
+
145
161
  /**
146
162
  * Run the founder membership bootstrap against the freshly-composed strand DB.
147
163
  *
@@ -247,6 +263,19 @@ export class StrandDatabase {
247
263
  return this.db!;
248
264
  }
249
265
 
266
+ /**
267
+ * The durability of a recent commit on this database, by its `StampId()` — the id a single
268
+ * `insert … returning StampId() as Tx` statement returns. Read it after that statement
269
+ * resolves: the plugin reports the commit before the statement returns, and a commit already
270
+ * held everywhere is marked held by then too. `undefined` for a commit that wrote nothing, one
271
+ * made before this database was built, or one more than 256 commits back. Each value is a
272
+ * snapshot: read again after `strand:commit-held` to see the change.
273
+ */
274
+ commitDurability(transactionId: string): StrandCommitDurability | undefined {
275
+ this.ensureInitialized();
276
+ return this.commits.get(transactionId);
277
+ }
278
+
250
279
  /**
251
280
  * The Optimystic transactor this strand's connection resolved to — always
252
281
  * `'network'` here, since cadre-core passes no `transactor` option and takes
@@ -261,11 +290,14 @@ export class StrandDatabase {
261
290
 
262
291
  /**
263
292
  * Close the database and cleanup resources. Runs the strand-connection
264
- * shutdown (collection-factory teardown, which also stops the injected node),
265
- * then closes the `Database`. `StrandInstanceManager.releaseRuntime` issues a
266
- * further idempotent `node.stop()` after this returns.
293
+ * shutdown (collection-factory teardown; the injected node keeps running),
294
+ * then closes the `Database`. Safe on an instance whose {@link initialize}
295
+ * failed part-way.
267
296
  */
268
297
  async close(): Promise<void> {
298
+ // First, so an event from the closing connection reaches no app handler.
299
+ this.unsubscribeCommits?.();
300
+ this.unsubscribeCommits = null;
269
301
  if (this.shutdownStrand) {
270
302
  await this.shutdownStrand();
271
303
  this.shutdownStrand = null;
@@ -55,7 +55,7 @@
55
55
  * seed writes the catalog collection (`optimystic/schema` plus two hash-named blocks,
56
56
  * measured 2026-09-16) alone, during `connectToStrand`'s schema apply, before any peer
57
57
  * contact. (One with a seed cannot until a seed peer answers — Optimystic refuses the
58
- * absence ruling — and is rebuilt on a timer by `StrandInstanceManager.deferUnreachableJoin`.) That is the same "two histories under one id" shape this gate
58
+ * absence ruling — and retries its database build on a timer, `StrandInstanceManager.deferUnreachableJoin`.) That is the same "two histories under one id" shape this gate
59
59
  * exists to prevent, and it is only safe because both sides derive byte-identical content
60
60
  * from the same schema text. If the catalog ever carries per-machine or ordering-dependent
61
61
  * content (optimystic's own `0.5-same-named-tables-in-two-schemas-share-storage` re-keys
@@ -294,6 +294,12 @@ export interface StrandFirstSyncGateDeps {
294
294
  * Holds one launch's `StrandDatabase` until {@link strandFirstSyncComplete}. Probes are
295
295
  * sequential (the next is scheduled only after the previous read settles), so a slow
296
296
  * network read never stacks probes.
297
+ *
298
+ * NOTE: a probe's reads dial the founder, through the relay when that is the only route, even
299
+ * while the founder is unreachable: one run of `joiner-first-commit-over-slow-relay` logged 17
300
+ * `hop connect denied` at the relay in about 10 s, each a few frames on the joiner's relay
301
+ * connection. If that connection ever shows saturated while no strand member is connected, hold
302
+ * the probe while the strand member redial (`strand-peer-redial.ts`) reports none connected.
297
303
  */
298
304
  export class StrandFirstSyncGate {
299
305
  private readonly scheduler: TimeoutScheduler;