@effectstream/sync 0.200.4 → 0.200.6

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.
@@ -0,0 +1,694 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ getMidnightTip,
4
+ MidnightTipError,
5
+ } from "../src/sync-protocols/midnight/tip.ts";
6
+
7
+ type TestServer = ReturnType<typeof Bun.serve>;
8
+
9
+ type Fixture = {
10
+ server: TestServer;
11
+ url: string;
12
+ port: number;
13
+ hits: number;
14
+ activeRequests: number;
15
+ };
16
+
17
+ const fixtures = new Set<Fixture>();
18
+
19
+ async function startFixture(
20
+ handler: (request: Request) => Response | Promise<Response>,
21
+ ): Promise<Fixture> {
22
+ let lastError: unknown;
23
+
24
+ for (let attempt = 0; attempt < 50; attempt++) {
25
+ try {
26
+ let fixture!: Fixture;
27
+ const server = Bun.serve({
28
+ hostname: "127.0.0.1",
29
+ port: 0,
30
+ async fetch(request) {
31
+ fixture.hits++;
32
+ fixture.activeRequests++;
33
+ try {
34
+ return await handler(request);
35
+ } finally {
36
+ fixture.activeRequests--;
37
+ }
38
+ },
39
+ });
40
+ const port = server.port;
41
+ if (port <= 10_000) {
42
+ await server.stop(true);
43
+ continue;
44
+ }
45
+
46
+ fixture = {
47
+ server,
48
+ url: `http://127.0.0.1:${port}/graphql`,
49
+ port,
50
+ hits: 0,
51
+ activeRequests: 0,
52
+ };
53
+ fixtures.add(fixture);
54
+ return fixture;
55
+ } catch (error) {
56
+ lastError = error;
57
+ }
58
+ }
59
+
60
+ throw lastError ?? new Error("Unable to allocate a loopback fixture port above 10000");
61
+ }
62
+
63
+ async function stopFixture(fixture: Fixture): Promise<void> {
64
+ fixtures.delete(fixture);
65
+ await fixture.server.stop(true);
66
+ }
67
+
68
+ async function waitFor(
69
+ predicate: () => boolean,
70
+ label: string,
71
+ timeoutMs = 2_000,
72
+ ): Promise<void> {
73
+ const deadline = Date.now() + timeoutMs;
74
+ while (!predicate()) {
75
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for ${label}`);
76
+ await Bun.sleep(5);
77
+ }
78
+ }
79
+
80
+ async function expectFixtureIdle(fixture: Fixture): Promise<void> {
81
+ await waitFor(
82
+ () =>
83
+ fixture.activeRequests === 0 &&
84
+ Number((fixture.server as TestServer & { pendingRequests?: number }).pendingRequests ?? 0) === 0,
85
+ "fixture request cleanup",
86
+ );
87
+ }
88
+
89
+ function json(value: unknown, status = 200): Response {
90
+ return Response.json(value, { status });
91
+ }
92
+
93
+ async function expectTipError(
94
+ promise: Promise<unknown>,
95
+ code: MidnightTipError["code"],
96
+ ): Promise<MidnightTipError> {
97
+ try {
98
+ await promise;
99
+ } catch (error) {
100
+ expect(error).toBeInstanceOf(MidnightTipError);
101
+ expect((error as MidnightTipError).code).toBe(code);
102
+ return error as MidnightTipError;
103
+ }
104
+ throw new Error(`Expected MidnightTipError(${code}) without a partial value`);
105
+ }
106
+
107
+ function trackedAbortSignal(): {
108
+ signal: AbortSignal;
109
+ abort: (reason?: unknown) => void;
110
+ listenerCount: () => number;
111
+ } {
112
+ const controller = new AbortController();
113
+ const listeners = new Set<EventListenerOrEventListenerObject>();
114
+ const signal = {
115
+ get aborted() {
116
+ return controller.signal.aborted;
117
+ },
118
+ get reason() {
119
+ return controller.signal.reason;
120
+ },
121
+ addEventListener(
122
+ type: string,
123
+ listener: EventListenerOrEventListenerObject,
124
+ options?: AddEventListenerOptions | boolean,
125
+ ) {
126
+ if (type === "abort") listeners.add(listener);
127
+ controller.signal.addEventListener(type, listener, options);
128
+ },
129
+ removeEventListener(
130
+ type: string,
131
+ listener: EventListenerOrEventListenerObject,
132
+ options?: EventListenerOptions | boolean,
133
+ ) {
134
+ if (type === "abort") listeners.delete(listener);
135
+ controller.signal.removeEventListener(type, listener, options);
136
+ },
137
+ } as AbortSignal;
138
+
139
+ return {
140
+ signal,
141
+ abort: (reason?: unknown) => controller.abort(reason),
142
+ listenerCount: () => listeners.size,
143
+ };
144
+ }
145
+
146
+ afterEach(async () => {
147
+ const remaining = [...fixtures];
148
+ fixtures.clear();
149
+ for (const fixture of remaining) await fixture.server.stop(true);
150
+ });
151
+
152
+ describe("getMidnightTip request contract", () => {
153
+ test.each([0, 917_331, Number.MAX_SAFE_INTEGER])(
154
+ "performs one exact POST and accepts height %d",
155
+ async (height) => {
156
+ let method: string | undefined;
157
+ let body: string | undefined;
158
+ let contentType: string | null | undefined;
159
+ const fixture = await startFixture(async (request) => {
160
+ method = request.method;
161
+ body = await request.text();
162
+ contentType = request.headers.get("content-type");
163
+ return json({ data: { block: { height } } });
164
+ });
165
+
166
+ await expect(getMidnightTip({ indexer: fixture.url })).resolves.toEqual({ height });
167
+ expect(fixture.hits).toBe(1);
168
+ expect(method).toBe("POST");
169
+ expect(body).toBe('{"query":"query { block { height } }"}');
170
+ expect(contentType).toBe("application/json");
171
+ await expectFixtureIdle(fixture);
172
+ },
173
+ );
174
+
175
+ test.each([
176
+ [301, "Moved Permanently"],
177
+ [307, "Temporary Redirect"],
178
+ ])(
179
+ "treats redirect status %d as HTTP without following the target",
180
+ async (status, statusText) => {
181
+ const target = await startFixture(() =>
182
+ json({ data: { block: { height: 999 } } }));
183
+ const origin = await startFixture(() =>
184
+ new Response(null, {
185
+ status,
186
+ statusText,
187
+ headers: { Location: target.url },
188
+ }));
189
+
190
+ let value: unknown;
191
+ let caught: unknown;
192
+ try {
193
+ value = await getMidnightTip({ indexer: origin.url });
194
+ } catch (error) {
195
+ caught = error;
196
+ }
197
+
198
+ expect({ originHits: origin.hits, targetHits: target.hits, value }).toEqual({
199
+ originHits: 1,
200
+ targetHits: 0,
201
+ value: undefined,
202
+ });
203
+ expect(caught).toBeInstanceOf(MidnightTipError);
204
+ const error = caught as MidnightTipError;
205
+ expect(error.code).toBe("HTTP");
206
+ expect(error.status).toBe(status);
207
+ expect(error.statusText).toBe(statusText);
208
+ expect(error.cause).toBeUndefined();
209
+ expect(error.graphqlErrors).toBeUndefined();
210
+ await expectFixtureIdle(origin);
211
+ await expectFixtureIdle(target);
212
+ },
213
+ );
214
+
215
+ test("reports HTTP before attempting body parsing", async () => {
216
+ const fixture = await startFixture(() =>
217
+ new Response("not-json", { status: 503, statusText: "Service Unavailable" }));
218
+
219
+ const error = await expectTipError(getMidnightTip({ indexer: fixture.url }), "HTTP");
220
+ expect(error.status).toBe(503);
221
+ expect(error.statusText).toBe("Service Unavailable");
222
+ expect(error.cause).toBeUndefined();
223
+ expect(error.graphqlErrors).toBeUndefined();
224
+ expect(fixture.hits).toBe(1);
225
+ await expectFixtureIdle(fixture);
226
+ });
227
+
228
+ test("cancels an unread streaming body before settling HTTP", async () => {
229
+ const runtime = globalThis as typeof globalThis & { fetch: typeof fetch };
230
+ const originalFetch = runtime.fetch;
231
+ let attempts = 0;
232
+ let streamCancelled = 0;
233
+ runtime.fetch = (async () => {
234
+ attempts++;
235
+ return new Response(
236
+ new ReadableStream<Uint8Array>({
237
+ start(controller) {
238
+ controller.enqueue(new TextEncoder().encode("not-json"));
239
+ },
240
+ cancel() {
241
+ streamCancelled++;
242
+ },
243
+ }),
244
+ { status: 503, statusText: "Service Unavailable" },
245
+ );
246
+ }) as typeof fetch;
247
+
248
+ try {
249
+ const error = await expectTipError(
250
+ getMidnightTip({ indexer: "http://127.0.0.1:10001/graphql" }),
251
+ "HTTP",
252
+ );
253
+ expect(error.status).toBe(503);
254
+ expect(error.statusText).toBe("Service Unavailable");
255
+ expect(attempts).toBe(1);
256
+ expect(streamCancelled).toBe(1);
257
+ } finally {
258
+ runtime.fetch = originalFetch;
259
+ }
260
+ });
261
+
262
+ test("GraphQL errors win over partial data", async () => {
263
+ const fixture = await startFixture(() =>
264
+ json({
265
+ errors: [{ message: "bad query" }],
266
+ data: { block: { height: 42 } },
267
+ }));
268
+
269
+ const error = await expectTipError(getMidnightTip({ indexer: fixture.url }), "GRAPHQL");
270
+ expect(error.graphqlErrors).toEqual([{ message: "bad query" }]);
271
+ expect(Object.isFrozen(error.graphqlErrors)).toBe(true);
272
+ expect(error.status).toBeUndefined();
273
+ expect(error.cause).toBeUndefined();
274
+ expect(fixture.hits).toBe(1);
275
+ await expectFixtureIdle(fixture);
276
+ });
277
+
278
+ test("GraphQL error details are a frozen shallow copy", async () => {
279
+ const runtime = globalThis as typeof globalThis & { fetch: typeof fetch };
280
+ const originalFetch = runtime.fetch;
281
+ const detail = { message: "same object" };
282
+ const details = [detail];
283
+ let attempts = 0;
284
+ runtime.fetch = (async () => {
285
+ attempts++;
286
+ return {
287
+ ok: true,
288
+ status: 200,
289
+ statusText: "OK",
290
+ json: async () => ({ errors: details }),
291
+ } as Response;
292
+ }) as typeof fetch;
293
+
294
+ try {
295
+ const error = await expectTipError(
296
+ getMidnightTip({ indexer: "http://127.0.0.1:10001/graphql" }),
297
+ "GRAPHQL",
298
+ );
299
+ expect(attempts).toBe(1);
300
+ expect(error.graphqlErrors).not.toBe(details);
301
+ expect(error.graphqlErrors?.[0]).toBe(detail);
302
+ expect(Object.isFrozen(error.graphqlErrors)).toBe(true);
303
+ } finally {
304
+ runtime.fetch = originalFetch;
305
+ }
306
+ });
307
+
308
+ test("an empty GraphQL errors array does not mask valid data", async () => {
309
+ const fixture = await startFixture(() =>
310
+ json({ errors: [], data: { block: { height: 42 } } }));
311
+ await expect(getMidnightTip({ indexer: fixture.url })).resolves.toEqual({ height: 42 });
312
+ expect(fixture.hits).toBe(1);
313
+ await expectFixtureIdle(fixture);
314
+ });
315
+
316
+ test("malformed JSON is INVALID_RESPONSE and preserves the parser cause", async () => {
317
+ const fixture = await startFixture(() =>
318
+ new Response("{", { headers: { "Content-Type": "application/json" } }));
319
+ const error = await expectTipError(
320
+ getMidnightTip({ indexer: fixture.url }),
321
+ "INVALID_RESPONSE",
322
+ );
323
+ expect(error.cause).toBeInstanceOf(Error);
324
+ expect(error.status).toBeUndefined();
325
+ expect(error.graphqlErrors).toBeUndefined();
326
+ expect(fixture.hits).toBe(1);
327
+ await expectFixtureIdle(fixture);
328
+ });
329
+
330
+ test.each([
331
+ ["array body", "[]"],
332
+ ["null body", "null"],
333
+ ["scalar body", "true"],
334
+ ["present non-array errors", '{"errors":{}}'],
335
+ ["present null errors", '{"errors":null}'],
336
+ ["missing data", "{}"],
337
+ ["null data", '{"data":null}'],
338
+ ["array data", '{"data":[]}'],
339
+ ["missing block", '{"data":{}}'],
340
+ ["null block", '{"data":{"block":null}}'],
341
+ ["array block", '{"data":{"block":[]}}'],
342
+ ["missing height", '{"data":{"block":{}}}'],
343
+ ["null height", '{"data":{"block":{"height":null}}}'],
344
+ ["string height", '{"data":{"block":{"height":"12"}}}'],
345
+ ["fractional height", '{"data":{"block":{"height":1.5}}}'],
346
+ ["negative height", '{"data":{"block":{"height":-1}}}'],
347
+ ["non-finite height", '{"data":{"block":{"height":1e400}}}'],
348
+ ["unsafe height", '{"data":{"block":{"height":9007199254740992}}}'],
349
+ ])("rejects %s without retry or partial data", async (_name, rawBody) => {
350
+ const fixture = await startFixture(() =>
351
+ new Response(rawBody, { headers: { "Content-Type": "application/json" } }));
352
+ const error = await expectTipError(
353
+ getMidnightTip({ indexer: fixture.url }),
354
+ "INVALID_RESPONSE",
355
+ );
356
+ expect(error.status).toBeUndefined();
357
+ expect(error.graphqlErrors).toBeUndefined();
358
+ expect(fixture.hits).toBe(1);
359
+ await expectFixtureIdle(fixture);
360
+ });
361
+ });
362
+
363
+ describe("getMidnightTip option and transport failures", () => {
364
+ test.each([
365
+ ["missing options", undefined],
366
+ ["null options", null],
367
+ ["missing indexer", {}],
368
+ ["non-string indexer", { indexer: new URL("http://127.0.0.1:10001") }],
369
+ ["empty URL", { indexer: "" }],
370
+ ["relative URL", { indexer: "/graphql" }],
371
+ ["unsupported protocol", { indexer: "ftp://127.0.0.1:10001/graphql" }],
372
+ ["zero timeout", { indexer: "http://127.0.0.1:10001", requestTimeoutMs: 0 }],
373
+ ["negative timeout", { indexer: "http://127.0.0.1:10001", requestTimeoutMs: -1 }],
374
+ ["fractional timeout", { indexer: "http://127.0.0.1:10001", requestTimeoutMs: 1.5 }],
375
+ ["infinite timeout", { indexer: "http://127.0.0.1:10001", requestTimeoutMs: Infinity }],
376
+ ["NaN timeout", { indexer: "http://127.0.0.1:10001", requestTimeoutMs: NaN }],
377
+ ["null timeout", { indexer: "http://127.0.0.1:10001", requestTimeoutMs: null }],
378
+ [
379
+ "unsafe timeout",
380
+ { indexer: "http://127.0.0.1:10001", requestTimeoutMs: Number.MAX_SAFE_INTEGER + 1 },
381
+ ],
382
+ ["invalid signal", { indexer: "http://127.0.0.1:10001", signal: {} }],
383
+ ["null signal", { indexer: "http://127.0.0.1:10001", signal: null }],
384
+ ])("validates %s before timer or I/O allocation", async (_name, options) => {
385
+ const runtime = globalThis as any;
386
+ const originalFetch = runtime.fetch;
387
+ const originalSetTimeout = runtime.setTimeout;
388
+ let fetches = 0;
389
+ let timers = 0;
390
+ runtime.fetch = () => {
391
+ fetches++;
392
+ throw new Error("fetch must not be called");
393
+ };
394
+ runtime.setTimeout = (...args: unknown[]) => {
395
+ timers++;
396
+ return originalSetTimeout(...args);
397
+ };
398
+
399
+ try {
400
+ await expectTipError(getMidnightTip(options as any), "INVALID_OPTIONS");
401
+ expect(fetches).toBe(0);
402
+ expect(timers).toBe(0);
403
+ } finally {
404
+ runtime.fetch = originalFetch;
405
+ runtime.setTimeout = originalSetTimeout;
406
+ }
407
+ });
408
+
409
+ test("already-aborted input preserves its reason and performs no timer or fetch", async () => {
410
+ const runtime = globalThis as any;
411
+ const originalFetch = runtime.fetch;
412
+ const originalSetTimeout = runtime.setTimeout;
413
+ const caller = trackedAbortSignal();
414
+ const reason = new Error("caller stopped");
415
+ let fetches = 0;
416
+ let timers = 0;
417
+ caller.abort(reason);
418
+ runtime.fetch = () => {
419
+ fetches++;
420
+ throw new Error("fetch must not be called");
421
+ };
422
+ runtime.setTimeout = (...args: unknown[]) => {
423
+ timers++;
424
+ return originalSetTimeout(...args);
425
+ };
426
+
427
+ try {
428
+ const error = await expectTipError(
429
+ getMidnightTip({ indexer: "http://127.0.0.1:10001", signal: caller.signal }),
430
+ "ABORTED",
431
+ );
432
+ expect(error.cause).toBe(reason);
433
+ expect(error.status).toBeUndefined();
434
+ expect(error.graphqlErrors).toBeUndefined();
435
+ expect(fetches).toBe(0);
436
+ expect(timers).toBe(0);
437
+ expect(caller.listenerCount()).toBe(0);
438
+ } finally {
439
+ runtime.fetch = originalFetch;
440
+ runtime.setTimeout = originalSetTimeout;
441
+ }
442
+ });
443
+
444
+ test("a local connection rejection is NETWORK with the original cause and one attempt", async () => {
445
+ const fixture = await startFixture(() => json({ data: { block: { height: 1 } } }));
446
+ const { url, port } = fixture;
447
+ await stopFixture(fixture);
448
+
449
+ const runtime = globalThis as typeof globalThis & { fetch: typeof fetch };
450
+ const originalFetch = runtime.fetch;
451
+ let attempts = 0;
452
+ runtime.fetch = ((input: URL | RequestInfo, init?: RequestInit) => {
453
+ attempts++;
454
+ return originalFetch(input, init);
455
+ }) as typeof fetch;
456
+
457
+ try {
458
+ const error = await expectTipError(
459
+ getMidnightTip({ indexer: url, requestTimeoutMs: 500 }),
460
+ "NETWORK",
461
+ );
462
+ expect(attempts).toBe(1);
463
+ expect(error.cause).toBeDefined();
464
+ expect(error.status).toBeUndefined();
465
+ expect(error.graphqlErrors).toBeUndefined();
466
+ } finally {
467
+ runtime.fetch = originalFetch;
468
+ }
469
+
470
+ const rebound = Bun.serve({
471
+ hostname: "127.0.0.1",
472
+ port,
473
+ fetch: () => new Response(),
474
+ });
475
+ await rebound.stop(true);
476
+ });
477
+ });
478
+
479
+ describe("getMidnightTip cancellation, deadline, and cleanup", () => {
480
+ async function stalledResponse(request: Request): Promise<Response> {
481
+ await new Promise<void>((resolve) => {
482
+ if (request.signal.aborted) {
483
+ resolve();
484
+ return;
485
+ }
486
+ const onAbort = () => {
487
+ request.signal.removeEventListener("abort", onAbort);
488
+ resolve();
489
+ };
490
+ request.signal.addEventListener("abort", onAbort, { once: true });
491
+ });
492
+ return new Response(null, { status: 499 });
493
+ }
494
+
495
+ test("caller abort wins after one request and releases request/listener/timer ownership", async () => {
496
+ const fixture = await startFixture(stalledResponse);
497
+ const caller = trackedAbortSignal();
498
+ const reason = { caller: true };
499
+ const pending = getMidnightTip({
500
+ indexer: fixture.url,
501
+ signal: caller.signal,
502
+ requestTimeoutMs: 1_000,
503
+ });
504
+ await waitFor(() => fixture.hits === 1, "caller-abort fixture request");
505
+ expect(caller.listenerCount()).toBe(1);
506
+ caller.abort(reason);
507
+
508
+ const error = await expectTipError(pending, "ABORTED");
509
+ expect(error.cause).toBe(reason);
510
+ expect(error.status).toBeUndefined();
511
+ expect(error.graphqlErrors).toBeUndefined();
512
+ expect(fixture.hits).toBe(1);
513
+ expect(caller.listenerCount()).toBe(0);
514
+ await expectFixtureIdle(fixture);
515
+ });
516
+
517
+ test("timeout wins after one request and stays authoritative over a later caller abort", async () => {
518
+ const fixture = await startFixture(stalledResponse);
519
+ const caller = trackedAbortSignal();
520
+ const lateReason = new Error("late caller abort");
521
+ const pending = getMidnightTip({
522
+ indexer: fixture.url,
523
+ signal: caller.signal,
524
+ requestTimeoutMs: 300,
525
+ });
526
+ await waitFor(() => fixture.hits === 1, "timeout fixture request");
527
+
528
+ const error = await expectTipError(pending, "TIMEOUT");
529
+ caller.abort(lateReason);
530
+ expect(error.cause).toBeUndefined();
531
+ expect(error.status).toBeUndefined();
532
+ expect(error.graphqlErrors).toBeUndefined();
533
+ expect(fixture.hits).toBe(1);
534
+ expect(caller.listenerCount()).toBe(0);
535
+ await expectFixtureIdle(fixture);
536
+ });
537
+
538
+ test("default and host-boundary deadlines are exact, chunked, and cleared", async () => {
539
+ type CapturedTimer = {
540
+ callback: () => void;
541
+ delay: number;
542
+ cleared: boolean;
543
+ };
544
+ const runtime = globalThis as any;
545
+ const originalSetTimeout = runtime.setTimeout;
546
+ const originalClearTimeout = runtime.clearTimeout;
547
+ const originalFetch = runtime.fetch;
548
+ const timers: CapturedTimer[] = [];
549
+ let fetchMode: "pending" | "success" = "pending";
550
+ let activeFetches = 0;
551
+
552
+ runtime.setTimeout = (callback: () => void, delay: number) => {
553
+ const timer = { callback, delay, cleared: false };
554
+ timers.push(timer);
555
+ return timer;
556
+ };
557
+ runtime.clearTimeout = (timer: CapturedTimer) => {
558
+ timer.cleared = true;
559
+ };
560
+ runtime.fetch = (_input: unknown, init?: RequestInit) => {
561
+ if (fetchMode === "success") {
562
+ return Promise.resolve(json({ data: { block: { height: 99 } } }));
563
+ }
564
+ activeFetches++;
565
+ const signal = init?.signal;
566
+ return new Promise<Response>((_resolve, reject) => {
567
+ const rejectAbort = () => {
568
+ activeFetches--;
569
+ reject(signal?.reason);
570
+ };
571
+ if (signal?.aborted) rejectAbort();
572
+ else signal?.addEventListener("abort", rejectAbort, { once: true });
573
+ });
574
+ };
575
+
576
+ try {
577
+ fetchMode = "success";
578
+ await expect(getMidnightTip({
579
+ indexer: "http://127.0.0.1:10001/graphql",
580
+ })).resolves.toEqual({ height: 99 });
581
+ expect(timers).toHaveLength(1);
582
+ expect(timers[0].delay).toBe(15_000);
583
+ expect(timers[0].cleared).toBe(true);
584
+
585
+ timers.length = 0;
586
+ fetchMode = "pending";
587
+ const exactMax = getMidnightTip({
588
+ indexer: "http://127.0.0.1:10001/graphql",
589
+ requestTimeoutMs: 2_147_483_647,
590
+ });
591
+ expect(timers).toHaveLength(1);
592
+ expect(timers[0].delay).toBe(2_147_483_647);
593
+ timers[0].callback();
594
+ await expectTipError(exactMax, "TIMEOUT");
595
+ expect(activeFetches).toBe(0);
596
+
597
+ timers.length = 0;
598
+ const aboveMax = getMidnightTip({
599
+ indexer: "http://127.0.0.1:10001/graphql",
600
+ requestTimeoutMs: 2_147_483_648,
601
+ });
602
+ let aboveMaxSettled = false;
603
+ void aboveMax.then(
604
+ () => { aboveMaxSettled = true; },
605
+ () => { aboveMaxSettled = true; },
606
+ );
607
+ expect(timers).toHaveLength(1);
608
+ expect(timers[0].delay).toBe(2_147_483_647);
609
+ timers[0].callback();
610
+ expect(timers).toHaveLength(2);
611
+ expect(timers[1].delay).toBe(1);
612
+ await Promise.resolve();
613
+ expect(aboveMaxSettled).toBe(false);
614
+ timers[1].callback();
615
+ await expectTipError(aboveMax, "TIMEOUT");
616
+ expect(activeFetches).toBe(0);
617
+
618
+ timers.length = 0;
619
+ const caller = trackedAbortSignal();
620
+ const reason = new Error("caller cancelled long deadline");
621
+ const aborted = getMidnightTip({
622
+ indexer: "http://127.0.0.1:10001/graphql",
623
+ requestTimeoutMs: 2_147_483_648,
624
+ signal: caller.signal,
625
+ });
626
+ expect(caller.listenerCount()).toBe(1);
627
+ expect(timers[0].delay).toBe(2_147_483_647);
628
+ caller.abort(reason);
629
+ const abortedError = await expectTipError(aborted, "ABORTED");
630
+ expect(abortedError.cause).toBe(reason);
631
+ expect(timers[0].cleared).toBe(true);
632
+ expect(caller.listenerCount()).toBe(0);
633
+ expect(activeFetches).toBe(0);
634
+ } finally {
635
+ runtime.setTimeout = originalSetTimeout;
636
+ runtime.clearTimeout = originalClearTimeout;
637
+ runtime.fetch = originalFetch;
638
+ }
639
+ });
640
+
641
+ test("deadline and caller listener are cleared across non-cancellation settlements", async () => {
642
+ type CapturedTimer = { cleared: boolean };
643
+ const runtime = globalThis as any;
644
+ const originalSetTimeout = runtime.setTimeout;
645
+ const originalClearTimeout = runtime.clearTimeout;
646
+ const originalFetch = runtime.fetch;
647
+ const outcomes: Array<{
648
+ code?: MidnightTipError["code"];
649
+ fetch: () => Promise<Response>;
650
+ }> = [
651
+ { fetch: async () => json({ data: { block: { height: 7 } } }) },
652
+ { code: "NETWORK", fetch: async () => { throw new Error("network"); } },
653
+ { code: "HTTP", fetch: async () => new Response(null, { status: 503 }) },
654
+ { code: "GRAPHQL", fetch: async () => json({ errors: [{ message: "bad" }] }) },
655
+ { code: "INVALID_RESPONSE", fetch: async () => json({ data: null }) },
656
+ ];
657
+
658
+ try {
659
+ for (const outcome of outcomes) {
660
+ const timers: CapturedTimer[] = [];
661
+ const caller = trackedAbortSignal();
662
+ let attempts = 0;
663
+ runtime.setTimeout = () => {
664
+ const timer = { cleared: false };
665
+ timers.push(timer);
666
+ return timer;
667
+ };
668
+ runtime.clearTimeout = (timer: CapturedTimer) => {
669
+ timer.cleared = true;
670
+ };
671
+ runtime.fetch = () => {
672
+ attempts++;
673
+ return outcome.fetch();
674
+ };
675
+
676
+ const pending = getMidnightTip({
677
+ indexer: "http://127.0.0.1:10001/graphql",
678
+ signal: caller.signal,
679
+ });
680
+ if (outcome.code) await expectTipError(pending, outcome.code);
681
+ else await expect(pending).resolves.toEqual({ height: 7 });
682
+
683
+ expect(attempts).toBe(1);
684
+ expect(timers).toHaveLength(1);
685
+ expect(timers[0].cleared).toBe(true);
686
+ expect(caller.listenerCount()).toBe(0);
687
+ }
688
+ } finally {
689
+ runtime.setTimeout = originalSetTimeout;
690
+ runtime.clearTimeout = originalClearTimeout;
691
+ runtime.fetch = originalFetch;
692
+ }
693
+ });
694
+ });