@fedify/fedify 2.4.0-dev.2200 → 2.4.0-dev.2212
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/audience-CHQ-FlLD.mjs +95 -0
- package/dist/{builder-BGSNMnVH.mjs → builder-CKAVFvcH.mjs} +2 -2
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/public-audience.test.mjs +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-Zpg7Fbk9.mjs → compound-proof-BP5SOdXv.mjs} +2 -2
- package/dist/{context-B2rlkkyJ.mjs → context-8Tu9kea0.mjs} +2 -0
- package/dist/{context-CDcAgm4l.d.cts → context-Bl_Y7T_3.d.cts} +80 -1
- package/dist/{context-9T--NSo4.d.ts → context-DMd11IAu.d.ts} +80 -1
- package/dist/{deno-CxoV48e0.mjs → deno-BKp9fYB3.mjs} +1 -1
- package/dist/{docloader-U141HKIl.mjs → docloader-BjJreYQU.mjs} +3 -3
- package/dist/federation/audience.test.d.mts +2 -0
- package/dist/federation/audience.test.mjs +130 -0
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +4 -4
- package/dist/federation/handler-compound-proof.test.mjs +5 -5
- package/dist/federation/handler.test.mjs +7 -7
- package/dist/federation/hashlink-media.test.mjs +3 -3
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/metrics.test.mjs +2 -2
- package/dist/federation/middleware.test.mjs +9 -9
- 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/outgoing-proof.test.mjs +6 -6
- package/dist/federation/portable-actor.test.mjs +4 -4
- package/dist/federation/portable-chain.test.mjs +4 -4
- package/dist/federation/portable-collection.test.mjs +8 -6
- package/dist/federation/portable-delivery.test.d.mts +2 -0
- package/dist/federation/portable-delivery.test.mjs +746 -0
- package/dist/federation/portable-gateway-keys.test.mjs +6 -6
- package/dist/federation/portable-inbox.test.mjs +5 -5
- package/dist/federation/portable-parse-uri.test.mjs +2 -2
- package/dist/federation/portable-route-activity.test.mjs +3 -3
- package/dist/federation/portable.test.mjs +234 -19
- package/dist/federation/send.test.mjs +4 -4
- package/dist/federation/tasks/enqueue.test.mjs +2 -2
- package/dist/federation/tasks/tasks.test.mjs +3 -3
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +3 -3
- package/dist/{federation-Ij8VLHpy.cjs → federation-B-0EXZPH.cjs} +679 -35
- package/dist/{http-BQq1Uldg.mjs → http--IGUT1mg.mjs} +5 -5
- package/dist/{http-D8XakCKn.cjs → http-ByBlnY3D.cjs} +1 -1
- package/dist/{http-CshNXXui.js → http-Di8yiDMY.js} +1 -1
- package/dist/{key-DYgoirTo.mjs → key-ChfV9C3v.mjs} +2 -2
- package/dist/{kv-cache-f_GNq0e7.cjs → kv-cache-BDwUzxBJ.cjs} +1 -1
- package/dist/{kv-cache-D0Mn-VQW.mjs → kv-cache-DFIUR-E5.mjs} +1 -1
- package/dist/{kv-cache-Bpnt_Cck.js → kv-cache-DpwDPt9V.js} +1 -1
- package/dist/{ld-eIekvNtm.mjs → ld-DBBVabBC.mjs} +3 -3
- package/dist/{metrics-BVpdjsaR.mjs → metrics-DPQKFkiJ.mjs} +1 -1
- package/dist/{middleware-BbTJ6C1-.mjs → middleware-BW7-tHAG.mjs} +422 -44
- package/dist/{middleware-rpYJG85a.mjs → middleware-G9zpxUsD.mjs} +1 -1
- package/dist/{middleware-CS3H_4tQ.js → middleware-VXGLiHtY.js} +681 -37
- package/dist/{mod-B3OBLj5g.d.ts → mod-BVNGHgVr.d.ts} +1 -1
- package/dist/{mod-D58KCqUg.d.cts → mod-Cyy1Wahy.d.cts} +1 -1
- package/dist/mod.cjs +4 -4
- package/dist/mod.d.cts +4 -4
- package/dist/mod.d.ts +4 -4
- package/dist/mod.js +4 -4
- package/dist/nodeinfo/client.test.mjs +1 -1
- package/dist/nodeinfo/handler.test.mjs +2 -2
- package/dist/{owner-DpxyMeo9.mjs → owner-BW5VsXG3.mjs} +3 -3
- package/dist/{portable-collection-DyJwtPz1.cjs → portable-collection-CXcAlwei.cjs} +1 -1
- package/dist/{portable-collection-DOL3l2C9.mjs → portable-collection-Ct99JsBe.mjs} +2 -2
- package/dist/{portable-collection-BvJ5om91.js → portable-collection-FTPa5D91.js} +1 -1
- package/dist/{portable-key-B8kTV4n8.mjs → portable-key-U3lW06QJ.mjs} +1 -1
- package/dist/{proof-B9DEHHiO.mjs → proof-MFz2960e.mjs} +4 -4
- package/dist/{send-CZckda2X.mjs → send-Duj8xys1.mjs} +191 -10
- 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 +3 -3
- package/dist/sig/compound-proof.test.mjs +3 -3
- package/dist/sig/http.test.mjs +3 -3
- package/dist/sig/key-owner-evidence.test.mjs +2 -2
- package/dist/sig/key.test.mjs +2 -2
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +2 -2
- package/dist/sig/portable-key-id.test.mjs +2 -2
- package/dist/sig/portable-key.test.mjs +7 -7
- package/dist/sig/proof.test.mjs +4 -4
- package/dist/{temporal-Cc1ymEup.mjs → temporal-CVcPKB1N.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +79 -0
- package/dist/testing/mod.mjs +2 -2
- package/dist/{testing-C1itIjqW.mjs → testing-t8xDeJZ5.mjs} +1 -1
- package/dist/utils/docloader.test.mjs +6 -6
- package/dist/utils/kv-cache.test.mjs +2 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
import { Temporal } from "temporal-polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
|
|
5
|
+
import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
|
|
6
|
+
import { i as rsaPrivateKey2, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
|
|
7
|
+
import { n as FederationImpl, r as InboxContextImpl } from "../middleware-BW7-tHAG.mjs";
|
|
8
|
+
import { n as extractInboxes, t as SendActivityError } from "../send-Duj8xys1.mjs";
|
|
9
|
+
import { Create, Endpoints, Note, Person } from "@fedify/vocab";
|
|
10
|
+
import { parseIri } from "@fedify/vocab-runtime";
|
|
11
|
+
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
12
|
+
import { deepStrictEqual, ok, rejects, strictEqual } from "node:assert/strict";
|
|
13
|
+
//#region src/federation/portable-delivery.test.ts
|
|
14
|
+
const DID = "did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2";
|
|
15
|
+
const OTHER_DID = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
|
|
16
|
+
const PORTABLE_INBOX = `ap+ef61://${DID}/actor/inbox`;
|
|
17
|
+
const ACTOR_ID = `ap+ef61://${encodeURIComponent(DID)}/actor`;
|
|
18
|
+
const inboxOn = (gateway, did = DID) => `${gateway}/.well-known/apgateway/${did}/actor/inbox`;
|
|
19
|
+
function portableActor(options = {}) {
|
|
20
|
+
const did = options.did ?? DID;
|
|
21
|
+
return new Person({
|
|
22
|
+
id: parseIri(options.id ?? `ap://${did}/actor`),
|
|
23
|
+
inbox: parseIri(options.inbox ?? `ap://${did}/actor/inbox`),
|
|
24
|
+
gateways: (options.gateways ?? []).map((g) => new URL(g)),
|
|
25
|
+
endpoints: options.sharedInbox == null ? null : new Endpoints({ sharedInbox: new URL(options.sharedInbox) })
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
test("extractInboxes() resolves portable inboxes", async (t) => {
|
|
29
|
+
await t.step("through the recipient's gateways", () => {
|
|
30
|
+
deepStrictEqual(extractInboxes({ recipients: [portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] })] }), { [inboxOn("https://gw1.example")]: {
|
|
31
|
+
actorIds: /* @__PURE__ */ new Set([ACTOR_ID]),
|
|
32
|
+
sharedInbox: false,
|
|
33
|
+
portableInbox: PORTABLE_INBOX,
|
|
34
|
+
gatewayInboxes: [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]
|
|
35
|
+
} });
|
|
36
|
+
});
|
|
37
|
+
await t.step("through @gateway hints of the inbox", () => {
|
|
38
|
+
const inboxes = extractInboxes({ recipients: [portableActor({ inbox: `ap://${DID}/actor/inbox?@gateway=https%3A%2F%2Fhint1.example&@gateway=https%3A%2F%2Fhint2.example` })] });
|
|
39
|
+
deepStrictEqual(Object.keys(inboxes), [inboxOn("https://hint1.example")]);
|
|
40
|
+
deepStrictEqual(inboxes[inboxOn("https://hint1.example")].gatewayInboxes, [inboxOn("https://hint1.example"), inboxOn("https://hint2.example")]);
|
|
41
|
+
});
|
|
42
|
+
await t.step("preferring gateways to hints", () => {
|
|
43
|
+
deepStrictEqual(extractInboxes({ recipients: [portableActor({
|
|
44
|
+
inbox: `ap://${DID}/actor/inbox?@gateway=https%3A%2F%2Fhint.example`,
|
|
45
|
+
gateways: ["https://gw.example"]
|
|
46
|
+
})] })[inboxOn("https://gw.example")].gatewayInboxes, [inboxOn("https://gw.example")]);
|
|
47
|
+
});
|
|
48
|
+
await t.step("skipping a recipient without gateways", () => {
|
|
49
|
+
deepStrictEqual(extractInboxes({ recipients: [portableActor()] }), {});
|
|
50
|
+
});
|
|
51
|
+
await t.step("skipping invalid gateways", () => {
|
|
52
|
+
const inboxes = extractInboxes({ recipients: [{
|
|
53
|
+
id: parseIri(`ap://${DID}/actor`),
|
|
54
|
+
inboxId: parseIri(`ap://${DID}/actor/inbox`),
|
|
55
|
+
gateways: [new URL("https://bad.example/path"), new URL("https://gw.example")]
|
|
56
|
+
}] });
|
|
57
|
+
deepStrictEqual(Object.keys(inboxes), [inboxOn("https://gw.example")]);
|
|
58
|
+
});
|
|
59
|
+
await t.step("capping the number of gateways", () => {
|
|
60
|
+
strictEqual(extractInboxes({ recipients: [portableActor({ gateways: Array.from({ length: 8 }, (_, i) => `https://gw${i}.example`) })] })[inboxOn("https://gw0.example")].gatewayInboxes?.length, 5);
|
|
61
|
+
});
|
|
62
|
+
await t.step("merging spellings of the same portable inbox", () => {
|
|
63
|
+
const inboxes = extractInboxes({ recipients: [portableActor({ gateways: ["https://gw1.example"] }), portableActor({
|
|
64
|
+
id: `ap+ef61://${encodeURIComponent(DID)}/actor`,
|
|
65
|
+
inbox: `ap+ef61://${encodeURIComponent(DID)}/actor/inbox`,
|
|
66
|
+
gateways: ["https://gw2.example", "https://gw1.example"]
|
|
67
|
+
})] });
|
|
68
|
+
deepStrictEqual(Object.keys(inboxes), [inboxOn("https://gw1.example")]);
|
|
69
|
+
deepStrictEqual(inboxes[inboxOn("https://gw1.example")].gatewayInboxes, [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
|
|
70
|
+
});
|
|
71
|
+
await t.step("merging with a compatible identifier inbox", () => {
|
|
72
|
+
const compatible = {
|
|
73
|
+
id: new URL(`https://gw1.example/.well-known/apgateway/${DID}/actor`),
|
|
74
|
+
inboxId: new URL(inboxOn("https://gw1.example"))
|
|
75
|
+
};
|
|
76
|
+
const portable = portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] });
|
|
77
|
+
for (const recipients of [[compatible, portable], [portable, compatible]]) deepStrictEqual(extractInboxes({ recipients }), { [inboxOn("https://gw1.example")]: {
|
|
78
|
+
actorIds: new Set(recipients.map((r) => r.id.href)),
|
|
79
|
+
sharedInbox: false,
|
|
80
|
+
portableInbox: PORTABLE_INBOX,
|
|
81
|
+
gatewayInboxes: [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]
|
|
82
|
+
} });
|
|
83
|
+
});
|
|
84
|
+
await t.step("merging with an inbox on a later gateway", () => {
|
|
85
|
+
const compatible = {
|
|
86
|
+
id: new URL(`https://gw2.example/.well-known/apgateway/${DID}/actor`),
|
|
87
|
+
inboxId: new URL(inboxOn("https://gw2.example"))
|
|
88
|
+
};
|
|
89
|
+
const portable = portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] });
|
|
90
|
+
for (const recipients of [[compatible, portable], [portable, compatible]]) {
|
|
91
|
+
const inboxes = extractInboxes({ recipients });
|
|
92
|
+
const keys = Object.keys(inboxes);
|
|
93
|
+
strictEqual(keys.length, 1);
|
|
94
|
+
const entry = inboxes[keys[0]];
|
|
95
|
+
strictEqual(entry.gatewayInboxes?.[0], keys[0]);
|
|
96
|
+
deepStrictEqual(new Set(entry.gatewayInboxes), /* @__PURE__ */ new Set([inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]));
|
|
97
|
+
strictEqual(entry.portableInbox, PORTABLE_INBOX);
|
|
98
|
+
strictEqual(entry.actorIds.size, 2);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
await t.step("merging inboxes on several gateways", () => {
|
|
102
|
+
const onGateway = (gateway) => ({
|
|
103
|
+
id: new URL(`${gateway}/.well-known/apgateway/${DID}/actor`),
|
|
104
|
+
inboxId: new URL(inboxOn(gateway))
|
|
105
|
+
});
|
|
106
|
+
const portable = portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] });
|
|
107
|
+
const gw1 = onGateway("https://gw1.example");
|
|
108
|
+
const gw2 = onGateway("https://gw2.example");
|
|
109
|
+
for (const recipients of [
|
|
110
|
+
[
|
|
111
|
+
gw1,
|
|
112
|
+
gw2,
|
|
113
|
+
portable
|
|
114
|
+
],
|
|
115
|
+
[
|
|
116
|
+
gw2,
|
|
117
|
+
gw1,
|
|
118
|
+
portable
|
|
119
|
+
],
|
|
120
|
+
[
|
|
121
|
+
gw1,
|
|
122
|
+
portable,
|
|
123
|
+
gw2
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
portable,
|
|
127
|
+
gw2,
|
|
128
|
+
gw1
|
|
129
|
+
]
|
|
130
|
+
]) {
|
|
131
|
+
const inboxes = extractInboxes({ recipients });
|
|
132
|
+
const keys = Object.keys(inboxes);
|
|
133
|
+
strictEqual(keys.length, 1);
|
|
134
|
+
const entry = inboxes[keys[0]];
|
|
135
|
+
strictEqual(entry.gatewayInboxes?.[0], keys[0]);
|
|
136
|
+
deepStrictEqual(new Set(entry.gatewayInboxes), /* @__PURE__ */ new Set([inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]));
|
|
137
|
+
strictEqual(entry.portableInbox, PORTABLE_INBOX);
|
|
138
|
+
strictEqual(entry.actorIds.size, 3);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
await t.step("merging with a shared inbox of the same URL", () => {
|
|
142
|
+
const shared = {
|
|
143
|
+
id: new URL("https://other.example/users/bob"),
|
|
144
|
+
inboxId: new URL("https://other.example/users/bob/inbox"),
|
|
145
|
+
endpoints: { sharedInbox: new URL(inboxOn("https://gw1.example")) }
|
|
146
|
+
};
|
|
147
|
+
const portable = portableActor({ gateways: ["https://gw1.example"] });
|
|
148
|
+
for (const recipients of [[shared, portable], [portable, shared]]) {
|
|
149
|
+
const inboxes = extractInboxes({
|
|
150
|
+
recipients,
|
|
151
|
+
preferSharedInbox: true
|
|
152
|
+
});
|
|
153
|
+
strictEqual(inboxes[inboxOn("https://gw1.example")].sharedInbox, false);
|
|
154
|
+
strictEqual(inboxes[inboxOn("https://gw1.example")].portableInbox, PORTABLE_INBOX);
|
|
155
|
+
strictEqual(inboxes[inboxOn("https://gw1.example")].actorIds.size, 2);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
await t.step("not preferring the shared inbox of a portable inbox", () => {
|
|
159
|
+
const inboxes = extractInboxes({
|
|
160
|
+
recipients: [portableActor({
|
|
161
|
+
gateways: ["https://gw.example"],
|
|
162
|
+
sharedInbox: "https://gw.example/inbox"
|
|
163
|
+
})],
|
|
164
|
+
preferSharedInbox: true
|
|
165
|
+
});
|
|
166
|
+
deepStrictEqual(Object.keys(inboxes), [inboxOn("https://gw.example")]);
|
|
167
|
+
strictEqual(inboxes[inboxOn("https://gw.example")].sharedInbox, false);
|
|
168
|
+
});
|
|
169
|
+
await t.step("excluding gateways by excludeBaseUris", () => {
|
|
170
|
+
const recipients = [portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] })];
|
|
171
|
+
let inboxes = extractInboxes({
|
|
172
|
+
recipients,
|
|
173
|
+
excludeBaseUris: [new URL("https://gw1.example/")]
|
|
174
|
+
});
|
|
175
|
+
deepStrictEqual(inboxes[inboxOn("https://gw2.example")].gatewayInboxes, [inboxOn("https://gw2.example")]);
|
|
176
|
+
inboxes = extractInboxes({
|
|
177
|
+
recipients,
|
|
178
|
+
excludeBaseUris: [new URL("https://gw1.example/"), new URL("https://gw2.example/foo")]
|
|
179
|
+
});
|
|
180
|
+
deepStrictEqual(inboxes, {});
|
|
181
|
+
});
|
|
182
|
+
await t.step("keeping ordinary inboxes unchanged", () => {
|
|
183
|
+
deepStrictEqual(extractInboxes({ recipients: [new Person({
|
|
184
|
+
id: new URL("https://example.com/alice"),
|
|
185
|
+
inbox: new URL("https://example.com/alice/inbox")
|
|
186
|
+
})] }), { "https://example.com/alice/inbox": {
|
|
187
|
+
actorIds: /* @__PURE__ */ new Set(["https://example.com/alice"]),
|
|
188
|
+
sharedInbox: false
|
|
189
|
+
} });
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
function createQueue(options = {}) {
|
|
193
|
+
const queued = [];
|
|
194
|
+
return {
|
|
195
|
+
queue: {
|
|
196
|
+
nativeRetrial: options.nativeRetrial,
|
|
197
|
+
enqueue(message, options) {
|
|
198
|
+
queued.push({
|
|
199
|
+
message: JSON.parse(JSON.stringify(message)),
|
|
200
|
+
options
|
|
201
|
+
});
|
|
202
|
+
return Promise.resolve();
|
|
203
|
+
},
|
|
204
|
+
listen() {
|
|
205
|
+
return Promise.resolve();
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
queued
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const sender = {
|
|
212
|
+
keyId: rsaPublicKey2.id,
|
|
213
|
+
privateKey: rsaPrivateKey2
|
|
214
|
+
};
|
|
215
|
+
function createActivity(id = "https://example.com/activities/1") {
|
|
216
|
+
return new Create({
|
|
217
|
+
id: new URL(id),
|
|
218
|
+
actor: new URL("https://example.com/person2"),
|
|
219
|
+
object: new Note({
|
|
220
|
+
id: new URL(`${id}/note`),
|
|
221
|
+
content: "Hello"
|
|
222
|
+
})
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
test("Context.sendActivity() to a portable inbox", async (t) => {
|
|
226
|
+
esm_default.spyGlobal();
|
|
227
|
+
await t.step("immediately", async () => {
|
|
228
|
+
esm_default.hardReset();
|
|
229
|
+
esm_default.spyGlobal();
|
|
230
|
+
const requests = [];
|
|
231
|
+
esm_default.post("begin:https://gw1.example/", (call) => {
|
|
232
|
+
requests.push(call.url);
|
|
233
|
+
return 202;
|
|
234
|
+
});
|
|
235
|
+
await new FederationImpl({
|
|
236
|
+
kv: new MemoryKvStore(),
|
|
237
|
+
allowPrivateAddress: true
|
|
238
|
+
}).createContext(new URL("https://example.com/")).sendActivity(sender, portableActor({ gateways: ["https://gw1.example"] }), createActivity(), { immediate: true });
|
|
239
|
+
deepStrictEqual(requests, [inboxOn("https://gw1.example")]);
|
|
240
|
+
});
|
|
241
|
+
await t.step("immediately, failing over to the next gateway", async () => {
|
|
242
|
+
esm_default.hardReset();
|
|
243
|
+
esm_default.spyGlobal();
|
|
244
|
+
const requests = [];
|
|
245
|
+
esm_default.post("begin:https://gw1.example/", (call) => {
|
|
246
|
+
requests.push(call.url);
|
|
247
|
+
return 404;
|
|
248
|
+
});
|
|
249
|
+
esm_default.post("begin:https://gw2.example/", (call) => {
|
|
250
|
+
requests.push(call.url);
|
|
251
|
+
return 202;
|
|
252
|
+
});
|
|
253
|
+
await new FederationImpl({
|
|
254
|
+
kv: new MemoryKvStore(),
|
|
255
|
+
allowPrivateAddress: true
|
|
256
|
+
}).createContext(new URL("https://example.com/")).sendActivity(sender, portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] }), createActivity(), { immediate: true });
|
|
257
|
+
deepStrictEqual([...new Set(requests)], [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
|
|
258
|
+
});
|
|
259
|
+
await t.step("immediately, failing on every gateway", async () => {
|
|
260
|
+
esm_default.hardReset();
|
|
261
|
+
esm_default.spyGlobal();
|
|
262
|
+
esm_default.post("begin:https://gw1.example/", 500);
|
|
263
|
+
esm_default.post("begin:https://gw2.example/", 404);
|
|
264
|
+
const ctx = new FederationImpl({
|
|
265
|
+
kv: new MemoryKvStore(),
|
|
266
|
+
allowPrivateAddress: true
|
|
267
|
+
}).createContext(new URL("https://example.com/"));
|
|
268
|
+
await rejects(() => ctx.sendActivity(sender, portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] }), createActivity(), { immediate: true }), (error) => error instanceof SendActivityError && error.statusCode === 404 && error.inbox.href === inboxOn("https://gw2.example"));
|
|
269
|
+
});
|
|
270
|
+
await t.step("rejecting private gateways", async () => {
|
|
271
|
+
esm_default.hardReset();
|
|
272
|
+
esm_default.spyGlobal();
|
|
273
|
+
let requests = 0;
|
|
274
|
+
esm_default.post("begin:http://127.0.0.1/", () => {
|
|
275
|
+
requests++;
|
|
276
|
+
return 202;
|
|
277
|
+
});
|
|
278
|
+
const ctx = new FederationImpl({ kv: new MemoryKvStore() }).createContext(new URL("https://example.com/"));
|
|
279
|
+
await rejects(() => ctx.sendActivity(sender, portableActor({ gateways: ["http://127.0.0.1"] }), createActivity(), { immediate: true }));
|
|
280
|
+
strictEqual(requests, 0);
|
|
281
|
+
});
|
|
282
|
+
await t.step("through the queue", async () => {
|
|
283
|
+
esm_default.hardReset();
|
|
284
|
+
esm_default.spyGlobal();
|
|
285
|
+
const requests = [];
|
|
286
|
+
esm_default.post("begin:https://gw1.example/", (call) => {
|
|
287
|
+
requests.push(call.url);
|
|
288
|
+
return 404;
|
|
289
|
+
});
|
|
290
|
+
esm_default.post("begin:https://gw2.example/", (call) => {
|
|
291
|
+
requests.push(call.url);
|
|
292
|
+
return 202;
|
|
293
|
+
});
|
|
294
|
+
const { queue, queued } = createQueue();
|
|
295
|
+
const federation = new FederationImpl({
|
|
296
|
+
kv: new MemoryKvStore(),
|
|
297
|
+
queue,
|
|
298
|
+
manuallyStartQueue: true,
|
|
299
|
+
allowPrivateAddress: true
|
|
300
|
+
});
|
|
301
|
+
await federation.createContext(new URL("https://example.com/")).sendActivity(sender, [portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] }), portableActor({
|
|
302
|
+
did: OTHER_DID,
|
|
303
|
+
gateways: ["https://gw1.example"]
|
|
304
|
+
})], createActivity(), {
|
|
305
|
+
orderingKey: "key",
|
|
306
|
+
fanout: "skip"
|
|
307
|
+
});
|
|
308
|
+
strictEqual(queued.length, 2);
|
|
309
|
+
const messages = queued.map((q) => q.message);
|
|
310
|
+
deepStrictEqual(messages.map((m) => m.inbox), [inboxOn("https://gw1.example"), inboxOn("https://gw1.example", OTHER_DID)]);
|
|
311
|
+
deepStrictEqual(messages[0].gatewayInboxes, [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
|
|
312
|
+
strictEqual(messages[0].portableInbox, PORTABLE_INBOX);
|
|
313
|
+
deepStrictEqual(queued.map((q) => q.options?.orderingKey), [`key\n${DID}`, `key\n${OTHER_DID}`]);
|
|
314
|
+
queued.length = 0;
|
|
315
|
+
await federation.processQueuedTask(void 0, messages[0]);
|
|
316
|
+
deepStrictEqual([...new Set(requests)], [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
|
|
317
|
+
strictEqual(queued.length, 0);
|
|
318
|
+
});
|
|
319
|
+
await t.step("through the fanout queue", async () => {
|
|
320
|
+
esm_default.hardReset();
|
|
321
|
+
esm_default.spyGlobal();
|
|
322
|
+
const { queue, queued } = createQueue();
|
|
323
|
+
const federation = new FederationImpl({
|
|
324
|
+
kv: new MemoryKvStore(),
|
|
325
|
+
queue,
|
|
326
|
+
manuallyStartQueue: true,
|
|
327
|
+
allowPrivateAddress: true
|
|
328
|
+
});
|
|
329
|
+
await federation.createContext(new URL("https://example.com/")).sendActivity(sender, portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] }), createActivity(), { fanout: "force" });
|
|
330
|
+
strictEqual(queued.length, 1);
|
|
331
|
+
const fanout = queued[0].message;
|
|
332
|
+
deepStrictEqual(fanout.inboxes, { [inboxOn("https://gw1.example")]: {
|
|
333
|
+
actorIds: [ACTOR_ID],
|
|
334
|
+
sharedInbox: false,
|
|
335
|
+
portableInbox: PORTABLE_INBOX,
|
|
336
|
+
gatewayInboxes: [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]
|
|
337
|
+
} });
|
|
338
|
+
queued.length = 0;
|
|
339
|
+
await federation.processQueuedTask(void 0, fanout);
|
|
340
|
+
strictEqual(queued.length, 1);
|
|
341
|
+
const outbox = queued[0].message;
|
|
342
|
+
strictEqual(outbox.inbox, inboxOn("https://gw1.example"));
|
|
343
|
+
deepStrictEqual(outbox.gatewayInboxes, [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
|
|
344
|
+
});
|
|
345
|
+
esm_default.hardReset();
|
|
346
|
+
});
|
|
347
|
+
test("InboxContext.forwardActivity() to a portable inbox", async (t) => {
|
|
348
|
+
esm_default.spyGlobal();
|
|
349
|
+
const activity = {
|
|
350
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
351
|
+
type: "Create",
|
|
352
|
+
id: "https://example.com/activities/1",
|
|
353
|
+
actor: "https://example.com/person2"
|
|
354
|
+
};
|
|
355
|
+
const recipient = portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] });
|
|
356
|
+
function createInboxContext(federation) {
|
|
357
|
+
return new InboxContextImpl(null, activity, activity.id, "https://www.w3.org/ns/activitystreams#Create", {
|
|
358
|
+
data: void 0,
|
|
359
|
+
federation,
|
|
360
|
+
url: new URL("https://example.com/"),
|
|
361
|
+
documentLoader: mockDocumentLoader,
|
|
362
|
+
contextLoader: mockDocumentLoader
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
await t.step("immediately", async () => {
|
|
366
|
+
esm_default.hardReset();
|
|
367
|
+
esm_default.spyGlobal();
|
|
368
|
+
const requests = [];
|
|
369
|
+
esm_default.post("begin:https://gw1.example/", (call) => {
|
|
370
|
+
requests.push(call.url);
|
|
371
|
+
return 500;
|
|
372
|
+
});
|
|
373
|
+
esm_default.post("begin:https://gw2.example/", (call) => {
|
|
374
|
+
requests.push(call.url);
|
|
375
|
+
return 202;
|
|
376
|
+
});
|
|
377
|
+
await createInboxContext(new FederationImpl({
|
|
378
|
+
kv: new MemoryKvStore(),
|
|
379
|
+
allowPrivateAddress: true
|
|
380
|
+
})).forwardActivity(sender, recipient, {
|
|
381
|
+
immediate: true,
|
|
382
|
+
skipIfUnsigned: false
|
|
383
|
+
});
|
|
384
|
+
deepStrictEqual([...new Set(requests)], [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
|
|
385
|
+
});
|
|
386
|
+
await t.step("through the queue", async () => {
|
|
387
|
+
esm_default.hardReset();
|
|
388
|
+
esm_default.spyGlobal();
|
|
389
|
+
const { queue, queued } = createQueue();
|
|
390
|
+
await createInboxContext(new FederationImpl({
|
|
391
|
+
kv: new MemoryKvStore(),
|
|
392
|
+
queue,
|
|
393
|
+
manuallyStartQueue: true,
|
|
394
|
+
allowPrivateAddress: true
|
|
395
|
+
})).forwardActivity(sender, recipient, {
|
|
396
|
+
orderingKey: "key",
|
|
397
|
+
skipIfUnsigned: false
|
|
398
|
+
});
|
|
399
|
+
strictEqual(queued.length, 1);
|
|
400
|
+
const message = queued[0].message;
|
|
401
|
+
strictEqual(message.inbox, inboxOn("https://gw1.example"));
|
|
402
|
+
strictEqual(message.portableInbox, PORTABLE_INBOX);
|
|
403
|
+
deepStrictEqual(message.gatewayInboxes, [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
|
|
404
|
+
strictEqual(queued[0].options?.orderingKey, `key\n${DID}`);
|
|
405
|
+
});
|
|
406
|
+
esm_default.hardReset();
|
|
407
|
+
});
|
|
408
|
+
test("processQueuedTask() with portable inbox gateways", async (t) => {
|
|
409
|
+
esm_default.spyGlobal();
|
|
410
|
+
function createMessage(gateways, overrides = {}) {
|
|
411
|
+
const gatewayInboxes = gateways.map((g) => inboxOn(g));
|
|
412
|
+
return {
|
|
413
|
+
type: "outbox",
|
|
414
|
+
id: crypto.randomUUID(),
|
|
415
|
+
baseUrl: "https://example.com",
|
|
416
|
+
keys: [],
|
|
417
|
+
activity: {
|
|
418
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
419
|
+
type: "Create",
|
|
420
|
+
id: "https://example.com/activities/1",
|
|
421
|
+
actor: "https://example.com/person2",
|
|
422
|
+
object: {
|
|
423
|
+
type: "Note",
|
|
424
|
+
content: "test"
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
activityId: "https://example.com/activities/1",
|
|
428
|
+
activityType: "https://www.w3.org/ns/activitystreams#Create",
|
|
429
|
+
inbox: gatewayInboxes[0],
|
|
430
|
+
sharedInbox: false,
|
|
431
|
+
actorIds: [`ap+ef61://${DID}/actor`],
|
|
432
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
433
|
+
attempt: 0,
|
|
434
|
+
headers: {},
|
|
435
|
+
orderingKey: `key\n${DID}`,
|
|
436
|
+
portableInbox: PORTABLE_INBOX,
|
|
437
|
+
gatewayInboxes,
|
|
438
|
+
traceContext: {},
|
|
439
|
+
...overrides
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function setup(options = {}, queueOptions = {}) {
|
|
443
|
+
const { queue, queued } = createQueue(queueOptions);
|
|
444
|
+
const permanentFailures = [];
|
|
445
|
+
const federation = new FederationImpl({
|
|
446
|
+
kv: new MemoryKvStore(),
|
|
447
|
+
queue,
|
|
448
|
+
manuallyStartQueue: true,
|
|
449
|
+
allowPrivateAddress: true,
|
|
450
|
+
...options
|
|
451
|
+
});
|
|
452
|
+
federation.setOutboxPermanentFailureHandler((_ctx, values) => {
|
|
453
|
+
permanentFailures.push({
|
|
454
|
+
inbox: values.inbox,
|
|
455
|
+
statusCode: values.statusCode
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
federation,
|
|
460
|
+
queued,
|
|
461
|
+
permanentFailures
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function mockGateways(responses) {
|
|
465
|
+
esm_default.hardReset();
|
|
466
|
+
esm_default.spyGlobal();
|
|
467
|
+
const requests = [];
|
|
468
|
+
for (const [gateway, response] of Object.entries(responses)) esm_default.post(`begin:${gateway}/`, (call) => {
|
|
469
|
+
const { origin } = new URL(call.url);
|
|
470
|
+
if (requests.at(-1) !== origin) requests.push(origin);
|
|
471
|
+
return response instanceof Response ? response.clone() : response;
|
|
472
|
+
});
|
|
473
|
+
return requests;
|
|
474
|
+
}
|
|
475
|
+
await t.step("fails over from a transport error", async () => {
|
|
476
|
+
esm_default.hardReset();
|
|
477
|
+
esm_default.spyGlobal();
|
|
478
|
+
const requests = [];
|
|
479
|
+
esm_default.post("begin:https://gw1.example/", () => {
|
|
480
|
+
requests.push("https://gw1.example");
|
|
481
|
+
throw new TypeError("network error");
|
|
482
|
+
});
|
|
483
|
+
esm_default.post("begin:https://gw2.example/", () => {
|
|
484
|
+
requests.push("https://gw2.example");
|
|
485
|
+
return 202;
|
|
486
|
+
});
|
|
487
|
+
const { federation, queued } = setup();
|
|
488
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
|
|
489
|
+
deepStrictEqual(requests, ["https://gw1.example", "https://gw2.example"]);
|
|
490
|
+
strictEqual(queued.length, 0);
|
|
491
|
+
});
|
|
492
|
+
await t.step("retries the remaining gateways as one attempt", async () => {
|
|
493
|
+
const requests = mockGateways({
|
|
494
|
+
"https://gw1.example": 500,
|
|
495
|
+
"https://gw2.example": 404,
|
|
496
|
+
"https://gw3.example": 502
|
|
497
|
+
});
|
|
498
|
+
const { federation, queued, permanentFailures } = setup();
|
|
499
|
+
await federation.processQueuedTask(void 0, createMessage([
|
|
500
|
+
"https://gw1.example",
|
|
501
|
+
"https://gw2.example",
|
|
502
|
+
"https://gw3.example"
|
|
503
|
+
]));
|
|
504
|
+
deepStrictEqual(requests, [
|
|
505
|
+
"https://gw1.example",
|
|
506
|
+
"https://gw2.example",
|
|
507
|
+
"https://gw3.example"
|
|
508
|
+
]);
|
|
509
|
+
strictEqual(permanentFailures.length, 0);
|
|
510
|
+
strictEqual(queued.length, 1);
|
|
511
|
+
const retry = queued[0].message;
|
|
512
|
+
strictEqual(retry.attempt, 1);
|
|
513
|
+
strictEqual(retry.inbox, inboxOn("https://gw1.example"));
|
|
514
|
+
deepStrictEqual(retry.gatewayInboxes, [inboxOn("https://gw1.example"), inboxOn("https://gw3.example")]);
|
|
515
|
+
strictEqual(queued[0].options?.orderingKey, `key\n${DID}`);
|
|
516
|
+
ok(queued[0].options?.delay != null);
|
|
517
|
+
});
|
|
518
|
+
await t.step("reports a permanent failure once", async () => {
|
|
519
|
+
mockGateways({
|
|
520
|
+
"https://gw1.example": 410,
|
|
521
|
+
"https://gw2.example": 404
|
|
522
|
+
});
|
|
523
|
+
const { federation, queued, permanentFailures } = setup();
|
|
524
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
|
|
525
|
+
strictEqual(queued.length, 0);
|
|
526
|
+
deepStrictEqual(permanentFailures, [{
|
|
527
|
+
inbox: new URL(inboxOn("https://gw2.example")),
|
|
528
|
+
statusCode: 404
|
|
529
|
+
}]);
|
|
530
|
+
});
|
|
531
|
+
await t.step("respects custom permanent failure statuses", async () => {
|
|
532
|
+
mockGateways({
|
|
533
|
+
"https://gw1.example": 404,
|
|
534
|
+
"https://gw2.example": 500
|
|
535
|
+
});
|
|
536
|
+
const { federation, queued, permanentFailures } = setup({ permanentFailureStatusCodes: [500] });
|
|
537
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
|
|
538
|
+
strictEqual(permanentFailures.length, 0);
|
|
539
|
+
deepStrictEqual(queued[0].message.gatewayInboxes, [inboxOn("https://gw1.example")]);
|
|
540
|
+
});
|
|
541
|
+
await t.step("gives up when the retry policy does", async () => {
|
|
542
|
+
mockGateways({
|
|
543
|
+
"https://gw1.example": 500,
|
|
544
|
+
"https://gw2.example": 500
|
|
545
|
+
});
|
|
546
|
+
const { federation, queued, permanentFailures } = setup({ outboxRetryPolicy: () => null });
|
|
547
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
|
|
548
|
+
strictEqual(queued.length, 0);
|
|
549
|
+
strictEqual(permanentFailures.length, 0);
|
|
550
|
+
});
|
|
551
|
+
await t.step("throws with a natively retrying queue", async () => {
|
|
552
|
+
mockGateways({
|
|
553
|
+
"https://gw1.example": 500,
|
|
554
|
+
"https://gw2.example": 502
|
|
555
|
+
});
|
|
556
|
+
const { federation, queued } = setup({}, { nativeRetrial: true });
|
|
557
|
+
await rejects(() => federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"])), (error) => error instanceof SendActivityError && error.statusCode === 502);
|
|
558
|
+
strictEqual(queued.length, 0);
|
|
559
|
+
});
|
|
560
|
+
await t.step("excludes permanent failures from native retries", async () => {
|
|
561
|
+
mockGateways({
|
|
562
|
+
"https://gw1.example": 404,
|
|
563
|
+
"https://gw2.example": 500
|
|
564
|
+
});
|
|
565
|
+
const { federation, queued } = setup({}, { nativeRetrial: true });
|
|
566
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
|
|
567
|
+
strictEqual(queued.length, 1);
|
|
568
|
+
const retry = queued[0].message;
|
|
569
|
+
strictEqual(retry.attempt, 1);
|
|
570
|
+
deepStrictEqual(retry.gatewayInboxes, [inboxOn("https://gw2.example")]);
|
|
571
|
+
});
|
|
572
|
+
await t.step("drops a held delivery deferred by Retry-After", async () => {
|
|
573
|
+
const requests = mockGateways({
|
|
574
|
+
"https://gw1.example": 202,
|
|
575
|
+
"https://gw2.example": 202
|
|
576
|
+
});
|
|
577
|
+
const { federation, queued } = setup({ circuitBreaker: { heldActivityTtl: { hours: 1 } } });
|
|
578
|
+
const later = Temporal.Now.instant().add({ hours: 9 }).toString();
|
|
579
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"], {
|
|
580
|
+
circuitHeld: true,
|
|
581
|
+
circuitHeldSince: Temporal.Now.instant().subtract({ hours: 1 }).toString(),
|
|
582
|
+
gatewayNotBefore: {
|
|
583
|
+
[inboxOn("https://gw1.example")]: later,
|
|
584
|
+
[inboxOn("https://gw2.example")]: later
|
|
585
|
+
}
|
|
586
|
+
}));
|
|
587
|
+
deepStrictEqual(requests, []);
|
|
588
|
+
strictEqual(queued.length, 0);
|
|
589
|
+
});
|
|
590
|
+
await t.step("retries when the first gateway is due", async () => {
|
|
591
|
+
mockGateways({
|
|
592
|
+
"https://gw1.example": new Response(null, {
|
|
593
|
+
status: 429,
|
|
594
|
+
headers: { "Retry-After": "60" }
|
|
595
|
+
}),
|
|
596
|
+
"https://gw2.example": new Response(null, {
|
|
597
|
+
status: 503,
|
|
598
|
+
headers: { "Retry-After": "3600" }
|
|
599
|
+
})
|
|
600
|
+
});
|
|
601
|
+
const { federation, queued } = setup();
|
|
602
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
|
|
603
|
+
strictEqual(queued.length, 1);
|
|
604
|
+
const retry = queued[0].message;
|
|
605
|
+
strictEqual(retry.attempt, 1);
|
|
606
|
+
deepStrictEqual(Object.keys(retry.gatewayNotBefore ?? {}), [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
|
|
607
|
+
strictEqual(Math.round(queued[0].options.delay.total("second")), 60);
|
|
608
|
+
});
|
|
609
|
+
await t.step("does not wait for Retry-After of every gateway", async () => {
|
|
610
|
+
mockGateways({
|
|
611
|
+
"https://gw1.example": new Response(null, {
|
|
612
|
+
status: 429,
|
|
613
|
+
headers: { "Retry-After": "3600" }
|
|
614
|
+
}),
|
|
615
|
+
"https://gw2.example": 500
|
|
616
|
+
});
|
|
617
|
+
const { federation, queued } = setup({ outboxRetryPolicy: () => Temporal.Duration.from({ seconds: 30 }) });
|
|
618
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
|
|
619
|
+
strictEqual(queued.length, 1);
|
|
620
|
+
const retry = queued[0].message;
|
|
621
|
+
deepStrictEqual(Object.keys(retry.gatewayNotBefore ?? {}), [inboxOn("https://gw1.example")]);
|
|
622
|
+
strictEqual(Math.round(queued[0].options.delay.total("second")), 30);
|
|
623
|
+
});
|
|
624
|
+
await t.step("ignores a Retry-After too long to honor", async () => {
|
|
625
|
+
const requests = mockGateways({
|
|
626
|
+
"https://gw1.example": new Response(null, {
|
|
627
|
+
status: 429,
|
|
628
|
+
headers: { "Retry-After": "9000000000000" }
|
|
629
|
+
}),
|
|
630
|
+
"https://gw2.example": 202
|
|
631
|
+
});
|
|
632
|
+
const { federation, queued } = setup();
|
|
633
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
|
|
634
|
+
deepStrictEqual(requests, ["https://gw1.example", "https://gw2.example"]);
|
|
635
|
+
strictEqual(queued.length, 0);
|
|
636
|
+
});
|
|
637
|
+
await t.step("skips gateways that are not to be tried yet", async () => {
|
|
638
|
+
const requests = mockGateways({
|
|
639
|
+
"https://gw1.example": 202,
|
|
640
|
+
"https://gw2.example": 202
|
|
641
|
+
});
|
|
642
|
+
const { federation, queued } = setup();
|
|
643
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"], { gatewayNotBefore: { [inboxOn("https://gw1.example")]: Temporal.Now.instant().add({ hours: 1 }).toString() } }));
|
|
644
|
+
deepStrictEqual(requests, ["https://gw2.example"]);
|
|
645
|
+
strictEqual(queued.length, 0);
|
|
646
|
+
});
|
|
647
|
+
await t.step("holds when every gateway is held", async () => {
|
|
648
|
+
const requests = mockGateways({
|
|
649
|
+
"https://gw1.example": 202,
|
|
650
|
+
"https://gw2.example": 202
|
|
651
|
+
});
|
|
652
|
+
const { federation, queued } = setup();
|
|
653
|
+
const inAnHour = Temporal.Now.instant().add({ hours: 1 }).toString();
|
|
654
|
+
const inAMinute = Temporal.Now.instant().add({ minutes: 1 }).toString();
|
|
655
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"], { gatewayNotBefore: {
|
|
656
|
+
[inboxOn("https://gw1.example")]: inAnHour,
|
|
657
|
+
[inboxOn("https://gw2.example")]: inAMinute
|
|
658
|
+
} }));
|
|
659
|
+
deepStrictEqual(requests, []);
|
|
660
|
+
strictEqual(queued.length, 1);
|
|
661
|
+
const held = queued[0].message;
|
|
662
|
+
strictEqual(held.attempt, 0);
|
|
663
|
+
strictEqual(held.circuitHeld, void 0);
|
|
664
|
+
ok(queued[0].options.delay.total("second") <= 60);
|
|
665
|
+
ok(queued[0].options.delay.total("second") > 50);
|
|
666
|
+
});
|
|
667
|
+
await t.step("skips a gateway whose circuit is open", async () => {
|
|
668
|
+
const requests = mockGateways({
|
|
669
|
+
"https://gw1.example": 500,
|
|
670
|
+
"https://gw2.example": 202
|
|
671
|
+
});
|
|
672
|
+
const { federation, queued } = setup({ circuitBreaker: {
|
|
673
|
+
failureThreshold: 1,
|
|
674
|
+
recoveryDelay: { hours: 1 }
|
|
675
|
+
} });
|
|
676
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
|
|
677
|
+
deepStrictEqual(requests, ["https://gw1.example", "https://gw2.example"]);
|
|
678
|
+
strictEqual(queued.length, 0);
|
|
679
|
+
requests.length = 0;
|
|
680
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
|
|
681
|
+
deepStrictEqual(requests, ["https://gw2.example"]);
|
|
682
|
+
strictEqual(queued.length, 0);
|
|
683
|
+
});
|
|
684
|
+
await t.step("holds a permanent failure and an open circuit", async () => {
|
|
685
|
+
const requests = mockGateways({
|
|
686
|
+
"https://gw1.example": 500,
|
|
687
|
+
"https://gw2.example": 404
|
|
688
|
+
});
|
|
689
|
+
const { federation, queued, permanentFailures } = setup({ circuitBreaker: {
|
|
690
|
+
failureThreshold: 1,
|
|
691
|
+
recoveryDelay: { hours: 1 }
|
|
692
|
+
} });
|
|
693
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
|
|
694
|
+
deepStrictEqual(requests, ["https://gw1.example", "https://gw2.example"]);
|
|
695
|
+
strictEqual(permanentFailures.length, 0);
|
|
696
|
+
strictEqual(queued.length, 1);
|
|
697
|
+
const held = queued[0].message;
|
|
698
|
+
strictEqual(held.attempt, 0);
|
|
699
|
+
strictEqual(held.circuitHeld, true);
|
|
700
|
+
ok(held.circuitHeldSince != null);
|
|
701
|
+
strictEqual(held.inbox, inboxOn("https://gw1.example"));
|
|
702
|
+
deepStrictEqual(held.gatewayInboxes, [inboxOn("https://gw1.example")]);
|
|
703
|
+
strictEqual(Math.round(queued[0].options.delay.total("minute")), 60);
|
|
704
|
+
});
|
|
705
|
+
await t.step("preserves when the delivery was first held", async () => {
|
|
706
|
+
mockGateways({
|
|
707
|
+
"https://gw1.example": 500,
|
|
708
|
+
"https://gw2.example": 500
|
|
709
|
+
});
|
|
710
|
+
const { federation, queued } = setup({ circuitBreaker: { failureThreshold: 10 } });
|
|
711
|
+
const heldSince = Temporal.Now.instant().subtract({ hours: 1 }).toString();
|
|
712
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"], {
|
|
713
|
+
circuitHeld: true,
|
|
714
|
+
circuitHeldSince: heldSince
|
|
715
|
+
}));
|
|
716
|
+
strictEqual(queued.length, 1);
|
|
717
|
+
const retry = queued[0].message;
|
|
718
|
+
strictEqual(retry.attempt, 1);
|
|
719
|
+
strictEqual(retry.circuitHeld, void 0);
|
|
720
|
+
strictEqual(retry.circuitHeldSince, heldSince);
|
|
721
|
+
});
|
|
722
|
+
await t.step("drops a delivery held for too long", async () => {
|
|
723
|
+
const requests = mockGateways({
|
|
724
|
+
"https://gw1.example": 202,
|
|
725
|
+
"https://gw2.example": 202
|
|
726
|
+
});
|
|
727
|
+
const { federation, queued } = setup({ circuitBreaker: { heldActivityTtl: { hours: 1 } } });
|
|
728
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"], {
|
|
729
|
+
circuitHeld: true,
|
|
730
|
+
circuitHeldSince: Temporal.Now.instant().subtract({ hours: 2 }).toString()
|
|
731
|
+
}));
|
|
732
|
+
deepStrictEqual(requests, []);
|
|
733
|
+
strictEqual(queued.length, 0);
|
|
734
|
+
});
|
|
735
|
+
await t.step("falls back to the ordinary path for one gateway", async () => {
|
|
736
|
+
const requests = mockGateways({ "https://gw1.example": 404 });
|
|
737
|
+
const { federation, queued, permanentFailures } = setup();
|
|
738
|
+
await federation.processQueuedTask(void 0, createMessage(["https://gw1.example"]));
|
|
739
|
+
deepStrictEqual(requests, ["https://gw1.example"]);
|
|
740
|
+
strictEqual(queued.length, 0);
|
|
741
|
+
strictEqual(permanentFailures.length, 1);
|
|
742
|
+
});
|
|
743
|
+
esm_default.hardReset();
|
|
744
|
+
});
|
|
745
|
+
//#endregion
|
|
746
|
+
export {};
|