@fedify/fedify 2.4.0-dev.1564 → 2.4.0-dev.1570

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 (113) hide show
  1. package/README.md +4 -0
  2. package/dist/{assert-OguE97r2.mjs → assert-DvVHoIZ0.mjs} +1 -1
  3. package/dist/{assert_instance_of-DBC5X09g.mjs → assert_instance_of-CxoC8bVq.mjs} +1 -1
  4. package/dist/{assert_not_equals-DkVK8oqV.mjs → assert_not_equals-C8vcTTA3.mjs} +1 -1
  5. package/dist/{assert_rejects-DN60FHPX.mjs → assert_rejects-DAdaaIWM.mjs} +2 -2
  6. package/dist/{assert_strict_equals-XEgZAlrj.mjs → assert_strict_equals-BzqyKuYL.mjs} +1 -1
  7. package/dist/{assert_throws-BOkhLGYc.mjs → assert_throws-zN79lgIC.mjs} +1 -1
  8. package/dist/{builder-DQrTY2uR.mjs → builder-CJnWbgbj.mjs} +23 -3
  9. package/dist/codec-nfBU4Up4.mjs +124 -0
  10. package/dist/compat/mod.d.cts +1 -1
  11. package/dist/compat/mod.d.ts +1 -1
  12. package/dist/compat/outgoing-jsonld.test.mjs +3 -3
  13. package/dist/compat/public-audience.test.mjs +3 -3
  14. package/dist/compat/transformers.test.mjs +5 -5
  15. package/dist/{context-DVoTs_wM.mjs → context-BBRTgkrs.mjs} +8 -2
  16. package/dist/{context-BBVLF7lx.d.cts → context-BLe0NPtZ.d.cts} +530 -259
  17. package/dist/{context-BU6jSQdo.d.ts → context-GfQA-CqB.d.ts} +529 -259
  18. package/dist/{deno-0Q35TVjL.mjs → deno-klNxGW7W.mjs} +1 -1
  19. package/dist/{docloader-DnNi7FT5.mjs → docloader-CuFH35j1.mjs} +3 -3
  20. package/dist/federation/builder.test.mjs +5 -5
  21. package/dist/federation/circuit-breaker.test.mjs +5 -5
  22. package/dist/federation/collection.test.mjs +3 -3
  23. package/dist/federation/handler.test.mjs +13 -23
  24. package/dist/federation/idempotency.test.mjs +6 -6
  25. package/dist/federation/inbox.test.mjs +3 -3
  26. package/dist/federation/keycache.test.mjs +5 -5
  27. package/dist/federation/kv.test.mjs +3 -3
  28. package/dist/federation/metrics.test.mjs +4 -4
  29. package/dist/federation/middleware.test.mjs +204 -19
  30. package/dist/federation/mod.cjs +3 -259
  31. package/dist/federation/mod.d.cts +4 -4
  32. package/dist/federation/mod.d.ts +4 -4
  33. package/dist/federation/mod.js +1 -257
  34. package/dist/federation/mq.test.mjs +103 -261
  35. package/dist/federation/negotiation.test.mjs +4 -4
  36. package/dist/federation/retry.test.mjs +3 -3
  37. package/dist/federation/router.test.mjs +5 -5
  38. package/dist/federation/send.test.mjs +10 -10
  39. package/dist/federation/tasks/codec.test.d.mts +2 -0
  40. package/dist/federation/tasks/codec.test.mjs +302 -0
  41. package/dist/federation/tasks/enqueue.test.d.mts +2 -0
  42. package/dist/federation/tasks/enqueue.test.mjs +1057 -0
  43. package/dist/federation/tasks/tasks.test.d.mts +2 -0
  44. package/dist/federation/tasks/tasks.test.mjs +1228 -0
  45. package/dist/federation/temporal.test.mjs +4 -4
  46. package/dist/federation/webfinger.test.mjs +6 -6
  47. package/dist/{http-BjTMQwgu.js → http-D0jH0kco.js} +6 -4
  48. package/dist/{http-Blx3a-we.cjs → http-DeuPW1Oh.cjs} +6 -4
  49. package/dist/{http-ZYObKFyR.mjs → http-JdRPUYku.mjs} +4 -4
  50. package/dist/{key-By86iSF1.mjs → key-o0_ipvUR.mjs} +2 -2
  51. package/dist/{kv-cache-BB6kQbPO.cjs → kv-cache-C3rqzQvu.cjs} +1 -1
  52. package/dist/{kv-cache-W72QYEvy.mjs → kv-cache-CM7w20U5.mjs} +1 -1
  53. package/dist/{kv-cache-3GvEiOJJ.js → kv-cache-DSWq2JHU.js} +1 -1
  54. package/dist/{ld-uNPJsHf9.mjs → ld-C__CEgQH.mjs} +3 -3
  55. package/dist/{metrics-D7atQvAx.mjs → metrics-CLSPm4dZ.mjs} +6 -4
  56. package/dist/{mq-D-nlpY04.d.ts → metrics-CoUJLQhZ.d.cts} +69 -1
  57. package/dist/{mq-D8uSFzxe.d.cts → metrics-Dc4jR-IC.d.ts} +69 -1
  58. package/dist/{middleware-DM5XOWG5.cjs → middleware-B502nwxx.cjs} +874 -134
  59. package/dist/{middleware-DYaez_JC.mjs → middleware-CdMiavoh.mjs} +1 -1
  60. package/dist/{middleware-B3XvOhoB.mjs → middleware-CjHqy-1i.mjs} +469 -150
  61. package/dist/{middleware-ICGLZUHG.js → middleware-zuLSkPIk.js} +863 -135
  62. package/dist/{mod-C0F6kvgS.d.cts → mod-B8Efb9qp.d.cts} +1 -1
  63. package/dist/{mod-yvIXFAEi.d.cts → mod-BtEaIEcD.d.cts} +1 -31
  64. package/dist/{mod-vPYVoa5n.d.ts → mod-Cmr39iWq.d.ts} +1 -1
  65. package/dist/{mod-DFvNJcNb.d.ts → mod-UMmveiO0.d.ts} +1 -31
  66. package/dist/mod.cjs +6 -6
  67. package/dist/mod.d.cts +6 -6
  68. package/dist/mod.d.ts +6 -6
  69. package/dist/mod.js +5 -5
  70. package/dist/mq-03vS-C9P.mjs +276 -0
  71. package/dist/nodeinfo/client.test.mjs +3 -3
  72. package/dist/nodeinfo/handler.test.mjs +5 -5
  73. package/dist/nodeinfo/types.test.mjs +4 -4
  74. package/dist/otel/exporter.test.mjs +3 -3
  75. package/dist/{outgoing-jsonld-L_DbOaFe.mjs → outgoing-jsonld-BNL8AC14.mjs} +1 -1
  76. package/dist/{owner-C1IdnJbE.mjs → owner-gz4TUfKG.mjs} +2 -2
  77. package/dist/{proof-B0wYpR4P.cjs → proof-CiDzRtr0.cjs} +1 -1
  78. package/dist/{proof-3th48TRO.mjs → proof-CuQpZ_AT.mjs} +5 -5
  79. package/dist/{proof-hzw73GGW.js → proof-DQWTf6oB.js} +1 -1
  80. package/dist/{send-BqPEr21p.mjs → send-B60teucv.mjs} +3 -3
  81. package/dist/sig/accept.test.mjs +1 -1
  82. package/dist/sig/http.test.mjs +8 -8
  83. package/dist/sig/key.test.mjs +6 -6
  84. package/dist/sig/ld.test.mjs +7 -7
  85. package/dist/sig/mod.cjs +2 -2
  86. package/dist/sig/mod.js +2 -2
  87. package/dist/sig/owner.test.mjs +6 -6
  88. package/dist/sig/proof.test.mjs +8 -8
  89. package/dist/{std__assert-BBjXFNOb.mjs → std__assert-Dh6uLf_q.mjs} +4 -4
  90. package/dist/{temporal-C7NUQ3nU.mjs → temporal-CvVMrvJJ.mjs} +1 -1
  91. package/dist/testing/mod.d.mts +1037 -8
  92. package/dist/testing/mod.mjs +3 -2
  93. package/dist/testing-Dy-WhqpN.mjs +77 -0
  94. package/dist/utils/docloader.test.mjs +6 -6
  95. package/dist/utils/kv-cache.test.mjs +1 -1
  96. package/dist/utils/mod.cjs +1 -1
  97. package/dist/utils/mod.d.cts +1 -1
  98. package/dist/utils/mod.d.ts +1 -1
  99. package/dist/utils/mod.js +1 -1
  100. package/package.json +8 -6
  101. /package/dist/{accept-CPkZzmGN.mjs → accept-CgDcxvjV.mjs} +0 -0
  102. /package/dist/{activity-listener-tztVvlNb.mjs → activity-listener-BeTGV3wc.mjs} +0 -0
  103. /package/dist/{assert_equals-C-ZRDbaf.mjs → assert_equals-PBVKNMJd.mjs} +0 -0
  104. /package/dist/{circuit-breaker-CSWsyoef.mjs → circuit-breaker-hJBB6jwA.mjs} +0 -0
  105. /package/dist/{client-ByXmQhYD.mjs → client-B_A6mfn3.mjs} +0 -0
  106. /package/dist/{collection-Cc3DVAhE.mjs → collection-Dp5ky45w.mjs} +0 -0
  107. /package/dist/{keycache-BeU0LCII.mjs → keycache-CaOR6NYg.mjs} +0 -0
  108. /package/dist/{keys-DGu1NFwu.mjs → keys-CSYsOMFG.mjs} +0 -0
  109. /package/dist/{kv-rV3vodCc.mjs → kv-CIvR3Non.mjs} +0 -0
  110. /package/dist/{negotiation-DDstyBvc.mjs → negotiation-m_L2nyS3.mjs} +0 -0
  111. /package/dist/{public-audience-Cvbr2Gzt.mjs → public-audience-c9zmYKgA.mjs} +0 -0
  112. /package/dist/{retry-CXg_MBI-.mjs → retry-DI4O-zYl.mjs} +0 -0
  113. /package/dist/{types-J53Kw7so.mjs → types-BFowWFTT.mjs} +0 -0
@@ -0,0 +1,1228 @@
1
+ import { Temporal } from "@js-temporal/polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as MemoryKvStore } from "../../kv-CIvR3Non.mjs";
5
+ import { a as numberSchema, i as makeSchema, n as baseOptions, o as stringSchema, t as MockQueue } from "../../testing-Dy-WhqpN.mjs";
6
+ import { o as createFederation } from "../../middleware-CjHqy-1i.mjs";
7
+ import { r as createFederationBuilder } from "../../builder-CJnWbgbj.mjs";
8
+ import { t as TaskCodec } from "../../codec-nfBU4Up4.mjs";
9
+ import { t as InProcessMessageQueue } from "../../mq-03vS-C9P.mjs";
10
+ import { W3CTraceContextPropagator } from "@opentelemetry/core";
11
+ import { Note } from "@fedify/vocab";
12
+ import { SpanStatusCode, propagation } from "@opentelemetry/api";
13
+ import { delay } from "es-toolkit";
14
+ import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
15
+ import { deepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert/strict";
16
+ //#region src/federation/tasks/tasks.test.ts
17
+ const envelopeSchema = makeSchema((data) => typeof data === "object" && data != null && data.note instanceof Note && typeof data.title === "string");
18
+ const makeTaskMessage = async (taskName, data, overrides = {}) => ({
19
+ type: "task",
20
+ id: crypto.randomUUID(),
21
+ baseUrl: "https://example.com/",
22
+ taskName,
23
+ data: await new TaskCodec({ contextLoader: mockDocumentLoader }).serialize(data),
24
+ started: Temporal.Now.instant().toString(),
25
+ attempt: 0,
26
+ traceContext: {},
27
+ ...overrides
28
+ });
29
+ async function waitFor(predicate, timeoutMs) {
30
+ const started = Date.now();
31
+ while (!predicate()) {
32
+ await delay(50);
33
+ if (Date.now() - started > timeoutMs) throw new Error("Timeout");
34
+ }
35
+ }
36
+ test("defineTask()", async (t) => {
37
+ await t.step("returns a handle carrying name and schema", () => {
38
+ const task = createFederation({
39
+ ...baseOptions,
40
+ queue: { task: new MockQueue() }
41
+ }).defineTask("greet", {
42
+ schema: stringSchema,
43
+ handler: () => {}
44
+ });
45
+ strictEqual(task.name, "greet");
46
+ strictEqual(task.schema, stringSchema);
47
+ });
48
+ await t.step("throws on a duplicate name", () => {
49
+ const federation = createFederation({
50
+ ...baseOptions,
51
+ queue: { task: new MockQueue() }
52
+ });
53
+ federation.defineTask("dup", {
54
+ schema: stringSchema,
55
+ handler: () => {}
56
+ });
57
+ throws(() => federation.defineTask("dup", {
58
+ schema: stringSchema,
59
+ handler: () => {}
60
+ }), {
61
+ name: "TypeError",
62
+ message: /already defined/
63
+ });
64
+ });
65
+ await t.step("accepts names that collide with Object.prototype", () => {
66
+ const federation = createFederation({
67
+ ...baseOptions,
68
+ queue: { task: new MockQueue() }
69
+ });
70
+ for (const name of [
71
+ "constructor",
72
+ "toString",
73
+ "hasOwnProperty"
74
+ ]) {
75
+ strictEqual(federation.defineTask(name, {
76
+ schema: stringSchema,
77
+ handler: () => {}
78
+ }).name, name);
79
+ strictEqual(federation.taskDefinitions.get(name)?.name, name);
80
+ }
81
+ throws(() => federation.defineTask("toString", {
82
+ schema: stringSchema,
83
+ handler: () => {}
84
+ }), {
85
+ name: "TypeError",
86
+ message: /already defined/
87
+ });
88
+ });
89
+ await t.step("build() clones the task registry", async () => {
90
+ const builder = createFederationBuilder();
91
+ builder.defineTask("first", {
92
+ schema: stringSchema,
93
+ handler: () => {}
94
+ });
95
+ const f1 = await builder.build({
96
+ ...baseOptions,
97
+ queue: { task: new MockQueue() }
98
+ });
99
+ builder.defineTask("second", {
100
+ schema: stringSchema,
101
+ handler: () => {}
102
+ });
103
+ const f2 = await builder.build({
104
+ ...baseOptions,
105
+ queue: { task: new MockQueue() }
106
+ });
107
+ deepStrictEqual([...f1.taskDefinitions.keys()], ["first"]);
108
+ deepStrictEqual([...f2.taskDefinitions.keys()], ["first", "second"]);
109
+ f1.defineTask("third", {
110
+ schema: stringSchema,
111
+ handler: () => {}
112
+ });
113
+ deepStrictEqual([...f2.taskDefinitions.keys()], ["first", "second"]);
114
+ });
115
+ });
116
+ test("task type-level guards", () => {});
117
+ test("Context.enqueueTask() end-to-end", async (t) => {
118
+ await t.step("round-trips a typed payload to the handler", async () => {
119
+ const queue = new InProcessMessageQueue();
120
+ const federation = createFederation({
121
+ ...baseOptions,
122
+ queue: { task: queue }
123
+ });
124
+ const received = [];
125
+ const task = federation.defineTask("notify", {
126
+ schema: envelopeSchema,
127
+ handler: (ctx, data) => {
128
+ received.push({
129
+ ctx,
130
+ data
131
+ });
132
+ }
133
+ });
134
+ await federation.createContext(new URL("https://example.com/"), void 0).enqueueTask(task, {
135
+ note: new Note({ content: "Hello, world!" }),
136
+ title: "greeting"
137
+ });
138
+ const controller = new AbortController();
139
+ const listening = federation.startQueue(void 0, {
140
+ signal: controller.signal,
141
+ queue: "task"
142
+ });
143
+ try {
144
+ await waitFor(() => received.length > 0, 15e3);
145
+ } finally {
146
+ controller.abort();
147
+ await listening;
148
+ }
149
+ strictEqual(received.length, 1);
150
+ const { ctx: handlerCtx, data } = received[0];
151
+ ok(data.note instanceof Note);
152
+ strictEqual(data.note.content?.toString(), "Hello, world!");
153
+ strictEqual(data.title, "greeting");
154
+ strictEqual(handlerCtx.origin, "https://example.com");
155
+ });
156
+ });
157
+ test("task queue routing", async (t) => {
158
+ await t.step("prefers the per-task queue", async () => {
159
+ const taskQueue = new MockQueue();
160
+ const perTaskQueue = new MockQueue();
161
+ const federation = createFederation({
162
+ ...baseOptions,
163
+ queue: { task: taskQueue }
164
+ });
165
+ const task = federation.defineTask("isolated", {
166
+ schema: stringSchema,
167
+ handler: () => {},
168
+ queue: perTaskQueue
169
+ });
170
+ await federation.createContext(new URL("https://example.com/"), void 0).enqueueTask(task, "data");
171
+ strictEqual(perTaskQueue.enqueued.length, 1);
172
+ strictEqual(taskQueue.enqueued.length, 0);
173
+ });
174
+ await t.step("falls back to the outbox queue by default", async () => {
175
+ const outboxQueue = new MockQueue();
176
+ const federation = createFederation({
177
+ ...baseOptions,
178
+ queue: { outbox: outboxQueue }
179
+ });
180
+ const task = federation.defineTask("fallback", {
181
+ schema: stringSchema,
182
+ handler: () => {}
183
+ });
184
+ await federation.createContext(new URL("https://example.com/"), void 0).enqueueTask(task, "data");
185
+ strictEqual(outboxQueue.enqueued.length, 1);
186
+ });
187
+ await t.step("taskQueueResolution: \"strict\" throws at enqueue instead", async () => {
188
+ const outboxQueue = new MockQueue();
189
+ const federation = createFederation({
190
+ ...baseOptions,
191
+ queue: { outbox: outboxQueue },
192
+ taskQueueResolution: "strict"
193
+ });
194
+ const task = federation.defineTask("strict", {
195
+ schema: stringSchema,
196
+ handler: () => {}
197
+ });
198
+ const ctx = federation.createContext(new URL("https://example.com/"), void 0);
199
+ await rejects(() => ctx.enqueueTask(task, "data"), {
200
+ name: "TypeError",
201
+ message: /No message queue is configured/
202
+ });
203
+ strictEqual(outboxQueue.enqueued.length, 0);
204
+ });
205
+ await t.step("throws when no queue is configured at all", async () => {
206
+ const federation = createFederation({ ...baseOptions });
207
+ const task = federation.defineTask("queueless", {
208
+ schema: stringSchema,
209
+ handler: () => {}
210
+ });
211
+ const ctx = federation.createContext(new URL("https://example.com/"), void 0);
212
+ await rejects(() => ctx.enqueueTask(task, "data"), {
213
+ name: "TypeError",
214
+ message: /No message queue is configured/
215
+ });
216
+ });
217
+ });
218
+ test("startQueue() task worker", async (t) => {
219
+ await t.step("starts only the task worker for queue: \"task\"", async () => {
220
+ const inbox = new MockQueue();
221
+ const outbox = new MockQueue();
222
+ const fanout = new MockQueue();
223
+ const taskQueue = new MockQueue();
224
+ const federation = createFederation({
225
+ ...baseOptions,
226
+ queue: {
227
+ inbox,
228
+ outbox,
229
+ fanout,
230
+ task: taskQueue
231
+ }
232
+ });
233
+ const controller = new AbortController();
234
+ const listening = federation.startQueue(void 0, {
235
+ signal: controller.signal,
236
+ queue: "task"
237
+ });
238
+ strictEqual(taskQueue.listenCount, 1);
239
+ strictEqual(inbox.listenCount, 0);
240
+ strictEqual(outbox.listenCount, 0);
241
+ strictEqual(fanout.listenCount, 0);
242
+ controller.abort();
243
+ await listening;
244
+ });
245
+ await t.step("starts the worker for a task-only deployment", async () => {
246
+ const taskQueue = new MockQueue();
247
+ const federation = createFederation({
248
+ ...baseOptions,
249
+ queue: { task: taskQueue }
250
+ });
251
+ const controller = new AbortController();
252
+ const listening = federation.startQueue(void 0, { signal: controller.signal });
253
+ strictEqual(taskQueue.listenCount, 1);
254
+ controller.abort();
255
+ await listening;
256
+ });
257
+ await t.step("does not double-listen on a shared queue", async () => {
258
+ const shared = new MockQueue();
259
+ const federation = createFederation({
260
+ ...baseOptions,
261
+ queue: {
262
+ outbox: shared,
263
+ task: shared
264
+ }
265
+ });
266
+ const controller = new AbortController();
267
+ const listening = federation.startQueue(void 0, { signal: controller.signal });
268
+ strictEqual(shared.listenCount, 1);
269
+ controller.abort();
270
+ await listening;
271
+ });
272
+ await t.step("starts a worker for a dedicated per-task queue", async () => {
273
+ const taskQueue = new MockQueue();
274
+ const dedicated = new MockQueue();
275
+ const federation = createFederation({
276
+ ...baseOptions,
277
+ queue: { task: taskQueue }
278
+ });
279
+ federation.defineTask("dedicated", {
280
+ schema: stringSchema,
281
+ handler: () => {},
282
+ queue: dedicated
283
+ });
284
+ const controller = new AbortController();
285
+ const listening = federation.startQueue(void 0, { signal: controller.signal });
286
+ strictEqual(taskQueue.listenCount, 1);
287
+ strictEqual(dedicated.listenCount, 1);
288
+ controller.abort();
289
+ await listening;
290
+ });
291
+ await t.step("starts a per-task queue even without a federation queue", async () => {
292
+ const dedicated = new MockQueue();
293
+ const federation = createFederation({ ...baseOptions });
294
+ federation.defineTask("dedicated", {
295
+ schema: stringSchema,
296
+ handler: () => {},
297
+ queue: dedicated
298
+ });
299
+ const controller = new AbortController();
300
+ const listening = federation.startQueue(void 0, { signal: controller.signal });
301
+ strictEqual(dedicated.listenCount, 1);
302
+ controller.abort();
303
+ await listening;
304
+ });
305
+ await t.step("does not listen twice on a per-task queue shared with a standard queue", async () => {
306
+ const shared = new MockQueue();
307
+ const federation = createFederation({
308
+ ...baseOptions,
309
+ queue: { task: shared }
310
+ });
311
+ federation.defineTask("reuses-task-queue", {
312
+ schema: stringSchema,
313
+ handler: () => {},
314
+ queue: shared
315
+ });
316
+ const controller = new AbortController();
317
+ const listening = federation.startQueue(void 0, { signal: controller.signal });
318
+ strictEqual(shared.listenCount, 1);
319
+ controller.abort();
320
+ await listening;
321
+ });
322
+ await t.step("routes an enqueued task on a dedicated queue to its handler", async () => {
323
+ const dedicated = new MockQueue();
324
+ const federation = createFederation({ ...baseOptions });
325
+ let received;
326
+ const task = federation.defineTask("dedicated-end-to-end", {
327
+ schema: stringSchema,
328
+ handler: (_ctx, data) => {
329
+ received = data;
330
+ },
331
+ queue: dedicated
332
+ });
333
+ await federation.createContext(new URL("https://example.com/"), void 0).enqueueTask(task, "payload");
334
+ strictEqual(dedicated.enqueued.length, 1);
335
+ await federation.processQueuedTask(void 0, dedicated.enqueued[0].message);
336
+ strictEqual(received, "payload");
337
+ });
338
+ await t.step("queue: \"task\" starts the outbox fallback when no task queue is set", async () => {
339
+ const inbox = new MockQueue();
340
+ const outbox = new MockQueue();
341
+ const fanout = new MockQueue();
342
+ const federation = createFederation({
343
+ ...baseOptions,
344
+ queue: {
345
+ inbox,
346
+ outbox,
347
+ fanout
348
+ }
349
+ });
350
+ federation.defineTask("fallback", {
351
+ schema: stringSchema,
352
+ handler: () => {}
353
+ });
354
+ const controller = new AbortController();
355
+ const listening = federation.startQueue(void 0, {
356
+ signal: controller.signal,
357
+ queue: "task"
358
+ });
359
+ strictEqual(outbox.listenCount, 1);
360
+ strictEqual(inbox.listenCount, 0);
361
+ strictEqual(fanout.listenCount, 0);
362
+ controller.abort();
363
+ await listening;
364
+ });
365
+ await t.step("queue: \"task\" starts a task queue shared with the outbox once", async () => {
366
+ const inbox = new MockQueue();
367
+ const shared = new MockQueue();
368
+ const fanout = new MockQueue();
369
+ const federation = createFederation({
370
+ ...baseOptions,
371
+ queue: {
372
+ inbox,
373
+ outbox: shared,
374
+ fanout,
375
+ task: shared
376
+ }
377
+ });
378
+ const controller = new AbortController();
379
+ const listening = federation.startQueue(void 0, {
380
+ signal: controller.signal,
381
+ queue: "task"
382
+ });
383
+ strictEqual(shared.listenCount, 1);
384
+ strictEqual(inbox.listenCount, 0);
385
+ strictEqual(fanout.listenCount, 0);
386
+ controller.abort();
387
+ await listening;
388
+ });
389
+ });
390
+ test("MockQueue.listen() resolves on a pre-aborted signal", async () => {
391
+ const queue = new MockQueue();
392
+ const controller = new AbortController();
393
+ controller.abort();
394
+ let timer;
395
+ const settled = await Promise.race([queue.listen(() => {}, { signal: controller.signal }).then(() => "resolved"), new Promise((resolve) => {
396
+ timer = setTimeout(() => resolve("pending"), 50);
397
+ })]);
398
+ clearTimeout(timer);
399
+ strictEqual(settled, "resolved");
400
+ });
401
+ test("processQueuedTask() task dispatch", async (t) => {
402
+ await t.step("drops an unknown task with a warning", async () => {
403
+ const queue = new MockQueue();
404
+ const federation = createFederation({
405
+ ...baseOptions,
406
+ queue: { task: queue }
407
+ });
408
+ const message = await makeTaskMessage("never-defined", "data");
409
+ await federation.processQueuedTask(void 0, message);
410
+ strictEqual(queue.enqueued.length, 0);
411
+ });
412
+ await t.step("drops an undecodable payload without retry", async () => {
413
+ const queue = new MockQueue();
414
+ const federation = createFederation({
415
+ ...baseOptions,
416
+ queue: { task: queue }
417
+ });
418
+ let called = 0;
419
+ federation.defineTask("broken-wire", {
420
+ schema: stringSchema,
421
+ handler: () => {
422
+ called++;
423
+ }
424
+ });
425
+ const message = await makeTaskMessage("broken-wire", "data");
426
+ await federation.processQueuedTask(void 0, {
427
+ ...message,
428
+ data: "garbage that is not devalue"
429
+ });
430
+ strictEqual(called, 0);
431
+ strictEqual(queue.enqueued.length, 0);
432
+ });
433
+ await t.step("drops a drifted payload without retry", async () => {
434
+ const queue = new MockQueue();
435
+ const federation = createFederation({
436
+ ...baseOptions,
437
+ queue: { task: queue }
438
+ });
439
+ let called = 0;
440
+ federation.defineTask("drifted", {
441
+ schema: numberSchema,
442
+ handler: () => {
443
+ called++;
444
+ }
445
+ });
446
+ const message = await makeTaskMessage("drifted", "stringly-typed");
447
+ await federation.processQueuedTask(void 0, message);
448
+ strictEqual(called, 0);
449
+ strictEqual(queue.enqueued.length, 0);
450
+ });
451
+ await t.step("re-enqueues with attempt + 1 when the handler throws", async () => {
452
+ const queue = new MockQueue();
453
+ const federation = createFederation({
454
+ ...baseOptions,
455
+ queue: { task: queue }
456
+ });
457
+ const errors = [];
458
+ federation.defineTask("flaky", {
459
+ schema: stringSchema,
460
+ handler: () => {
461
+ throw new Error("boom");
462
+ },
463
+ retryPolicy: () => Temporal.Duration.from({ milliseconds: 1 }),
464
+ onError: (_ctx, error, data) => {
465
+ errors.push([error, data]);
466
+ }
467
+ });
468
+ const message = await makeTaskMessage("flaky", "data", { orderingKey: "k" });
469
+ await federation.processQueuedTask(void 0, message);
470
+ strictEqual(queue.enqueued.length, 1);
471
+ const retry = queue.enqueued[0];
472
+ strictEqual(retry.message.attempt, 1);
473
+ strictEqual(retry.message.taskName, "flaky");
474
+ strictEqual(retry.message.orderingKey, "k");
475
+ strictEqual(retry.options?.orderingKey, "k");
476
+ ok(retry.options?.delay instanceof Temporal.Duration);
477
+ strictEqual(errors.length, 1);
478
+ deepStrictEqual(errors[0], [/* @__PURE__ */ new Error("boom"), "data"]);
479
+ });
480
+ await t.step("gives up when the retry policy returns null", async () => {
481
+ const queue = new MockQueue();
482
+ const federation = createFederation({
483
+ ...baseOptions,
484
+ queue: { task: queue }
485
+ });
486
+ federation.defineTask("hopeless", {
487
+ schema: stringSchema,
488
+ handler: () => {
489
+ throw new Error("boom");
490
+ },
491
+ retryPolicy: () => null
492
+ });
493
+ const message = await makeTaskMessage("hopeless", "data");
494
+ await federation.processQueuedTask(void 0, message);
495
+ strictEqual(queue.enqueued.length, 0);
496
+ });
497
+ await t.step("per-task retryPolicy overrides the federation default", async () => {
498
+ const queue = new MockQueue();
499
+ const federation = createFederation({
500
+ ...baseOptions,
501
+ queue: { task: queue },
502
+ taskRetryPolicy: () => null
503
+ });
504
+ federation.defineTask("override", {
505
+ schema: stringSchema,
506
+ handler: () => {
507
+ throw new Error("boom");
508
+ },
509
+ retryPolicy: () => Temporal.Duration.from({ milliseconds: 1 })
510
+ });
511
+ federation.defineTask("default", {
512
+ schema: stringSchema,
513
+ handler: () => {
514
+ throw new Error("boom");
515
+ }
516
+ });
517
+ await federation.processQueuedTask(void 0, await makeTaskMessage("override", "data"));
518
+ strictEqual(queue.enqueued.length, 1);
519
+ await federation.processQueuedTask(void 0, await makeTaskMessage("default", "data"));
520
+ strictEqual(queue.enqueued.length, 1);
521
+ });
522
+ await t.step("still retries when message.started is malformed", async () => {
523
+ const queue = new MockQueue();
524
+ const federation = createFederation({
525
+ ...baseOptions,
526
+ queue: { task: queue }
527
+ });
528
+ federation.defineTask("bad-started", {
529
+ schema: stringSchema,
530
+ handler: () => {
531
+ throw new Error("boom");
532
+ },
533
+ retryPolicy: () => Temporal.Duration.from({ milliseconds: 1 })
534
+ });
535
+ const message = await makeTaskMessage("bad-started", "data", { started: "not-an-instant" });
536
+ await federation.processQueuedTask(void 0, message);
537
+ strictEqual(queue.enqueued.length, 1);
538
+ strictEqual(queue.enqueued[0].message.attempt, 1);
539
+ });
540
+ await t.step("rethrows on a nativeRetrial queue", async () => {
541
+ const queue = new MockQueue({ nativeRetrial: true });
542
+ const federation = createFederation({
543
+ ...baseOptions,
544
+ queue: { task: queue }
545
+ });
546
+ federation.defineTask("native", {
547
+ schema: stringSchema,
548
+ handler: () => {
549
+ throw new Error("boom");
550
+ }
551
+ });
552
+ const message = await makeTaskMessage("native", "data");
553
+ await rejects(() => federation.processQueuedTask(void 0, message), { message: /boom/ });
554
+ strictEqual(queue.enqueued.length, 0);
555
+ });
556
+ });
557
+ /**
558
+ * A {@link KvStore} that delegates to an in-memory store but deliberately
559
+ * omits `cas`, so that `kv.cas == null`. This drives the deduplication
560
+ * fallback branches that fire when no conditional-write primitive exists.
561
+ */
562
+ var CaslessKvStore = class {
563
+ inner = new MemoryKvStore();
564
+ get(key) {
565
+ return this.inner.get(key);
566
+ }
567
+ set(key, value, options) {
568
+ return this.inner.set(key, value, options);
569
+ }
570
+ delete(key) {
571
+ return this.inner.delete(key);
572
+ }
573
+ list(prefix) {
574
+ return this.inner.list(prefix);
575
+ }
576
+ };
577
+ async function collectKeys(kv, prefix) {
578
+ const keys = [];
579
+ for await (const { key } of kv.list(prefix)) keys.push(key);
580
+ return keys;
581
+ }
582
+ const TASK_DEDUP_PREFIX = ["_fedify", "taskDeduplication"];
583
+ const ACTIVITY_IDEMPOTENCE_PREFIX = ["_fedify", "activityIdempotence"];
584
+ test("task deduplication", async (t) => {
585
+ await t.step("forwards the key to a nativeDeduplication queue without writing KV", async () => {
586
+ const queue = new MockQueue({ nativeDeduplication: true });
587
+ const kv = new MemoryKvStore();
588
+ const federation = createFederation({
589
+ ...baseOptions,
590
+ kv,
591
+ queue: { task: queue }
592
+ });
593
+ const task = federation.defineTask("native-dedup", {
594
+ schema: stringSchema,
595
+ handler: () => {}
596
+ });
597
+ await federation.createContext(new URL("https://example.com/"), void 0).enqueueTask(task, "payload", { deduplicationKey: "k" });
598
+ strictEqual(queue.enqueued.length, 1);
599
+ strictEqual(queue.enqueued[0].options?.deduplicationKey, "k");
600
+ strictEqual((await collectKeys(kv, TASK_DEDUP_PREFIX)).length, 0);
601
+ });
602
+ await t.step("skips a second enqueue with the same key within the TTL", async () => {
603
+ const queue = new MockQueue();
604
+ const federation = createFederation({
605
+ ...baseOptions,
606
+ kv: new MemoryKvStore(),
607
+ queue: { task: queue }
608
+ });
609
+ const task = federation.defineTask("kv-dedup", {
610
+ schema: stringSchema,
611
+ handler: () => {}
612
+ });
613
+ const ctx = federation.createContext(new URL("https://example.com/"), void 0);
614
+ await ctx.enqueueTask(task, "first", { deduplicationKey: "k" });
615
+ await ctx.enqueueTask(task, "second", { deduplicationKey: "k" });
616
+ strictEqual(queue.enqueued.length, 1);
617
+ strictEqual(queue.enqueued[0].message.taskName, "kv-dedup");
618
+ strictEqual(queue.enqueued[0].options?.deduplicationKey, void 0);
619
+ });
620
+ await t.step("re-enqueues with the same key after the TTL expires", async () => {
621
+ const queue = new MockQueue();
622
+ const federation = createFederation({
623
+ ...baseOptions,
624
+ kv: new MemoryKvStore(),
625
+ queue: { task: queue },
626
+ taskDeduplicationTtl: { milliseconds: 100 }
627
+ });
628
+ const task = federation.defineTask("kv-dedup-ttl", {
629
+ schema: stringSchema,
630
+ handler: () => {}
631
+ });
632
+ const ctx = federation.createContext(new URL("https://example.com/"), void 0);
633
+ await ctx.enqueueTask(task, "first", { deduplicationKey: "k" });
634
+ strictEqual(queue.enqueued.length, 1);
635
+ await delay(300);
636
+ await ctx.enqueueTask(task, "second", { deduplicationKey: "k" });
637
+ strictEqual(queue.enqueued.length, 2);
638
+ });
639
+ await t.step("rejects with TypeError when fallback is \"closed\" and no cas exists", async () => {
640
+ const queue = new MockQueue();
641
+ const federation = createFederation({
642
+ ...baseOptions,
643
+ kv: new CaslessKvStore(),
644
+ queue: { task: queue },
645
+ taskDeduplicationFallback: "closed"
646
+ });
647
+ const task = federation.defineTask("closed-fallback", {
648
+ schema: stringSchema,
649
+ handler: () => {}
650
+ });
651
+ const ctx = federation.createContext(new URL("https://example.com/"), void 0);
652
+ await rejects(() => ctx.enqueueTask(task, "payload", { deduplicationKey: "k" }), { name: "TypeError" });
653
+ strictEqual(queue.enqueued.length, 0);
654
+ });
655
+ await t.step("proceeds when fallback is \"open\" and no cas exists", async () => {
656
+ const queue = new MockQueue();
657
+ const federation = createFederation({
658
+ ...baseOptions,
659
+ kv: new CaslessKvStore(),
660
+ queue: { task: queue },
661
+ taskDeduplicationFallback: "open"
662
+ });
663
+ const task = federation.defineTask("open-fallback", {
664
+ schema: stringSchema,
665
+ handler: () => {}
666
+ });
667
+ await federation.createContext(new URL("https://example.com/"), void 0).enqueueTask(task, "payload", { deduplicationKey: "k" });
668
+ strictEqual(queue.enqueued.length, 1);
669
+ strictEqual(queue.enqueued[0].options?.deduplicationKey, void 0);
670
+ });
671
+ await t.step("writes only under taskDeduplication, never activityIdempotence", async () => {
672
+ const queue = new MockQueue();
673
+ const kv = new MemoryKvStore();
674
+ const federation = createFederation({
675
+ ...baseOptions,
676
+ kv,
677
+ queue: { task: queue }
678
+ });
679
+ const task = federation.defineTask("prefix-isolation", {
680
+ schema: stringSchema,
681
+ handler: () => {}
682
+ });
683
+ await federation.createContext(new URL("https://example.com/"), void 0).enqueueTask(task, "payload", { deduplicationKey: "k" });
684
+ strictEqual((await collectKeys(kv, TASK_DEDUP_PREFIX)).length, 1);
685
+ strictEqual((await collectKeys(kv, ACTIVITY_IDEMPOTENCE_PREFIX)).length, 0);
686
+ });
687
+ await t.step("applies one batch-level key to enqueueTaskMany", async () => {
688
+ const queue = new MockQueue({ supportsEnqueueMany: true });
689
+ const federation = createFederation({
690
+ ...baseOptions,
691
+ kv: new MemoryKvStore(),
692
+ queue: { task: queue }
693
+ });
694
+ const task = federation.defineTask("batch-dedup", {
695
+ schema: stringSchema,
696
+ handler: () => {}
697
+ });
698
+ const ctx = federation.createContext(new URL("https://example.com/"), void 0);
699
+ await ctx.enqueueTaskMany(task, [
700
+ "a",
701
+ "b",
702
+ "c"
703
+ ], { deduplicationKey: "batch" });
704
+ await ctx.enqueueTaskMany(task, [
705
+ "a",
706
+ "b",
707
+ "c"
708
+ ], { deduplicationKey: "batch" });
709
+ strictEqual(queue.enqueuedMany.length, 1);
710
+ strictEqual(queue.enqueuedMany[0].messages.length, 3);
711
+ });
712
+ });
713
+ test("task deduplication validates every payload before reserving the key", async () => {
714
+ const queue = new MockQueue();
715
+ const kv = new MemoryKvStore();
716
+ const federation = createFederation({
717
+ ...baseOptions,
718
+ kv,
719
+ queue: { task: queue }
720
+ });
721
+ const task = federation.defineTask("dedup-validation", {
722
+ schema: stringSchema,
723
+ handler: () => {}
724
+ });
725
+ const ctx = federation.createContext(new URL("https://example.com/"), void 0);
726
+ await rejects(() => ctx.enqueueTask(task, 123, { deduplicationKey: "k" }));
727
+ strictEqual(queue.enqueued.length, 0);
728
+ deepStrictEqual(await collectKeys(kv, TASK_DEDUP_PREFIX), []);
729
+ await ctx.enqueueTask(task, "valid", { deduplicationKey: "k" });
730
+ strictEqual(queue.enqueued.length, 1);
731
+ deepStrictEqual(await collectKeys(kv, TASK_DEDUP_PREFIX), [[...TASK_DEDUP_PREFIX, "k"]]);
732
+ await ctx.enqueueTask(task, "duplicate", { deduplicationKey: "k" });
733
+ strictEqual(queue.enqueued.length, 1);
734
+ });
735
+ test("native task batch deduplication is one enqueueMany operation per call", async () => {
736
+ class NativeBatchDeduplicatingQueue {
737
+ nativeDeduplication = true;
738
+ #seen = /* @__PURE__ */ new Set();
739
+ attempts = [];
740
+ accepted = [];
741
+ enqueue() {
742
+ throw new Error("A multi-item native batch must use enqueueMany().");
743
+ }
744
+ enqueueMany(messages, options) {
745
+ const key = options?.deduplicationKey;
746
+ if (key == null) throw new TypeError("Native batch enqueue requires a deduplication key.");
747
+ this.attempts.push({
748
+ messages,
749
+ options
750
+ });
751
+ if (this.#seen.has(key)) return Promise.resolve();
752
+ this.#seen.add(key);
753
+ this.accepted.push({
754
+ messages,
755
+ options
756
+ });
757
+ return Promise.resolve();
758
+ }
759
+ listen() {
760
+ return Promise.resolve();
761
+ }
762
+ }
763
+ const queue = new NativeBatchDeduplicatingQueue();
764
+ const kv = new MemoryKvStore();
765
+ const federation = createFederation({
766
+ ...baseOptions,
767
+ kv,
768
+ queue: { task: queue }
769
+ });
770
+ const task = federation.defineTask("native-batch-dedup", {
771
+ schema: stringSchema,
772
+ handler: () => {}
773
+ });
774
+ const ctx = federation.createContext(new URL("https://example.com/"), void 0);
775
+ await ctx.enqueueTaskMany(task, [
776
+ "a1",
777
+ "a2",
778
+ "a3"
779
+ ], { deduplicationKey: "batch-a" });
780
+ await ctx.enqueueTaskMany(task, [
781
+ "duplicate1",
782
+ "duplicate2",
783
+ "duplicate3"
784
+ ], { deduplicationKey: "batch-a" });
785
+ await ctx.enqueueTaskMany(task, [
786
+ "b1",
787
+ "b2",
788
+ "b3"
789
+ ], { deduplicationKey: "batch-b" });
790
+ strictEqual(queue.attempts.length, 3);
791
+ deepStrictEqual(queue.attempts.map(({ messages }) => messages.length), [
792
+ 3,
793
+ 3,
794
+ 3
795
+ ]);
796
+ deepStrictEqual(queue.attempts.map(({ options }) => options?.deduplicationKey), [
797
+ "batch-a",
798
+ "batch-a",
799
+ "batch-b"
800
+ ]);
801
+ strictEqual(queue.accepted.length, 2);
802
+ deepStrictEqual(queue.accepted.map(({ messages }) => messages.length), [3, 3]);
803
+ deepStrictEqual(queue.accepted.map(({ options }) => options?.deduplicationKey), ["batch-a", "batch-b"]);
804
+ deepStrictEqual(await collectKeys(kv, TASK_DEDUP_PREFIX), []);
805
+ });
806
+ test("native task batch deduplication rejects without enqueueMany", async () => {
807
+ const queue = new MockQueue({ nativeDeduplication: true });
808
+ const kv = new MemoryKvStore();
809
+ const federation = createFederation({
810
+ ...baseOptions,
811
+ kv,
812
+ queue: { task: queue }
813
+ });
814
+ const task = federation.defineTask("native-batch-without-enqueue-many", {
815
+ schema: stringSchema,
816
+ handler: () => {}
817
+ });
818
+ const ctx = federation.createContext(new URL("https://example.com/"), void 0);
819
+ await rejects(() => ctx.enqueueTaskMany(task, [
820
+ "a",
821
+ "b",
822
+ "c"
823
+ ], { deduplicationKey: "batch" }), {
824
+ name: "TypeError",
825
+ message: /enqueueMany/
826
+ });
827
+ strictEqual(queue.enqueued.length, 0);
828
+ strictEqual(queue.enqueuedMany.length, 0);
829
+ deepStrictEqual(await collectKeys(kv, TASK_DEDUP_PREFIX), []);
830
+ await ctx.enqueueTaskMany(task, ["single"], { deduplicationKey: "single" });
831
+ strictEqual(queue.enqueued.length, 1);
832
+ strictEqual(queue.enqueued[0].options?.deduplicationKey, "single");
833
+ });
834
+ test("deduplication - native batch capability errors precede payload validation", async () => {
835
+ let validationCalls = 0;
836
+ const schema = makeSchema((data) => {
837
+ validationCalls++;
838
+ return typeof data === "string";
839
+ });
840
+ const queue = new MockQueue({ nativeDeduplication: true });
841
+ const kv = new MemoryKvStore();
842
+ const federation = createFederation({
843
+ ...baseOptions,
844
+ kv,
845
+ queue: { task: queue }
846
+ });
847
+ const task = federation.defineTask("native-batch-capability-order", {
848
+ schema,
849
+ handler: () => {}
850
+ });
851
+ const ctx = federation.createContext(new URL("https://example.com/"), void 0);
852
+ let caught;
853
+ try {
854
+ await ctx.enqueueTaskMany(task, [
855
+ 1,
856
+ 2,
857
+ 3
858
+ ], { deduplicationKey: "batch" });
859
+ } catch (error) {
860
+ caught = error;
861
+ }
862
+ strictEqual(validationCalls, 0);
863
+ ok(caught instanceof TypeError);
864
+ ok(caught.message.includes("enqueueMany"));
865
+ strictEqual(queue.enqueued.length, 0);
866
+ strictEqual(queue.enqueuedMany.length, 0);
867
+ deepStrictEqual(await collectKeys(kv, TASK_DEDUP_PREFIX), []);
868
+ });
869
+ test("closed deduplication fallback errors precede payload validation", async () => {
870
+ let validationCalls = 0;
871
+ const schema = makeSchema((data) => {
872
+ validationCalls++;
873
+ return typeof data === "string";
874
+ });
875
+ const queue = new MockQueue();
876
+ const kv = new CaslessKvStore();
877
+ const federation = createFederation({
878
+ ...baseOptions,
879
+ kv,
880
+ queue: { task: queue },
881
+ taskDeduplicationFallback: "closed"
882
+ });
883
+ const task = federation.defineTask("closed-fallback-order", {
884
+ schema,
885
+ handler: () => {}
886
+ });
887
+ const ctx = federation.createContext(new URL("https://example.com/"), void 0);
888
+ let caught;
889
+ try {
890
+ await ctx.enqueueTask(task, 1, { deduplicationKey: "k" });
891
+ } catch (error) {
892
+ caught = error;
893
+ }
894
+ strictEqual(validationCalls, 0);
895
+ ok(caught instanceof TypeError);
896
+ ok(caught.message.includes("conditional write"));
897
+ strictEqual(queue.enqueued.length, 0);
898
+ strictEqual(queue.enqueuedMany.length, 0);
899
+ deepStrictEqual(await collectKeys(kv, TASK_DEDUP_PREFIX), []);
900
+ });
901
+ /** Wires test telemetry doubles into a fresh federation for the suite below. */
902
+ const instrument = (options) => {
903
+ const [meterProvider, recorder] = createTestMeterProvider();
904
+ const [tracerProvider, exporter] = createTestTracerProvider();
905
+ return {
906
+ federation: createFederation({
907
+ ...options,
908
+ meterProvider,
909
+ tracerProvider
910
+ }),
911
+ recorder,
912
+ exporter
913
+ };
914
+ };
915
+ test("task observability", async (t) => {
916
+ await t.step("opens a fedify.task span carrying name and attempt on success", async () => {
917
+ const queue = new MockQueue();
918
+ const { federation, recorder, exporter } = instrument({
919
+ ...baseOptions,
920
+ queue: { task: queue }
921
+ });
922
+ federation.defineTask("sendDigest", {
923
+ schema: stringSchema,
924
+ handler: () => {}
925
+ });
926
+ await federation.processQueuedTask(void 0, await makeTaskMessage("sendDigest", "payload"));
927
+ const spans = exporter.getSpans("fedify.task");
928
+ strictEqual(spans.length, 1);
929
+ strictEqual(spans[0].attributes["fedify.task.name"], "sendDigest");
930
+ strictEqual(spans[0].attributes["fedify.task.attempt"], 0);
931
+ strictEqual(spans[0].attributes["fedify.task.failure_reason"], void 0);
932
+ strictEqual(spans[0].status.code, SpanStatusCode.UNSET);
933
+ const started = recorder.getMeasurements("fedify.queue.task.started");
934
+ strictEqual(started.length, 1);
935
+ strictEqual(started[0].attributes["fedify.queue.role"], "task");
936
+ strictEqual(started[0].attributes["fedify.task.name"], "sendDigest");
937
+ const completed = recorder.getMeasurements("fedify.queue.task.completed");
938
+ strictEqual(completed.length, 1);
939
+ strictEqual(completed[0].attributes["fedify.queue.role"], "task");
940
+ strictEqual(completed[0].attributes["fedify.task.name"], "sendDigest");
941
+ strictEqual(completed[0].attributes["fedify.queue.task.result"], "completed");
942
+ strictEqual(completed[0].attributes["fedify.task.failure_reason"], void 0);
943
+ strictEqual(recorder.getMeasurements("fedify.queue.task.failed").length, 0);
944
+ });
945
+ await t.step("inherits the parent trace context from the enqueue site", async () => {
946
+ const traceId = "0af7651916cd43dd8448eb211c80319c";
947
+ const spanId = "b7ad6b7169203331";
948
+ propagation.setGlobalPropagator(new W3CTraceContextPropagator());
949
+ try {
950
+ const queue = new MockQueue();
951
+ const { federation, exporter } = instrument({
952
+ ...baseOptions,
953
+ queue: { task: queue }
954
+ });
955
+ federation.defineTask("traced", {
956
+ schema: stringSchema,
957
+ handler: () => {}
958
+ });
959
+ await federation.processQueuedTask(void 0, await makeTaskMessage("traced", "payload", { traceContext: { traceparent: `00-${traceId}-${spanId}-01` } }));
960
+ const span = exporter.getSpans("fedify.task")[0];
961
+ ok(span != null);
962
+ strictEqual(span.spanContext().traceId, traceId);
963
+ strictEqual(span.parentSpanContext?.spanId, spanId);
964
+ } finally {
965
+ propagation.disable();
966
+ }
967
+ });
968
+ await t.step("attributes a deserialization failure and drops without retry", async () => {
969
+ const queue = new MockQueue();
970
+ const { federation, recorder, exporter } = instrument({
971
+ ...baseOptions,
972
+ queue: { task: queue }
973
+ });
974
+ let called = 0;
975
+ federation.defineTask("decode-me", {
976
+ schema: stringSchema,
977
+ handler: () => {
978
+ called++;
979
+ }
980
+ });
981
+ const message = await makeTaskMessage("decode-me", "payload");
982
+ await federation.processQueuedTask(void 0, {
983
+ ...message,
984
+ data: "garbage that is not devalue"
985
+ });
986
+ strictEqual(called, 0);
987
+ strictEqual(queue.enqueued.length, 0);
988
+ const failed = recorder.getMeasurements("fedify.queue.task.failed");
989
+ strictEqual(failed.length, 1);
990
+ strictEqual(failed[0].attributes["fedify.queue.role"], "task");
991
+ strictEqual(failed[0].attributes["fedify.task.failure_reason"], "deserialization");
992
+ const span = exporter.getSpans("fedify.task")[0];
993
+ strictEqual(span.attributes["fedify.task.failure_reason"], "deserialization");
994
+ strictEqual(span.status.code, SpanStatusCode.ERROR);
995
+ });
996
+ await t.step("attributes a validation failure and drops without retry", async () => {
997
+ const queue = new MockQueue();
998
+ const { federation, recorder, exporter } = instrument({
999
+ ...baseOptions,
1000
+ queue: { task: queue }
1001
+ });
1002
+ let called = 0;
1003
+ federation.defineTask("strict-shape", {
1004
+ schema: numberSchema,
1005
+ handler: () => {
1006
+ called++;
1007
+ }
1008
+ });
1009
+ await federation.processQueuedTask(void 0, await makeTaskMessage("strict-shape", "not a number"));
1010
+ strictEqual(called, 0);
1011
+ strictEqual(queue.enqueued.length, 0);
1012
+ const failed = recorder.getMeasurements("fedify.queue.task.failed");
1013
+ strictEqual(failed.length, 1);
1014
+ strictEqual(failed[0].attributes["fedify.task.failure_reason"], "validation");
1015
+ const span = exporter.getSpans("fedify.task")[0];
1016
+ strictEqual(span.attributes["fedify.task.failure_reason"], "validation");
1017
+ strictEqual(span.status.code, SpanStatusCode.ERROR);
1018
+ });
1019
+ await t.step("attributes an unknown task and drops without retry", async () => {
1020
+ const queue = new MockQueue();
1021
+ const { federation, recorder, exporter } = instrument({
1022
+ ...baseOptions,
1023
+ queue: { task: queue }
1024
+ });
1025
+ await federation.processQueuedTask(void 0, await makeTaskMessage("never-registered", "payload"));
1026
+ strictEqual(queue.enqueued.length, 0);
1027
+ const failed = recorder.getMeasurements("fedify.queue.task.failed");
1028
+ strictEqual(failed.length, 1);
1029
+ strictEqual(failed[0].attributes["fedify.task.failure_reason"], "unknown_task");
1030
+ strictEqual(failed[0].attributes["fedify.queue.backend"], "MockQueue");
1031
+ strictEqual(failed[0].attributes["fedify.task.name"], void 0);
1032
+ strictEqual(recorder.getMeasurements("fedify.queue.task.started")[0].attributes["fedify.task.name"], void 0);
1033
+ const span = exporter.getSpans("fedify.task")[0];
1034
+ strictEqual(span.attributes["fedify.task.name"], "never-registered");
1035
+ strictEqual(span.attributes["fedify.task.failure_reason"], "unknown_task");
1036
+ strictEqual(span.status.code, SpanStatusCode.ERROR);
1037
+ });
1038
+ await t.step("attributes a handler failure on a terminal give-up", async () => {
1039
+ const queue = new MockQueue();
1040
+ const { federation, recorder, exporter } = instrument({
1041
+ ...baseOptions,
1042
+ queue: { task: queue }
1043
+ });
1044
+ federation.defineTask("explode", {
1045
+ schema: stringSchema,
1046
+ handler: () => {
1047
+ throw new Error("boom");
1048
+ },
1049
+ retryPolicy: () => null
1050
+ });
1051
+ await federation.processQueuedTask(void 0, await makeTaskMessage("explode", "payload"));
1052
+ strictEqual(queue.enqueued.length, 0);
1053
+ const failed = recorder.getMeasurements("fedify.queue.task.failed");
1054
+ strictEqual(failed.length, 1);
1055
+ strictEqual(failed[0].attributes["fedify.task.failure_reason"], "handler");
1056
+ const span = exporter.getSpans("fedify.task")[0];
1057
+ strictEqual(span.attributes["fedify.task.failure_reason"], "handler");
1058
+ strictEqual(span.status.code, SpanStatusCode.ERROR);
1059
+ });
1060
+ await t.step("reports the resolved outbox queue as the backend on fallback", async () => {
1061
+ class FallbackOutboxQueue extends MockQueue {}
1062
+ const outboxQueue = new FallbackOutboxQueue();
1063
+ const { federation, recorder } = instrument({
1064
+ ...baseOptions,
1065
+ queue: { outbox: outboxQueue }
1066
+ });
1067
+ federation.defineTask("fallback", {
1068
+ schema: stringSchema,
1069
+ handler: () => {}
1070
+ });
1071
+ await federation.processQueuedTask(void 0, await makeTaskMessage("fallback", "payload"));
1072
+ const completed = recorder.getMeasurements("fedify.queue.task.completed");
1073
+ strictEqual(completed.length, 1);
1074
+ strictEqual(completed[0].attributes["fedify.queue.backend"], "FallbackOutboxQueue");
1075
+ });
1076
+ await t.step("records an enqueue measurement with role task", async () => {
1077
+ const queue = new MockQueue();
1078
+ const { federation, recorder } = instrument({
1079
+ ...baseOptions,
1080
+ queue: { task: queue }
1081
+ });
1082
+ const task = federation.defineTask("enqueue-me", {
1083
+ schema: stringSchema,
1084
+ handler: () => {}
1085
+ });
1086
+ await federation.createContext(new URL("https://example.com/"), void 0).enqueueTask(task, "payload");
1087
+ const enqueued = recorder.getMeasurements("fedify.queue.task.enqueued");
1088
+ strictEqual(enqueued.length, 1);
1089
+ strictEqual(enqueued[0].attributes["fedify.queue.role"], "task");
1090
+ strictEqual(enqueued[0].attributes["fedify.task.name"], "enqueue-me");
1091
+ strictEqual(enqueued[0].attributes["fedify.queue.task.attempt"], 0);
1092
+ });
1093
+ await t.step("records the retry re-enqueue with role task and a bumped attempt", async () => {
1094
+ const queue = new MockQueue();
1095
+ const { federation, recorder, exporter } = instrument({
1096
+ ...baseOptions,
1097
+ queue: { task: queue }
1098
+ });
1099
+ federation.defineTask("retry-me", {
1100
+ schema: stringSchema,
1101
+ handler: () => {
1102
+ throw new Error("boom");
1103
+ },
1104
+ retryPolicy: () => Temporal.Duration.from({ milliseconds: 1 })
1105
+ });
1106
+ await federation.processQueuedTask(void 0, await makeTaskMessage("retry-me", "payload"));
1107
+ strictEqual(queue.enqueued.length, 1);
1108
+ strictEqual(queue.enqueued[0].message.attempt, 1);
1109
+ const enqueued = recorder.getMeasurements("fedify.queue.task.enqueued");
1110
+ strictEqual(enqueued.length, 1);
1111
+ strictEqual(enqueued[0].attributes["fedify.queue.role"], "task");
1112
+ strictEqual(enqueued[0].attributes["fedify.task.name"], "retry-me");
1113
+ strictEqual(enqueued[0].attributes["fedify.queue.task.attempt"], 1);
1114
+ const span = exporter.getSpans("fedify.task")[0];
1115
+ strictEqual(span.attributes["fedify.task.failure_reason"], void 0);
1116
+ strictEqual(span.status.code, SpanStatusCode.UNSET);
1117
+ strictEqual(recorder.getMeasurements("fedify.queue.task.failed").length, 0);
1118
+ const completed = recorder.getMeasurements("fedify.queue.task.completed");
1119
+ strictEqual(completed.length, 1);
1120
+ strictEqual(completed[0].attributes["fedify.task.failure_reason"], void 0);
1121
+ });
1122
+ await t.step("attributes retry_enqueue when the retry re-enqueue itself fails", async () => {
1123
+ class RejectingQueue {
1124
+ enqueue() {
1125
+ return Promise.reject(/* @__PURE__ */ new Error("backend unavailable"));
1126
+ }
1127
+ listen() {
1128
+ return Promise.resolve();
1129
+ }
1130
+ }
1131
+ const queue = new RejectingQueue();
1132
+ const { federation, recorder, exporter } = instrument({
1133
+ ...baseOptions,
1134
+ queue: { task: queue }
1135
+ });
1136
+ const handlerError = /* @__PURE__ */ new Error("boom");
1137
+ federation.defineTask("retry-enqueue-fails", {
1138
+ schema: stringSchema,
1139
+ handler: () => {
1140
+ throw handlerError;
1141
+ },
1142
+ retryPolicy: () => Temporal.Duration.from({ milliseconds: 1 })
1143
+ });
1144
+ const message = await makeTaskMessage("retry-enqueue-fails", "payload");
1145
+ await rejects(() => federation.processQueuedTask(void 0, message), (e) => e instanceof Error && e.cause === handlerError);
1146
+ const failed = recorder.getMeasurements("fedify.queue.task.failed");
1147
+ strictEqual(failed.length, 1);
1148
+ strictEqual(failed[0].attributes["fedify.task.failure_reason"], "retry_enqueue");
1149
+ strictEqual(recorder.getMeasurements("fedify.queue.task.enqueued").length, 0);
1150
+ const span = exporter.getSpans("fedify.task")[0];
1151
+ strictEqual(span.attributes["fedify.task.failure_reason"], "retry_enqueue");
1152
+ strictEqual(span.status.code, SpanStatusCode.ERROR);
1153
+ ok(span.status.message?.includes("boom"));
1154
+ });
1155
+ await t.step("records an abort as aborted, without a failure reason or error status", async () => {
1156
+ const queue = new MockQueue({ nativeRetrial: true });
1157
+ const { federation, recorder, exporter } = instrument({
1158
+ ...baseOptions,
1159
+ queue: { task: queue }
1160
+ });
1161
+ federation.defineTask("aborts", {
1162
+ schema: stringSchema,
1163
+ handler: () => {
1164
+ throw globalThis.Object.assign(/* @__PURE__ */ new Error("shutting down"), { name: "AbortError" });
1165
+ }
1166
+ });
1167
+ const message = await makeTaskMessage("aborts", "payload");
1168
+ await rejects(() => federation.processQueuedTask(void 0, message), { name: "AbortError" });
1169
+ const span = exporter.getSpans("fedify.task")[0];
1170
+ ok(span != null);
1171
+ strictEqual(span.attributes["fedify.task.failure_reason"], void 0);
1172
+ strictEqual(span.status.code, SpanStatusCode.UNSET);
1173
+ strictEqual(recorder.getMeasurements("fedify.queue.task.failed").length, 0);
1174
+ const durations = recorder.getMeasurements("fedify.queue.task.duration");
1175
+ strictEqual(durations.length, 1);
1176
+ strictEqual(durations[0].attributes["fedify.queue.task.result"], "aborted");
1177
+ });
1178
+ await t.step("on a non-native queue an aborted handler that gives up is aborted", async () => {
1179
+ const queue = new MockQueue();
1180
+ const { federation, recorder, exporter } = instrument({
1181
+ ...baseOptions,
1182
+ queue: { task: queue }
1183
+ });
1184
+ federation.defineTask("aborts-give-up", {
1185
+ schema: stringSchema,
1186
+ handler: () => {
1187
+ throw globalThis.Object.assign(/* @__PURE__ */ new Error("shutting down"), { name: "AbortError" });
1188
+ },
1189
+ retryPolicy: () => null
1190
+ });
1191
+ await federation.processQueuedTask(void 0, await makeTaskMessage("aborts-give-up", "payload"));
1192
+ strictEqual(queue.enqueued.length, 0);
1193
+ const span = exporter.getSpans("fedify.task")[0];
1194
+ strictEqual(span.attributes["fedify.task.failure_reason"], void 0);
1195
+ strictEqual(span.status.code, SpanStatusCode.UNSET);
1196
+ strictEqual(recorder.getMeasurements("fedify.queue.task.failed").length, 0);
1197
+ strictEqual(recorder.getMeasurements("fedify.queue.task.completed").length, 0);
1198
+ const durations = recorder.getMeasurements("fedify.queue.task.duration");
1199
+ strictEqual(durations.length, 1);
1200
+ strictEqual(durations[0].attributes["fedify.queue.task.result"], "aborted");
1201
+ });
1202
+ await t.step("on a non-native queue an aborted handler is retried, not failed", async () => {
1203
+ const queue = new MockQueue();
1204
+ const { federation, recorder, exporter } = instrument({
1205
+ ...baseOptions,
1206
+ queue: { task: queue }
1207
+ });
1208
+ federation.defineTask("aborts-soft", {
1209
+ schema: stringSchema,
1210
+ handler: () => {
1211
+ throw globalThis.Object.assign(/* @__PURE__ */ new Error("shutting down"), { name: "AbortError" });
1212
+ },
1213
+ retryPolicy: () => Temporal.Duration.from({ milliseconds: 1 })
1214
+ });
1215
+ await federation.processQueuedTask(void 0, await makeTaskMessage("aborts-soft", "payload"));
1216
+ strictEqual(queue.enqueued.length, 1);
1217
+ strictEqual(queue.enqueued[0].message.attempt, 1);
1218
+ const span = exporter.getSpans("fedify.task")[0];
1219
+ strictEqual(span.attributes["fedify.task.failure_reason"], void 0);
1220
+ strictEqual(span.status.code, SpanStatusCode.UNSET);
1221
+ strictEqual(recorder.getMeasurements("fedify.queue.task.failed").length, 0);
1222
+ const completed = recorder.getMeasurements("fedify.queue.task.completed");
1223
+ strictEqual(completed.length, 1);
1224
+ strictEqual(completed[0].attributes["fedify.task.failure_reason"], void 0);
1225
+ });
1226
+ });
1227
+ //#endregion
1228
+ export {};