instar 1.3.524 → 1.3.526
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/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +77 -5
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +23 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/ConflictStore.d.ts +150 -0
- package/dist/core/ConflictStore.d.ts.map +1 -0
- package/dist/core/ConflictStore.js +247 -0
- package/dist/core/ConflictStore.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +116 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/ReplicatedStoreReader.d.ts +83 -0
- package/dist/core/ReplicatedStoreReader.d.ts.map +1 -0
- package/dist/core/ReplicatedStoreReader.js +100 -0
- package/dist/core/ReplicatedStoreReader.js.map +1 -0
- package/dist/core/ReplicationBudget.d.ts +183 -0
- package/dist/core/ReplicationBudget.d.ts.map +1 -0
- package/dist/core/ReplicationBudget.js +253 -0
- package/dist/core/ReplicationBudget.js.map +1 -0
- package/dist/core/RollbackUnmerge.d.ts +134 -0
- package/dist/core/RollbackUnmerge.d.ts.map +1 -0
- package/dist/core/RollbackUnmerge.js +286 -0
- package/dist/core/RollbackUnmerge.js.map +1 -0
- package/dist/core/UnionReader.d.ts +161 -0
- package/dist/core/UnionReader.d.ts.map +1 -0
- package/dist/core/UnionReader.js +0 -0
- package/dist/core/UnionReader.js.map +1 -0
- package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts +56 -2
- package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts.map +1 -1
- package/dist/monitoring/ActiveWorkSilenceSentinel.js +174 -10
- package/dist/monitoring/ActiveWorkSilenceSentinel.js.map +1 -1
- package/dist/monitoring/PromiseBeacon.d.ts +48 -0
- package/dist/monitoring/PromiseBeacon.d.ts.map +1 -1
- package/dist/monitoring/PromiseBeacon.js +186 -41
- package/dist/monitoring/PromiseBeacon.js.map +1 -1
- package/dist/monitoring/SubagentTracker.d.ts +9 -0
- package/dist/monitoring/SubagentTracker.d.ts.map +1 -1
- package/dist/monitoring/SubagentTracker.js +11 -0
- package/dist/monitoring/SubagentTracker.js.map +1 -1
- package/dist/monitoring/sentinelWiring.d.ts +20 -1
- package/dist/monitoring/sentinelWiring.d.ts.map +1 -1
- package/dist/monitoring/sentinelWiring.js +26 -0
- package/dist/monitoring/sentinelWiring.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +13 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +7 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +3 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +10 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +67 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +13 -0
- package/upgrades/1.3.525.md +32 -0
- package/upgrades/1.3.526.md +83 -0
- package/upgrades/side-effects/hlc-step4-5-union-rollback-bounds.md +148 -0
- package/upgrades/side-effects/honest-progress-messaging.md +150 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReplicatedStoreReader — the LOWEST store-access primitive, the single funnel
|
|
3
|
+
* every replicated-store read routes through so no caller can bypass the
|
|
4
|
+
* no-clobber union rule (WS2 replicated-store foundation, Component 6 / §7.2).
|
|
5
|
+
*
|
|
6
|
+
* Spec: docs/specs/multi-machine-replicated-store-foundation.md §7.1 (namespaced
|
|
7
|
+
* per-origin storage), §7.2 (the union of local + replicated state computed at the
|
|
8
|
+
* LOWEST primitive — enforced by a wiring-integrity test, §12 #11), §7.4 (a
|
|
9
|
+
* dropped origin is excluded from the union LIVE), §11 (machine-local-by-design:
|
|
10
|
+
* computed from the local own + local replica namespaces, no cross-machine call
|
|
11
|
+
* on the read path).
|
|
12
|
+
*
|
|
13
|
+
* THE BYPASS-PROOF FUNNEL. A store's materialized replicated state is the UNION
|
|
14
|
+
* over every participating origin namespace: the own stream + each non-dropped
|
|
15
|
+
* peer replica. This reader is the ONE place that composes that union (via
|
|
16
|
+
* UnionReader.readUnion) — a consumer store reads THROUGH it, never around it. The
|
|
17
|
+
* wiring-integrity test asserts the reader's seams are dependency-injected and not
|
|
18
|
+
* null / not no-ops.
|
|
19
|
+
*
|
|
20
|
+
* PURE-ish: all I/O is the injected `loadOriginRecords` seam (the real wiring reads
|
|
21
|
+
* the CoherenceJournal own stream + the peers/ replica streams for the store's
|
|
22
|
+
* kind(s)). The reader itself only composes + merges + records conflicts —
|
|
23
|
+
* bounded, no fs, no Date directly.
|
|
24
|
+
*/
|
|
25
|
+
import type { ReplicatedKindRegistry } from './ReplicatedRecordEnvelope.js';
|
|
26
|
+
import { type ImpactTier, type OriginRecord, type UnionResult } from './UnionReader.js';
|
|
27
|
+
import type { ConflictStore } from './ConflictStore.js';
|
|
28
|
+
import type { DroppedOriginRegistry } from './RollbackUnmerge.js';
|
|
29
|
+
import type { StateSyncStores } from './ReplicatedRecordEnvelope.js';
|
|
30
|
+
/** The seams the reader needs (DI'd so the wiring-integrity test can assert they
|
|
31
|
+
* are real, never null/no-op). */
|
|
32
|
+
export interface ReplicatedStoreReaderSeams {
|
|
33
|
+
/** The replicated-kind registry — the reader serves ONLY registered stores. */
|
|
34
|
+
registry: ReplicatedKindRegistry;
|
|
35
|
+
/** The per-store stateSync flags (a store read is a no-op when its store is not
|
|
36
|
+
* enabled — the dark-by-default posture). */
|
|
37
|
+
stores: StateSyncStores | undefined;
|
|
38
|
+
/** The store's impact tier (high/low) — governs concurrent-edit behavior. A
|
|
39
|
+
* store declares its tier at registration; the default is HIGH (the
|
|
40
|
+
* conservative side — append-both-and-flag never silently clobbers). */
|
|
41
|
+
tierOf: (store: string) => ImpactTier;
|
|
42
|
+
/**
|
|
43
|
+
* Load EVERY origin's current record for (store, recordKey) — the own stream +
|
|
44
|
+
* each peer replica namespace, keyed by origin (one record per origin, the
|
|
45
|
+
* latest by HLC). This is the per-origin materialization the union merges. The
|
|
46
|
+
* real wiring reads the CoherenceJournal own stream + the peers/ replica files
|
|
47
|
+
* for the store's kind(s); a single-machine install returns only the own
|
|
48
|
+
* origin (so the union is a strict no-op = that one record).
|
|
49
|
+
*/
|
|
50
|
+
loadOriginRecords: (store: string, recordKey: string) => OriginRecord[];
|
|
51
|
+
/** Enumerate every recordKey the store currently holds across all origins (for
|
|
52
|
+
* readAll). The real wiring scans the own + replica streams. */
|
|
53
|
+
listRecordKeys: (store: string) => string[];
|
|
54
|
+
/** The durable un-merged (store, origin) set — a dropped origin is excluded
|
|
55
|
+
* from the union LIVE (§7.4). */
|
|
56
|
+
droppedOrigins: DroppedOriginRegistry;
|
|
57
|
+
/** The conflict ledger — a HIGH-impact concurrent divergence is recorded here
|
|
58
|
+
* (idempotent on the stable conflictId). */
|
|
59
|
+
conflictStore: ConflictStore;
|
|
60
|
+
}
|
|
61
|
+
export declare class ReplicatedStoreReader {
|
|
62
|
+
private readonly seams;
|
|
63
|
+
constructor(seams: ReplicatedStoreReaderSeams);
|
|
64
|
+
/** Is this store registered + enabled (so a read is non-dark)? A read on an
|
|
65
|
+
* unregistered or disabled store returns a strict no-op (no record). */
|
|
66
|
+
private isLive;
|
|
67
|
+
/**
|
|
68
|
+
* Read one recordKey through the union (§7.2). Returns the no-clobber union
|
|
69
|
+
* result: the resolved value, a conflict descriptor (HIGH-impact concurrent),
|
|
70
|
+
* or a divergence flag (LOW-impact concurrent). A HIGH-impact conflict is
|
|
71
|
+
* RECORDED in the conflict ledger (idempotent) as a side-effect so it is
|
|
72
|
+
* surfaced for operator resolution — the read NEVER clobbers a divergent record.
|
|
73
|
+
*
|
|
74
|
+
* Live-recompute (§7.4): the dropped-origin set is consulted HERE, so a
|
|
75
|
+
* rolled-back origin is excluded from the union instantly with no rewrite.
|
|
76
|
+
*/
|
|
77
|
+
read(store: string, recordKey: string): UnionResult;
|
|
78
|
+
/** Read every recordKey the store holds, as a recordKey → UnionResult map (§7.2).
|
|
79
|
+
* Conflicts are recorded as a side-effect (same as read). Bounded by the live
|
|
80
|
+
* key count. */
|
|
81
|
+
readAll(store: string): Map<string, UnionResult>;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=ReplicatedStoreReader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReplicatedStoreReader.d.ts","sourceRoot":"","sources":["../../src/core/ReplicatedStoreReader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAErE;mCACmC;AACnC,MAAM,WAAW,0BAA0B;IACzC,+EAA+E;IAC/E,QAAQ,EAAE,sBAAsB,CAAC;IACjC;kDAC8C;IAC9C,MAAM,EAAE,eAAe,GAAG,SAAS,CAAC;IACpC;;6EAEyE;IACzE,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,UAAU,CAAC;IACtC;;;;;;;OAOG;IACH,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,YAAY,EAAE,CAAC;IACxE;qEACiE;IACjE,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAC5C;sCACkC;IAClC,cAAc,EAAE,qBAAqB,CAAC;IACtC;iDAC6C;IAC7C,aAAa,EAAE,aAAa,CAAC;CAC9B;AASD,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;gBAEvC,KAAK,EAAE,0BAA0B;IAY7C;6EACyE;IACzE,OAAO,CAAC,MAAM;IAKd;;;;;;;;;OASG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW;IAkBnD;;qBAEiB;IACjB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC;CAQjD"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReplicatedStoreReader — the LOWEST store-access primitive, the single funnel
|
|
3
|
+
* every replicated-store read routes through so no caller can bypass the
|
|
4
|
+
* no-clobber union rule (WS2 replicated-store foundation, Component 6 / §7.2).
|
|
5
|
+
*
|
|
6
|
+
* Spec: docs/specs/multi-machine-replicated-store-foundation.md §7.1 (namespaced
|
|
7
|
+
* per-origin storage), §7.2 (the union of local + replicated state computed at the
|
|
8
|
+
* LOWEST primitive — enforced by a wiring-integrity test, §12 #11), §7.4 (a
|
|
9
|
+
* dropped origin is excluded from the union LIVE), §11 (machine-local-by-design:
|
|
10
|
+
* computed from the local own + local replica namespaces, no cross-machine call
|
|
11
|
+
* on the read path).
|
|
12
|
+
*
|
|
13
|
+
* THE BYPASS-PROOF FUNNEL. A store's materialized replicated state is the UNION
|
|
14
|
+
* over every participating origin namespace: the own stream + each non-dropped
|
|
15
|
+
* peer replica. This reader is the ONE place that composes that union (via
|
|
16
|
+
* UnionReader.readUnion) — a consumer store reads THROUGH it, never around it. The
|
|
17
|
+
* wiring-integrity test asserts the reader's seams are dependency-injected and not
|
|
18
|
+
* null / not no-ops.
|
|
19
|
+
*
|
|
20
|
+
* PURE-ish: all I/O is the injected `loadOriginRecords` seam (the real wiring reads
|
|
21
|
+
* the CoherenceJournal own stream + the peers/ replica streams for the store's
|
|
22
|
+
* kind(s)). The reader itself only composes + merges + records conflicts —
|
|
23
|
+
* bounded, no fs, no Date directly.
|
|
24
|
+
*/
|
|
25
|
+
import { readUnion, } from './UnionReader.js';
|
|
26
|
+
// RULE 3: EXEMPT — this is NOT a state-detector. ReplicatedStoreReader is a
|
|
27
|
+
// pure store-access funnel that composes the no-clobber union over already-loaded
|
|
28
|
+
// per-origin records (via the injected loadOriginRecords seam). It does not parse
|
|
29
|
+
// provider/CLI output, does not detect external/environment state, and has no
|
|
30
|
+
// signature-matching that could drift across providers — it merely merges
|
|
31
|
+
// in-memory records by HLC + the witness rule. The "*Reader" name matches the
|
|
32
|
+
// Rule-3 pattern heuristic but the substance is a deterministic in-memory merge.
|
|
33
|
+
export class ReplicatedStoreReader {
|
|
34
|
+
seams;
|
|
35
|
+
constructor(seams) {
|
|
36
|
+
// Wiring-integrity preconditions (§12 #11): the seams MUST be real, not null.
|
|
37
|
+
if (!seams)
|
|
38
|
+
throw new Error('ReplicatedStoreReader: seams are required');
|
|
39
|
+
if (!seams.registry)
|
|
40
|
+
throw new Error('ReplicatedStoreReader: registry seam is required (not null)');
|
|
41
|
+
if (typeof seams.loadOriginRecords !== 'function')
|
|
42
|
+
throw new Error('ReplicatedStoreReader: loadOriginRecords seam must be a function (not a no-op)');
|
|
43
|
+
if (typeof seams.listRecordKeys !== 'function')
|
|
44
|
+
throw new Error('ReplicatedStoreReader: listRecordKeys seam must be a function (not a no-op)');
|
|
45
|
+
if (!seams.droppedOrigins)
|
|
46
|
+
throw new Error('ReplicatedStoreReader: droppedOrigins seam is required (not null)');
|
|
47
|
+
if (!seams.conflictStore)
|
|
48
|
+
throw new Error('ReplicatedStoreReader: conflictStore seam is required (not null)');
|
|
49
|
+
if (typeof seams.tierOf !== 'function')
|
|
50
|
+
throw new Error('ReplicatedStoreReader: tierOf seam must be a function');
|
|
51
|
+
this.seams = seams;
|
|
52
|
+
}
|
|
53
|
+
/** Is this store registered + enabled (so a read is non-dark)? A read on an
|
|
54
|
+
* unregistered or disabled store returns a strict no-op (no record). */
|
|
55
|
+
isLive(store) {
|
|
56
|
+
if (!this.seams.registry.getByStore(store))
|
|
57
|
+
return false;
|
|
58
|
+
return this.seams.stores?.[store]?.enabled === true;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read one recordKey through the union (§7.2). Returns the no-clobber union
|
|
62
|
+
* result: the resolved value, a conflict descriptor (HIGH-impact concurrent),
|
|
63
|
+
* or a divergence flag (LOW-impact concurrent). A HIGH-impact conflict is
|
|
64
|
+
* RECORDED in the conflict ledger (idempotent) as a side-effect so it is
|
|
65
|
+
* surfaced for operator resolution — the read NEVER clobbers a divergent record.
|
|
66
|
+
*
|
|
67
|
+
* Live-recompute (§7.4): the dropped-origin set is consulted HERE, so a
|
|
68
|
+
* rolled-back origin is excluded from the union instantly with no rewrite.
|
|
69
|
+
*/
|
|
70
|
+
read(store, recordKey) {
|
|
71
|
+
if (!this.isLive(store)) {
|
|
72
|
+
return { recordKey, value: null, conflict: null, divergenceFlag: false };
|
|
73
|
+
}
|
|
74
|
+
const dropped = this.seams.droppedOrigins.droppedOrigins(store);
|
|
75
|
+
const all = this.seams.loadOriginRecords(store, recordKey);
|
|
76
|
+
// §7.4 LIVE exclusion: drop any rolled-back origin from the participating set.
|
|
77
|
+
const participating = dropped.size === 0 ? all : all.filter((r) => !dropped.has(r.origin));
|
|
78
|
+
const result = readUnion(recordKey, participating, this.seams.tierOf(store));
|
|
79
|
+
if (result.conflict) {
|
|
80
|
+
// Record the HIGH-impact conflict (idempotent on its stable id; raises ONE
|
|
81
|
+
// deduped attention item). The read returns value=null (neither version
|
|
82
|
+
// clobbers) — the operator resolves it.
|
|
83
|
+
this.seams.conflictStore.recordConflict(store, result.conflict);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
/** Read every recordKey the store holds, as a recordKey → UnionResult map (§7.2).
|
|
88
|
+
* Conflicts are recorded as a side-effect (same as read). Bounded by the live
|
|
89
|
+
* key count. */
|
|
90
|
+
readAll(store) {
|
|
91
|
+
const out = new Map();
|
|
92
|
+
if (!this.isLive(store))
|
|
93
|
+
return out;
|
|
94
|
+
for (const recordKey of this.seams.listRecordKeys(store)) {
|
|
95
|
+
out.set(recordKey, this.read(store, recordKey));
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=ReplicatedStoreReader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReplicatedStoreReader.js","sourceRoot":"","sources":["../../src/core/ReplicatedStoreReader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReplicationBudget — Component 7 bounds (WS2 replicated-store foundation, §8 +
|
|
3
|
+
* §8.1 Phase C).
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/multi-machine-replicated-store-foundation.md §8 (per-store /
|
|
6
|
+
* per-record-class bounds, replication rate cap with COALESCING, the AGGREGATE
|
|
7
|
+
* journal budget that caps TOTAL bytes/sec across ALL kinds with proportional
|
|
8
|
+
* fair-share throttle, the sustained-failure breaker, the dark-peer accumulation
|
|
9
|
+
* bound + tombstone horizon forced full-snapshot re-join), §8.1 (Phase C: budgets
|
|
10
|
+
* = per-peer allowance × live online-peer count, hard pool-wide ceiling,
|
|
11
|
+
* hysteresis on the multiplier — NOT a 2-machine constant).
|
|
12
|
+
*
|
|
13
|
+
* PURE-ish: the clock is injected so the token buckets + hysteresis are
|
|
14
|
+
* unit-testable across simulated windows. No fs, no Date directly, no network.
|
|
15
|
+
*
|
|
16
|
+
* The ANTI-STARVATION invariant (§8): a flood on kind A must NOT consume kind B's
|
|
17
|
+
* replication share. The aggregate budget caps total bytes/sec; when pressured,
|
|
18
|
+
* per-kind caps throttle PROPORTIONALLY (fair-share) and the throttle is SURFACED
|
|
19
|
+
* in degradation (never a silent stall). A test enforces the budget ACROSS kinds.
|
|
20
|
+
*/
|
|
21
|
+
import { type HlcTimestamp } from './HybridLogicalClock.js';
|
|
22
|
+
import type { KindRetention, RateCapConfig } from './CoherenceJournal.js';
|
|
23
|
+
/**
|
|
24
|
+
* The per-replicated-kind bounds a concrete store (WS2.1+) ships when it
|
|
25
|
+
* registers its kind (§8): a `retention` window (maxFileBytes + rotateKeep) AND a
|
|
26
|
+
* token-bucket rate cap. The foundation provides the SHAPE + conservative
|
|
27
|
+
* defaults; a store overrides per kind. These extend the journal's existing
|
|
28
|
+
* DEFAULT_RETENTION / DEFAULT_RATE_CAP machinery — same types, applied to the
|
|
29
|
+
* replicated kind's own stream.
|
|
30
|
+
*/
|
|
31
|
+
export interface ReplicatedKindBounds {
|
|
32
|
+
retention: KindRetention;
|
|
33
|
+
rateCap: RateCapConfig;
|
|
34
|
+
}
|
|
35
|
+
/** Conservative default bounds for a replicated kind that does not override them.
|
|
36
|
+
* A high-write store (preferences) should ship a tighter window; this is the
|
|
37
|
+
* bounded fallback so a kind is NEVER unbounded. */
|
|
38
|
+
export declare const DEFAULT_REPLICATED_KIND_BOUNDS: ReplicatedKindBounds;
|
|
39
|
+
/** One coalesced pending replication: the LATEST record for a recordKey within
|
|
40
|
+
* the current interval (a burst on one key collapses to one record). */
|
|
41
|
+
export interface CoalescedRecord {
|
|
42
|
+
recordKey: string;
|
|
43
|
+
/** The opaque serialized record line (the journal entry to replicate). */
|
|
44
|
+
line: string;
|
|
45
|
+
/** Bytes the line would consume (for the budget accounting). */
|
|
46
|
+
bytes: number;
|
|
47
|
+
/** The HLC of the latest record (for last-wins coalescing within the interval). */
|
|
48
|
+
hlc: HlcTimestamp;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* CoalescingReplicator — collapses a burst of edits to one recordKey into ONE
|
|
52
|
+
* replicated record per interval (§8 coalescing). `stage(recordKey, line, bytes,
|
|
53
|
+
* hlc)` keeps only the HLC-LATEST per key; `drain()` emits the coalesced set and
|
|
54
|
+
* clears the buffer for the next interval. Per replicated kind (one instance per
|
|
55
|
+
* kind), so a chatty kind cannot starve another — the aggregate budget below is
|
|
56
|
+
* the cross-kind guard.
|
|
57
|
+
*/
|
|
58
|
+
export declare class CoalescingReplicator {
|
|
59
|
+
private pending;
|
|
60
|
+
/** Stage a write; keeps only the HLC-latest per recordKey within the interval. */
|
|
61
|
+
stage(recordKey: string, line: string, bytes: number, hlc: HlcTimestamp): void;
|
|
62
|
+
/** The number of distinct keys currently coalesced (for observability). */
|
|
63
|
+
get pendingCount(): number;
|
|
64
|
+
/** The total bytes the coalesced set would replicate this interval. */
|
|
65
|
+
pendingBytes(): number;
|
|
66
|
+
/** Emit + clear the coalesced set (the LATEST per key). */
|
|
67
|
+
drain(): CoalescedRecord[];
|
|
68
|
+
}
|
|
69
|
+
/** Phase-C budget config (§8.1). */
|
|
70
|
+
export interface PhaseCBudgetConfig {
|
|
71
|
+
/** Per-peer byte allowance (the unit scaled by online-peer count). */
|
|
72
|
+
perPeerBytes: number;
|
|
73
|
+
/** Hard pool-wide ceiling — the scaled budget is clamped here (protects a single
|
|
74
|
+
* machine's disk/CPU regardless of pool size). */
|
|
75
|
+
hardCeilingBytes: number;
|
|
76
|
+
/** Hysteresis: the effective online-peer MULTIPLIER only RISES after the higher
|
|
77
|
+
* peer count has been observed for `hysteresisRiseMs`; it falls immediately
|
|
78
|
+
* (shrinking the budget on a real peer loss is the safe direction). A transient
|
|
79
|
+
* peer-count SPIKE does not instantly widen the budget. */
|
|
80
|
+
hysteresisRiseMs: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Compute the Phase-C aggregate budget (§8.1): `min(perPeerBytes ×
|
|
84
|
+
* effectivePeerCount, hardCeilingBytes)`. NOT a 2-machine constant — a 5-machine
|
|
85
|
+
* pool gets a proportionally larger budget, bounded by the hard ceiling.
|
|
86
|
+
*
|
|
87
|
+
* The `effectivePeerCount` is the HYSTERESIS-damped online count (computed by
|
|
88
|
+
* PhaseCBudgetController below); this pure helper just applies the formula so it
|
|
89
|
+
* is independently testable.
|
|
90
|
+
*/
|
|
91
|
+
export declare function phaseCBudget(perPeerBytes: number, effectivePeerCount: number, hardCeilingBytes: number): number;
|
|
92
|
+
/**
|
|
93
|
+
* PhaseCBudgetController — tracks the live online-peer count with hysteresis on
|
|
94
|
+
* the multiplier (§8.1). A RISE in peer count only takes effect after it has held
|
|
95
|
+
* for `hysteresisRiseMs` (a transient spike does not widen the budget); a FALL
|
|
96
|
+
* takes effect immediately (shrink-on-loss is safe). `currentBudget()` returns the
|
|
97
|
+
* live aggregate byte budget.
|
|
98
|
+
*/
|
|
99
|
+
export declare class PhaseCBudgetController {
|
|
100
|
+
private readonly cfg;
|
|
101
|
+
private readonly now;
|
|
102
|
+
private effectivePeerCount;
|
|
103
|
+
/** Seeded on the first observation — there is no prior narrower budget to
|
|
104
|
+
* protect at bootstrap, so the first count takes effect immediately; only a
|
|
105
|
+
* SUBSEQUENT rise is hysteresis-gated. */
|
|
106
|
+
private seeded;
|
|
107
|
+
/** A pending higher count + when it was first observed (for the rise hysteresis). */
|
|
108
|
+
private pendingRise;
|
|
109
|
+
constructor(cfg: PhaseCBudgetConfig, now: () => number);
|
|
110
|
+
/** Feed the live online-peer count (from the capacity heartbeat). Applies the
|
|
111
|
+
* rise-hysteresis / immediate-fall policy and returns the effective count. */
|
|
112
|
+
observePeerCount(online: number): number;
|
|
113
|
+
/** The current effective (hysteresis-damped) online-peer count. */
|
|
114
|
+
getEffectivePeerCount(): number;
|
|
115
|
+
/** The current aggregate byte budget (§8.1). */
|
|
116
|
+
currentBudget(): number;
|
|
117
|
+
}
|
|
118
|
+
/** A per-kind throttle verdict (§8). When the aggregate budget is pressured, each
|
|
119
|
+
* kind's allowance is its FAIR SHARE of the budget; an over-share request is
|
|
120
|
+
* throttled (deferred to the next interval) and the throttle is surfaced. */
|
|
121
|
+
export interface AggregateThrottleResult {
|
|
122
|
+
/** The bytes admitted this interval for the kind. */
|
|
123
|
+
admittedBytes: number;
|
|
124
|
+
/** The bytes deferred (throttled) — coalesced to the next interval, never dropped. */
|
|
125
|
+
throttledBytes: number;
|
|
126
|
+
/** True iff this kind was throttled (surfaced in degradation; never silent). */
|
|
127
|
+
throttled: boolean;
|
|
128
|
+
}
|
|
129
|
+
/** Degradation surface for the aggregate budget (§8 — never a silent stall). */
|
|
130
|
+
export interface AggregateBudgetDegradation {
|
|
131
|
+
/** Intervals in which at least one kind was throttled. */
|
|
132
|
+
throttledIntervals: number;
|
|
133
|
+
/** Total bytes deferred across all kinds (cumulative). */
|
|
134
|
+
totalThrottledBytes: number;
|
|
135
|
+
/** Per-kind cumulative throttled bytes (so a starving kind is visible). */
|
|
136
|
+
perKindThrottledBytes: Record<string, number>;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* AggregateJournalBudget — the cross-kind anti-starvation guard (§8). Each
|
|
140
|
+
* interval, the controller admits each kind's pending bytes up to its FAIR SHARE
|
|
141
|
+
* of the live aggregate budget; an over-share request is throttled (the surplus
|
|
142
|
+
* defers to the next interval, never dropped — coalescing already collapsed it to
|
|
143
|
+
* the latest state). The fair share is `budget / activeKindCount`, with UNUSED
|
|
144
|
+
* share from quiet kinds redistributed to the others (so a single chatty kind on
|
|
145
|
+
* an otherwise-idle pool still gets the whole budget — fair, not wasteful), but
|
|
146
|
+
* NEVER beyond a kind's own pending demand and never letting one kind exceed the
|
|
147
|
+
* aggregate while another has unserved demand.
|
|
148
|
+
*
|
|
149
|
+
* The §12 #9/#15 invariant: a flood on kind A is capped at its fair share when
|
|
150
|
+
* kind B also has demand — B keeps its share; A's surplus is deferred + surfaced.
|
|
151
|
+
*/
|
|
152
|
+
export declare class AggregateJournalBudget {
|
|
153
|
+
private readonly degradation;
|
|
154
|
+
/**
|
|
155
|
+
* Allocate the live aggregate `budgetBytes` across the kinds' pending demands
|
|
156
|
+
* for ONE interval. Returns per-kind throttle verdicts. Fair-share with
|
|
157
|
+
* redistribution of unused share, bounded by each kind's demand and the
|
|
158
|
+
* aggregate.
|
|
159
|
+
*/
|
|
160
|
+
allocate(budgetBytes: number, demand: Record<string, number>): Record<string, AggregateThrottleResult>;
|
|
161
|
+
/** The cumulative degradation surface (§8 — surfaced, never a silent stall). */
|
|
162
|
+
getDegradation(): AggregateBudgetDegradation;
|
|
163
|
+
}
|
|
164
|
+
/** The re-join verdict for a recovering dark peer (§8). */
|
|
165
|
+
export type RejoinVerdict = {
|
|
166
|
+
mode: 'tail';
|
|
167
|
+
fromSeq: number;
|
|
168
|
+
} | {
|
|
169
|
+
mode: 'full-snapshot';
|
|
170
|
+
reason: 'beyond-tombstone-horizon' | 'below-oldest-retained';
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Decide how a recovering peer re-joins a kind's stream (§8 dark-peer accumulation
|
|
174
|
+
* bound + tombstone horizon). If the peer's last-held seq is below the holder's
|
|
175
|
+
* `oldestRetainedSeq` (the tombstone for a delete may have rotated out), a stale
|
|
176
|
+
* tail would risk delete-resurrection — so the peer is FORCED to a full
|
|
177
|
+
* single-origin snapshot re-join (§6.5 delete-resurrection guard). Otherwise a
|
|
178
|
+
* normal seq tail from `lastHeldSeq` is safe.
|
|
179
|
+
*
|
|
180
|
+
* PURE: a function of the seqs — independently testable (§12 #13).
|
|
181
|
+
*/
|
|
182
|
+
export declare function rejoinVerdict(lastHeldSeq: number, oldestRetainedSeq: number): RejoinVerdict;
|
|
183
|
+
//# sourceMappingURL=ReplicationBudget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReplicationBudget.d.ts","sourceRoot":"","sources":["../../src/core/ReplicationBudget.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReplicationBudget — Component 7 bounds (WS2 replicated-store foundation, §8 +
|
|
3
|
+
* §8.1 Phase C).
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/multi-machine-replicated-store-foundation.md §8 (per-store /
|
|
6
|
+
* per-record-class bounds, replication rate cap with COALESCING, the AGGREGATE
|
|
7
|
+
* journal budget that caps TOTAL bytes/sec across ALL kinds with proportional
|
|
8
|
+
* fair-share throttle, the sustained-failure breaker, the dark-peer accumulation
|
|
9
|
+
* bound + tombstone horizon forced full-snapshot re-join), §8.1 (Phase C: budgets
|
|
10
|
+
* = per-peer allowance × live online-peer count, hard pool-wide ceiling,
|
|
11
|
+
* hysteresis on the multiplier — NOT a 2-machine constant).
|
|
12
|
+
*
|
|
13
|
+
* PURE-ish: the clock is injected so the token buckets + hysteresis are
|
|
14
|
+
* unit-testable across simulated windows. No fs, no Date directly, no network.
|
|
15
|
+
*
|
|
16
|
+
* The ANTI-STARVATION invariant (§8): a flood on kind A must NOT consume kind B's
|
|
17
|
+
* replication share. The aggregate budget caps total bytes/sec; when pressured,
|
|
18
|
+
* per-kind caps throttle PROPORTIONALLY (fair-share) and the throttle is SURFACED
|
|
19
|
+
* in degradation (never a silent stall). A test enforces the budget ACROSS kinds.
|
|
20
|
+
*/
|
|
21
|
+
import { HybridLogicalClock } from './HybridLogicalClock.js';
|
|
22
|
+
/** Conservative default bounds for a replicated kind that does not override them.
|
|
23
|
+
* A high-write store (preferences) should ship a tighter window; this is the
|
|
24
|
+
* bounded fallback so a kind is NEVER unbounded. */
|
|
25
|
+
export const DEFAULT_REPLICATED_KIND_BOUNDS = {
|
|
26
|
+
retention: { maxFileBytes: 8 * 1024 * 1024, rotateKeep: 4 },
|
|
27
|
+
rateCap: { capacity: 100, refillPerSec: 50 },
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* CoalescingReplicator — collapses a burst of edits to one recordKey into ONE
|
|
31
|
+
* replicated record per interval (§8 coalescing). `stage(recordKey, line, bytes,
|
|
32
|
+
* hlc)` keeps only the HLC-LATEST per key; `drain()` emits the coalesced set and
|
|
33
|
+
* clears the buffer for the next interval. Per replicated kind (one instance per
|
|
34
|
+
* kind), so a chatty kind cannot starve another — the aggregate budget below is
|
|
35
|
+
* the cross-kind guard.
|
|
36
|
+
*/
|
|
37
|
+
export class CoalescingReplicator {
|
|
38
|
+
pending = new Map();
|
|
39
|
+
/** Stage a write; keeps only the HLC-latest per recordKey within the interval. */
|
|
40
|
+
stage(recordKey, line, bytes, hlc) {
|
|
41
|
+
const prev = this.pending.get(recordKey);
|
|
42
|
+
if (prev && HybridLogicalClock.compare(hlc, prev.hlc) <= 0)
|
|
43
|
+
return; // older or equal — keep the newer.
|
|
44
|
+
this.pending.set(recordKey, { recordKey, line, bytes, hlc });
|
|
45
|
+
}
|
|
46
|
+
/** The number of distinct keys currently coalesced (for observability). */
|
|
47
|
+
get pendingCount() {
|
|
48
|
+
return this.pending.size;
|
|
49
|
+
}
|
|
50
|
+
/** The total bytes the coalesced set would replicate this interval. */
|
|
51
|
+
pendingBytes() {
|
|
52
|
+
let n = 0;
|
|
53
|
+
for (const r of this.pending.values())
|
|
54
|
+
n += r.bytes;
|
|
55
|
+
return n;
|
|
56
|
+
}
|
|
57
|
+
/** Emit + clear the coalesced set (the LATEST per key). */
|
|
58
|
+
drain() {
|
|
59
|
+
const out = [...this.pending.values()];
|
|
60
|
+
this.pending.clear();
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Compute the Phase-C aggregate budget (§8.1): `min(perPeerBytes ×
|
|
66
|
+
* effectivePeerCount, hardCeilingBytes)`. NOT a 2-machine constant — a 5-machine
|
|
67
|
+
* pool gets a proportionally larger budget, bounded by the hard ceiling.
|
|
68
|
+
*
|
|
69
|
+
* The `effectivePeerCount` is the HYSTERESIS-damped online count (computed by
|
|
70
|
+
* PhaseCBudgetController below); this pure helper just applies the formula so it
|
|
71
|
+
* is independently testable.
|
|
72
|
+
*/
|
|
73
|
+
export function phaseCBudget(perPeerBytes, effectivePeerCount, hardCeilingBytes) {
|
|
74
|
+
const scaled = perPeerBytes * Math.max(0, effectivePeerCount);
|
|
75
|
+
return Math.min(scaled, hardCeilingBytes);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* PhaseCBudgetController — tracks the live online-peer count with hysteresis on
|
|
79
|
+
* the multiplier (§8.1). A RISE in peer count only takes effect after it has held
|
|
80
|
+
* for `hysteresisRiseMs` (a transient spike does not widen the budget); a FALL
|
|
81
|
+
* takes effect immediately (shrink-on-loss is safe). `currentBudget()` returns the
|
|
82
|
+
* live aggregate byte budget.
|
|
83
|
+
*/
|
|
84
|
+
export class PhaseCBudgetController {
|
|
85
|
+
cfg;
|
|
86
|
+
now;
|
|
87
|
+
effectivePeerCount = 0;
|
|
88
|
+
/** Seeded on the first observation — there is no prior narrower budget to
|
|
89
|
+
* protect at bootstrap, so the first count takes effect immediately; only a
|
|
90
|
+
* SUBSEQUENT rise is hysteresis-gated. */
|
|
91
|
+
seeded = false;
|
|
92
|
+
/** A pending higher count + when it was first observed (for the rise hysteresis). */
|
|
93
|
+
pendingRise = null;
|
|
94
|
+
constructor(cfg, now) {
|
|
95
|
+
this.cfg = cfg;
|
|
96
|
+
this.now = now;
|
|
97
|
+
}
|
|
98
|
+
/** Feed the live online-peer count (from the capacity heartbeat). Applies the
|
|
99
|
+
* rise-hysteresis / immediate-fall policy and returns the effective count. */
|
|
100
|
+
observePeerCount(online) {
|
|
101
|
+
const t = this.now();
|
|
102
|
+
// First observation seeds the baseline immediately (no prior budget to widen).
|
|
103
|
+
if (!this.seeded) {
|
|
104
|
+
this.seeded = true;
|
|
105
|
+
this.effectivePeerCount = online;
|
|
106
|
+
return this.effectivePeerCount;
|
|
107
|
+
}
|
|
108
|
+
if (online < this.effectivePeerCount) {
|
|
109
|
+
// Immediate fall — shrinking the budget on a real peer loss is safe.
|
|
110
|
+
this.effectivePeerCount = online;
|
|
111
|
+
this.pendingRise = null;
|
|
112
|
+
return this.effectivePeerCount;
|
|
113
|
+
}
|
|
114
|
+
if (online === this.effectivePeerCount) {
|
|
115
|
+
this.pendingRise = null;
|
|
116
|
+
return this.effectivePeerCount;
|
|
117
|
+
}
|
|
118
|
+
// online > effective — gate the rise behind hysteresis.
|
|
119
|
+
if (!this.pendingRise || this.pendingRise.count !== online) {
|
|
120
|
+
this.pendingRise = { count: online, since: t };
|
|
121
|
+
return this.effectivePeerCount;
|
|
122
|
+
}
|
|
123
|
+
if (t - this.pendingRise.since >= this.cfg.hysteresisRiseMs) {
|
|
124
|
+
this.effectivePeerCount = online;
|
|
125
|
+
this.pendingRise = null;
|
|
126
|
+
}
|
|
127
|
+
return this.effectivePeerCount;
|
|
128
|
+
}
|
|
129
|
+
/** The current effective (hysteresis-damped) online-peer count. */
|
|
130
|
+
getEffectivePeerCount() {
|
|
131
|
+
return this.effectivePeerCount;
|
|
132
|
+
}
|
|
133
|
+
/** The current aggregate byte budget (§8.1). */
|
|
134
|
+
currentBudget() {
|
|
135
|
+
return phaseCBudget(this.cfg.perPeerBytes, this.effectivePeerCount, this.cfg.hardCeilingBytes);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* AggregateJournalBudget — the cross-kind anti-starvation guard (§8). Each
|
|
140
|
+
* interval, the controller admits each kind's pending bytes up to its FAIR SHARE
|
|
141
|
+
* of the live aggregate budget; an over-share request is throttled (the surplus
|
|
142
|
+
* defers to the next interval, never dropped — coalescing already collapsed it to
|
|
143
|
+
* the latest state). The fair share is `budget / activeKindCount`, with UNUSED
|
|
144
|
+
* share from quiet kinds redistributed to the others (so a single chatty kind on
|
|
145
|
+
* an otherwise-idle pool still gets the whole budget — fair, not wasteful), but
|
|
146
|
+
* NEVER beyond a kind's own pending demand and never letting one kind exceed the
|
|
147
|
+
* aggregate while another has unserved demand.
|
|
148
|
+
*
|
|
149
|
+
* The §12 #9/#15 invariant: a flood on kind A is capped at its fair share when
|
|
150
|
+
* kind B also has demand — B keeps its share; A's surplus is deferred + surfaced.
|
|
151
|
+
*/
|
|
152
|
+
export class AggregateJournalBudget {
|
|
153
|
+
degradation = {
|
|
154
|
+
throttledIntervals: 0,
|
|
155
|
+
totalThrottledBytes: 0,
|
|
156
|
+
perKindThrottledBytes: {},
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Allocate the live aggregate `budgetBytes` across the kinds' pending demands
|
|
160
|
+
* for ONE interval. Returns per-kind throttle verdicts. Fair-share with
|
|
161
|
+
* redistribution of unused share, bounded by each kind's demand and the
|
|
162
|
+
* aggregate.
|
|
163
|
+
*/
|
|
164
|
+
allocate(budgetBytes, demand) {
|
|
165
|
+
const kinds = Object.keys(demand).filter((k) => demand[k] > 0);
|
|
166
|
+
const out = {};
|
|
167
|
+
for (const k of Object.keys(demand)) {
|
|
168
|
+
out[k] = { admittedBytes: 0, throttledBytes: 0, throttled: false };
|
|
169
|
+
}
|
|
170
|
+
if (kinds.length === 0 || budgetBytes <= 0) {
|
|
171
|
+
// Everything is throttled if there is demand but no budget.
|
|
172
|
+
let anyThrottled = false;
|
|
173
|
+
for (const k of kinds) {
|
|
174
|
+
out[k] = { admittedBytes: 0, throttledBytes: demand[k], throttled: true };
|
|
175
|
+
this.degradation.perKindThrottledBytes[k] = (this.degradation.perKindThrottledBytes[k] ?? 0) + demand[k];
|
|
176
|
+
this.degradation.totalThrottledBytes += demand[k];
|
|
177
|
+
anyThrottled = true;
|
|
178
|
+
}
|
|
179
|
+
if (anyThrottled)
|
|
180
|
+
this.degradation.throttledIntervals++;
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
// Iterative fair-share with redistribution: repeatedly hand each
|
|
184
|
+
// not-yet-satisfied kind an EQUAL slice of the remaining budget, capping at its
|
|
185
|
+
// remaining demand; surplus from a kind that fit under its slice spills to the
|
|
186
|
+
// others. Bounded by kinds.length iterations (each pass satisfies ≥1 kind or
|
|
187
|
+
// exhausts the budget). This guarantees no kind exceeds the aggregate while
|
|
188
|
+
// another has unserved demand (the anti-starvation invariant).
|
|
189
|
+
let remainingBudget = budgetBytes;
|
|
190
|
+
const remainingDemand = new Map(kinds.map((k) => [k, demand[k]]));
|
|
191
|
+
let active = [...kinds];
|
|
192
|
+
let guard = active.length + 1;
|
|
193
|
+
while (active.length > 0 && remainingBudget > 0 && guard-- > 0) {
|
|
194
|
+
const share = remainingBudget / active.length;
|
|
195
|
+
const stillActive = [];
|
|
196
|
+
for (const k of active) {
|
|
197
|
+
const want = remainingDemand.get(k);
|
|
198
|
+
const grant = Math.min(want, share);
|
|
199
|
+
out[k].admittedBytes += grant;
|
|
200
|
+
remainingDemand.set(k, want - grant);
|
|
201
|
+
remainingBudget -= grant;
|
|
202
|
+
if (want - grant > 0)
|
|
203
|
+
stillActive.push(k); // still has demand under its share.
|
|
204
|
+
}
|
|
205
|
+
// If no progress was possible (every active kind already at its share), stop.
|
|
206
|
+
if (stillActive.length === active.length && share <= 0)
|
|
207
|
+
break;
|
|
208
|
+
active = stillActive;
|
|
209
|
+
}
|
|
210
|
+
let anyThrottled = false;
|
|
211
|
+
for (const k of kinds) {
|
|
212
|
+
const left = remainingDemand.get(k) ?? 0;
|
|
213
|
+
if (left > 0) {
|
|
214
|
+
out[k].throttledBytes = left;
|
|
215
|
+
out[k].throttled = true;
|
|
216
|
+
this.degradation.perKindThrottledBytes[k] = (this.degradation.perKindThrottledBytes[k] ?? 0) + left;
|
|
217
|
+
this.degradation.totalThrottledBytes += left;
|
|
218
|
+
anyThrottled = true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (anyThrottled)
|
|
222
|
+
this.degradation.throttledIntervals++;
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
/** The cumulative degradation surface (§8 — surfaced, never a silent stall). */
|
|
226
|
+
getDegradation() {
|
|
227
|
+
return {
|
|
228
|
+
throttledIntervals: this.degradation.throttledIntervals,
|
|
229
|
+
totalThrottledBytes: this.degradation.totalThrottledBytes,
|
|
230
|
+
perKindThrottledBytes: { ...this.degradation.perKindThrottledBytes },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Decide how a recovering peer re-joins a kind's stream (§8 dark-peer accumulation
|
|
236
|
+
* bound + tombstone horizon). If the peer's last-held seq is below the holder's
|
|
237
|
+
* `oldestRetainedSeq` (the tombstone for a delete may have rotated out), a stale
|
|
238
|
+
* tail would risk delete-resurrection — so the peer is FORCED to a full
|
|
239
|
+
* single-origin snapshot re-join (§6.5 delete-resurrection guard). Otherwise a
|
|
240
|
+
* normal seq tail from `lastHeldSeq` is safe.
|
|
241
|
+
*
|
|
242
|
+
* PURE: a function of the seqs — independently testable (§12 #13).
|
|
243
|
+
*/
|
|
244
|
+
export function rejoinVerdict(lastHeldSeq, oldestRetainedSeq) {
|
|
245
|
+
// The peer's next-needed seq has rotated out of the holder's retained window ⇒
|
|
246
|
+
// a tail would skip the gap (incl. possibly a delete tombstone) ⇒ force a full
|
|
247
|
+
// snapshot re-join (the delete-resurrection guard).
|
|
248
|
+
if (lastHeldSeq + 1 < oldestRetainedSeq) {
|
|
249
|
+
return { mode: 'full-snapshot', reason: 'below-oldest-retained' };
|
|
250
|
+
}
|
|
251
|
+
return { mode: 'tail', fromSeq: lastHeldSeq };
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=ReplicationBudget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReplicationBudget.js","sourceRoot":"","sources":["../../src/core/ReplicationBudget.ts"],"names":[],"mappings":"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"}
|