@serfab/cadre-core 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cadre-node.d.ts +38 -15
- package/dist/cadre-node.js +77 -29
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-database.d.ts +24 -10
- package/dist/control-database.js +48 -16
- package/dist/control-database.js.map +1 -1
- package/dist/control-read-retry.d.ts +12 -0
- package/dist/control-read-retry.js +12 -0
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-write-retry.d.ts +5 -4
- package/dist/control-write-retry.js +24 -7
- package/dist/control-write-retry.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/link-budget.d.ts +182 -0
- package/dist/link-budget.js +195 -0
- package/dist/link-budget.js.map +1 -0
- package/dist/peer-dial.d.ts +36 -17
- package/dist/peer-dial.js +37 -17
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +58 -4
- package/dist/peer-join-backfill.js +165 -20
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/relay-reservation.d.ts +59 -11
- package/dist/relay-reservation.js +169 -43
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.js +2 -1
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +43 -5
- package/dist/strand-first-sync-gate.js +55 -7
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +27 -2
- package/dist/strand-instance-manager.js +56 -13
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +200 -45
- package/dist/strand-membership-reconciler.js +302 -86
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +3 -2
- package/dist/strand-membership-writer.js +67 -90
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-config.d.ts +2 -2
- package/dist/strand-network-config.js +2 -2
- package/dist/types.d.ts +96 -9
- package/dist/types.js +14 -4
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/cadre-node.ts +7445 -7397
- package/src/control-database.ts +48 -16
- package/src/control-read-retry.ts +214 -202
- package/src/control-write-retry.ts +24 -7
- package/src/index.ts +581 -563
- package/src/link-budget.ts +217 -0
- package/src/peer-dial.ts +39 -17
- package/src/peer-join-backfill.ts +192 -23
- package/src/relay-reservation.ts +1119 -951
- package/src/seed-bootstrap.ts +2 -1
- package/src/strand-first-sync-gate.ts +55 -7
- package/src/strand-instance-manager.ts +1516 -1458
- package/src/strand-membership-reconciler.ts +334 -92
- package/src/strand-membership-writer.ts +1962 -1987
- package/src/strand-network-config.ts +2 -2
- package/src/types.ts +95 -8
package/src/seed-bootstrap.ts
CHANGED
|
@@ -811,7 +811,8 @@ export class SeedBootstrapService {
|
|
|
811
811
|
// (`dialPeerAddrs`), so an owner whose first address never answers neither
|
|
812
812
|
// stalls seed application nor goes undialed at its other addresses.
|
|
813
813
|
// NOTE: `handleSeedStream` acks only after this loop, so an unreachable owner
|
|
814
|
-
// can hold the ack for up to `dialBudget.totalMs`
|
|
814
|
+
// can hold the ack for up to `dialBudget.totalMs` each — 32 s at the default declared link
|
|
815
|
+
// round trip, more on a host that declared a slower one (`link-budget.ts`) — past a sender's
|
|
815
816
|
// 10 s `seedDeliverTimeoutMs`; the seed is still applied. If senders start
|
|
816
817
|
// reporting accepted seeds as timed out, ack before dialing.
|
|
817
818
|
const selfPeerId = this.libp2pNode.peerId?.toString();
|
|
@@ -64,12 +64,42 @@ export const DEFAULT_STRAND_FIRST_SYNC_POLL_MS = 500;
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* How long `CadreNode.addStrand` waits for a joining machine's first sync before rejecting
|
|
67
|
-
* with {@link StrandAwaitingFirstSyncError}.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
67
|
+
* with {@link StrandAwaitingFirstSyncError}.
|
|
68
|
+
*
|
|
69
|
+
* Measured over a DIRECT connection (2026-09-16): the joiner reads the founder's rows about
|
|
70
|
+
* 1.3 s after `addStrand` would previously have resolved. That is still the fast case, and it
|
|
71
|
+
* is not what this budget has to cover.
|
|
72
|
+
*
|
|
73
|
+
* Measured over a RELAYED SLOW LINK (2026-09-26): one Windows developer machine, two
|
|
74
|
+
* relay-only `CadreNode`s (`listenAddrs: []`) on a shared loopback dedicated relay, with a
|
|
75
|
+
* 900 ms one-way per-frame outbound delay applied to every websocket by
|
|
76
|
+
* `integration-tests/src/harness/ws-latency.ts` in `pipelined` mode — frames stay overlapped
|
|
77
|
+
* in flight, so the figure is latency and bandwidth stays unlimited; the harness's other mode
|
|
78
|
+
* (`serial`) is a per-socket frame-rate cap and its delays are NOT comparable
|
|
79
|
+
* (`docs/testing.md` → "Link latency"). A round trip therefore costs about
|
|
80
|
+
* 1.8 s. Time from `addStrand` to writable, for a machine holding nothing of the strand yet:
|
|
81
|
+
* 23, 27, 31 and 41 s over four runs at optimystic's 1000 ms cohort read deadline, and 35, 42
|
|
82
|
+
* and 46 s over three runs at a 5000 ms one. **The second band is the one in force**: sereus
|
|
83
|
+
* declares 5000 ms (`COHORT_READ_DEADLINE_MS`, `quereus-plugin-sereus/src/cluster-size.ts`),
|
|
84
|
+
* because 1000 ms is shorter than one round trip on that link and leaves every cohort read with
|
|
85
|
+
* no answer to corroborate against. Widening that deadline makes a consult against a peer that
|
|
86
|
+
* cannot answer cost longer, and this phase runs several of those — which is why the two bands
|
|
87
|
+
* are both recorded here, and why a change to either number has to be weighed against the
|
|
88
|
+
* other. 120 s clears the worst sample of the band in force by about 2.6x. The previous 30 s
|
|
89
|
+
* default sat INSIDE the first band, so roughly half of those joins were rejected as "not
|
|
90
|
+
* writable yet" while the sync was progressing normally and went on to complete.
|
|
91
|
+
*
|
|
92
|
+
* NOTE: accepted tradeoff — what 120 s costs. This wait is what an app's `addStrand` sits in
|
|
93
|
+
* before it is told "not yet", so a strand none of whose members is reachable at all takes two
|
|
94
|
+
* minutes to report instead of thirty seconds. The slow report was weighed against refusing
|
|
95
|
+
* joins that were working, and the refusal is the worse failure. The cost is bounded — the
|
|
96
|
+
* rejection is retryable, the strand stays launched and keeps probing, and `strand:writable`
|
|
97
|
+
* fires the moment the sync lands, so an app that listens for the event rather than awaiting
|
|
98
|
+
* the call is unaffected either way. Revisit if the gate ever learns whether ANY other member
|
|
99
|
+
* is connected: "no peer at all" could then be reported at once and this budget would only
|
|
100
|
+
* ever be spent on a sync that is actually in progress.
|
|
71
101
|
*/
|
|
72
|
-
export const DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS =
|
|
102
|
+
export const DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS = 120_000;
|
|
73
103
|
|
|
74
104
|
/** Embedder-facing tuning for the gate, threaded from `CadreNodeConfig.strandFirstSync`. */
|
|
75
105
|
export interface StrandFirstSyncConfig {
|
|
@@ -242,8 +272,7 @@ export class StrandFirstSyncGate {
|
|
|
242
272
|
private async probe(): Promise<void> {
|
|
243
273
|
if (this.stopped || this.opened || this.probing) return;
|
|
244
274
|
this.probing = true;
|
|
245
|
-
const held = await
|
|
246
|
-
.finally(() => { this.probing = false; });
|
|
275
|
+
const held = await this.probeHeld();
|
|
247
276
|
// Re-check after the await: a release or a force-open may have landed mid-read.
|
|
248
277
|
if (this.stopped || this.opened) return;
|
|
249
278
|
if (!held) {
|
|
@@ -255,4 +284,23 @@ export class StrandFirstSyncGate {
|
|
|
255
284
|
log('[%s] first-sync gate opened: Strand.Header and every App table read from a peer', this.deps.label);
|
|
256
285
|
this.deps.onHeaderHeld();
|
|
257
286
|
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* One probe's answer, with a throw from OUTSIDE {@link strandFirstSyncComplete}'s own read
|
|
290
|
+
* guards — the `App` schema lookup, or `getDatabase()` itself — reported as "not yet" like
|
|
291
|
+
* any failing read. The loop is scheduled from `void this.probe()`, so an escaping rejection
|
|
292
|
+
* would be unhandled AND leave nothing scheduled: the strand would stay gated for the whole
|
|
293
|
+
* `timeoutMs` and never recover, which is the one failure this gate must not have.
|
|
294
|
+
*/
|
|
295
|
+
private async probeHeld(): Promise<boolean> {
|
|
296
|
+
try {
|
|
297
|
+
return await strandFirstSyncComplete(this.deps.database.getDatabase(), this.deps.label);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
log('[%s] first-sync probe threw (treated as not yet synced): %s', this.deps.label,
|
|
300
|
+
error instanceof Error ? error.message : String(error));
|
|
301
|
+
return false;
|
|
302
|
+
} finally {
|
|
303
|
+
this.probing = false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
258
306
|
}
|