@fedify/fedify 2.5.0-dev.2597 → 2.5.0-dev.2614

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.
Files changed (123) hide show
  1. package/dist/{builder-Da_V_LDj.mjs → builder-B2H8GsyI.mjs} +82 -3
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{compound-proof-BqqP38Sf.mjs → compound-proof-BavXW-ow.mjs} +2 -2
  6. package/dist/{context-ByflxsY9.d.cts → context-C1NqcIct.d.cts} +1061 -443
  7. package/dist/{context-uQUY5fQT.d.ts → context-xrqq4Hrq.d.ts} +1064 -446
  8. package/dist/{deno-4cGKg59R.mjs → deno-CRXki9-m.mjs} +1 -1
  9. package/dist/{docloader-BiHD6NSO.mjs → docloader-Ca6i6ddp.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/compound-fanout.test.mjs +3 -3
  12. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  13. package/dist/federation/handler.test.mjs +4 -4
  14. package/dist/federation/hashlink-media.test.mjs +2 -2
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/inbox-actorless.test.mjs +5 -5
  17. package/dist/federation/inbox-report.test.mjs +220 -5
  18. package/dist/federation/inbox-view.test.mjs +1 -1
  19. package/dist/federation/inbox.test.mjs +1 -1
  20. package/dist/federation/metrics.test.mjs +1 -1
  21. package/dist/federation/middleware.test.mjs +7 -7
  22. package/dist/federation/mod.cjs +1 -1
  23. package/dist/federation/mod.d.cts +3 -3
  24. package/dist/federation/mod.d.ts +4 -4
  25. package/dist/federation/mod.js +1 -1
  26. package/dist/federation/mq.test.mjs +1 -1
  27. package/dist/federation/outbound-body.test.d.mts +2 -0
  28. package/dist/federation/outbound-body.test.mjs +149 -0
  29. package/dist/federation/outbound-capture.test.d.mts +2 -0
  30. package/dist/federation/outbound-capture.test.mjs +394 -0
  31. package/dist/federation/outbound-dispatch.test.d.mts +2 -0
  32. package/dist/federation/outbound-dispatch.test.mjs +368 -0
  33. package/dist/federation/outbound-document.test.d.mts +2 -0
  34. package/dist/federation/outbound-document.test.mjs +171 -0
  35. package/dist/federation/outbound-provenance.test.d.mts +2 -0
  36. package/dist/federation/outbound-provenance.test.mjs +543 -0
  37. package/dist/federation/outbound-report.test.d.mts +2 -0
  38. package/dist/federation/outbound-report.test.mjs +5475 -0
  39. package/dist/federation/outbound-workers.test.d.mts +2 -0
  40. package/dist/federation/outbound-workers.test.mjs +64 -0
  41. package/dist/federation/outbound-workflow.test.d.mts +2 -0
  42. package/dist/federation/outbound-workflow.test.mjs +154 -0
  43. package/dist/federation/outgoing-proof.test.mjs +4 -4
  44. package/dist/federation/portable-actor.test.mjs +3 -3
  45. package/dist/federation/portable-chain.test.mjs +3 -3
  46. package/dist/federation/portable-collection.test.mjs +4 -4
  47. package/dist/federation/portable-delivery.test.mjs +106 -7
  48. package/dist/federation/portable-gateway-keys.test.mjs +5 -5
  49. package/dist/federation/portable-inbox.test.mjs +430 -5
  50. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  51. package/dist/federation/portable-route-activity.test.mjs +2 -2
  52. package/dist/federation/portable.test.mjs +3 -3
  53. package/dist/federation/send.test.mjs +44 -3
  54. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  55. package/dist/federation/tasks/tasks.test.mjs +2 -2
  56. package/dist/federation/temporal.test.mjs +1 -1
  57. package/dist/federation/webfinger.test.mjs +1 -1
  58. package/dist/{federation-BKJZEC0O.cjs → federation-DRl81KbJ.cjs} +2358 -405
  59. package/dist/{http-DQP4rw46.js → http-C2H4DT6t.js} +62 -32
  60. package/dist/{http-KU7_TLD9.cjs → http-DHSIMipE.cjs} +62 -32
  61. package/dist/{http-D5n4S1oL.mjs → http-DqOXkYbX.mjs} +64 -34
  62. package/dist/{inbox-CElAglrC.mjs → inbox-C4iW1g0F.mjs} +90 -3
  63. package/dist/{inbox-view-BTUuIIf6.mjs → inbox-view-Bu_aSwFJ.mjs} +1 -1
  64. package/dist/{key-DbLNT-vA.d.ts → key-dwwAg5bA.d.ts} +1 -1
  65. package/dist/{kv-cache-CFBwLUZr.js → kv-cache-B-x_OrBo.js} +1 -1
  66. package/dist/{kv-cache-DrQn-ugb.cjs → kv-cache-CHR_BVyM.cjs} +1 -1
  67. package/dist/{kv-cache-BV55jAE6.mjs → kv-cache-DJsxu9nI.mjs} +1 -1
  68. package/dist/{ld-CmAQYDWE.mjs → ld-C5JOBBFj.mjs} +2 -2
  69. package/dist/{ld-Inagww4R.d.ts → ld-o4LnhCYY.d.ts} +2 -2
  70. package/dist/{metrics-BvV60TwE.mjs → metrics-CTteRnEr.mjs} +1 -1
  71. package/dist/{metrics-DoRtI0qU.d.ts → metrics-DVKYY6K1.d.ts} +1 -1
  72. package/dist/{middleware-Di4y_M3y.js → middleware-B-tMVSUA.js} +2303 -351
  73. package/dist/{middleware-D02eC0j7.mjs → middleware-BkRqMGPU.mjs} +1573 -325
  74. package/dist/{middleware-iAJcDIM2.mjs → middleware-CPigKLnt.mjs} +1 -1
  75. package/dist/{mod-B7j30IQf.d.ts → mod-CQC9xlLZ.d.ts} +3 -3
  76. package/dist/{mod-B4kBw1-8.d.ts → mod-D22ZsiKi.d.ts} +2 -2
  77. package/dist/{mod-BN0TTfyQ.d.ts → mod-DO0epJGH.d.ts} +1 -1
  78. package/dist/{mod-DF2gbN_d.d.cts → mod-TwiQgEf3.d.cts} +1 -1
  79. package/dist/mod.cjs +4 -4
  80. package/dist/mod.d.cts +4 -4
  81. package/dist/mod.d.ts +8 -8
  82. package/dist/mod.js +4 -4
  83. package/dist/nodeinfo/handler.test.mjs +1 -1
  84. package/dist/outbound-body-CckPA5gc.mjs +83 -0
  85. package/dist/outbound-capture-DVgnRzlf.mjs +184 -0
  86. package/dist/outbound-document-BMgwLXTY.mjs +179 -0
  87. package/dist/{owner-C1-csjTo.mjs → owner-CKlRIkeF.mjs} +3 -3
  88. package/dist/{portable-collection-C0MNEEED.mjs → portable-collection-B7RBHVGl.mjs} +2 -2
  89. package/dist/{portable-collection-DkTLVfoM.js → portable-collection-CByyPjLc.js} +2 -2
  90. package/dist/{portable-collection-w6Qn-vJ3.cjs → portable-collection-CmLjeKsP.cjs} +2 -2
  91. package/dist/{portable-key-jtaxB7TD.mjs → portable-key-Clg0IH8C.mjs} +1 -1
  92. package/dist/{proof-CzWCRhnC.mjs → proof-tAb5_Dq4.mjs} +3 -3
  93. package/dist/{send-C9-dwpEH.mjs → send-DzfFREML.mjs} +153 -40
  94. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  95. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  96. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  97. package/dist/sig/compound-proof.test.mjs +2 -2
  98. package/dist/sig/http.test.mjs +38 -2
  99. package/dist/sig/key.test.mjs +1 -1
  100. package/dist/sig/ld.test.mjs +1 -1
  101. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  102. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  103. package/dist/sig/mod.cjs +2 -2
  104. package/dist/sig/mod.d.ts +3 -3
  105. package/dist/sig/mod.js +2 -2
  106. package/dist/sig/owner.test.mjs +1 -1
  107. package/dist/sig/portable-collection.test.mjs +1 -1
  108. package/dist/sig/portable-interop.test.mjs +3 -3
  109. package/dist/sig/portable-key.test.mjs +5 -5
  110. package/dist/sig/proof.test.mjs +3 -3
  111. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  112. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  113. package/dist/sig/verification.test.mjs +3 -3
  114. package/dist/{temporal-lDCl8Zxn.mjs → temporal-D9Nt8Fm4.mjs} +1 -1
  115. package/dist/testing/kv.test.mjs +1 -1
  116. package/dist/testing/mod.d.mts +1030 -412
  117. package/dist/utils/docloader.test.mjs +2 -2
  118. package/dist/utils/kv-cache.test.mjs +1 -1
  119. package/dist/utils/mod.cjs +1 -1
  120. package/dist/utils/mod.d.ts +1 -1
  121. package/dist/utils/mod.js +1 -1
  122. package/package.json +6 -6
  123. package/skills/fedify/SKILL.md +60 -1
@@ -0,0 +1,149 @@
1
+ import "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as ResponseBodyPump } from "../outbound-body-CckPA5gc.mjs";
5
+ import { test } from "@fedify/fixture";
6
+ import { deepStrictEqual, ok, rejects, strictEqual } from "node:assert/strict";
7
+ //#region src/federation/outbound-body.test.ts
8
+ test("response body pump pulls once on demand and releases at EOF", async () => {
9
+ let pulls = 0;
10
+ let cancels = 0;
11
+ let controller;
12
+ let pulled;
13
+ const pulling = new Promise((resolve) => {
14
+ pulled = resolve;
15
+ });
16
+ const body = new ReadableStream({
17
+ start(value) {
18
+ controller = value;
19
+ },
20
+ pull() {
21
+ pulls++;
22
+ pulled();
23
+ },
24
+ cancel() {
25
+ cancels++;
26
+ }
27
+ }, { highWaterMark: 0 });
28
+ const pump = new ResponseBodyPump(body, 1e3);
29
+ try {
30
+ strictEqual(pulls, 0);
31
+ ok(body.locked);
32
+ const first = pump.read();
33
+ const concurrent = pump.read();
34
+ strictEqual(first, concurrent);
35
+ await pulling;
36
+ strictEqual(pulls, 1);
37
+ controller.enqueue(new Uint8Array([1, 2]));
38
+ const result = await first;
39
+ deepStrictEqual(result, {
40
+ done: false,
41
+ value: new Uint8Array([1, 2])
42
+ });
43
+ strictEqual(await concurrent, result);
44
+ strictEqual(pulls, 1);
45
+ controller.close();
46
+ deepStrictEqual(await pump.read(), {
47
+ done: true,
48
+ value: void 0
49
+ });
50
+ strictEqual(body.locked, false);
51
+ strictEqual(cancels, 0);
52
+ deepStrictEqual(await pump.read(), {
53
+ done: true,
54
+ value: void 0
55
+ });
56
+ } finally {
57
+ pump.close();
58
+ }
59
+ });
60
+ test("response body pump deadline bounds stalled reads and cancellation cleanup", async (t) => {
61
+ for (const mode of [
62
+ "no-pull",
63
+ "stalled",
64
+ "late-rejection"
65
+ ]) await t.step(mode, async () => {
66
+ let pulls = 0;
67
+ let cancels = 0;
68
+ let cancelled;
69
+ const cancelling = new Promise((resolve) => {
70
+ cancelled = resolve;
71
+ });
72
+ let rejectCancel;
73
+ const cleanup = new Promise((_resolve, reject) => {
74
+ rejectCancel = reject;
75
+ });
76
+ const body = new ReadableStream({
77
+ pull() {
78
+ pulls++;
79
+ },
80
+ cancel() {
81
+ cancels++;
82
+ cancelled();
83
+ return cleanup;
84
+ }
85
+ }, { highWaterMark: 0 });
86
+ const pump = new ResponseBodyPump(body, 25);
87
+ try {
88
+ if (mode === "no-pull") await cancelling;
89
+ else strictEqual(await pump.read(), void 0);
90
+ strictEqual(cancels, 1);
91
+ strictEqual(body.locked, false);
92
+ strictEqual(pulls, mode === "no-pull" ? 0 : 1);
93
+ strictEqual(await pump.read(), void 0);
94
+ pump.close();
95
+ strictEqual(cancels, 1);
96
+ if (mode === "late-rejection") {
97
+ rejectCancel(/* @__PURE__ */ new Error("late cleanup failure"));
98
+ await Promise.resolve();
99
+ await Promise.resolve();
100
+ }
101
+ } finally {
102
+ pump.close();
103
+ }
104
+ });
105
+ });
106
+ test("response body pump deadline also stops continuously ready empty chunks", async () => {
107
+ let pulls = 0;
108
+ let cancels = 0;
109
+ const body = new ReadableStream({
110
+ pull(controller) {
111
+ pulls++;
112
+ controller.enqueue(/* @__PURE__ */ new Uint8Array());
113
+ },
114
+ cancel() {
115
+ cancels++;
116
+ }
117
+ }, { highWaterMark: 0 });
118
+ const pump = new ResponseBodyPump(body, 25);
119
+ try {
120
+ while (await pump.read() != null);
121
+ ok(pulls > 0);
122
+ strictEqual(cancels, 1);
123
+ strictEqual(body.locked, false);
124
+ const stoppedAt = pulls;
125
+ strictEqual(await pump.read(), void 0);
126
+ strictEqual(pulls, stoppedAt);
127
+ } finally {
128
+ pump.close();
129
+ }
130
+ });
131
+ test("response body pump preserves read failure and releases ownership", async () => {
132
+ const failure = /* @__PURE__ */ new Error("broken body");
133
+ const body = new ReadableStream({ pull(controller) {
134
+ controller.error(failure);
135
+ } }, { highWaterMark: 0 });
136
+ const pump = new ResponseBodyPump(body, 1e3);
137
+ try {
138
+ await rejects(pump.read(), (error) => error === failure);
139
+ strictEqual(body.locked, false);
140
+ deepStrictEqual(await pump.read(), {
141
+ done: true,
142
+ value: void 0
143
+ });
144
+ } finally {
145
+ pump.close();
146
+ }
147
+ });
148
+ //#endregion
149
+ export {};
@@ -0,0 +1,2 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import { URLPattern } from "urlpattern-polyfill";
@@ -0,0 +1,394 @@
1
+ import "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { n as snapshotBodyCapture, r as takeCapturedDiagnostic, t as captureResponseBody } from "../outbound-capture-DVgnRzlf.mjs";
5
+ import { n as FederationBuilderImpl, r as createFederationBuilder } from "../builder-B2H8GsyI.mjs";
6
+ import { test } from "@fedify/fixture";
7
+ import { deepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert/strict";
8
+ //#region src/federation/outbound-capture.test.ts
9
+ const limits = {
10
+ maxBytes: 32,
11
+ timeoutMs: 1e3
12
+ };
13
+ async function drain(body) {
14
+ const reader = body.getReader();
15
+ const bytes = [];
16
+ try {
17
+ while (true) {
18
+ const value = await reader.read();
19
+ if (value.done) return bytes;
20
+ bytes.push(...value.value);
21
+ }
22
+ } catch {
23
+ return bytes;
24
+ } finally {
25
+ reader.releaseLock();
26
+ }
27
+ }
28
+ test("response body capture validates explicit limits before replacing registration", () => {
29
+ const config = {
30
+ maxBytes: 32,
31
+ timeoutMs: .5,
32
+ onBody() {}
33
+ };
34
+ const builder = createFederationBuilder();
35
+ ok(builder instanceof FederationBuilderImpl);
36
+ builder.setOutboundObservers({ responseBodyCapture: config });
37
+ const original = builder.outboundObservers;
38
+ deepStrictEqual(original.responseBodyCapture, {
39
+ ...config,
40
+ timeoutMs: 1,
41
+ responses: "all"
42
+ });
43
+ ok(Object.isFrozen(original.responseBodyCapture));
44
+ config.maxBytes = 1;
45
+ strictEqual(original.responseBodyCapture?.maxBytes, 32);
46
+ for (const [value, error] of [
47
+ [null, TypeError],
48
+ [{ ...limits }, TypeError],
49
+ [{
50
+ onBody() {},
51
+ timeoutMs: 1
52
+ }, TypeError],
53
+ [{
54
+ onBody() {},
55
+ maxBytes: 1
56
+ }, TypeError],
57
+ [{
58
+ ...limits,
59
+ onBody() {},
60
+ maxBytes: "1"
61
+ }, TypeError],
62
+ [{
63
+ ...limits,
64
+ onBody() {},
65
+ maxBytes: 0
66
+ }, RangeError],
67
+ [{
68
+ ...limits,
69
+ onBody() {},
70
+ maxBytes: 1.5
71
+ }, RangeError],
72
+ [{
73
+ ...limits,
74
+ onBody() {},
75
+ maxBytes: Number.MAX_SAFE_INTEGER + 1
76
+ }, RangeError],
77
+ [{
78
+ ...limits,
79
+ onBody() {},
80
+ timeoutMs: Infinity
81
+ }, RangeError],
82
+ [{
83
+ ...limits,
84
+ onBody() {},
85
+ timeoutMs: NaN
86
+ }, RangeError],
87
+ [{
88
+ ...limits,
89
+ onBody() {},
90
+ timeoutMs: 0
91
+ }, RangeError],
92
+ [{
93
+ ...limits,
94
+ onBody() {},
95
+ timeoutMs: 2147483647.1
96
+ }, RangeError],
97
+ [{
98
+ ...limits,
99
+ onBody() {},
100
+ responses: "failures"
101
+ }, TypeError]
102
+ ]) {
103
+ throws(() => builder.setOutboundObservers({ responseBodyCapture: value }), error);
104
+ strictEqual(builder.outboundObservers, original);
105
+ }
106
+ strictEqual(snapshotBodyCapture(void 0), void 0);
107
+ });
108
+ test("response body capture distinguishes selection EOF and exact byte limits", async (t) => {
109
+ for (const mode of [
110
+ "filtered",
111
+ "no-body",
112
+ "empty",
113
+ "short",
114
+ "exact",
115
+ "over"
116
+ ]) await t.step(mode, async () => {
117
+ const response = new Response(mode === "no-body" ? null : mode === "empty" ? "" : mode === "over" ? "abcd" : "abc");
118
+ let calls = 0;
119
+ let exposed = [];
120
+ let signal;
121
+ const result = await captureResponseBody(response, {
122
+ maxBytes: mode === "exact" || mode === "over" ? 3 : 32,
123
+ timeoutMs: 1e3,
124
+ ...mode === "filtered" ? { responses: "non-success-http-responses" } : {}
125
+ }, async (body, current) => {
126
+ calls++;
127
+ signal = current;
128
+ exposed = await drain(body);
129
+ });
130
+ strictEqual(result.type, mode === "filtered" || mode === "no-body" ? mode : mode === "exact" || mode === "over" ? "limit-reached" : "complete");
131
+ strictEqual(result.bytesExposed, mode === "filtered" || mode === "no-body" || mode === "empty" ? 0 : 3);
132
+ strictEqual(calls, mode === "filtered" || mode === "no-body" ? 0 : 1);
133
+ if (calls) {
134
+ ok(signal?.aborted);
135
+ strictEqual(response.body?.locked, false);
136
+ }
137
+ if (result.bytesExposed) deepStrictEqual(exposed, [
138
+ 97,
139
+ 98,
140
+ 99
141
+ ]);
142
+ ok(Object.isFrozen(result));
143
+ });
144
+ });
145
+ test("response body capture revokes locked readers and bounds callback cleanup", async (t) => {
146
+ for (const mode of [
147
+ "returned",
148
+ "cancelled",
149
+ "throws",
150
+ "hangs",
151
+ "pending-read",
152
+ "body-error"
153
+ ]) await t.step(mode, async () => {
154
+ const failure = new Error(mode);
155
+ let signal;
156
+ let retained;
157
+ let pending;
158
+ let rejectLate;
159
+ const late = new Promise((_resolve, reject) => {
160
+ rejectLate = reject;
161
+ });
162
+ const body = new ReadableStream({
163
+ pull(controller) {
164
+ if (mode === "body-error") controller.error(failure);
165
+ },
166
+ cancel() {
167
+ return new Promise(() => {});
168
+ }
169
+ }, { highWaterMark: 0 });
170
+ const result = await captureResponseBody(new Response(body), {
171
+ ...limits,
172
+ timeoutMs: 25
173
+ }, async (stream, current) => {
174
+ signal = current;
175
+ retained = stream.getReader();
176
+ if (mode === "throws") throw failure;
177
+ if (mode === "cancelled") await retained.cancel(failure);
178
+ if (mode === "hangs") await late;
179
+ if (mode === "pending-read") pending = retained.read().catch((error) => error);
180
+ if (mode === "body-error") await retained.read();
181
+ });
182
+ const expected = mode === "returned" || mode === "pending-read" ? "callback-returned" : mode === "cancelled" ? "consumer-stopped" : mode === "hangs" ? "deadline" : "failed";
183
+ strictEqual(result.type, expected);
184
+ strictEqual(result.bytesExposed, 0);
185
+ ok(signal.aborted);
186
+ strictEqual(body.locked, false);
187
+ if (result.type === "failed") {
188
+ strictEqual(result.phase, mode === "throws" ? "observer" : "body");
189
+ strictEqual(result.error, failure);
190
+ }
191
+ if (result.type === "consumer-stopped") strictEqual(result.reason, failure);
192
+ if (mode !== "cancelled") await rejects(retained.read());
193
+ if (pending) ok(await pending instanceof Error);
194
+ retained.releaseLock();
195
+ if (mode === "hangs") {
196
+ rejectLate(failure);
197
+ await Promise.resolve();
198
+ await Promise.resolve();
199
+ }
200
+ });
201
+ });
202
+ test("response body capture shares one reader with bounded diagnostics", async () => {
203
+ const response = new Response("diagnostic failure", { status: 503 });
204
+ ok(response.body);
205
+ const getReader = response.body.getReader.bind(response.body);
206
+ let acquisitions = 0;
207
+ Object.defineProperty(response.body, "getReader", { value: () => {
208
+ acquisitions++;
209
+ return getReader();
210
+ } });
211
+ const summary = await captureResponseBody(response, {
212
+ ...limits,
213
+ maxBytes: 4
214
+ }, async (body) => {
215
+ await drain(body);
216
+ });
217
+ strictEqual(summary.type, "limit-reached");
218
+ strictEqual(summary.bytesExposed, 4);
219
+ strictEqual(acquisitions, 1);
220
+ strictEqual(takeCapturedDiagnostic(response), "diagnostic failure");
221
+ strictEqual(takeCapturedDiagnostic(response), void 0);
222
+ });
223
+ test("response body capture isolates unavailable readers and diagnostic deadlines", async (t) => {
224
+ await t.step("already locked source", async () => {
225
+ const response = new Response("locked");
226
+ const reader = response.body.getReader();
227
+ try {
228
+ const result = await captureResponseBody(response, limits, () => {
229
+ throw new Error("must not invoke callback");
230
+ });
231
+ ok(result.type === "failed");
232
+ strictEqual(result.phase, "body");
233
+ strictEqual(result.bytesExposed, 0);
234
+ } finally {
235
+ await reader.cancel();
236
+ reader.releaseLock();
237
+ }
238
+ });
239
+ await t.step("diagnostics retain their own deadline", async () => {
240
+ let cancels = 0;
241
+ const body = new ReadableStream({
242
+ pull() {},
243
+ cancel() {
244
+ cancels++;
245
+ return new Promise(() => {});
246
+ }
247
+ }, { highWaterMark: 0 });
248
+ const response = new Response(body, { status: 503 });
249
+ deepStrictEqual(await captureResponseBody(response, {
250
+ ...limits,
251
+ timeoutMs: 1e4
252
+ }, () => {}), {
253
+ type: "callback-returned",
254
+ bytesExposed: 0
255
+ });
256
+ strictEqual(takeCapturedDiagnostic(response), "… (timed out)");
257
+ strictEqual(cancels, 1);
258
+ strictEqual(body.locked, false);
259
+ });
260
+ });
261
+ test("response body capture bounds oversized and nonterminating sources", async (t) => {
262
+ for (const mode of [
263
+ "oversized",
264
+ "infinite",
265
+ "empty-infinite"
266
+ ]) await t.step(mode, async () => {
267
+ let pulls = 0;
268
+ let cancels = 0;
269
+ const source = new Uint8Array(mode === "oversized" ? 1024 * 1024 : mode === "infinite" ? 1 : 0).fill(255);
270
+ const original = new ReadableStream({
271
+ pull(controller) {
272
+ pulls++;
273
+ controller.enqueue(source);
274
+ },
275
+ cancel() {
276
+ cancels++;
277
+ }
278
+ }, { highWaterMark: 0 });
279
+ let exposed = 0;
280
+ const result = await captureResponseBody(new Response(original), {
281
+ maxBytes: 17,
282
+ timeoutMs: 50
283
+ }, async (body) => {
284
+ const reader = body.getReader();
285
+ try {
286
+ while (true) {
287
+ const chunk = await reader.read();
288
+ if (chunk.done) break;
289
+ exposed += chunk.value.length;
290
+ ok(chunk.value.buffer.byteLength <= 17);
291
+ strictEqual(chunk.value[0], 255);
292
+ chunk.value.fill(0);
293
+ }
294
+ } catch {} finally {
295
+ reader.releaseLock();
296
+ }
297
+ });
298
+ strictEqual(result.type, mode === "empty-infinite" ? "deadline" : "limit-reached");
299
+ strictEqual(result.bytesExposed, mode === "empty-infinite" ? 0 : 17);
300
+ strictEqual(exposed, result.bytesExposed);
301
+ strictEqual(cancels, 1);
302
+ strictEqual(original.locked, false);
303
+ if (mode !== "empty-infinite") {
304
+ strictEqual(pulls, mode === "oversized" ? 1 : 17);
305
+ strictEqual(source[0], 255);
306
+ }
307
+ });
308
+ });
309
+ test("response body capture applies backpressure and revokes a paused consumer", async () => {
310
+ let pulls = 0;
311
+ let cancels = 0;
312
+ let retained;
313
+ let exposed = 0;
314
+ const source = new ReadableStream({
315
+ pull(controller) {
316
+ pulls++;
317
+ controller.enqueue(new Uint8Array([1]));
318
+ },
319
+ cancel() {
320
+ cancels++;
321
+ return new Promise(() => {});
322
+ }
323
+ }, { highWaterMark: 0 });
324
+ deepStrictEqual(await captureResponseBody(new Response(source), {
325
+ maxBytes: 32,
326
+ timeoutMs: 50
327
+ }, async (body, signal) => {
328
+ retained = body.getReader();
329
+ const first = await retained.read();
330
+ ok(!first.done);
331
+ exposed += first.value.length;
332
+ await new Promise((resolve) => {
333
+ if (signal.aborted) resolve();
334
+ else signal.addEventListener("abort", () => resolve(), { once: true });
335
+ });
336
+ strictEqual(pulls, 1);
337
+ await rejects(retained.read());
338
+ }), {
339
+ type: "deadline",
340
+ bytesExposed: 1
341
+ });
342
+ strictEqual(exposed, 1);
343
+ strictEqual(pulls, 1);
344
+ strictEqual(cancels, 1);
345
+ strictEqual(source.locked, false);
346
+ await rejects(retained.read());
347
+ retained.releaseLock();
348
+ });
349
+ test("response body capture rejects late source data and observes late pull failure", async () => {
350
+ let controller;
351
+ let rejectPull;
352
+ const pulling = new Promise((_resolve, reject) => {
353
+ rejectPull = reject;
354
+ });
355
+ let cancels = 0;
356
+ const source = new ReadableStream({
357
+ start(value) {
358
+ controller = value;
359
+ },
360
+ pull() {
361
+ return pulling;
362
+ },
363
+ cancel() {
364
+ cancels++;
365
+ return new Promise(() => {});
366
+ }
367
+ }, { highWaterMark: 0 });
368
+ let retained;
369
+ let exposed = 0;
370
+ deepStrictEqual(await captureResponseBody(new Response(source), {
371
+ maxBytes: 32,
372
+ timeoutMs: 25
373
+ }, async (body) => {
374
+ retained = body.getReader();
375
+ try {
376
+ const chunk = await retained.read();
377
+ if (!chunk.done) exposed += chunk.value.length;
378
+ } catch {}
379
+ }), {
380
+ type: "deadline",
381
+ bytesExposed: 0
382
+ });
383
+ strictEqual(exposed, 0);
384
+ strictEqual(cancels, 1);
385
+ throws(() => controller.enqueue(new Uint8Array([1])), TypeError);
386
+ rejectPull(/* @__PURE__ */ new Error("late upstream failure"));
387
+ await Promise.resolve();
388
+ await Promise.resolve();
389
+ await rejects(retained.read());
390
+ retained.releaseLock();
391
+ strictEqual(source.locked, false);
392
+ });
393
+ //#endregion
394
+ export {};
@@ -0,0 +1,2 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import { URLPattern } from "urlpattern-polyfill";