@optimystic/db-core 0.21.0 → 0.24.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/README.md +336 -336
- package/dist/src/btree/btree.d.ts +2 -1
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +1 -1
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/chain/chain.d.ts +1 -1
- package/dist/src/chain/chain.d.ts.map +1 -1
- package/dist/src/chain/chain.js +1 -1
- package/dist/src/chain/chain.js.map +1 -1
- package/dist/src/cluster/structs.d.ts +39 -1
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +24 -0
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/collection/collection.d.ts +20 -1
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +31 -4
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/diary/diary.d.ts.map +1 -1
- package/dist/src/collections/diary/diary.js +2 -1
- package/dist/src/collections/diary/diary.js.map +1 -1
- package/dist/src/collections/diary/struct.js +1 -1
- package/dist/src/collections/diary/struct.js.map +1 -1
- package/dist/src/collections/tree/collection-trunk.js +1 -1
- package/dist/src/collections/tree/collection-trunk.js.map +1 -1
- package/dist/src/collections/tree/struct.d.ts +1 -1
- package/dist/src/collections/tree/struct.d.ts.map +1 -1
- package/dist/src/collections/tree/struct.js +2 -1
- package/dist/src/collections/tree/struct.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +5 -0
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +7 -0
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/log/log.d.ts +1 -1
- package/dist/src/log/log.d.ts.map +1 -1
- package/dist/src/log/log.js +2 -2
- package/dist/src/log/log.js.map +1 -1
- package/dist/src/network/i-peer-network.d.ts +16 -0
- package/dist/src/network/i-peer-network.d.ts.map +1 -1
- package/dist/src/network/struct.d.ts +39 -2
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js +18 -0
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +95 -8
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +133 -12
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +2 -1
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +1 -1
- package/dist/src/transaction/transaction.js +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +57 -18
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +25 -2
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/cache-source.js +1 -1
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/helpers.d.ts +6 -1
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +7 -6
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +2 -1
- package/dist/src/transform/tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/btree/btree.ts +2 -1
- package/src/chain/chain.ts +2 -1
- package/src/cluster/membership.ts +85 -85
- package/src/cluster/structs.ts +43 -4
- package/src/cohort-topic/addressing.ts +120 -120
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
- package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
- package/src/cohort-topic/antidos/index.ts +5 -5
- package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
- package/src/cohort-topic/antidos/replay-guard.ts +146 -146
- package/src/cohort-topic/antidos/topic-budget.ts +160 -160
- package/src/cohort-topic/antiflood/index.ts +2 -2
- package/src/cohort-topic/antiflood/invariants.ts +108 -108
- package/src/cohort-topic/antiflood/jitter.ts +117 -117
- package/src/cohort-topic/coldstart.ts +237 -237
- package/src/cohort-topic/dmax.ts +88 -88
- package/src/cohort-topic/gossip/bus.ts +254 -254
- package/src/cohort-topic/gossip/index.ts +3 -3
- package/src/cohort-topic/gossip/records.ts +45 -45
- package/src/cohort-topic/gossip/view.ts +91 -91
- package/src/cohort-topic/index.ts +20 -20
- package/src/cohort-topic/load/barometer.ts +134 -134
- package/src/cohort-topic/load/index.ts +1 -1
- package/src/cohort-topic/member-engine.ts +430 -430
- package/src/cohort-topic/membership/index.ts +3 -3
- package/src/cohort-topic/membership/publisher.ts +163 -163
- package/src/cohort-topic/membership/source.ts +41 -41
- package/src/cohort-topic/membership/verifier.ts +461 -461
- package/src/cohort-topic/ports.ts +157 -157
- package/src/cohort-topic/promotion.ts +405 -405
- package/src/cohort-topic/registration/bytes.ts +37 -37
- package/src/cohort-topic/registration/handoff.ts +154 -154
- package/src/cohort-topic/registration/index.ts +6 -6
- package/src/cohort-topic/registration/renewal.ts +495 -495
- package/src/cohort-topic/registration/sharding.ts +61 -61
- package/src/cohort-topic/registration/store.ts +81 -81
- package/src/cohort-topic/registration/types.ts +91 -91
- package/src/cohort-topic/ring-hash.ts +50 -50
- package/src/cohort-topic/service.ts +416 -416
- package/src/cohort-topic/sig/index.ts +2 -2
- package/src/cohort-topic/sig/payloads.ts +59 -59
- package/src/cohort-topic/sig/threshold.ts +64 -64
- package/src/cohort-topic/tiers.ts +74 -74
- package/src/cohort-topic/traffic.ts +233 -233
- package/src/cohort-topic/walk.ts +326 -326
- package/src/cohort-topic/willingness.ts +237 -237
- package/src/cohort-topic/wire/codec.ts +216 -216
- package/src/cohort-topic/wire/index.ts +18 -18
- package/src/cohort-topic/wire/payloads.ts +126 -126
- package/src/cohort-topic/wire/primitives.ts +188 -188
- package/src/cohort-topic/wire/types.ts +475 -475
- package/src/cohort-topic/wire/validate.ts +512 -512
- package/src/collection/collection-type-registry.ts +37 -37
- package/src/collection/collection.ts +32 -4
- package/src/collections/diary/diary.ts +68 -67
- package/src/collections/diary/struct.ts +1 -1
- package/src/collections/tree/collection-trunk.ts +1 -1
- package/src/collections/tree/readme.md +4 -0
- package/src/collections/tree/struct.ts +3 -1
- package/src/collections/tree/tree.ts +320 -312
- package/src/log/log.ts +2 -2
- package/src/matchmaking/capability-filter.ts +45 -45
- package/src/matchmaking/config.ts +98 -98
- package/src/matchmaking/index.ts +21 -21
- package/src/matchmaking/multi-cohort-seeker.ts +234 -234
- package/src/matchmaking/provider.ts +123 -123
- package/src/matchmaking/query-eval.ts +105 -105
- package/src/matchmaking/seeker-walk.ts +127 -127
- package/src/matchmaking/seeker.ts +86 -86
- package/src/matchmaking/topic-anchor.ts +90 -90
- package/src/matchmaking/voting-quorum.ts +394 -394
- package/src/matchmaking/wire.ts +603 -603
- package/src/network/i-peer-network.ts +17 -0
- package/src/network/stale-failure.ts +43 -43
- package/src/network/struct.ts +41 -2
- package/src/network/types.ts +37 -37
- package/src/reactivity/backfill.ts +220 -220
- package/src/reactivity/backpressure.ts +191 -191
- package/src/reactivity/checkpoint.ts +308 -308
- package/src/reactivity/config.ts +172 -172
- package/src/reactivity/dedupe.ts +132 -132
- package/src/reactivity/forwarder.ts +87 -87
- package/src/reactivity/index.ts +34 -34
- package/src/reactivity/notification.ts +123 -123
- package/src/reactivity/policy.ts +79 -79
- package/src/reactivity/push-state.ts +310 -310
- package/src/reactivity/recover.ts +153 -153
- package/src/reactivity/replay-buffer.ts +141 -141
- package/src/reactivity/resume.ts +549 -549
- package/src/reactivity/rotation.ts +415 -415
- package/src/reactivity/subscriber.ts +132 -132
- package/src/reactivity/subscription.ts +66 -66
- package/src/reactivity/topic-anchor.ts +71 -71
- package/src/reactivity/verify.ts +73 -73
- package/src/reactivity/wire-validate.ts +13 -13
- package/src/reactivity/wire.ts +224 -224
- package/src/testing/async-wait.ts +65 -65
- package/src/testing/index.ts +2 -2
- package/src/testing/test-transactor.ts +638 -489
- package/src/transaction/coordinator.ts +2 -1
- package/src/transaction/errors.ts +91 -91
- package/src/transaction/operations-hash.ts +196 -196
- package/src/transaction/read-dependency-collector.ts +78 -78
- package/src/transaction/transaction.ts +1 -1
- package/src/transactor/change-notifier.ts +80 -80
- package/src/transactor/index.ts +5 -5
- package/src/transactor/network-transactor.ts +58 -19
- package/src/transactor/transactor-source.ts +25 -2
- package/src/transform/atomic-proxy.ts +92 -92
- package/src/transform/cache-source.ts +1 -1
- package/src/transform/helpers.ts +159 -158
- package/src/transform/tracker.ts +2 -1
- package/src/utility/backoff.ts +95 -95
- package/src/utility/batch-coordinator.ts +191 -191
- package/dist/src/transaction/context.d.ts +0 -60
- package/dist/src/transaction/context.d.ts.map +0 -1
- package/dist/src/transaction/context.js +0 -91
- package/dist/src/transaction/context.js.map +0 -1
|
@@ -1,191 +1,191 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reactivity — slow-subscriber backpressure (`docs/reactivity.md` §Slow-subscriber backpressure).
|
|
3
|
-
*
|
|
4
|
-
* A forwarder's primary maintains a **per-subscriber** bounded queue with **drop-oldest** semantics. When
|
|
5
|
-
* a subscriber's queue is full and a new notification arrives, the *oldest* queued entry is dropped and a
|
|
6
|
-
* monotone `dropped` counter increments. The subscriber detects the resulting revision jump on its next
|
|
7
|
-
* delivery and issues a `BackfillV1` against the replay buffer (the backfill RPC is owned by
|
|
8
|
-
* [reactivity-backfill-resume-checkpoints]; this module only produces the gap).
|
|
9
|
-
*
|
|
10
|
-
* The point is **isolation**: each subscriber has its own queue, so one phone on a flaky link fills and
|
|
11
|
-
* drops *its own* queue without stalling fan-out to the rest of the cohort's attached subscribers. Memory
|
|
12
|
-
* is bounded by `cohort_subscribers × queue_max × notification_size` — the per-subscriber queue depth is
|
|
13
|
-
* the small `queue_max` (default 32), never the unbounded backlog of the slowest receiver.
|
|
14
|
-
*
|
|
15
|
-
* This is the primary-local fan-out buffer, **not** cohort soft state: it is never gossiped (only the
|
|
16
|
-
* primary drives delivery), so it is absent from {@link import("./push-state.js").PushStateGossipV1}. A
|
|
17
|
-
* `cohortEpoch` handoff rebuilds it empty at the new primary — a few dropped notifications at handoff are
|
|
18
|
-
* exactly what the replay buffer + backfill path recover.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { QUEUE_MAX_DEFAULT } from "./config.js";
|
|
22
|
-
import type { NotificationV1 } from "./wire.js";
|
|
23
|
-
|
|
24
|
-
/** The result of enqueuing one notification onto a subscriber's bounded queue. */
|
|
25
|
-
export interface EnqueueResult {
|
|
26
|
-
/** `true` iff the queue was full and its oldest entry was evicted to make room (drop-oldest). */
|
|
27
|
-
readonly droppedOldest: boolean;
|
|
28
|
-
/** The evicted notification, when `droppedOldest`; the subscriber will detect the gap and backfill. */
|
|
29
|
-
readonly evicted?: NotificationV1;
|
|
30
|
-
/** Queue depth after the enqueue (`<= capacity`). */
|
|
31
|
-
readonly depth: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* A bounded, drop-oldest per-subscriber delivery queue (`docs/reactivity.md` §Slow-subscriber
|
|
36
|
-
* backpressure). Holds at most `capacity` (= `queue_max`) pending notifications; a full queue evicts its
|
|
37
|
-
* oldest on the next enqueue and increments {@link dropped}.
|
|
38
|
-
*/
|
|
39
|
-
export class BoundedQueue {
|
|
40
|
-
readonly capacity: number;
|
|
41
|
-
private readonly entries: NotificationV1[] = [];
|
|
42
|
-
private droppedCount = 0;
|
|
43
|
-
|
|
44
|
-
constructor(capacity: number = QUEUE_MAX_DEFAULT) {
|
|
45
|
-
if (!Number.isInteger(capacity) || capacity < 1) {
|
|
46
|
-
throw new RangeError(`reactivity bounded queue: capacity must be an integer >= 1, got ${capacity}`);
|
|
47
|
-
}
|
|
48
|
-
this.capacity = capacity;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Pending depth (`0..capacity`). */
|
|
52
|
-
get size(): number {
|
|
53
|
-
return this.entries.length;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Monotone count of notifications dropped to drop-oldest pressure over this queue's lifetime. */
|
|
57
|
-
get dropped(): number {
|
|
58
|
-
return this.droppedCount;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** True iff a further enqueue would evict the oldest entry. */
|
|
62
|
-
get full(): boolean {
|
|
63
|
-
return this.entries.length >= this.capacity;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Enqueue `n` for delivery. On overflow the **oldest** entry is dropped (not the incoming one — a slow
|
|
68
|
-
* subscriber wants the freshest revisions, and the dropped span is recovered via backfill) and
|
|
69
|
-
* {@link dropped} increments. Returns whether an eviction occurred and the resulting depth.
|
|
70
|
-
*/
|
|
71
|
-
enqueue(n: NotificationV1): EnqueueResult {
|
|
72
|
-
let evicted: NotificationV1 | undefined;
|
|
73
|
-
if (this.entries.length >= this.capacity) {
|
|
74
|
-
evicted = this.entries.shift();
|
|
75
|
-
this.droppedCount += 1;
|
|
76
|
-
}
|
|
77
|
-
this.entries.push(n);
|
|
78
|
-
return evicted !== undefined
|
|
79
|
-
? { droppedOldest: true, evicted, depth: this.entries.length }
|
|
80
|
-
: { droppedOldest: false, depth: this.entries.length };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** The oldest pending notification, or `undefined` when empty (does not dequeue). */
|
|
84
|
-
peek(): NotificationV1 | undefined {
|
|
85
|
-
return this.entries[0];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Remove and return the oldest pending notification (FIFO delivery), or `undefined` when empty. */
|
|
89
|
-
dequeue(): NotificationV1 | undefined {
|
|
90
|
-
return this.entries.shift();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Drain and return all pending notifications in FIFO order, leaving the queue empty. */
|
|
94
|
-
drain(): NotificationV1[] {
|
|
95
|
-
return this.entries.splice(0, this.entries.length);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** A read-only snapshot of the pending notifications, oldest first (does not mutate). */
|
|
99
|
-
pending(): readonly NotificationV1[] {
|
|
100
|
-
return [...this.entries];
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** The per-subscriber drop record surfaced by {@link SubscriberBackpressure.enqueue}. */
|
|
105
|
-
export interface SubscriberEnqueueResult extends EnqueueResult {
|
|
106
|
-
/** The subscriber this enqueue targeted, base64url peer id. */
|
|
107
|
-
readonly subscriberId: string;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* The forwarder primary's per-subscriber backpressure map (the `PushState.perSubscriberQueue` field). One
|
|
112
|
-
* {@link BoundedQueue} per attached subscriber, created lazily on first delivery. `enqueue` routes a
|
|
113
|
-
* notification to exactly one subscriber's queue, so a slow subscriber's drops never touch another
|
|
114
|
-
* subscriber's queue — the isolation the §Slow-subscriber backpressure section requires.
|
|
115
|
-
*/
|
|
116
|
-
export class SubscriberBackpressure {
|
|
117
|
-
readonly capacity: number;
|
|
118
|
-
private readonly queues = new Map<string, BoundedQueue>();
|
|
119
|
-
|
|
120
|
-
constructor(capacity: number = QUEUE_MAX_DEFAULT) {
|
|
121
|
-
if (!Number.isInteger(capacity) || capacity < 1) {
|
|
122
|
-
throw new RangeError(`reactivity backpressure: capacity must be an integer >= 1, got ${capacity}`);
|
|
123
|
-
}
|
|
124
|
-
this.capacity = capacity;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Number of subscribers with a live queue. */
|
|
128
|
-
get subscriberCount(): number {
|
|
129
|
-
return this.queues.size;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** The live queue for `subscriberId`, creating an empty one on first use. */
|
|
133
|
-
queue(subscriberId: string): BoundedQueue {
|
|
134
|
-
let q = this.queues.get(subscriberId);
|
|
135
|
-
if (q === undefined) {
|
|
136
|
-
q = new BoundedQueue(this.capacity);
|
|
137
|
-
this.queues.set(subscriberId, q);
|
|
138
|
-
}
|
|
139
|
-
return q;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** The live queue for `subscriberId`, or `undefined` if none exists yet (no lazy creation). */
|
|
143
|
-
peekQueue(subscriberId: string): BoundedQueue | undefined {
|
|
144
|
-
return this.queues.get(subscriberId);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Enqueue `n` for one subscriber. A drop on that subscriber's queue isolates it from the others. */
|
|
148
|
-
enqueue(subscriberId: string, n: NotificationV1): SubscriberEnqueueResult {
|
|
149
|
-
return { subscriberId, ...this.queue(subscriberId).enqueue(n) };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Fan one notification out to every named subscriber, returning only the subscribers whose queue
|
|
154
|
-
* dropped its oldest under pressure. A slow subscriber that drops is the *only* one affected — fast
|
|
155
|
-
* subscribers' queues accept the same notification contiguously (the isolation property).
|
|
156
|
-
*/
|
|
157
|
-
fanOut(subscriberIds: Iterable<string>, n: NotificationV1): SubscriberEnqueueResult[] {
|
|
158
|
-
const drops: SubscriberEnqueueResult[] = [];
|
|
159
|
-
for (const id of subscriberIds) {
|
|
160
|
-
const result = this.enqueue(id, n);
|
|
161
|
-
if (result.droppedOldest) {
|
|
162
|
-
drops.push(result);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return drops;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Drop a departed subscriber's queue (TTL eviction / withdrawal), reclaiming its memory. */
|
|
169
|
-
remove(subscriberId: string): void {
|
|
170
|
-
this.queues.delete(subscriberId);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** The total notifications dropped across all subscribers (diagnostics). */
|
|
174
|
-
totalDropped(): number {
|
|
175
|
-
let total = 0;
|
|
176
|
-
for (const q of this.queues.values()) {
|
|
177
|
-
total += q.dropped;
|
|
178
|
-
}
|
|
179
|
-
return total;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** The subscriber ids currently holding a queue (for iteration / diagnostics). */
|
|
183
|
-
subscribers(): IterableIterator<string> {
|
|
184
|
-
return this.queues.keys();
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Build a {@link SubscriberBackpressure} map with the configured per-subscriber depth (default `queue_max`). */
|
|
189
|
-
export function createSubscriberBackpressure(capacity: number = QUEUE_MAX_DEFAULT): SubscriberBackpressure {
|
|
190
|
-
return new SubscriberBackpressure(capacity);
|
|
191
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity — slow-subscriber backpressure (`docs/reactivity.md` §Slow-subscriber backpressure).
|
|
3
|
+
*
|
|
4
|
+
* A forwarder's primary maintains a **per-subscriber** bounded queue with **drop-oldest** semantics. When
|
|
5
|
+
* a subscriber's queue is full and a new notification arrives, the *oldest* queued entry is dropped and a
|
|
6
|
+
* monotone `dropped` counter increments. The subscriber detects the resulting revision jump on its next
|
|
7
|
+
* delivery and issues a `BackfillV1` against the replay buffer (the backfill RPC is owned by
|
|
8
|
+
* [reactivity-backfill-resume-checkpoints]; this module only produces the gap).
|
|
9
|
+
*
|
|
10
|
+
* The point is **isolation**: each subscriber has its own queue, so one phone on a flaky link fills and
|
|
11
|
+
* drops *its own* queue without stalling fan-out to the rest of the cohort's attached subscribers. Memory
|
|
12
|
+
* is bounded by `cohort_subscribers × queue_max × notification_size` — the per-subscriber queue depth is
|
|
13
|
+
* the small `queue_max` (default 32), never the unbounded backlog of the slowest receiver.
|
|
14
|
+
*
|
|
15
|
+
* This is the primary-local fan-out buffer, **not** cohort soft state: it is never gossiped (only the
|
|
16
|
+
* primary drives delivery), so it is absent from {@link import("./push-state.js").PushStateGossipV1}. A
|
|
17
|
+
* `cohortEpoch` handoff rebuilds it empty at the new primary — a few dropped notifications at handoff are
|
|
18
|
+
* exactly what the replay buffer + backfill path recover.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { QUEUE_MAX_DEFAULT } from "./config.js";
|
|
22
|
+
import type { NotificationV1 } from "./wire.js";
|
|
23
|
+
|
|
24
|
+
/** The result of enqueuing one notification onto a subscriber's bounded queue. */
|
|
25
|
+
export interface EnqueueResult {
|
|
26
|
+
/** `true` iff the queue was full and its oldest entry was evicted to make room (drop-oldest). */
|
|
27
|
+
readonly droppedOldest: boolean;
|
|
28
|
+
/** The evicted notification, when `droppedOldest`; the subscriber will detect the gap and backfill. */
|
|
29
|
+
readonly evicted?: NotificationV1;
|
|
30
|
+
/** Queue depth after the enqueue (`<= capacity`). */
|
|
31
|
+
readonly depth: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A bounded, drop-oldest per-subscriber delivery queue (`docs/reactivity.md` §Slow-subscriber
|
|
36
|
+
* backpressure). Holds at most `capacity` (= `queue_max`) pending notifications; a full queue evicts its
|
|
37
|
+
* oldest on the next enqueue and increments {@link dropped}.
|
|
38
|
+
*/
|
|
39
|
+
export class BoundedQueue {
|
|
40
|
+
readonly capacity: number;
|
|
41
|
+
private readonly entries: NotificationV1[] = [];
|
|
42
|
+
private droppedCount = 0;
|
|
43
|
+
|
|
44
|
+
constructor(capacity: number = QUEUE_MAX_DEFAULT) {
|
|
45
|
+
if (!Number.isInteger(capacity) || capacity < 1) {
|
|
46
|
+
throw new RangeError(`reactivity bounded queue: capacity must be an integer >= 1, got ${capacity}`);
|
|
47
|
+
}
|
|
48
|
+
this.capacity = capacity;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Pending depth (`0..capacity`). */
|
|
52
|
+
get size(): number {
|
|
53
|
+
return this.entries.length;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Monotone count of notifications dropped to drop-oldest pressure over this queue's lifetime. */
|
|
57
|
+
get dropped(): number {
|
|
58
|
+
return this.droppedCount;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True iff a further enqueue would evict the oldest entry. */
|
|
62
|
+
get full(): boolean {
|
|
63
|
+
return this.entries.length >= this.capacity;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Enqueue `n` for delivery. On overflow the **oldest** entry is dropped (not the incoming one — a slow
|
|
68
|
+
* subscriber wants the freshest revisions, and the dropped span is recovered via backfill) and
|
|
69
|
+
* {@link dropped} increments. Returns whether an eviction occurred and the resulting depth.
|
|
70
|
+
*/
|
|
71
|
+
enqueue(n: NotificationV1): EnqueueResult {
|
|
72
|
+
let evicted: NotificationV1 | undefined;
|
|
73
|
+
if (this.entries.length >= this.capacity) {
|
|
74
|
+
evicted = this.entries.shift();
|
|
75
|
+
this.droppedCount += 1;
|
|
76
|
+
}
|
|
77
|
+
this.entries.push(n);
|
|
78
|
+
return evicted !== undefined
|
|
79
|
+
? { droppedOldest: true, evicted, depth: this.entries.length }
|
|
80
|
+
: { droppedOldest: false, depth: this.entries.length };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The oldest pending notification, or `undefined` when empty (does not dequeue). */
|
|
84
|
+
peek(): NotificationV1 | undefined {
|
|
85
|
+
return this.entries[0];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Remove and return the oldest pending notification (FIFO delivery), or `undefined` when empty. */
|
|
89
|
+
dequeue(): NotificationV1 | undefined {
|
|
90
|
+
return this.entries.shift();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Drain and return all pending notifications in FIFO order, leaving the queue empty. */
|
|
94
|
+
drain(): NotificationV1[] {
|
|
95
|
+
return this.entries.splice(0, this.entries.length);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A read-only snapshot of the pending notifications, oldest first (does not mutate). */
|
|
99
|
+
pending(): readonly NotificationV1[] {
|
|
100
|
+
return [...this.entries];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The per-subscriber drop record surfaced by {@link SubscriberBackpressure.enqueue}. */
|
|
105
|
+
export interface SubscriberEnqueueResult extends EnqueueResult {
|
|
106
|
+
/** The subscriber this enqueue targeted, base64url peer id. */
|
|
107
|
+
readonly subscriberId: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The forwarder primary's per-subscriber backpressure map (the `PushState.perSubscriberQueue` field). One
|
|
112
|
+
* {@link BoundedQueue} per attached subscriber, created lazily on first delivery. `enqueue` routes a
|
|
113
|
+
* notification to exactly one subscriber's queue, so a slow subscriber's drops never touch another
|
|
114
|
+
* subscriber's queue — the isolation the §Slow-subscriber backpressure section requires.
|
|
115
|
+
*/
|
|
116
|
+
export class SubscriberBackpressure {
|
|
117
|
+
readonly capacity: number;
|
|
118
|
+
private readonly queues = new Map<string, BoundedQueue>();
|
|
119
|
+
|
|
120
|
+
constructor(capacity: number = QUEUE_MAX_DEFAULT) {
|
|
121
|
+
if (!Number.isInteger(capacity) || capacity < 1) {
|
|
122
|
+
throw new RangeError(`reactivity backpressure: capacity must be an integer >= 1, got ${capacity}`);
|
|
123
|
+
}
|
|
124
|
+
this.capacity = capacity;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Number of subscribers with a live queue. */
|
|
128
|
+
get subscriberCount(): number {
|
|
129
|
+
return this.queues.size;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The live queue for `subscriberId`, creating an empty one on first use. */
|
|
133
|
+
queue(subscriberId: string): BoundedQueue {
|
|
134
|
+
let q = this.queues.get(subscriberId);
|
|
135
|
+
if (q === undefined) {
|
|
136
|
+
q = new BoundedQueue(this.capacity);
|
|
137
|
+
this.queues.set(subscriberId, q);
|
|
138
|
+
}
|
|
139
|
+
return q;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The live queue for `subscriberId`, or `undefined` if none exists yet (no lazy creation). */
|
|
143
|
+
peekQueue(subscriberId: string): BoundedQueue | undefined {
|
|
144
|
+
return this.queues.get(subscriberId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Enqueue `n` for one subscriber. A drop on that subscriber's queue isolates it from the others. */
|
|
148
|
+
enqueue(subscriberId: string, n: NotificationV1): SubscriberEnqueueResult {
|
|
149
|
+
return { subscriberId, ...this.queue(subscriberId).enqueue(n) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Fan one notification out to every named subscriber, returning only the subscribers whose queue
|
|
154
|
+
* dropped its oldest under pressure. A slow subscriber that drops is the *only* one affected — fast
|
|
155
|
+
* subscribers' queues accept the same notification contiguously (the isolation property).
|
|
156
|
+
*/
|
|
157
|
+
fanOut(subscriberIds: Iterable<string>, n: NotificationV1): SubscriberEnqueueResult[] {
|
|
158
|
+
const drops: SubscriberEnqueueResult[] = [];
|
|
159
|
+
for (const id of subscriberIds) {
|
|
160
|
+
const result = this.enqueue(id, n);
|
|
161
|
+
if (result.droppedOldest) {
|
|
162
|
+
drops.push(result);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return drops;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Drop a departed subscriber's queue (TTL eviction / withdrawal), reclaiming its memory. */
|
|
169
|
+
remove(subscriberId: string): void {
|
|
170
|
+
this.queues.delete(subscriberId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The total notifications dropped across all subscribers (diagnostics). */
|
|
174
|
+
totalDropped(): number {
|
|
175
|
+
let total = 0;
|
|
176
|
+
for (const q of this.queues.values()) {
|
|
177
|
+
total += q.dropped;
|
|
178
|
+
}
|
|
179
|
+
return total;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The subscriber ids currently holding a queue (for iteration / diagnostics). */
|
|
183
|
+
subscribers(): IterableIterator<string> {
|
|
184
|
+
return this.queues.keys();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Build a {@link SubscriberBackpressure} map with the configured per-subscriber depth (default `queue_max`). */
|
|
189
|
+
export function createSubscriberBackpressure(capacity: number = QUEUE_MAX_DEFAULT): SubscriberBackpressure {
|
|
190
|
+
return new SubscriberBackpressure(capacity);
|
|
191
|
+
}
|