@fedify/fedify 2.5.0-dev.2448 → 2.5.0-dev.2494
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 +3 -0
- package/dist/{builder-DbC8bn6k.mjs → builder-Y3pokaG2.mjs} +2 -2
- package/dist/{client-D-XP_Ccb.d.cts → client-3InDZbba.d.cts} +0 -1
- package/dist/{client-D-XP_Ccb.d.ts → client-3InDZbba.d.ts} +0 -1
- package/dist/{client-CEnNz8O7.mjs → client-5SIFzW3q.mjs} +113 -64
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-heeXW2_A.mjs → compound-proof-BM7AINht.mjs} +2 -2
- package/dist/{context-GlHrzdSP.d.ts → context-DQJE9YfH.d.ts} +29 -3
- package/dist/{context-BazwQrHn.d.cts → context-DSuL3YWx.d.cts} +29 -3
- package/dist/{deno-Dpewt_jJ.mjs → deno-D7blYLbb.mjs} +1 -1
- package/dist/docloader-DbBbDaY-.mjs +136 -0
- package/dist/{exporter-DThFfe7P.mjs → exporter-DVG68ozH.mjs} +36 -30
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +6 -3
- package/dist/federation/handler-compound-proof.test.mjs +3 -3
- package/dist/federation/handler.test.mjs +5 -5
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/inbox-actorless.test.mjs +5 -5
- package/dist/federation/inbox-report.test.mjs +5 -5
- package/dist/federation/inbox-view.test.mjs +1 -1
- package/dist/federation/inbox.test.mjs +1 -1
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +468 -33
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +3 -3
- package/dist/federation/mod.d.ts +3 -3
- package/dist/federation/mod.js +1 -1
- package/dist/federation/mq.test.mjs +1 -1
- package/dist/federation/outgoing-proof.test.mjs +9 -6
- package/dist/federation/portable-actor.test.mjs +3 -3
- package/dist/federation/portable-chain.test.mjs +3 -3
- package/dist/federation/portable-collection.test.mjs +4 -4
- package/dist/federation/portable-delivery.test.mjs +2 -2
- package/dist/federation/portable-gateway-keys.test.mjs +11 -5
- package/dist/federation/portable-inbox.test.mjs +4 -4
- package/dist/federation/portable-parse-uri.test.mjs +1 -1
- package/dist/federation/portable-route-activity.test.mjs +2 -2
- package/dist/federation/portable.test.mjs +3 -3
- package/dist/federation/send.test.mjs +502 -93
- package/dist/federation/tasks/enqueue.test.mjs +1 -1
- package/dist/federation/tasks/tasks.test.mjs +2 -2
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{federation-D8YexkP1.cjs → federation-CFaTsSMN.cjs} +246 -108
- package/dist/{http-CAl7Num5.cjs → http-2SwIR4b-.cjs} +179 -110
- package/dist/{http-BBr-VLil.js → http-CrYxBKqG.js} +178 -109
- package/dist/{http-BZkyQIkb.mjs → http-CxKd10Lq.mjs} +204 -106
- package/dist/{inbox-wDuMKaFW.mjs → inbox-CM0UktG8.mjs} +2 -2
- package/dist/{inbox-view-NE9wXdLo.mjs → inbox-view-BJ5DBPFo.mjs} +1 -1
- package/dist/{key-nYGY4QB9.d.cts → key-DbLNT-vA.d.ts} +1 -2
- package/dist/{key-DhsBxxZN.d.ts → key-DlHT4R6k.d.cts} +1 -2
- package/dist/{kv-cache-CTQSfkmp.mjs → kv-cache-CIdGATck.mjs} +1 -1
- package/dist/{kv-cache-CjgCOz3i.cjs → kv-cache-Cd8xKv-x.cjs} +72 -27
- package/dist/{kv-cache-U7uz5nx4.js → kv-cache-DbgFe35y.js} +73 -28
- package/dist/{ld-Cel1RRZ5.d.cts → ld-CWVpo_NM.d.cts} +1 -1
- package/dist/{ld-CQFuiah9.mjs → ld-DUeBHFWH.mjs} +2 -2
- package/dist/{ld-CcuZ4NZa.d.ts → ld-Inagww4R.d.ts} +1 -1
- package/dist/{metrics-B-QxRSSR.mjs → metrics-B7TvoAP7.mjs} +1 -1
- package/dist/{middleware-BdlNa8BD.mjs → middleware-BE4JXx-U.mjs} +1 -1
- package/dist/{middleware-CuVa0UuS.mjs → middleware-BEaUojg7.mjs} +89 -49
- package/dist/{middleware-P-AZEQMn.js → middleware-CId6efsJ.js} +247 -109
- package/dist/{mod-DvWOv2Ho.d.cts → mod-6TQ4UQZb.d.cts} +1 -1
- package/dist/{mod-BHQE-7WQ.d.ts → mod-B4kBw1-8.d.ts} +1 -1
- package/dist/{mod-DkhfZC1u.d.ts → mod-B7j30IQf.d.ts} +2 -9
- package/dist/{mod-B2783_3v.d.cts → mod-C1nT74IG.d.cts} +1 -1
- package/dist/{mod-DjZfJj9R.d.ts → mod-CIeBgsOC.d.ts} +1 -1
- package/dist/{mod-B5-wdzmJ.d.cts → mod-YvA4Rk8J.d.cts} +2 -9
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +7 -7
- package/dist/mod.d.ts +7 -7
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/client.test.mjs +337 -19
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.d.cts +1 -1
- package/dist/nodeinfo/mod.d.ts +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/otel/exporter.test.mjs +257 -1
- package/dist/otel/mod.cjs +36 -30
- package/dist/otel/mod.d.cts +13 -0
- package/dist/otel/mod.d.ts +13 -0
- package/dist/otel/mod.js +36 -30
- package/dist/{owner-ccKkQOAf.mjs → owner-CwijV0c5.mjs} +3 -3
- package/dist/{portable-collection-DUlQ3s2M.js → portable-collection-2jn_4V7Y.js} +1 -1
- package/dist/{portable-collection-CndnpXfo.mjs → portable-collection-B023xqhZ.mjs} +2 -2
- package/dist/{portable-collection-TC-kvkVI.cjs → portable-collection-BeS88JJp.cjs} +1 -1
- package/dist/{portable-key-D5tFOoV-.mjs → portable-key-BpBucRfb.mjs} +1 -1
- package/dist/{proof-DPoF06nz.mjs → proof-DQh2z9fx.mjs} +3 -3
- package/dist/{send-CF44O1Df.mjs → send-DN5HNBv-.mjs} +174 -76
- package/dist/sig/compound-proof-discovery.test.mjs +1 -1
- package/dist/sig/compound-proof-producer.test.mjs +1 -1
- package/dist/sig/compound-proof-verification.test.mjs +2 -2
- package/dist/sig/compound-proof.test.mjs +2 -2
- package/dist/sig/http.test.mjs +553 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +1 -1
- package/dist/sig/mitra-receive-interop.test.mjs +4 -4
- package/dist/sig/mitra-send-interop.test.mjs +3 -3
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +3 -3
- package/dist/sig/mod.d.ts +3 -3
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +1 -1
- package/dist/sig/portable-interop.test.mjs +3 -3
- package/dist/sig/portable-key.test.mjs +5 -5
- package/dist/sig/proof.test.mjs +4 -4
- package/dist/sig/tootik-request-interop.test.mjs +4 -4
- package/dist/sig/tootik-send-interop.test.mjs +3 -3
- package/dist/sig/verification.test.mjs +3 -3
- package/dist/{temporal-XlZ4Wb94.mjs → temporal-yl7Z7A6y.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +26 -0
- package/dist/{types-_S2_MeLF.js → types-DUjZb4i6.js} +112 -63
- package/dist/{types-poIao40f.cjs → types-hKCCjii0.cjs} +111 -62
- package/dist/utils/docloader.test.mjs +344 -36
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +8 -8
- package/dist/docloader-CpsDyqVN.mjs +0 -91
|
@@ -11,19 +11,22 @@ import { t as assertNotEquals } from "../assert_not_equals-CpPK2t89.mjs";
|
|
|
11
11
|
import "../std__assert-DvMDRPes.mjs";
|
|
12
12
|
import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
|
|
13
13
|
import { t as assert } from "../assert-D1Eafmd_.mjs";
|
|
14
|
-
import {
|
|
14
|
+
import { p as verifyRequest } from "../http-CxKd10Lq.mjs";
|
|
15
15
|
import { i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
|
|
16
|
-
import { t as doesActorOwnKey } from "../owner-
|
|
17
|
-
import { n as extractInboxes, r as
|
|
18
|
-
import { t as FedifySpanExporter } from "../exporter-
|
|
16
|
+
import { t as doesActorOwnKey } from "../owner-CwijV0c5.mjs";
|
|
17
|
+
import { i as sendActivity, n as extractInboxes, r as fetchWithValidatedRedirects, t as SendActivityError } from "../send-DN5HNBv-.mjs";
|
|
18
|
+
import { t as FedifySpanExporter } from "../exporter-DVG68ozH.mjs";
|
|
19
19
|
import { configure, getLogger, reset, withContext } from "@logtape/logtape";
|
|
20
20
|
import { Activity, Application, Endpoints, Group, Person, Service } from "@fedify/vocab";
|
|
21
21
|
import { FetchError, UrlError } from "@fedify/vocab-runtime";
|
|
22
22
|
import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
23
|
-
import
|
|
23
|
+
import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
|
|
24
24
|
import { AggregationTemporality, InMemoryMetricExporter, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
25
25
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
26
26
|
//#region src/federation/send.test.ts
|
|
27
|
+
function sendDirect(options) {
|
|
28
|
+
return sendActivity(options, { openHop: openDirectFetch });
|
|
29
|
+
}
|
|
27
30
|
test("extractInboxes()", () => {
|
|
28
31
|
const recipients = [
|
|
29
32
|
new Person({
|
|
@@ -159,7 +162,7 @@ test("sendActivity()", async (t) => {
|
|
|
159
162
|
"id": "https://example.com/activity",
|
|
160
163
|
"actor": "https://example.com/person"
|
|
161
164
|
};
|
|
162
|
-
await
|
|
165
|
+
await sendDirect({
|
|
163
166
|
activity,
|
|
164
167
|
keys: [{
|
|
165
168
|
privateKey: rsaPrivateKey2,
|
|
@@ -175,7 +178,7 @@ test("sendActivity()", async (t) => {
|
|
|
175
178
|
assertEquals(request?.headers.get("Content-Type"), "application/activity+json");
|
|
176
179
|
assertEquals(request?.headers.get("X-Test"), "test");
|
|
177
180
|
httpSigVerified = null;
|
|
178
|
-
await assertRejects(() =>
|
|
181
|
+
await assertRejects(() => sendDirect({
|
|
179
182
|
activity: {
|
|
180
183
|
...activity,
|
|
181
184
|
actor: "https://example.com/person2"
|
|
@@ -204,7 +207,7 @@ test("sendActivity()", async (t) => {
|
|
|
204
207
|
"id": "https://example.com/activity",
|
|
205
208
|
"actor": "https://example.com/person"
|
|
206
209
|
};
|
|
207
|
-
await assertRejects(() =>
|
|
210
|
+
await assertRejects(() => sendDirect({
|
|
208
211
|
activity,
|
|
209
212
|
activityId: "https://example.com/activity",
|
|
210
213
|
keys: [{
|
|
@@ -222,7 +225,7 @@ test("sendActivity()", async (t) => {
|
|
|
222
225
|
"actor": "https://example.com/person"
|
|
223
226
|
};
|
|
224
227
|
try {
|
|
225
|
-
await
|
|
228
|
+
await sendDirect({
|
|
226
229
|
activity,
|
|
227
230
|
activityId: "https://example.com/activity",
|
|
228
231
|
keys: [{
|
|
@@ -257,7 +260,7 @@ test("sendActivity()", async (t) => {
|
|
|
257
260
|
});
|
|
258
261
|
throw failure;
|
|
259
262
|
});
|
|
260
|
-
const error = await assertRejects(() =>
|
|
263
|
+
const error = await assertRejects(() => sendDirect({
|
|
261
264
|
activity,
|
|
262
265
|
activityId: "https://example.com/activity",
|
|
263
266
|
keys: [{
|
|
@@ -282,7 +285,7 @@ test("sendActivity()", async (t) => {
|
|
|
282
285
|
"actor": "https://example.com/person"
|
|
283
286
|
};
|
|
284
287
|
try {
|
|
285
|
-
await
|
|
288
|
+
await sendDirect({
|
|
286
289
|
activity,
|
|
287
290
|
activityId: "https://example.com/activity",
|
|
288
291
|
keys: [{
|
|
@@ -311,7 +314,7 @@ test("sendActivity()", async (t) => {
|
|
|
311
314
|
"actor": "https://example.com/person"
|
|
312
315
|
};
|
|
313
316
|
try {
|
|
314
|
-
await
|
|
317
|
+
await sendDirect({
|
|
315
318
|
activity,
|
|
316
319
|
activityId: "https://example.com/activity",
|
|
317
320
|
keys: [{
|
|
@@ -341,7 +344,7 @@ test("sendActivity()", async (t) => {
|
|
|
341
344
|
"actor": "https://example.com/person"
|
|
342
345
|
};
|
|
343
346
|
try {
|
|
344
|
-
await
|
|
347
|
+
await sendDirect({
|
|
345
348
|
activity,
|
|
346
349
|
activityId: "https://example.com/activity",
|
|
347
350
|
keys: [{
|
|
@@ -368,7 +371,7 @@ test("sendActivity()", async (t) => {
|
|
|
368
371
|
"actor": "https://example.com/person"
|
|
369
372
|
};
|
|
370
373
|
try {
|
|
371
|
-
await
|
|
374
|
+
await sendDirect({
|
|
372
375
|
activity,
|
|
373
376
|
activityId: "https://example.com/activity",
|
|
374
377
|
keys: [{
|
|
@@ -397,7 +400,7 @@ test("sendActivity()", async (t) => {
|
|
|
397
400
|
"actor": "https://example.com/person"
|
|
398
401
|
};
|
|
399
402
|
try {
|
|
400
|
-
await
|
|
403
|
+
await sendDirect({
|
|
401
404
|
activity,
|
|
402
405
|
activityId: "https://example.com/activity",
|
|
403
406
|
keys: [{
|
|
@@ -421,7 +424,7 @@ test("sendActivity() records OpenTelemetry span events", async (t) => {
|
|
|
421
424
|
await t.step("successful send", async () => {
|
|
422
425
|
esm_default.get("https://example.com/", { status: 404 });
|
|
423
426
|
esm_default.post("https://example.com/inbox", { status: 202 });
|
|
424
|
-
await
|
|
427
|
+
await sendDirect({
|
|
425
428
|
activity: {
|
|
426
429
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
427
430
|
type: "Create",
|
|
@@ -504,7 +507,7 @@ test("sendActivity() aligns the LogTape context with its own span", async (t) =>
|
|
|
504
507
|
try {
|
|
505
508
|
esm_default.post("https://example.com/inbox-warn", { status: 202 });
|
|
506
509
|
await withContext({ requestId: "outer-request-warn" }, async () => {
|
|
507
|
-
await
|
|
510
|
+
await sendDirect({
|
|
508
511
|
...deliveryParams("https://example.com/activity/warn", "https://example.com/inbox-warn", tracerProvider),
|
|
509
512
|
keys: [{
|
|
510
513
|
keyId: ed25519Multikey.id,
|
|
@@ -542,7 +545,7 @@ test("sendActivity() aligns the LogTape context with its own span", async (t) =>
|
|
|
542
545
|
body: "Internal Server Error"
|
|
543
546
|
});
|
|
544
547
|
await withContext(outerContext, async () => {
|
|
545
|
-
await assertRejects(() =>
|
|
548
|
+
await assertRejects(() => sendDirect(deliveryParams("https://example.com/activity/failing", "https://example.com/inbox-failing", tracerProvider)), SendActivityError);
|
|
546
549
|
getLogger([
|
|
547
550
|
"fedify",
|
|
548
551
|
"federation",
|
|
@@ -578,7 +581,7 @@ test("sendActivity() aligns the LogTape context with its own span", async (t) =>
|
|
|
578
581
|
try {
|
|
579
582
|
esm_default.post("https://example.com/inbox-ok", { status: 202 });
|
|
580
583
|
await withContext(outerContext, async () => {
|
|
581
|
-
await
|
|
584
|
+
await sendDirect(deliveryParams("https://example.com/activity/ok", "https://example.com/inbox-ok", tracerProvider));
|
|
582
585
|
getLogger([
|
|
583
586
|
"fedify",
|
|
584
587
|
"federation",
|
|
@@ -619,7 +622,7 @@ test("sendActivity() aligns the LogTape context with its own span", async (t) =>
|
|
|
619
622
|
};
|
|
620
623
|
esm_default.post("https://example.com/inbox-concurrent-a", handler());
|
|
621
624
|
esm_default.post("https://example.com/inbox-concurrent-b", handler());
|
|
622
|
-
const [resultA, resultB] = await Promise.allSettled([
|
|
625
|
+
const [resultA, resultB] = await Promise.allSettled([sendDirect(deliveryParams("https://example.com/activity/a", "https://example.com/inbox-concurrent-a", tracerProvider)).finally(release), sendDirect(deliveryParams("https://example.com/activity/b", "https://example.com/inbox-concurrent-b", tracerProvider)).finally(release)]);
|
|
623
626
|
assertEquals(resultA.status, "rejected");
|
|
624
627
|
assertEquals(resultB.status, "rejected");
|
|
625
628
|
const spans = exporter.getSpans("activitypub.send_activity");
|
|
@@ -647,6 +650,41 @@ test("sendActivity() aligns the LogTape context with its own span", async (t) =>
|
|
|
647
650
|
esm_default.hardReset();
|
|
648
651
|
}
|
|
649
652
|
});
|
|
653
|
+
test("sendActivity() validates the initial policy before serializing", async () => {
|
|
654
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
655
|
+
let serialized = false;
|
|
656
|
+
await assertRejects(() => sendActivity({
|
|
657
|
+
activity: { toJSON() {
|
|
658
|
+
serialized = true;
|
|
659
|
+
throw new Error("must not serialize");
|
|
660
|
+
} },
|
|
661
|
+
keys: [],
|
|
662
|
+
inbox: new URL("http://127.0.0.1/inbox"),
|
|
663
|
+
meterProvider
|
|
664
|
+
}), UrlError);
|
|
665
|
+
assertFalse(serialized);
|
|
666
|
+
assertEquals(recorder.getMeasurements("activitypub.delivery.sent"), []);
|
|
667
|
+
});
|
|
668
|
+
test("sendActivity() closes the initial session when serialization fails", async () => {
|
|
669
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
670
|
+
const url = "https://example.com/lifecycle/serialization-failure";
|
|
671
|
+
const hops = recordHops(() => ({
|
|
672
|
+
status: 202,
|
|
673
|
+
body: "Accepted"
|
|
674
|
+
}));
|
|
675
|
+
const failure = /* @__PURE__ */ new Error("serialization failed");
|
|
676
|
+
assertEquals(await assertRejects(() => sendActivity({
|
|
677
|
+
activity: { toJSON() {
|
|
678
|
+
throw failure;
|
|
679
|
+
} },
|
|
680
|
+
keys: [],
|
|
681
|
+
inbox: new URL(url),
|
|
682
|
+
meterProvider
|
|
683
|
+
}, { openHop: hops.openHop }), Error, "serialization failed"), failure);
|
|
684
|
+
assertEquals(hops.events, [`open ${url}`, `close ${url}`]);
|
|
685
|
+
assertEquals(recorder.getMeasurements("activitypub.delivery.sent"), []);
|
|
686
|
+
hops.assertInvariants();
|
|
687
|
+
});
|
|
650
688
|
test("sendActivity() records OpenTelemetry delivery metrics", async (t) => {
|
|
651
689
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
652
690
|
esm_default.spyGlobal();
|
|
@@ -887,85 +925,456 @@ for (const signed of [false, true]) {
|
|
|
887
925
|
});
|
|
888
926
|
});
|
|
889
927
|
}
|
|
890
|
-
for (const signed of [false, true]) test({
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
928
|
+
for (const signed of [false, true]) test(`sendActivity() classifies DNS failures (signed: ${signed})`, async (t) => {
|
|
929
|
+
const activity = {
|
|
930
|
+
type: "Create",
|
|
931
|
+
id: "https://example.com/activity"
|
|
932
|
+
};
|
|
933
|
+
const destination = "https://delivery.invalid/inbox";
|
|
934
|
+
const publicInbox = "https://8.8.8.8/inbox";
|
|
935
|
+
const keys = signed ? [{
|
|
936
|
+
privateKey: rsaPrivateKey2,
|
|
937
|
+
keyId: rsaPublicKey2.id
|
|
938
|
+
}] : [];
|
|
939
|
+
for (const result of [
|
|
940
|
+
"throws",
|
|
941
|
+
"empty",
|
|
942
|
+
"cname",
|
|
943
|
+
"private"
|
|
944
|
+
]) for (const redirected of [false, true]) await t.step(`${result}, redirected: ${redirected}`, async () => {
|
|
945
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
946
|
+
const resolverError = /* @__PURE__ */ new Error("Resolver unavailable");
|
|
947
|
+
const lookups = [];
|
|
948
|
+
const lookup = (hostname) => {
|
|
949
|
+
lookups.push(hostname);
|
|
950
|
+
assertEquals(hostname, "delivery.invalid");
|
|
951
|
+
if (result === "throws") return Promise.reject(resolverError);
|
|
952
|
+
return Promise.resolve(result === "empty" ? [] : [{
|
|
953
|
+
address: result === "private" ? "127.0.0.1" : "alias.invalid.",
|
|
954
|
+
family: 4
|
|
955
|
+
}]);
|
|
897
956
|
};
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}] : [];
|
|
904
|
-
for (const result of [
|
|
905
|
-
"throws",
|
|
906
|
-
"empty",
|
|
907
|
-
"cname",
|
|
908
|
-
"private"
|
|
909
|
-
]) for (const redirected of [false, true]) await t.step(`${result}, redirected: ${redirected}`, async () => {
|
|
910
|
-
const [meterProvider, recorder] = createTestMeterProvider();
|
|
911
|
-
const originalLookup = dns.lookup;
|
|
912
|
-
const resolverError = /* @__PURE__ */ new Error("Resolver unavailable");
|
|
913
|
-
const lookups = [];
|
|
914
|
-
dns.lookup = ((hostname, options) => {
|
|
915
|
-
lookups.push(hostname);
|
|
916
|
-
assertEquals(hostname, "delivery.invalid");
|
|
917
|
-
assertEquals(options, { all: true });
|
|
918
|
-
if (result === "throws") return Promise.reject(resolverError);
|
|
919
|
-
return Promise.resolve(result === "empty" ? [] : [{
|
|
920
|
-
address: result === "private" ? "127.0.0.1" : "alias.invalid.",
|
|
921
|
-
family: 4
|
|
922
|
-
}]);
|
|
957
|
+
try {
|
|
958
|
+
esm_default.mockGlobal().catch(202);
|
|
959
|
+
if (redirected) esm_default.route(publicInbox, {
|
|
960
|
+
status: 307,
|
|
961
|
+
headers: { Location: destination }
|
|
923
962
|
});
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
963
|
+
const send = () => sendActivity({
|
|
964
|
+
activity,
|
|
965
|
+
activityType: "https://www.w3.org/ns/activitystreams#Create",
|
|
966
|
+
meterProvider,
|
|
967
|
+
keys,
|
|
968
|
+
inbox: new URL(redirected ? publicInbox : destination)
|
|
969
|
+
}, { openHop: (url) => openPublicFetch(url, { lookup }) });
|
|
970
|
+
if (result === "private") assertEquals((await assertRejects(send, UrlError)).reason, "disallowed");
|
|
971
|
+
else {
|
|
972
|
+
const error = await assertRejects(send, FetchError);
|
|
973
|
+
assertEquals(error.url.href, destination);
|
|
974
|
+
assertInstanceOf(error.cause, UrlError);
|
|
975
|
+
assertEquals(error.cause.reason, "dns");
|
|
976
|
+
assertEquals(error.cause.cause, result === "throws" ? resolverError : void 0);
|
|
977
|
+
}
|
|
978
|
+
const expectedCount = result === "private" && !redirected ? 0 : 1;
|
|
979
|
+
const sent = recorder.getMeasurements("activitypub.delivery.sent");
|
|
980
|
+
const durations = recorder.getMeasurements("activitypub.delivery.duration");
|
|
981
|
+
assertEquals(sent.length, expectedCount);
|
|
982
|
+
assertEquals(durations.length, expectedCount);
|
|
983
|
+
if (expectedCount === 1) {
|
|
984
|
+
assertEquals(sent[0].value, 1);
|
|
985
|
+
assertEquals(sent[0].attributes, {
|
|
986
|
+
"activitypub.remote.host": redirected ? "8.8.8.8" : "delivery.invalid",
|
|
987
|
+
"activitypub.activity.type": "https://www.w3.org/ns/activitystreams#Create",
|
|
988
|
+
"activitypub.delivery.success": false
|
|
936
989
|
});
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
const error = await assertRejects(send, FetchError);
|
|
940
|
-
assertEquals(error.url.href, destination);
|
|
941
|
-
assertInstanceOf(error.cause, UrlError);
|
|
942
|
-
assertEquals(error.cause.reason, "dns");
|
|
943
|
-
assertEquals(error.cause.cause, result === "throws" ? resolverError : void 0);
|
|
944
|
-
}
|
|
945
|
-
const expectedCount = result === "private" && !redirected ? 0 : 1;
|
|
946
|
-
const sent = recorder.getMeasurements("activitypub.delivery.sent");
|
|
947
|
-
const durations = recorder.getMeasurements("activitypub.delivery.duration");
|
|
948
|
-
assertEquals(sent.length, expectedCount);
|
|
949
|
-
assertEquals(durations.length, expectedCount);
|
|
950
|
-
if (expectedCount === 1) {
|
|
951
|
-
assertEquals(sent[0].value, 1);
|
|
952
|
-
assertEquals(sent[0].attributes, {
|
|
953
|
-
"activitypub.remote.host": redirected ? "8.8.8.8" : "delivery.invalid",
|
|
954
|
-
"activitypub.activity.type": "https://www.w3.org/ns/activitystreams#Create",
|
|
955
|
-
"activitypub.delivery.success": false
|
|
956
|
-
});
|
|
957
|
-
assertEquals(durations[0].attributes, sent[0].attributes);
|
|
958
|
-
assertGreaterOrEqual(durations[0].value, 0);
|
|
959
|
-
}
|
|
960
|
-
assertEquals(lookups, ["delivery.invalid"]);
|
|
961
|
-
assertEquals(esm_default.callHistory.calls(destination).length, 0);
|
|
962
|
-
assertEquals(esm_default.callHistory.calls().length, redirected ? 1 : 0);
|
|
963
|
-
} finally {
|
|
964
|
-
dns.lookup = originalLookup;
|
|
965
|
-
esm_default.hardReset();
|
|
990
|
+
assertEquals(durations[0].attributes, sent[0].attributes);
|
|
991
|
+
assertGreaterOrEqual(durations[0].value, 0);
|
|
966
992
|
}
|
|
993
|
+
assertEquals(lookups, ["delivery.invalid"]);
|
|
994
|
+
assertEquals(esm_default.callHistory.calls(destination).length, 0);
|
|
995
|
+
assertEquals(esm_default.callHistory.calls().length, redirected ? 1 : 0);
|
|
996
|
+
} finally {
|
|
997
|
+
esm_default.hardReset();
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
});
|
|
1001
|
+
const lifecycleActivity = {
|
|
1002
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1003
|
+
type: "Create",
|
|
1004
|
+
id: "https://example.com/activity",
|
|
1005
|
+
actor: "https://example.com/person"
|
|
1006
|
+
};
|
|
1007
|
+
const signedKeys = [{
|
|
1008
|
+
privateKey: rsaPrivateKey2,
|
|
1009
|
+
keyId: rsaPublicKey2.id
|
|
1010
|
+
}];
|
|
1011
|
+
const unsignedKeys = [{
|
|
1012
|
+
privateKey: ed25519PrivateKey,
|
|
1013
|
+
keyId: ed25519Multikey.id
|
|
1014
|
+
}];
|
|
1015
|
+
test("sendActivity() cancels a successful response body", async (t) => {
|
|
1016
|
+
for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
|
|
1017
|
+
const inbox = "https://example.com/lifecycle/inbox";
|
|
1018
|
+
const hops = recordHops(() => ({
|
|
1019
|
+
status: 202,
|
|
1020
|
+
body: "Accepted"
|
|
1021
|
+
}));
|
|
1022
|
+
await sendActivity({
|
|
1023
|
+
activity: lifecycleActivity,
|
|
1024
|
+
keys: signed ? signedKeys : unsignedKeys,
|
|
1025
|
+
inbox: new URL(inbox)
|
|
1026
|
+
}, { openHop: hops.openHop });
|
|
1027
|
+
assertEquals(hops.events, [
|
|
1028
|
+
`open ${inbox}`,
|
|
1029
|
+
`fetch #1 ${inbox}`,
|
|
1030
|
+
"cancel #1",
|
|
1031
|
+
`close ${inbox}`
|
|
1032
|
+
]);
|
|
1033
|
+
hops.assertInvariants();
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
test("sendActivity() reads at most 1 KiB of an error response body", async (t) => {
|
|
1037
|
+
for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
|
|
1038
|
+
const inbox = "https://example.com/lifecycle/error";
|
|
1039
|
+
const hops = recordHops(() => ({
|
|
1040
|
+
status: 500,
|
|
1041
|
+
body: "x".repeat(4096)
|
|
1042
|
+
}));
|
|
1043
|
+
const error = await assertRejects(() => sendActivity({
|
|
1044
|
+
activity: lifecycleActivity,
|
|
1045
|
+
activityId: lifecycleActivity.id,
|
|
1046
|
+
keys: signed ? signedKeys : unsignedKeys,
|
|
1047
|
+
inbox: new URL(inbox)
|
|
1048
|
+
}, { openHop: hops.openHop }), SendActivityError);
|
|
1049
|
+
assertEquals(error.responseBody, "x".repeat(1024) + "… (truncated)");
|
|
1050
|
+
assert(error.message.includes("… (truncated)"));
|
|
1051
|
+
assertEquals(hops.events, [
|
|
1052
|
+
`open ${inbox}`,
|
|
1053
|
+
`fetch #1 ${inbox}`,
|
|
1054
|
+
"cancel #1",
|
|
1055
|
+
...signed ? [`fetch #2 ${inbox}`, "cancel #2"] : [],
|
|
1056
|
+
`close ${inbox}`
|
|
1057
|
+
]);
|
|
1058
|
+
hops.assertInvariants();
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
test("sendActivity() closes each redirect hop before opening the next", async (t) => {
|
|
1062
|
+
for (const [signed, status] of [[true, 307], [false, 308]]) await t.step(`signed: ${signed}, ${status}`, async () => {
|
|
1063
|
+
const inbox = "https://example.com/lifecycle/redirect";
|
|
1064
|
+
const next = "https://example.com/lifecycle/redirected";
|
|
1065
|
+
const hops = recordHops((request) => request.url === inbox ? {
|
|
1066
|
+
status,
|
|
1067
|
+
headers: { Location: next },
|
|
1068
|
+
body: "Moved"
|
|
1069
|
+
} : {
|
|
1070
|
+
status: 202,
|
|
1071
|
+
body: "Accepted"
|
|
967
1072
|
});
|
|
1073
|
+
await sendActivity({
|
|
1074
|
+
activity: lifecycleActivity,
|
|
1075
|
+
keys: signed ? signedKeys : unsignedKeys,
|
|
1076
|
+
inbox: new URL(inbox)
|
|
1077
|
+
}, { openHop: hops.openHop });
|
|
1078
|
+
assertEquals(hops.events, [
|
|
1079
|
+
`open ${inbox}`,
|
|
1080
|
+
`fetch #1 ${inbox}`,
|
|
1081
|
+
"cancel #1",
|
|
1082
|
+
`close ${inbox}`,
|
|
1083
|
+
`open ${next}`,
|
|
1084
|
+
`fetch #2 ${next}`,
|
|
1085
|
+
"cancel #2",
|
|
1086
|
+
`close ${next}`
|
|
1087
|
+
]);
|
|
1088
|
+
assertEquals(hops.requests[1].method, "POST");
|
|
1089
|
+
assertEquals(await hops.requests[1].text(), JSON.stringify(lifecycleActivity));
|
|
1090
|
+
hops.assertInvariants();
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
async function captureLogs(fn) {
|
|
1094
|
+
const records = [];
|
|
1095
|
+
await configure({
|
|
1096
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
1097
|
+
loggers: [{
|
|
1098
|
+
category: "fedify",
|
|
1099
|
+
sinks: ["buffer"],
|
|
1100
|
+
lowestLevel: "debug"
|
|
1101
|
+
}, {
|
|
1102
|
+
category: ["logtape", "meta"],
|
|
1103
|
+
sinks: []
|
|
1104
|
+
}],
|
|
1105
|
+
reset: true
|
|
1106
|
+
});
|
|
1107
|
+
try {
|
|
1108
|
+
await fn();
|
|
1109
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1110
|
+
} finally {
|
|
1111
|
+
await reset();
|
|
968
1112
|
}
|
|
1113
|
+
return records;
|
|
1114
|
+
}
|
|
1115
|
+
function getCancelFailureUrls(records) {
|
|
1116
|
+
return records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}").map((r) => r.properties.url);
|
|
1117
|
+
}
|
|
1118
|
+
test("fetchWithValidatedRedirects() logs a redirect body failing to cancel and follows the redirect", async (t) => {
|
|
1119
|
+
const url = "https://example.com/lifecycle/cancel-failure";
|
|
1120
|
+
const next = "https://example.com/lifecycle/cancel-failure-final";
|
|
1121
|
+
const respond = (_, index) => index === 1 ? {
|
|
1122
|
+
status: 302,
|
|
1123
|
+
headers: { Location: next },
|
|
1124
|
+
body: "Found",
|
|
1125
|
+
failCancel: true
|
|
1126
|
+
} : {
|
|
1127
|
+
status: 202,
|
|
1128
|
+
body: "Accepted"
|
|
1129
|
+
};
|
|
1130
|
+
await t.step("fetchWithValidatedRedirects()", async () => {
|
|
1131
|
+
const hops = recordHops(respond);
|
|
1132
|
+
let result;
|
|
1133
|
+
const records = await captureLogs(async () => {
|
|
1134
|
+
result = await fetchWithValidatedRedirects(new Request(url, {
|
|
1135
|
+
method: "POST",
|
|
1136
|
+
body: "activity"
|
|
1137
|
+
}), hops.openHop, (response) => response.text());
|
|
1138
|
+
});
|
|
1139
|
+
assertEquals(result, "Accepted");
|
|
1140
|
+
assertEquals(hops.events, [
|
|
1141
|
+
`open ${url}`,
|
|
1142
|
+
`fetch #1 ${url}`,
|
|
1143
|
+
"cancel #1",
|
|
1144
|
+
`close ${url}`,
|
|
1145
|
+
`open ${next}`,
|
|
1146
|
+
`fetch #2 ${next}`,
|
|
1147
|
+
"consumed #2",
|
|
1148
|
+
`close ${next}`
|
|
1149
|
+
]);
|
|
1150
|
+
assertEquals(getCancelFailureUrls(records), [url]);
|
|
1151
|
+
hops.assertInvariants();
|
|
1152
|
+
});
|
|
1153
|
+
for (const signed of [true, false]) await t.step(`signed sendActivity(): ${signed}`, async () => {
|
|
1154
|
+
const hops = recordHops(respond);
|
|
1155
|
+
const records = await captureLogs(() => sendActivity({
|
|
1156
|
+
activity: lifecycleActivity,
|
|
1157
|
+
keys: signed ? signedKeys : unsignedKeys,
|
|
1158
|
+
inbox: new URL(url)
|
|
1159
|
+
}, { openHop: hops.openHop }));
|
|
1160
|
+
assertEquals(hops.events, [
|
|
1161
|
+
`open ${url}`,
|
|
1162
|
+
`fetch #1 ${url}`,
|
|
1163
|
+
"cancel #1",
|
|
1164
|
+
`close ${url}`,
|
|
1165
|
+
`open ${next}`,
|
|
1166
|
+
`fetch #2 ${next}`,
|
|
1167
|
+
"cancel #2",
|
|
1168
|
+
`close ${next}`
|
|
1169
|
+
]);
|
|
1170
|
+
assertEquals(getCancelFailureUrls(records), [url]);
|
|
1171
|
+
hops.assertInvariants();
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
test("fetchWithValidatedRedirects() closes the session when consuming fails", async () => {
|
|
1175
|
+
const url = "https://example.com/lifecycle/consume-failure";
|
|
1176
|
+
const hops = recordHops(() => ({
|
|
1177
|
+
status: 200,
|
|
1178
|
+
body: "x".repeat(1024)
|
|
1179
|
+
}));
|
|
1180
|
+
await assertRejects(() => fetchWithValidatedRedirects(new Request(url, {
|
|
1181
|
+
method: "POST",
|
|
1182
|
+
body: "activity"
|
|
1183
|
+
}), hops.openHop, () => Promise.reject(/* @__PURE__ */ new Error("consume failed"))), Error, "consume failed");
|
|
1184
|
+
assertEquals(hops.events, [
|
|
1185
|
+
`open ${url}`,
|
|
1186
|
+
`fetch #1 ${url}`,
|
|
1187
|
+
"cancel #1 by close",
|
|
1188
|
+
`close ${url}`
|
|
1189
|
+
]);
|
|
1190
|
+
hops.assertInvariants();
|
|
1191
|
+
});
|
|
1192
|
+
test("sendActivity() logs a successful response body failing to cancel", async (t) => {
|
|
1193
|
+
for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
|
|
1194
|
+
const inbox = "https://example.com/lifecycle/cancel-failure-inbox";
|
|
1195
|
+
const hops = recordHops(() => ({
|
|
1196
|
+
status: 202,
|
|
1197
|
+
body: "Accepted",
|
|
1198
|
+
failCancel: true
|
|
1199
|
+
}));
|
|
1200
|
+
const records = await captureLogs(() => sendActivity({
|
|
1201
|
+
activity: lifecycleActivity,
|
|
1202
|
+
keys: signed ? signedKeys : unsignedKeys,
|
|
1203
|
+
inbox: new URL(inbox)
|
|
1204
|
+
}, { openHop: hops.openHop }));
|
|
1205
|
+
assertEquals(hops.events, [
|
|
1206
|
+
`open ${inbox}`,
|
|
1207
|
+
`fetch #1 ${inbox}`,
|
|
1208
|
+
"cancel #1",
|
|
1209
|
+
`close ${inbox}`
|
|
1210
|
+
]);
|
|
1211
|
+
assertEquals(getCancelFailureUrls(records), [inbox]);
|
|
1212
|
+
hops.assertInvariants();
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
test("sendActivity() is not blocked by a body cancellation that never settles", async (t) => {
|
|
1216
|
+
for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
|
|
1217
|
+
const inbox = "https://example.com/lifecycle/hanging-cancel";
|
|
1218
|
+
const next = "https://example.com/lifecycle/hanging-cancel-final";
|
|
1219
|
+
const hops = recordHops((request) => request.url === inbox ? {
|
|
1220
|
+
status: 302,
|
|
1221
|
+
headers: { Location: next },
|
|
1222
|
+
body: "Found",
|
|
1223
|
+
hangCancel: true
|
|
1224
|
+
} : {
|
|
1225
|
+
status: 202,
|
|
1226
|
+
body: "Accepted",
|
|
1227
|
+
hangCancel: true
|
|
1228
|
+
});
|
|
1229
|
+
await sendActivity({
|
|
1230
|
+
activity: lifecycleActivity,
|
|
1231
|
+
keys: signed ? signedKeys : unsignedKeys,
|
|
1232
|
+
inbox: new URL(inbox)
|
|
1233
|
+
}, { openHop: hops.openHop });
|
|
1234
|
+
assertEquals(hops.events, [
|
|
1235
|
+
`open ${inbox}`,
|
|
1236
|
+
`fetch #1 ${inbox}`,
|
|
1237
|
+
"cancel #1",
|
|
1238
|
+
`close ${inbox}`,
|
|
1239
|
+
`open ${next}`,
|
|
1240
|
+
`fetch #2 ${next}`,
|
|
1241
|
+
"cancel #2",
|
|
1242
|
+
`close ${next}`
|
|
1243
|
+
]);
|
|
1244
|
+
hops.assertInvariants();
|
|
1245
|
+
});
|
|
1246
|
+
await t.step("truncated error body", async () => {
|
|
1247
|
+
const inbox = "https://example.com/lifecycle/hanging-error";
|
|
1248
|
+
const hops = recordHops(() => ({
|
|
1249
|
+
status: 500,
|
|
1250
|
+
body: "x".repeat(4096),
|
|
1251
|
+
hangCancel: true
|
|
1252
|
+
}));
|
|
1253
|
+
assertEquals((await assertRejects(() => sendActivity({
|
|
1254
|
+
activity: lifecycleActivity,
|
|
1255
|
+
keys: unsignedKeys,
|
|
1256
|
+
inbox: new URL(inbox)
|
|
1257
|
+
}, { openHop: hops.openHop }), SendActivityError)).responseBody, "x".repeat(1024) + "… (truncated)");
|
|
1258
|
+
assertEquals(hops.events, [
|
|
1259
|
+
`open ${inbox}`,
|
|
1260
|
+
`fetch #1 ${inbox}`,
|
|
1261
|
+
"cancel #1",
|
|
1262
|
+
`close ${inbox}`
|
|
1263
|
+
]);
|
|
1264
|
+
hops.assertInvariants();
|
|
1265
|
+
});
|
|
969
1266
|
});
|
|
1267
|
+
test("sendActivity() completes with responses cloned by fetch-mock", async (t) => {
|
|
1268
|
+
for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
|
|
1269
|
+
esm_default.spyGlobal();
|
|
1270
|
+
try {
|
|
1271
|
+
esm_default.post("https://example.com/tee/inbox", () => new Response("Moved", {
|
|
1272
|
+
status: 307,
|
|
1273
|
+
headers: { Location: "https://example.com/tee/final" }
|
|
1274
|
+
}));
|
|
1275
|
+
esm_default.post("https://example.com/tee/final", () => new Response("Accepted", { status: 202 }));
|
|
1276
|
+
esm_default.post("https://example.com/tee/error", () => new Response("x".repeat(4096), { status: 404 }));
|
|
1277
|
+
const keys = signed ? signedKeys : unsignedKeys;
|
|
1278
|
+
await sendDirect({
|
|
1279
|
+
activity: lifecycleActivity,
|
|
1280
|
+
keys,
|
|
1281
|
+
inbox: new URL("https://example.com/tee/inbox")
|
|
1282
|
+
});
|
|
1283
|
+
assertEquals(esm_default.callHistory.calls("https://example.com/tee/final").length, 1);
|
|
1284
|
+
const error = await assertRejects(() => sendDirect({
|
|
1285
|
+
activity: lifecycleActivity,
|
|
1286
|
+
keys,
|
|
1287
|
+
inbox: new URL("https://example.com/tee/error")
|
|
1288
|
+
}), SendActivityError);
|
|
1289
|
+
assertEquals(error.statusCode, 404);
|
|
1290
|
+
assertEquals(error.responseBody, "x".repeat(1024) + "… (truncated)");
|
|
1291
|
+
} finally {
|
|
1292
|
+
esm_default.hardReset();
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
});
|
|
1296
|
+
function createRecordedResponse(events, index, init, cancelSuffix = () => "") {
|
|
1297
|
+
const bytes = new TextEncoder().encode(init.body ?? "");
|
|
1298
|
+
const chunkSize = init.chunkSize ?? 256;
|
|
1299
|
+
let offset = 0;
|
|
1300
|
+
let settled = false;
|
|
1301
|
+
const body = new ReadableStream({
|
|
1302
|
+
pull(controller) {
|
|
1303
|
+
if (offset >= bytes.length) {
|
|
1304
|
+
settled = true;
|
|
1305
|
+
events.push(`consumed #${index}`);
|
|
1306
|
+
controller.close();
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
controller.enqueue(bytes.slice(offset, offset + chunkSize));
|
|
1310
|
+
offset += chunkSize;
|
|
1311
|
+
},
|
|
1312
|
+
cancel() {
|
|
1313
|
+
settled = true;
|
|
1314
|
+
events.push(`cancel #${index}${cancelSuffix()}`);
|
|
1315
|
+
if (init.failCancel) throw new Error(`cancel #${index} failed`);
|
|
1316
|
+
if (init.hangCancel) return new Promise(() => {});
|
|
1317
|
+
}
|
|
1318
|
+
}, { highWaterMark: 0 });
|
|
1319
|
+
return {
|
|
1320
|
+
response: new Response(body, {
|
|
1321
|
+
status: init.status ?? 200,
|
|
1322
|
+
headers: init.headers
|
|
1323
|
+
}),
|
|
1324
|
+
settled: () => settled
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function recordHops(respond) {
|
|
1328
|
+
const events = [];
|
|
1329
|
+
const requests = [];
|
|
1330
|
+
const unsettledAtClose = [];
|
|
1331
|
+
let opens = 0;
|
|
1332
|
+
let closes = 0;
|
|
1333
|
+
let fetches = 0;
|
|
1334
|
+
return {
|
|
1335
|
+
events,
|
|
1336
|
+
requests,
|
|
1337
|
+
openHop(url) {
|
|
1338
|
+
opens++;
|
|
1339
|
+
events.push(`open ${url}`);
|
|
1340
|
+
const tracked = [];
|
|
1341
|
+
let closed = false;
|
|
1342
|
+
let closing = false;
|
|
1343
|
+
return Promise.resolve({
|
|
1344
|
+
url: new URL(url),
|
|
1345
|
+
async fetch(input, init) {
|
|
1346
|
+
if (closed) throw new TypeError("The session is closed.");
|
|
1347
|
+
const request = new Request(input instanceof Request ? input.clone() : input, init);
|
|
1348
|
+
const index = ++fetches;
|
|
1349
|
+
requests.push(request);
|
|
1350
|
+
events.push(`fetch #${index} ${request.url}`);
|
|
1351
|
+
const recorded = createRecordedResponse(events, index, await respond(request, index), () => closing ? " by close" : "");
|
|
1352
|
+
tracked.push({
|
|
1353
|
+
...recorded,
|
|
1354
|
+
index
|
|
1355
|
+
});
|
|
1356
|
+
return recorded.response;
|
|
1357
|
+
},
|
|
1358
|
+
close() {
|
|
1359
|
+
if (closed) return Promise.resolve();
|
|
1360
|
+
closed = true;
|
|
1361
|
+
closes++;
|
|
1362
|
+
closing = true;
|
|
1363
|
+
for (const { response, settled, index } of tracked) {
|
|
1364
|
+
if (settled()) continue;
|
|
1365
|
+
if (response.body?.locked) unsettledAtClose.push(index);
|
|
1366
|
+
else response.body?.cancel().catch(() => {});
|
|
1367
|
+
}
|
|
1368
|
+
events.push(`close ${url}`);
|
|
1369
|
+
return Promise.resolve();
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
},
|
|
1373
|
+
assertInvariants() {
|
|
1374
|
+
assertEquals(opens, closes);
|
|
1375
|
+
assertEquals(unsettledAtClose, []);
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
970
1379
|
//#endregion
|
|
971
1380
|
export {};
|