cursedbelt-server 1.1.0 → 2.0.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/server/sync/http.d.ts +20 -3
- package/dist/server/sync/http.js +20 -14
- package/dist/server/sync/index.d.ts +10 -2
- package/dist/server/sync/index.js +9 -1
- package/dist/server/sync/planner.d.ts +38 -8
- package/dist/server/sync/planner.js +32 -8
- package/dist/server/sync/signal.d.ts +161 -0
- package/dist/server/sync/signal.js +348 -0
- package/dist/server/sync/timer.d.ts +63 -19
- package/dist/server/sync/timer.js +104 -45
- package/dist/server/sync/types.d.ts +0 -2
- package/package.json +1 -1
- package/src/noTimerDialsAPeer.spec.ts +469 -0
- package/src/server/sync/http.ts +31 -16
- package/src/server/sync/index.ts +23 -1
- package/src/server/sync/planner.spec.ts +33 -16
- package/src/server/sync/planner.ts +48 -11
- package/src/server/sync/signal.spec.ts +306 -0
- package/src/server/sync/signal.ts +422 -0
- package/src/server/sync/timer.spec.ts +97 -16
- package/src/server/sync/timer.ts +124 -47
- package/src/server/sync/types.ts +0 -2
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The pure loop planner
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The pure loop planner: back off after a failure, else honor a debounced dirty
|
|
3
|
+
* flag, else **do not wake at all**. Pure so the timer is a thin wrapper and this
|
|
4
|
+
* is fake-clock testable.
|
|
5
|
+
*
|
|
6
|
+
* ── 🔴 There is no steady interval, and that is the whole point ──────────────────
|
|
7
|
+
*
|
|
8
|
+
* This used to end with `else: the steady interval` — `intervalMs: 5 * 60_000`, a
|
|
9
|
+
* dial at a configured peer every five minutes whether or not either side had
|
|
10
|
+
* anything to say. The owner ruled that out on 2026-09-15: *"There should be no
|
|
11
|
+
* polling in cb unless you can make some good case for it that beats the api
|
|
12
|
+
* option."* No such case was found, so the field is gone rather than defaulted to
|
|
13
|
+
* zero — a config key that reintroduces a poll is a config key somebody sets.
|
|
14
|
+
*
|
|
15
|
+
* What is left are the two wake-ups that have a REASON, and neither is a poll:
|
|
16
|
+
*
|
|
17
|
+
* · **debounce** — there are local ops to push. The side that has news says so;
|
|
18
|
+
* the wait only coalesces a burst.
|
|
19
|
+
* · **backoff** — the last attempt failed. This runs *only while disconnected*,
|
|
20
|
+
* never against a healthy peer, which is exactly the carve-out the ruling
|
|
21
|
+
* names for a reconnect timer.
|
|
22
|
+
*
|
|
23
|
+
* A clean, healthy loop returns `waitMs: null` — "nothing to do, do not book a
|
|
24
|
+
* timer" — and the process goes quiet until something happens. News from the far
|
|
25
|
+
* side arrives over `./signal`, not by asking.
|
|
5
26
|
*/
|
|
6
27
|
|
|
7
28
|
export interface SyncLoopConfig {
|
|
8
|
-
/** Steady poll cadence when reachable and idle. */
|
|
9
|
-
intervalMs: number;
|
|
10
29
|
/** How long after a local write to wait before syncing, so a burst coalesces. */
|
|
11
30
|
debounceMs: number;
|
|
12
31
|
/** First backoff step after a failure. */
|
|
@@ -16,7 +35,6 @@ export interface SyncLoopConfig {
|
|
|
16
35
|
}
|
|
17
36
|
|
|
18
37
|
export const DEFAULT_LOOP: SyncLoopConfig = {
|
|
19
|
-
intervalMs: 5 * 60_000,
|
|
20
38
|
debounceMs: 3_000,
|
|
21
39
|
backoffBaseMs: 30_000,
|
|
22
40
|
backoffMaxMs: 30 * 60_000,
|
|
@@ -31,25 +49,44 @@ export interface SyncLoopState {
|
|
|
31
49
|
dirtySince: number | null;
|
|
32
50
|
}
|
|
33
51
|
|
|
52
|
+
/**
|
|
53
|
+
* What the loop should do next.
|
|
54
|
+
*
|
|
55
|
+
* 🔴 `waitMs: null` means **do not schedule anything** — not "wait zero" and not
|
|
56
|
+
* "wait forever". It is the idle state, and a caller that turns it into a number
|
|
57
|
+
* has put the poll back.
|
|
58
|
+
*/
|
|
59
|
+
export interface SyncPlan {
|
|
60
|
+
runNow: boolean;
|
|
61
|
+
/** Milliseconds until the next wake-up, or `null` when there is no reason to wake. */
|
|
62
|
+
waitMs: number | null;
|
|
63
|
+
/** Why the loop will wake — `null` alongside `waitMs: null`. */
|
|
64
|
+
reason: "retry" | "dirty" | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
34
67
|
export function planNextSync(
|
|
35
68
|
state: SyncLoopState,
|
|
36
69
|
now: number,
|
|
37
70
|
cfg: SyncLoopConfig = DEFAULT_LOOP,
|
|
38
|
-
):
|
|
71
|
+
): SyncPlan {
|
|
39
72
|
if (state.consecutiveFailures > 0) {
|
|
40
73
|
const step = Math.min(
|
|
41
74
|
cfg.backoffMaxMs,
|
|
42
75
|
cfg.backoffBaseMs * 2 ** (state.consecutiveFailures - 1),
|
|
43
76
|
);
|
|
44
77
|
const due = state.lastAttempt + step;
|
|
45
|
-
return now >= due
|
|
78
|
+
return now >= due
|
|
79
|
+
? { runNow: true, waitMs: 0, reason: "retry" }
|
|
80
|
+
: { runNow: false, waitMs: due - now, reason: "retry" };
|
|
46
81
|
}
|
|
47
82
|
if (state.dirtySince !== null) {
|
|
48
83
|
const due = state.dirtySince + cfg.debounceMs;
|
|
49
|
-
return now >= due
|
|
84
|
+
return now >= due
|
|
85
|
+
? { runNow: true, waitMs: 0, reason: "dirty" }
|
|
86
|
+
: { runNow: false, waitMs: due - now, reason: "dirty" };
|
|
50
87
|
}
|
|
51
|
-
|
|
52
|
-
return
|
|
88
|
+
// Clean and healthy: nothing to push, nothing to retry. Stay silent.
|
|
89
|
+
return { runNow: false, waitMs: null, reason: null };
|
|
53
90
|
}
|
|
54
91
|
|
|
55
92
|
/** A peer that is asleep (the nightly EC2 window) or an internet-less Mac is NORMAL,
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The replacement for the dialer, proven to be one: a connected client asks the
|
|
3
|
+
* peer for NOTHING, and still learns the moment its log grows.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
connectSyncSignal,
|
|
8
|
+
createSyncSignalHub,
|
|
9
|
+
encodeNewsFrame,
|
|
10
|
+
parseNewsFrame,
|
|
11
|
+
signalStream,
|
|
12
|
+
type SyncNews,
|
|
13
|
+
} from "./signal";
|
|
14
|
+
|
|
15
|
+
const news = (head: number): SyncNews => ({ head, instanceId: "peer-a" });
|
|
16
|
+
|
|
17
|
+
describe("the signal hub", () => {
|
|
18
|
+
test("announces to every subscriber and stops on unsubscribe", () => {
|
|
19
|
+
const hub = createSyncSignalHub();
|
|
20
|
+
const seen: SyncNews[] = [];
|
|
21
|
+
const off = hub.subscribe((n) => seen.push(n));
|
|
22
|
+
hub.subscribe((n) => seen.push(n));
|
|
23
|
+
expect(hub.subscriberCount).toBe(2);
|
|
24
|
+
hub.announce(news(1));
|
|
25
|
+
expect(seen).toHaveLength(2);
|
|
26
|
+
off();
|
|
27
|
+
off(); // idempotent
|
|
28
|
+
expect(hub.subscriberCount).toBe(1);
|
|
29
|
+
hub.announce(news(2));
|
|
30
|
+
expect(seen).toHaveLength(3);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("a throwing listener is dropped, never raised to the producer", () => {
|
|
34
|
+
// `announce` may be called from inside an op-log write; it can never throw up.
|
|
35
|
+
const hub = createSyncSignalHub();
|
|
36
|
+
const seen: SyncNews[] = [];
|
|
37
|
+
hub.subscribe(() => {
|
|
38
|
+
throw new Error("this transport is gone");
|
|
39
|
+
});
|
|
40
|
+
hub.subscribe((n) => seen.push(n));
|
|
41
|
+
expect(() => hub.announce(news(1))).not.toThrow();
|
|
42
|
+
expect(seen).toHaveLength(1);
|
|
43
|
+
expect(hub.subscriberCount).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("a closed hub accepts no more subscribers and announces nothing", () => {
|
|
47
|
+
const hub = createSyncSignalHub();
|
|
48
|
+
const seen: SyncNews[] = [];
|
|
49
|
+
hub.subscribe((n) => seen.push(n));
|
|
50
|
+
hub.close();
|
|
51
|
+
expect(hub.subscriberCount).toBe(0);
|
|
52
|
+
hub.announce(news(1));
|
|
53
|
+
hub.subscribe((n) => seen.push(n));
|
|
54
|
+
hub.announce(news(2));
|
|
55
|
+
expect(seen).toHaveLength(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("frame coding", () => {
|
|
60
|
+
test("a news frame round-trips", () => {
|
|
61
|
+
expect(parseNewsFrame(encodeNewsFrame(news(7)).trimEnd())).toEqual(news(7));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a heartbeat and any malformed frame parse to null, never to a fake event", () => {
|
|
65
|
+
expect(parseNewsFrame(": keep-alive")).toBeNull();
|
|
66
|
+
expect(parseNewsFrame("event: news\ndata: not json")).toBeNull();
|
|
67
|
+
expect(parseNewsFrame("event: news\ndata: {}")).toBeNull();
|
|
68
|
+
expect(parseNewsFrame('event: other\ndata: {"head":1,"instanceId":"x"}')).toBeNull();
|
|
69
|
+
expect(parseNewsFrame('data: {"head":1,"instanceId":"x"}')).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/** Read whatever bytes are currently available on a stream, without blocking. */
|
|
74
|
+
async function drain(stream: ReadableStream<Uint8Array>, reads: number): Promise<string> {
|
|
75
|
+
const reader = stream.getReader();
|
|
76
|
+
const decoder = new TextDecoder();
|
|
77
|
+
let out = "";
|
|
78
|
+
for (let i = 0; i < reads; i++) {
|
|
79
|
+
const { done, value } = await reader.read();
|
|
80
|
+
if (done) break;
|
|
81
|
+
out += decoder.decode(value, { stream: true });
|
|
82
|
+
}
|
|
83
|
+
void reader.cancel();
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("the SSE stream", () => {
|
|
88
|
+
test("opens with the catch-up frame, then carries announcements", async () => {
|
|
89
|
+
const hub = createSyncSignalHub();
|
|
90
|
+
const stream = signalStream(hub, {
|
|
91
|
+
initial: news(10),
|
|
92
|
+
setIntervalFn: () => 0,
|
|
93
|
+
clearIntervalFn: () => undefined,
|
|
94
|
+
});
|
|
95
|
+
// Announce after `start` has run and subscribed.
|
|
96
|
+
await Promise.resolve();
|
|
97
|
+
hub.announce(news(11));
|
|
98
|
+
const text = await drain(stream, 2);
|
|
99
|
+
expect(text).toContain('"head":10');
|
|
100
|
+
expect(text).toContain('"head":11');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("the heartbeat is a comment frame — it wakes no handler and asks nothing", async () => {
|
|
104
|
+
const hub = createSyncSignalHub();
|
|
105
|
+
let beat: (() => void) | null = null;
|
|
106
|
+
const stream = signalStream(hub, {
|
|
107
|
+
heartbeatMs: 1,
|
|
108
|
+
setIntervalFn: (fn) => {
|
|
109
|
+
beat = fn;
|
|
110
|
+
return 1;
|
|
111
|
+
},
|
|
112
|
+
clearIntervalFn: () => undefined,
|
|
113
|
+
});
|
|
114
|
+
await Promise.resolve();
|
|
115
|
+
expect(beat).not.toBeNull();
|
|
116
|
+
beat?.();
|
|
117
|
+
const text = await drain(stream, 1);
|
|
118
|
+
expect(text).toBe(": keep-alive\n\n");
|
|
119
|
+
expect(parseNewsFrame(text)).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("an aborted request tears the subscription down — no leaked interval", async () => {
|
|
123
|
+
const hub = createSyncSignalHub();
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
let cleared = 0;
|
|
126
|
+
signalStream(hub, {
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
setIntervalFn: () => 1,
|
|
129
|
+
clearIntervalFn: () => {
|
|
130
|
+
cleared += 1;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
await Promise.resolve();
|
|
134
|
+
expect(hub.subscriberCount).toBe(1);
|
|
135
|
+
controller.abort();
|
|
136
|
+
expect(hub.subscriberCount).toBe(0);
|
|
137
|
+
expect(cleared).toBe(1);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/** A `fetch` that answers with a stream the test drives by hand. */
|
|
142
|
+
function fakePeer() {
|
|
143
|
+
let push: ((chunk: string) => void) | null = null;
|
|
144
|
+
let end: (() => void) | null = null;
|
|
145
|
+
let calls = 0;
|
|
146
|
+
const encoder = new TextEncoder();
|
|
147
|
+
const fetchImpl = (async () => {
|
|
148
|
+
calls += 1;
|
|
149
|
+
const body = new ReadableStream<Uint8Array>({
|
|
150
|
+
start(controller) {
|
|
151
|
+
push = (chunk) => controller.enqueue(encoder.encode(chunk));
|
|
152
|
+
end = () => {
|
|
153
|
+
try {
|
|
154
|
+
controller.close();
|
|
155
|
+
} catch {
|
|
156
|
+
// already closed
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
return new Response(body, { status: 200 });
|
|
162
|
+
}) as unknown as typeof fetch;
|
|
163
|
+
return {
|
|
164
|
+
fetchImpl,
|
|
165
|
+
calls: () => calls,
|
|
166
|
+
push: (chunk: string) => push?.(chunk),
|
|
167
|
+
end: () => end?.(),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const settle = async () => {
|
|
172
|
+
for (let i = 0; i < 5; i++) await new Promise((r) => setTimeout(r, 0));
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
describe("the signal client", () => {
|
|
176
|
+
/**
|
|
177
|
+
* 🔴 The measurement the 2026-09-15 ruling is about, on the replacement rather
|
|
178
|
+
* than on the thing it replaced. The old fast lane cost one request every 20
|
|
179
|
+
* seconds — 4,320 a day, 68 % of `vault`'s entire traffic. This asserts the new
|
|
180
|
+
* one costs **one**, total, for as long as the connection holds, and that no
|
|
181
|
+
* amount of elapsed time changes that: a connected client never sleeps, so a
|
|
182
|
+
* `sleepFn` that is never called IS the proof there is no retry cadence running
|
|
183
|
+
* underneath a healthy stream.
|
|
184
|
+
*/
|
|
185
|
+
test("a connected client issues exactly ONE outbound request, forever", async () => {
|
|
186
|
+
const peer = fakePeer();
|
|
187
|
+
let sleeps = 0;
|
|
188
|
+
const seen: SyncNews[] = [];
|
|
189
|
+
const client = connectSyncSignal({
|
|
190
|
+
basePath: "https://peer.example/api/sync",
|
|
191
|
+
token: "t",
|
|
192
|
+
selfId: "mac",
|
|
193
|
+
onNews: (n) => seen.push(n),
|
|
194
|
+
fetchImpl: peer.fetchImpl,
|
|
195
|
+
sleepFn: async (ms) => {
|
|
196
|
+
sleeps += 1;
|
|
197
|
+
void ms;
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
await settle();
|
|
201
|
+
expect(client.connected).toBe(true);
|
|
202
|
+
expect(client.requestCount).toBe(1);
|
|
203
|
+
|
|
204
|
+
// 24 hours' worth of the old cadence would be 4,320 requests. Drive the
|
|
205
|
+
// peer instead: news arrives on the SAME connection, costing nothing.
|
|
206
|
+
for (let i = 0; i < 4_320; i++) {
|
|
207
|
+
peer.push(encodeNewsFrame(news(i)));
|
|
208
|
+
}
|
|
209
|
+
await settle();
|
|
210
|
+
expect(seen).toHaveLength(4_320);
|
|
211
|
+
expect(
|
|
212
|
+
client.requestCount,
|
|
213
|
+
"a connected client asked the peer for something — that is the poll coming back",
|
|
214
|
+
).toBe(1);
|
|
215
|
+
expect(sleeps, "a connected client must never be in a backoff sleep").toBe(0);
|
|
216
|
+
expect(peer.calls()).toBe(1);
|
|
217
|
+
client.stop();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("a dropped stream reconnects with jittered backoff — the allowed timer", async () => {
|
|
221
|
+
const peer = fakePeer();
|
|
222
|
+
const waits: number[] = [];
|
|
223
|
+
const client = connectSyncSignal({
|
|
224
|
+
basePath: "https://peer.example/api/sync",
|
|
225
|
+
token: "t",
|
|
226
|
+
selfId: "mac",
|
|
227
|
+
onNews: () => undefined,
|
|
228
|
+
fetchImpl: peer.fetchImpl,
|
|
229
|
+
backoffBaseMs: 1_000,
|
|
230
|
+
jitterFn: () => 0, // deterministic: the low end of the jitter window
|
|
231
|
+
sleepFn: async (ms) => {
|
|
232
|
+
waits.push(ms);
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
await settle();
|
|
236
|
+
expect(client.requestCount).toBe(1);
|
|
237
|
+
peer.end(); // the peer goes away
|
|
238
|
+
await settle();
|
|
239
|
+
// It reconnected, and it waited before doing so.
|
|
240
|
+
expect(client.requestCount).toBeGreaterThan(1);
|
|
241
|
+
expect(waits.length).toBeGreaterThan(0);
|
|
242
|
+
expect(waits[0]).toBeGreaterThan(0);
|
|
243
|
+
client.stop();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("a heartbeat frame is not mistaken for news", async () => {
|
|
247
|
+
const peer = fakePeer();
|
|
248
|
+
const seen: SyncNews[] = [];
|
|
249
|
+
const client = connectSyncSignal({
|
|
250
|
+
basePath: "https://peer.example/api/sync",
|
|
251
|
+
token: "t",
|
|
252
|
+
selfId: "mac",
|
|
253
|
+
onNews: (n) => seen.push(n),
|
|
254
|
+
fetchImpl: peer.fetchImpl,
|
|
255
|
+
sleepFn: async () => undefined,
|
|
256
|
+
});
|
|
257
|
+
await settle();
|
|
258
|
+
peer.push(": keep-alive\n\n");
|
|
259
|
+
peer.push(": keep-alive\n\n");
|
|
260
|
+
await settle();
|
|
261
|
+
expect(seen).toHaveLength(0);
|
|
262
|
+
expect(client.connected).toBe(true);
|
|
263
|
+
client.stop();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("a frame split across chunks is reassembled, not dropped", async () => {
|
|
267
|
+
const peer = fakePeer();
|
|
268
|
+
const seen: SyncNews[] = [];
|
|
269
|
+
const client = connectSyncSignal({
|
|
270
|
+
basePath: "https://peer.example/api/sync",
|
|
271
|
+
token: "t",
|
|
272
|
+
selfId: "mac",
|
|
273
|
+
onNews: (n) => seen.push(n),
|
|
274
|
+
fetchImpl: peer.fetchImpl,
|
|
275
|
+
sleepFn: async () => undefined,
|
|
276
|
+
});
|
|
277
|
+
await settle();
|
|
278
|
+
const frame = encodeNewsFrame(news(42));
|
|
279
|
+
peer.push(frame.slice(0, 12));
|
|
280
|
+
await settle();
|
|
281
|
+
expect(seen).toHaveLength(0);
|
|
282
|
+
peer.push(frame.slice(12));
|
|
283
|
+
await settle();
|
|
284
|
+
expect(seen).toEqual([news(42)]);
|
|
285
|
+
client.stop();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("stop() ends the stream and books no further request", async () => {
|
|
289
|
+
const peer = fakePeer();
|
|
290
|
+
const client = connectSyncSignal({
|
|
291
|
+
basePath: "https://peer.example/api/sync",
|
|
292
|
+
token: "t",
|
|
293
|
+
selfId: "mac",
|
|
294
|
+
onNews: () => undefined,
|
|
295
|
+
fetchImpl: peer.fetchImpl,
|
|
296
|
+
sleepFn: async () => undefined,
|
|
297
|
+
});
|
|
298
|
+
await settle();
|
|
299
|
+
const before = client.requestCount;
|
|
300
|
+
client.stop();
|
|
301
|
+
peer.end();
|
|
302
|
+
await settle();
|
|
303
|
+
expect(client.requestCount).toBe(before);
|
|
304
|
+
expect(client.connected).toBe(false);
|
|
305
|
+
});
|
|
306
|
+
});
|