@fedify/fedify 2.4.0-dev.1567 → 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-DGWCUaNQ.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-Dho0TW0X.mjs → deno-klNxGW7W.mjs} +1 -1
  19. package/dist/{docloader-DCQSgDOg.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-sngIMb9S.js → http-D0jH0kco.js} +16 -80
  48. package/dist/{http-C7Lot0it.cjs → http-DeuPW1Oh.cjs} +14 -78
  49. package/dist/{http-Bf8YhWOi.mjs → http-JdRPUYku.mjs} +4 -4
  50. package/dist/{key-3rYgSl9f.mjs → key-o0_ipvUR.mjs} +10 -76
  51. package/dist/{kv-cache-CO2IaDJu.cjs → kv-cache-C3rqzQvu.cjs} +1 -1
  52. package/dist/{kv-cache-P3scUO6t.mjs → kv-cache-CM7w20U5.mjs} +1 -1
  53. package/dist/{kv-cache-B0siRaoZ.js → kv-cache-DSWq2JHU.js} +1 -1
  54. package/dist/{ld-CCOPy-Co.mjs → ld-C__CEgQH.mjs} +3 -3
  55. package/dist/{metrics-CR4UD_gF.mjs → metrics-CLSPm4dZ.mjs} +8 -6
  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-DUelXs9h.cjs → middleware-B502nwxx.cjs} +874 -134
  59. package/dist/{middleware-CIteB8IS.mjs → middleware-CdMiavoh.mjs} +1 -1
  60. package/dist/{middleware-DfBPuuvp.mjs → middleware-CjHqy-1i.mjs} +469 -150
  61. package/dist/{middleware-BoNiH9CQ.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-TUz_H-M9.mjs → owner-gz4TUfKG.mjs} +2 -2
  77. package/dist/{proof-DzKUmE7R.cjs → proof-CiDzRtr0.cjs} +1 -1
  78. package/dist/{proof-CrI3UpuA.mjs → proof-CuQpZ_AT.mjs} +5 -5
  79. package/dist/{proof-C5i7MhbD.js → proof-DQWTf6oB.js} +1 -1
  80. package/dist/{send-BBs_erdP.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 +7 -191
  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 +9 -69
  89. package/dist/{std__assert-BBjXFNOb.mjs → std__assert-Dh6uLf_q.mjs} +4 -4
  90. package/dist/{temporal-bBIy2G-F.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
@@ -2,10 +2,10 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./chunk-CRNNMoPX.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-BGMIq1cs.js";
5
- import { A as formatAcceptSignature, D as recordOutboxEnqueue, E as recordOutboxActivity, F as version, O as recordWebFingerHandle, P as name, S as recordCollectionTotalItems, T as recordInboxActivity, a as verifyRequestDetailed, b as recordCollectionPageItems, d as validateCryptoKey, f as getDurationMs, g as isAbortError, h as instrumentDocumentLoader, i as verifyRequest, k as registerQueueDepthGauge, m as getRemoteHost, n as parseRfc9421SignatureInput, o as exportJwk, p as getFederationMetrics, t as doubleKnock, u as importJwk, v as recordCircuitBreakerStateChange, w as recordFanoutRecipients, x as recordCollectionRequest, y as recordCollectionDispatchDuration } from "./http-sngIMb9S.js";
6
- import { _ as hasSignatureLike, b as signJsonLd, c as getKeyOwner, f as compactJsonLd, g as hasSignature, h as getNormalizationContextLoader, i as verifyObject, l as InvalidContextReferenceError, m as detachSignature, n as hasProofLike, o as normalizeOutgoingActivityJsonLd, r as signObject, s as doesActorOwnKey, u as assertSafeJsonLd, v as isClearlyMalformedContextReference, w as wrapContextLoaderForJsonLd, x as verifyCompactJsonLd, y as isInvalidUrlTypeError } from "./proof-C5i7MhbD.js";
5
+ import { A as formatAcceptSignature, D as recordOutboxEnqueue, E as recordOutboxActivity, F as version, O as recordWebFingerHandle, P as name, S as recordCollectionTotalItems, T as recordInboxActivity, a as verifyRequestDetailed, b as recordCollectionPageItems, d as validateCryptoKey, f as getDurationMs, g as isAbortError, h as instrumentDocumentLoader, i as verifyRequest, k as registerQueueDepthGauge, m as getRemoteHost, n as parseRfc9421SignatureInput, o as exportJwk, p as getFederationMetrics, t as doubleKnock, u as importJwk, v as recordCircuitBreakerStateChange, w as recordFanoutRecipients, x as recordCollectionRequest, y as recordCollectionDispatchDuration } from "./http-D0jH0kco.js";
6
+ import { _ as hasSignatureLike, b as signJsonLd, c as getKeyOwner, f as compactJsonLd, g as hasSignature, h as getNormalizationContextLoader, i as verifyObject, l as InvalidContextReferenceError, m as detachSignature, n as hasProofLike, o as normalizeOutgoingActivityJsonLd, r as signObject, s as doesActorOwnKey, u as assertSafeJsonLd, v as isClearlyMalformedContextReference, w as wrapContextLoaderForJsonLd, x as verifyCompactJsonLd, y as isInvalidUrlTypeError } from "./proof-DQWTf6oB.js";
7
7
  import { n as getNodeInfo, t as nodeInfoToJson } from "./types-CAY3OdLq.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-B0siRaoZ.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DSWq2JHU.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
11
11
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
@@ -17,6 +17,7 @@ import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_
17
17
  import jsonld from "@fedify/vocab-runtime/jsonld";
18
18
  import { lookupWebFinger } from "@fedify/webfinger";
19
19
  import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
20
+ import { parse, stringifyAsync } from "devalue";
20
21
  import { domainToASCII } from "node:url";
21
22
  //#region src/federation/activity-listener.ts
22
23
  var ActivityListenerSet = class {
@@ -107,6 +108,7 @@ var FederationBuilderImpl = class {
107
108
  idempotencyStrategy;
108
109
  collectionTypeIds;
109
110
  collectionCallbacks;
111
+ taskDefinitions;
110
112
  /**
111
113
  * Symbol registry for unique identification of unnamed symbols.
112
114
  */
@@ -117,6 +119,7 @@ var FederationBuilderImpl = class {
117
119
  this.objectTypeIds = {};
118
120
  this.collectionCallbacks = {};
119
121
  this.collectionTypeIds = {};
122
+ this.taskDefinitions = /* @__PURE__ */ new Map();
120
123
  }
121
124
  /**
122
125
  * Builds the federation object.
@@ -155,6 +158,7 @@ var FederationBuilderImpl = class {
155
158
  f.unverifiedActivityHandler = this.unverifiedActivityHandler;
156
159
  f.outboxPermanentFailureHandler = this.outboxPermanentFailureHandler;
157
160
  f.idempotencyStrategy = this.idempotencyStrategy;
161
+ f.taskDefinitions = new Map(this.taskDefinitions);
158
162
  return f;
159
163
  }
160
164
  _getTracer() {
@@ -287,6 +291,23 @@ var FederationBuilderImpl = class {
287
291
  setWebFingerLinksDispatcher(dispatcher) {
288
292
  this.webFingerLinksDispatcher = dispatcher;
289
293
  }
294
+ defineTask(name, options) {
295
+ if (this.taskDefinitions.has(name)) throw new TypeError(`Task ${JSON.stringify(name)} is already defined.`);
296
+ const handle = {
297
+ name,
298
+ schema: options.schema
299
+ };
300
+ this.taskDefinitions.set(name, {
301
+ name,
302
+ schema: options.schema,
303
+ handle,
304
+ handler: options.handler,
305
+ onError: options.onError,
306
+ retryPolicy: options.retryPolicy,
307
+ queue: options.queue
308
+ });
309
+ return handle;
310
+ }
290
311
  /**
291
312
  * The RFC 6570 template-literal `path` overloads were removed for
292
313
  * type-checking efficiency, so the URI variable types can no longer be
@@ -1392,7 +1413,7 @@ function acceptsJsonLd(request) {
1392
1413
  }
1393
1414
  //#endregion
1394
1415
  //#region src/federation/temporal.ts
1395
- function isPlainObject(value) {
1416
+ function isPlainObject$1(value) {
1396
1417
  return typeof value === "object" && value != null && !Array.isArray(value);
1397
1418
  }
1398
1419
  function normalizeDateTimeLiteral(value) {
@@ -1430,11 +1451,11 @@ const QUESTION_CLOSED_IRI = "https://www.w3.org/ns/activitystreams#closed";
1430
1451
  const XSD_DATE_TIME_IRI = "http://www.w3.org/2001/XMLSchema#dateTime";
1431
1452
  function hasMalformedExpandedDateTimeLiteral(value) {
1432
1453
  if (Array.isArray(value)) return value.some(hasMalformedExpandedDateTimeLiteral);
1433
- return isPlainObject(value) && "@value" in value && isMalformedDateTimeLiteral(value["@value"]);
1454
+ return isPlainObject$1(value) && "@value" in value && isMalformedDateTimeLiteral(value["@value"]);
1434
1455
  }
1435
1456
  function hasMalformedExpandedQuestionClosedLiteral(value) {
1436
1457
  if (Array.isArray(value)) return value.some(hasMalformedExpandedQuestionClosedLiteral);
1437
- if (!isPlainObject(value) || !("@value" in value)) return false;
1458
+ if (!isPlainObject$1(value) || !("@value" in value)) return false;
1438
1459
  const literal = value["@value"];
1439
1460
  if (typeof literal === "boolean") return false;
1440
1461
  if (typeof literal !== "string") return false;
@@ -1444,11 +1465,11 @@ function hasMalformedExpandedQuestionClosedLiteral(value) {
1444
1465
  }
1445
1466
  function hasMalformedExpandedDurationLiteral(value) {
1446
1467
  if (Array.isArray(value)) return value.some(hasMalformedExpandedDurationLiteral);
1447
- return isPlainObject(value) && "@value" in value && isMalformedDurationLiteral(value["@value"]);
1468
+ return isPlainObject$1(value) && "@value" in value && isMalformedDurationLiteral(value["@value"]);
1448
1469
  }
1449
1470
  function hasMalformedKnownTemporalLiteralInternal(value, visited) {
1450
1471
  if (Array.isArray(value)) return value.some((item) => hasMalformedKnownTemporalLiteralInternal(item, visited));
1451
- if (!isPlainObject(value)) return false;
1472
+ if (!isPlainObject$1(value)) return false;
1452
1473
  if (visited.has(value)) return false;
1453
1474
  visited.add(value);
1454
1475
  if ("@value" in value) return false;
@@ -3552,6 +3573,589 @@ function createExponentialBackoffPolicy(options = {}) {
3552
3573
  };
3553
3574
  }
3554
3575
  //#endregion
3576
+ //#region src/federation/tasks/codec.ts
3577
+ /**
3578
+ * Serializes and deserializes task payloads for the queue, preserving
3579
+ * `@fedify/vocab` objects across the wire by reducing them to JSON-LD and
3580
+ * rebuilding them on the worker with the bound {@link TaskCodecLoaders}.
3581
+ * @internal
3582
+ */
3583
+ var TaskCodec = class TaskCodec {
3584
+ options;
3585
+ constructor(options) {
3586
+ this.options = options;
3587
+ }
3588
+ /** Serializes `data`, encoding any vocabulary object as its JSON-LD. */
3589
+ serialize = (data) => stringifyAsync(data, { Vocab: this.#stringifyVocab });
3590
+ deserialize = async (raw) => await this.#revive(/* @__PURE__ */ new Map())(parse(raw, { Vocab: VocabHolder.from }));
3591
+ /** Validates `data` against `schema`, then serializes it. */
3592
+ encode = async (schema, data) => this.serialize(await TaskCodec.validate(schema, data));
3593
+ /** Deserializes `raw`, then validates the result against `schema`. */
3594
+ decode = async (schema, raw) => {
3595
+ let data;
3596
+ try {
3597
+ data = await this.deserialize(raw);
3598
+ } catch (error) {
3599
+ return {
3600
+ ok: false,
3601
+ phase: "deserialization",
3602
+ error
3603
+ };
3604
+ }
3605
+ try {
3606
+ return {
3607
+ ok: true,
3608
+ value: await TaskCodec.validate(schema, data)
3609
+ };
3610
+ } catch (error) {
3611
+ return {
3612
+ ok: false,
3613
+ phase: "validation",
3614
+ error
3615
+ };
3616
+ }
3617
+ };
3618
+ static validate = async (schema, data) => getValueIfSchema(await schema["~standard"].validate(data));
3619
+ #stringifyVocab = (value) => isVocab(value) && this.#toWire(value);
3620
+ #toWire = async (value) => ({
3621
+ kind: value instanceof Link ? "link" : "object",
3622
+ jsonLd: await value.toJsonLd({
3623
+ format: "expand",
3624
+ ...this.options
3625
+ })
3626
+ });
3627
+ #revive = (seen) => {
3628
+ const inner = async (node) => {
3629
+ if (node === null || typeof node !== "object") return node;
3630
+ if (seen.has(node)) return seen.get(node);
3631
+ for (const reviver of this.#classRevivers) {
3632
+ const out = reviver(seen, inner, node);
3633
+ if (out !== void 0) return await out;
3634
+ }
3635
+ return node;
3636
+ };
3637
+ return inner;
3638
+ };
3639
+ #classRevivers = [
3640
+ classReviver(isInstanceOf(VocabHolder), ({ kind, jsonLd }) => kind === "link" ? Link.fromJsonLd(jsonLd, this.options) : Object$1.fromJsonLd(jsonLd, this.options), () => {}),
3641
+ classReviver(isInstanceOf(Array), () => [], async (revive, node, arr) => {
3642
+ for (const item of await Array.fromAsync(node, revive)) arr.push(item);
3643
+ }),
3644
+ classReviver(isInstanceOf(Map), () => /* @__PURE__ */ new Map(), async (revive, node, map) => {
3645
+ for (const [k, v] of node) map.set(await revive(k), await revive(v));
3646
+ }),
3647
+ classReviver(isInstanceOf(Set), () => /* @__PURE__ */ new Set(), async (revive, node, set) => {
3648
+ for (const v of await Array.fromAsync(node, revive)) set.add(v);
3649
+ }),
3650
+ classReviver(isPlainObject, () => ({}), async (revive, node, obj) => {
3651
+ for (const [k, v] of globalThis.Object.entries(node)) obj[k] = await revive(v);
3652
+ })
3653
+ ];
3654
+ };
3655
+ const isVocab = (value) => value instanceof Object$1 || value instanceof Link;
3656
+ const isPlainObject = (value) => value === null || typeof value !== "object" ? false : isObjectPrototype(globalThis.Object.getPrototypeOf(value));
3657
+ const isObjectPrototype = (proto) => proto === null || proto === globalThis.Object.prototype;
3658
+ const isInstanceOf = (cls) => (v) => v instanceof cls;
3659
+ function getValueIfSchema(result) {
3660
+ assertSchema(result);
3661
+ return result.value;
3662
+ }
3663
+ function assertSchema(result) {
3664
+ if (result.issues && result.issues.length > 0) throw new TypeError(`Task data failed schema validation: ${JSON.stringify(result.issues)}`);
3665
+ }
3666
+ /**
3667
+ * A vocabulary object parked by the synchronous decode reviver, held until
3668
+ * the async revive pass can `fromJsonLd()` it back into an instance.
3669
+ */
3670
+ var VocabHolder = class VocabHolder {
3671
+ kind;
3672
+ jsonLd;
3673
+ constructor(kind, jsonLd) {
3674
+ this.kind = kind;
3675
+ this.jsonLd = jsonLd;
3676
+ }
3677
+ static from = ({ kind, jsonLd }) => new VocabHolder(kind, jsonLd);
3678
+ };
3679
+ /**
3680
+ * Ties a container filter to its empty-shell `init` and child-filling `set`
3681
+ * through one type parameter—a correlation the heterogeneous reviver list
3682
+ * cannot carry, which previously forced `@ts-ignore` at the dispatch site.
3683
+ */
3684
+ const classReviver = (filter, init, set) => (seen, revive, node) => {
3685
+ if (!filter(node)) return void 0;
3686
+ return (async () => {
3687
+ const out = await init(node);
3688
+ seen.set(node, out);
3689
+ await set(revive, node, out);
3690
+ return out;
3691
+ })();
3692
+ };
3693
+ //#endregion
3694
+ //#region src/federation/mq.ts
3695
+ /**
3696
+ * A message queue that processes messages in the same process.
3697
+ * Do not use this in production as it does neither persist messages nor
3698
+ * distribute them across multiple processes.
3699
+ *
3700
+ * @since 0.5.0
3701
+ */
3702
+ var InProcessMessageQueue = class {
3703
+ #messages;
3704
+ #monitors;
3705
+ #pollIntervalMs;
3706
+ #delayedMessages;
3707
+ /**
3708
+ * Tracks which ordering keys are currently being processed to ensure
3709
+ * sequential processing for messages with the same key.
3710
+ */
3711
+ #processingKeys;
3712
+ /**
3713
+ * In-process message queue does not provide native retry mechanisms.
3714
+ * @since 1.7.0
3715
+ */
3716
+ nativeRetrial = false;
3717
+ /**
3718
+ * In-process message queue does not deduplicate messages natively.
3719
+ * @since 2.4.0
3720
+ */
3721
+ nativeDeduplication = false;
3722
+ /**
3723
+ * Constructs a new {@link InProcessMessageQueue} with the given options.
3724
+ * @param options Additional options for the in-process message queue.
3725
+ */
3726
+ constructor(options = {}) {
3727
+ this.#messages = [];
3728
+ this.#monitors = {};
3729
+ this.#pollIntervalMs = Temporal.Duration.from(options.pollInterval ?? { seconds: 5 }).total("millisecond");
3730
+ this.#delayedMessages = 0;
3731
+ this.#processingKeys = /* @__PURE__ */ new Set();
3732
+ }
3733
+ enqueue(message, options) {
3734
+ const delay = options?.delay == null ? 0 : Math.max(options.delay.total("millisecond"), 0);
3735
+ if (delay > 0) {
3736
+ this.#delayedMessages++;
3737
+ setTimeout(() => {
3738
+ this.#delayedMessages--;
3739
+ this.#enqueueReady(message, options);
3740
+ }, delay);
3741
+ return Promise.resolve();
3742
+ }
3743
+ this.#enqueueReady(message, options);
3744
+ return Promise.resolve();
3745
+ }
3746
+ #enqueueReady(message, options) {
3747
+ const orderingKey = options?.orderingKey ?? null;
3748
+ this.#messages.push({
3749
+ message,
3750
+ orderingKey
3751
+ });
3752
+ this.#notifyMonitors();
3753
+ }
3754
+ #notifyMonitors() {
3755
+ for (const monitorId in this.#monitors) this.#monitors[monitorId]();
3756
+ }
3757
+ enqueueMany(messages, options) {
3758
+ if (messages.length === 0) return Promise.resolve();
3759
+ const delay = options?.delay == null ? 0 : Math.max(options.delay.total("millisecond"), 0);
3760
+ if (delay > 0) {
3761
+ const delayedCount = messages.length;
3762
+ const deferredMessages = [...messages];
3763
+ this.#delayedMessages += delayedCount;
3764
+ setTimeout(() => {
3765
+ this.#delayedMessages -= delayedCount;
3766
+ this.#enqueueManyReady(deferredMessages, options);
3767
+ }, delay);
3768
+ return Promise.resolve();
3769
+ }
3770
+ this.#enqueueManyReady(messages, options);
3771
+ return Promise.resolve();
3772
+ }
3773
+ #enqueueManyReady(messages, options) {
3774
+ const orderingKey = options?.orderingKey ?? null;
3775
+ for (const message of messages) this.#messages.push({
3776
+ message,
3777
+ orderingKey
3778
+ });
3779
+ this.#notifyMonitors();
3780
+ }
3781
+ async listen(handler, options = {}) {
3782
+ const signal = options.signal;
3783
+ while (signal == null || !signal.aborted) {
3784
+ const idx = this.#messages.findIndex((m) => m.orderingKey == null || !this.#processingKeys.has(m.orderingKey));
3785
+ if (idx >= 0) {
3786
+ const { message, orderingKey } = this.#messages.splice(idx, 1)[0];
3787
+ if (orderingKey != null) this.#processingKeys.add(orderingKey);
3788
+ try {
3789
+ await handler(message);
3790
+ } finally {
3791
+ if (orderingKey != null) this.#processingKeys.delete(orderingKey);
3792
+ }
3793
+ } else if (this.#messages.length === 0) await this.#wait(this.#pollIntervalMs, signal);
3794
+ else await this.#wait(10, signal);
3795
+ }
3796
+ }
3797
+ getDepth() {
3798
+ const ready = this.#messages.length;
3799
+ const delayed = this.#delayedMessages;
3800
+ return Promise.resolve({
3801
+ queued: ready + delayed,
3802
+ ready,
3803
+ delayed
3804
+ });
3805
+ }
3806
+ #wait(ms, signal) {
3807
+ let timer = null;
3808
+ return Promise.any([new Promise((resolve) => {
3809
+ signal?.addEventListener("abort", () => {
3810
+ if (timer != null) clearTimeout(timer);
3811
+ resolve();
3812
+ }, { once: true });
3813
+ const monitorId = crypto.randomUUID();
3814
+ this.#monitors[monitorId] = () => {
3815
+ delete this.#monitors[monitorId];
3816
+ if (timer != null) clearTimeout(timer);
3817
+ resolve();
3818
+ };
3819
+ }), new Promise((resolve) => timer = setTimeout(resolve, ms))]);
3820
+ }
3821
+ };
3822
+ /**
3823
+ * A message queue that processes messages in parallel. It takes another
3824
+ * {@link MessageQueue}, and processes messages in parallel up to a certain
3825
+ * number of workers.
3826
+ *
3827
+ * Actually, it's rather a decorator than a queue itself.
3828
+ *
3829
+ * Note that the workers do not run in truly parallel, in the sense that they
3830
+ * are not running in separate threads or processes. They are running in the
3831
+ * same process, but are scheduled to run in parallel. Hence, this is useful
3832
+ * for I/O-bound tasks, but not for CPU-bound tasks, which is okay for Fedify's
3833
+ * workloads.
3834
+ *
3835
+ * When using `ParallelMessageQueue`, the ordering guarantee is preserved
3836
+ * *only if* the underlying queue implementation delivers messages in a wrapper
3837
+ * format that includes the `__fedify_ordering_key__` property. Currently,
3838
+ * only `DenoKvMessageQueue` and `WorkersMessageQueue` use this format.
3839
+ * For other queue implementations (e.g., `InProcessMessageQueue`,
3840
+ * `RedisMessageQueue`, `PostgresMessageQueue`, `SqliteMessageQueue`,
3841
+ * `AmqpMessageQueue`), the ordering key cannot be detected by
3842
+ * `ParallelMessageQueue`, so ordering guarantees are handled by those
3843
+ * implementations directly rather than at the `ParallelMessageQueue` level.
3844
+ *
3845
+ * Messages with the same ordering key will never be processed concurrently
3846
+ * by different workers, ensuring sequential processing within each key.
3847
+ * Messages with different ordering keys (or no ordering key) can still be
3848
+ * processed in parallel.
3849
+ *
3850
+ * @since 1.0.0
3851
+ */
3852
+ var ParallelMessageQueue = class ParallelMessageQueue {
3853
+ queue;
3854
+ workers;
3855
+ /**
3856
+ * Inherits the native retry capability from the wrapped queue.
3857
+ * @since 1.7.0
3858
+ */
3859
+ nativeRetrial;
3860
+ /**
3861
+ * Inherits the native deduplication capability from the wrapped queue.
3862
+ * @since 2.4.0
3863
+ */
3864
+ nativeDeduplication;
3865
+ getDepth;
3866
+ /**
3867
+ * Tracks which ordering keys are currently being processed to ensure
3868
+ * sequential processing for messages with the same key.
3869
+ */
3870
+ #processingKeys = /* @__PURE__ */ new Set();
3871
+ /**
3872
+ * Pending messages waiting for their ordering key to become available.
3873
+ */
3874
+ #pendingMessages = [];
3875
+ /**
3876
+ * Constructs a new {@link ParallelMessageQueue} with the given queue and
3877
+ * number of workers.
3878
+ * @param queue The message queue to use under the hood. Note that
3879
+ * {@link ParallelMessageQueue} cannot be nested.
3880
+ * @param workers The number of workers to process messages in parallel.
3881
+ * @throws {TypeError} If the given queue is an instance of
3882
+ * {@link ParallelMessageQueue}.
3883
+ */
3884
+ constructor(queue, workers) {
3885
+ if (queue instanceof ParallelMessageQueue) throw new TypeError("Cannot nest ParallelMessageQueue.");
3886
+ this.queue = queue;
3887
+ this.workers = workers;
3888
+ this.nativeRetrial = queue.nativeRetrial;
3889
+ this.nativeDeduplication = queue.nativeDeduplication;
3890
+ if (queue.getDepth != null) this.getDepth = () => queue.getDepth();
3891
+ }
3892
+ enqueue(message, options) {
3893
+ return this.queue.enqueue(message, options);
3894
+ }
3895
+ async enqueueMany(messages, options) {
3896
+ if (this.queue.enqueueMany == null) {
3897
+ if (messages.length === 1) {
3898
+ await this.queue.enqueue(messages[0], options);
3899
+ return;
3900
+ }
3901
+ if (options?.deduplicationKey != null) throw new TypeError("Cannot enqueue a batch with a deduplicationKey: the wrapped queue does not implement enqueueMany, so ParallelMessageQueue would have to fan out to individual enqueue() calls that cannot share one deduplicationKey atomically. Wrap a queue that implements enqueueMany instead.");
3902
+ const errors = (await Promise.allSettled(messages.map((message) => this.queue.enqueue(message, options)))).filter((r) => r.status === "rejected").map((r) => r.reason);
3903
+ if (errors.length > 1) throw new AggregateError(errors, "Failed to enqueue messages.");
3904
+ else if (errors.length === 1) throw errors[0];
3905
+ return;
3906
+ }
3907
+ await this.queue.enqueueMany(messages, options);
3908
+ }
3909
+ /**
3910
+ * Extracts ordering key from a message if present.
3911
+ *
3912
+ * This method only works for queue implementations that deliver messages
3913
+ * in the wrapper format with `__fedify_ordering_key__` property. Currently,
3914
+ * only `DenoKvMessageQueue` and `WorkersMessageQueue` use this format.
3915
+ *
3916
+ * For other queue implementations (`InProcessMessageQueue`,
3917
+ * `RedisMessageQueue`, `PostgresMessageQueue`, `SqliteMessageQueue`,
3918
+ * `AmqpMessageQueue`), messages are delivered as raw payloads without the
3919
+ * wrapper, so the ordering key cannot be detected here. Those
3920
+ * implementations handle ordering guarantees internally.
3921
+ */
3922
+ #extractOrderingKey(message) {
3923
+ if (message != null && typeof message === "object") {
3924
+ if ("__fedify_ordering_key__" in message) return message.__fedify_ordering_key__;
3925
+ }
3926
+ }
3927
+ listen(handler, options = {}) {
3928
+ const workers = /* @__PURE__ */ new Map();
3929
+ return this.queue.listen(async (message) => {
3930
+ while (workers.size >= this.workers) {
3931
+ const consumedId = await Promise.any(workers.values());
3932
+ workers.delete(consumedId);
3933
+ }
3934
+ const workerId = crypto.randomUUID();
3935
+ const orderingKey = this.#extractOrderingKey(message);
3936
+ if (orderingKey != null && this.#processingKeys.has(orderingKey)) await new Promise((resolve) => {
3937
+ this.#pendingMessages.push({
3938
+ message,
3939
+ orderingKey,
3940
+ resolve
3941
+ });
3942
+ });
3943
+ if (orderingKey != null) this.#processingKeys.add(orderingKey);
3944
+ const promise = this.#work(workerId, handler, message, orderingKey);
3945
+ workers.set(workerId, promise);
3946
+ }, options);
3947
+ }
3948
+ async #work(workerId, handler, message, orderingKey) {
3949
+ await this.#sleep(0);
3950
+ try {
3951
+ await handler(message);
3952
+ } finally {
3953
+ if (orderingKey != null) {
3954
+ this.#processingKeys.delete(orderingKey);
3955
+ const pendingIdx = this.#pendingMessages.findIndex((p) => p.orderingKey === orderingKey);
3956
+ if (pendingIdx >= 0) this.#pendingMessages.splice(pendingIdx, 1)[0].resolve();
3957
+ }
3958
+ }
3959
+ return workerId;
3960
+ }
3961
+ #sleep(ms) {
3962
+ return new Promise((resolve) => setTimeout(resolve, ms));
3963
+ }
3964
+ };
3965
+ //#endregion
3966
+ //#region src/federation/tasks/enqueue.ts
3967
+ /**
3968
+ * The enqueue pipeline for custom background tasks. `ContextImpl.enqueueTask`
3969
+ * and `ContextImpl.enqueueTaskMany` delegate to {@link enqueueTasks} so the
3970
+ * handle validation, deduplication planning, payload encoding, and queue
3971
+ * dispatch live in one cohesive place instead of one oversized method.
3972
+ *
3973
+ * @module
3974
+ */
3975
+ /**
3976
+ * Validates the task handle, plans deduplication, encodes every payload, then
3977
+ * dispatches the resulting messages to the queue. A single item flows through
3978
+ * the same pipeline as a batch, so {@link Context.enqueueTask} and
3979
+ * {@link Context.enqueueTaskMany} share one implementation.
3980
+ * @template TContextData The context data to pass to the {@link Context}.
3981
+ * @template TData The type of the task payload, inferred from the task's schema.
3982
+ * @param ctx The enqueueing dependencies assembled by `ContextImpl`.
3983
+ * @param task The handle returned by `defineTask()`.
3984
+ * @param items The payloads to enqueue, in order.
3985
+ * @param options The enqueue options governing delay, ordering, and dedup.
3986
+ * @internal
3987
+ */
3988
+ const enqueueTasks = (ctx) => async function(task, items, options) {
3989
+ const def = ctx.federation.taskDefinitions.get(task.name);
3990
+ if (def == null || def.handle !== task) throw new TypeError(`Task ${JSON.stringify(task.name)} is not defined on this federation; pass a handle returned by its defineTask().`);
3991
+ const queue = ctx.federation.resolveTaskQueue(task.name);
3992
+ if (queue == null) throw new TypeError("No message queue is configured for tasks; pass `queue` to createFederation() or to defineTask().");
3993
+ if (items.length < 1) return;
3994
+ const plan = planDeduplication(ctx, queue, task.name, options, items.length);
3995
+ const messages = await Promise.all(items.map(encodeTaskMessage(ctx.codec, ctx.origin, task, options)));
3996
+ const claim = await claimDeduplication(ctx, plan, task.name);
3997
+ if (!claim.proceed) return;
3998
+ if (!ctx.federation.manuallyStartQueue) ctx.federation._startQueueInternal(ctx.data);
3999
+ try {
4000
+ await dispatch(queue, messages, {
4001
+ delay: getDurationIfDefined(options.delay),
4002
+ orderingKey: options.orderingKey,
4003
+ deduplicationKey: claim.forwardedDeduplicationKey
4004
+ }, {
4005
+ meterProvider: ctx.federation.meterProvider,
4006
+ taskName: task.name
4007
+ });
4008
+ } catch (error) {
4009
+ if (claim.rollback != null) try {
4010
+ await claim.rollback();
4011
+ } catch (rollbackError) {
4012
+ logger$1.warn("Failed to roll back the deduplication marker for task {taskName} after a failed enqueue; it will expire by TTL. {rollbackError}", {
4013
+ taskName: task.name,
4014
+ rollbackError
4015
+ });
4016
+ }
4017
+ throw error;
4018
+ }
4019
+ };
4020
+ const getDurationIfDefined = (item) => item == null ? void 0 : Temporal.Duration.from(item);
4021
+ /**
4022
+ * Decides how a `deduplicationKey` (if any) is honored: forwarded to a native
4023
+ * queue, claimed via `cas`, or—when neither is available—dropped or rejected
4024
+ * per the federation's `taskDeduplicationFallback`. Throws the fail-fast
4025
+ * `TypeError`s so they precede the encode.
4026
+ */
4027
+ function planDeduplication(ctx, queue, taskName, options, itemCount) {
4028
+ if (options.deduplicationKey == null) return { kind: "none" };
4029
+ const key = options.deduplicationKey;
4030
+ const native = queue.nativeDeduplication === true;
4031
+ const canCas = ctx.federation.kv.cas != null;
4032
+ const canBatchAtomically = queue.enqueueMany != null && !(queue instanceof ParallelMessageQueue && queue.queue.enqueueMany == null);
4033
+ if (itemCount > 1 && !canBatchAtomically && (native || canCas)) throw new TypeError(`Task ${JSON.stringify(taskName)} was enqueued as a batch with a deduplicationKey, but its message queue does not implement enqueueMany; a multi-item batch cannot be deduplicated atomically without it. Implement enqueueMany on the queue, or enqueue the tasks individually with enqueueTask().`);
4034
+ if (native) return {
4035
+ kind: "native",
4036
+ key
4037
+ };
4038
+ if (canCas) return {
4039
+ kind: "cas",
4040
+ key
4041
+ };
4042
+ if (ctx.federation.taskDeduplicationFallback === "closed") throw new TypeError("deduplicationKey was set but the message queue does not declare nativeDeduplication and the key-value store exposes no conditional write (cas); set taskDeduplicationFallback to \"open\" to proceed without deduplication, or use a backend that supports it.");
4043
+ return {
4044
+ kind: "open",
4045
+ key
4046
+ };
4047
+ }
4048
+ /**
4049
+ * Executes the planned deduplication once the payloads are encoded. A native
4050
+ * plan forwards its key to the queue; a `cas` plan claims the marker and stops
4051
+ * the enqueue when it loses the race; an `open` plan logs and proceeds.
4052
+ * @returns Whether to proceed, and the key (if any) to forward to the queue.
4053
+ */
4054
+ async function claimDeduplication(ctx, plan, taskName) {
4055
+ switch (plan.kind) {
4056
+ case "native": return {
4057
+ proceed: true,
4058
+ forwardedDeduplicationKey: plan.key
4059
+ };
4060
+ case "cas": {
4061
+ const cacheKey = [...ctx.federation.kvPrefixes.taskDeduplication, plan.key];
4062
+ const token = crypto.randomUUID();
4063
+ if (!await ctx.federation.kv.cas(cacheKey, void 0, token, { ttl: ctx.federation.taskDeduplicationTtl })) return { proceed: false };
4064
+ return {
4065
+ proceed: true,
4066
+ rollback: async () => {
4067
+ await ctx.federation.kv.cas(cacheKey, token, void 0);
4068
+ }
4069
+ };
4070
+ }
4071
+ case "open": logger$1.debug("deduplicationKey {deduplicationKey} for task {taskName} ignored: the message queue declares no nativeDeduplication and the key-value store has no cas; proceeding (taskDeduplicationFallback is \"open\").", {
4072
+ deduplicationKey: plan.key,
4073
+ taskName
4074
+ });
4075
+ case "none": return { proceed: true };
4076
+ default: throw new TypeError(`Unknown deduplication plan: ${JSON.stringify(plan)}`);
4077
+ }
4078
+ }
4079
+ /**
4080
+ * Sends the encoded messages to the queue, picking the bulk path when the
4081
+ * queue implements `enqueueMany` and otherwise fanning out parallel single
4082
+ * enqueues. The fan-out drops `deduplicationKey`, which is only ever set for a
4083
+ * native plan that the bulk paths already cover.
4084
+ */
4085
+ async function dispatch(queue, messages, options, { meterProvider, taskName }) {
4086
+ const metrics = getFederationMetrics(meterProvider);
4087
+ if (messages.length === 1) await queue.enqueue(messages[0], options);
4088
+ else if (queue.enqueueMany != null) await queue.enqueueMany(messages, options);
4089
+ else {
4090
+ const fanoutOptions = {
4091
+ delay: options.delay,
4092
+ orderingKey: options.orderingKey
4093
+ };
4094
+ const rejected = (await Promise.allSettled(messages.map(async (message) => {
4095
+ await queue.enqueue(message, fanoutOptions);
4096
+ metrics.recordQueueTaskEnqueued({
4097
+ role: "task",
4098
+ queue,
4099
+ taskName
4100
+ }, message.attempt);
4101
+ }))).find((result) => result.status === "rejected");
4102
+ if (rejected != null) throw rejected.reason;
4103
+ return;
4104
+ }
4105
+ metrics.recordQueueTaskEnqueued({
4106
+ role: "task",
4107
+ queue,
4108
+ taskName
4109
+ }, messages[0].attempt, messages.length);
4110
+ }
4111
+ /**
4112
+ * Builds the per-payload encoder: validates and serializes the payload, then
4113
+ * stamps the message envelope with a fresh id, the context's origin, and the
4114
+ * active trace context. Curried so the batch encode reuses one bound encoder.
4115
+ */
4116
+ const encodeTaskMessage = (codec, origin, task, options) => async (data) => {
4117
+ const encoded = await codec.encode(task.schema, data);
4118
+ const carrier = {};
4119
+ propagation.inject(context.active(), carrier);
4120
+ return {
4121
+ type: "task",
4122
+ id: crypto.randomUUID(),
4123
+ baseUrl: origin,
4124
+ taskName: task.name,
4125
+ data: encoded,
4126
+ started: Temporal.Now.instant().toString(),
4127
+ attempt: 0,
4128
+ orderingKey: options.orderingKey,
4129
+ traceContext: carrier
4130
+ };
4131
+ };
4132
+ const logger$1 = getLogger([
4133
+ "fedify",
4134
+ "federation",
4135
+ "task"
4136
+ ]);
4137
+ //#endregion
4138
+ //#region src/federation/tasks/error.ts
4139
+ var TaskRetryEnqueueError = class extends Error {
4140
+ constructor(cause) {
4141
+ super("Failed to re-enqueue a custom task for a retry.", { cause });
4142
+ this.name = "TaskRetryEnqueueError";
4143
+ }
4144
+ };
4145
+ const classifyAbortableError = (error) => isAbortError(error) ? { outcome: "aborted" } : {
4146
+ outcome: "failed",
4147
+ error
4148
+ };
4149
+ const classifyTaskError = (error) => isAbortError(error) ? { outcome: "aborted" } : error instanceof TaskRetryEnqueueError ? {
4150
+ outcome: "failed",
4151
+ failureReason: "retry_enqueue",
4152
+ error: error.cause ?? error
4153
+ } : {
4154
+ outcome: "failed",
4155
+ failureReason: "handler",
4156
+ error
4157
+ };
4158
+ //#endregion
3555
4159
  //#region src/federation/webfinger.ts
3556
4160
  const logger = getLogger([
3557
4161
  "fedify",
@@ -3885,9 +4489,8 @@ var FederationImpl = class extends FederationBuilderImpl {
3885
4489
  inboxQueue;
3886
4490
  outboxQueue;
3887
4491
  fanoutQueue;
3888
- inboxQueueStarted;
3889
- outboxQueueStarted;
3890
- fanoutQueueStarted;
4492
+ taskQueue;
4493
+ startedQueues;
3891
4494
  manuallyStartQueue;
3892
4495
  origin;
3893
4496
  documentLoaderFactory;
@@ -3901,6 +4504,10 @@ var FederationImpl = class extends FederationBuilderImpl {
3901
4504
  skipSignatureVerification;
3902
4505
  outboxRetryPolicy;
3903
4506
  inboxRetryPolicy;
4507
+ taskRetryPolicy;
4508
+ taskQueueResolution;
4509
+ taskDeduplicationTtl;
4510
+ taskDeduplicationFallback;
3904
4511
  circuitBreaker;
3905
4512
  activityTransformers;
3906
4513
  _tracerProvider;
@@ -3940,20 +4547,24 @@ var FederationImpl = class extends FederationBuilderImpl {
3940
4547
  httpMessageSignaturesSpec: ["_fedify", "httpMessageSignaturesSpec"],
3941
4548
  acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
3942
4549
  circuitBreaker: ["_fedify", "circuit"],
4550
+ taskDeduplication: ["_fedify", "taskDeduplication"],
3943
4551
  ...options.kvPrefixes ?? {}
3944
4552
  };
3945
4553
  if (options.queue == null) {
3946
4554
  this.inboxQueue = void 0;
3947
4555
  this.outboxQueue = void 0;
3948
4556
  this.fanoutQueue = void 0;
4557
+ this.taskQueue = void 0;
3949
4558
  } else if ("enqueue" in options.queue && "listen" in options.queue) {
3950
4559
  this.inboxQueue = options.queue;
3951
4560
  this.outboxQueue = options.queue;
3952
4561
  this.fanoutQueue = options.queue;
4562
+ this.taskQueue = void 0;
3953
4563
  } else {
3954
4564
  this.inboxQueue = options.queue.inbox;
3955
4565
  this.outboxQueue = options.queue.outbox;
3956
4566
  this.fanoutQueue = options.queue.fanout;
4567
+ this.taskQueue = options.queue.task;
3957
4568
  }
3958
4569
  if (options.circuitBreaker !== false && this.outboxQueue != null) {
3959
4570
  this.circuitBreaker = new CircuitBreaker({
@@ -3974,9 +4585,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3974
4585
  ]).warn("The configured key-value store does not support CAS; outbound delivery circuit breaker updates may race under concurrent workers.");
3975
4586
  }
3976
4587
  }
3977
- this.inboxQueueStarted = false;
3978
- this.outboxQueueStarted = false;
3979
- this.fanoutQueueStarted = false;
4588
+ this.startedQueues = /* @__PURE__ */ new Set();
3980
4589
  this.manuallyStartQueue = options.manuallyStartQueue ?? false;
3981
4590
  if (options.origin != null) if (typeof options.origin === "string") {
3982
4591
  if (!URL.canParse(options.origin) || !options.origin.match(/^https?:\/\//)) throw new TypeError(`Invalid origin: ${JSON.stringify(options.origin)}`);
@@ -4058,6 +4667,10 @@ var FederationImpl = class extends FederationBuilderImpl {
4058
4667
  this.inboxChallengePolicy = options.inboxChallengePolicy;
4059
4668
  this.outboxRetryPolicy = options.outboxRetryPolicy ?? createExponentialBackoffPolicy();
4060
4669
  this.inboxRetryPolicy = options.inboxRetryPolicy ?? createExponentialBackoffPolicy();
4670
+ this.taskRetryPolicy = options.taskRetryPolicy ?? createExponentialBackoffPolicy();
4671
+ this.taskQueueResolution = options.taskQueueResolution ?? "fallback";
4672
+ this.taskDeduplicationTtl = Temporal.Duration.from(options.taskDeduplicationTtl ?? { hours: 1 });
4673
+ this.taskDeduplicationFallback = options.taskDeduplicationFallback ?? "open";
4061
4674
  this.activityTransformers = options.activityTransformers ?? getDefaultActivityTransformers();
4062
4675
  this._tracerProvider = options.tracerProvider;
4063
4676
  if (benchmarkMode) {
@@ -4090,6 +4703,9 @@ var FederationImpl = class extends FederationBuilderImpl {
4090
4703
  this.#registerQueueDepthGauge(meterProvider);
4091
4704
  return meterProvider;
4092
4705
  }
4706
+ get metrics() {
4707
+ return getFederationMetrics(this.meterProvider);
4708
+ }
4093
4709
  #registerQueueDepthGauge(meterProvider) {
4094
4710
  if (meterProvider === this.#queueDepthGaugeMeterProvider) return;
4095
4711
  registerQueueDepthGauge(meterProvider, this.#queueDepthGaugeEntries, { sourceId: this.#queueDepthGaugeSourceId });
@@ -4106,35 +4722,38 @@ var FederationImpl = class extends FederationBuilderImpl {
4106
4722
  _getTracer() {
4107
4723
  return this.tracerProvider.getTracer(name, version);
4108
4724
  }
4725
+ resolveTaskQueue(taskName) {
4726
+ const resolved = this.taskDefinitions.get(taskName)?.queue ?? this.taskQueue;
4727
+ if (resolved != null) return resolved;
4728
+ return this.taskQueueResolution === "strict" ? void 0 : this.outboxQueue;
4729
+ }
4109
4730
  async _startQueueInternal(ctxData, signal, queue) {
4110
- if (this.inboxQueue == null && this.outboxQueue == null) return;
4731
+ const taskQueue = this.taskQueue ?? (this.taskQueueResolution === "fallback" ? this.outboxQueue : void 0);
4732
+ const customQueues = this.taskDefinitions.values().map((def) => ["task", def.queue]);
4733
+ const targets = [
4734
+ ["inbox", this.inboxQueue],
4735
+ ["outbox", this.outboxQueue],
4736
+ ["fanout", this.fanoutQueue],
4737
+ ["task", taskQueue],
4738
+ ...customQueues
4739
+ ];
4111
4740
  const logger = getLogger([
4112
4741
  "fedify",
4113
4742
  "federation",
4114
4743
  "queue"
4115
4744
  ]);
4116
4745
  const promises = [];
4117
- if (this.inboxQueue != null && (queue == null || queue === "inbox") && !this.inboxQueueStarted) {
4118
- logger.debug("Starting an inbox task worker.");
4119
- this.inboxQueueStarted = true;
4120
- promises.push(this.inboxQueue.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
4121
- }
4122
- if (this.outboxQueue != null && this.outboxQueue !== this.inboxQueue && (queue == null || queue === "outbox") && !this.outboxQueueStarted) {
4123
- logger.debug("Starting an outbox task worker.");
4124
- this.outboxQueueStarted = true;
4125
- promises.push(this.outboxQueue.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
4126
- }
4127
- if (this.fanoutQueue != null && this.fanoutQueue !== this.inboxQueue && this.fanoutQueue !== this.outboxQueue && (queue == null || queue === "fanout") && !this.fanoutQueueStarted) {
4128
- logger.debug("Starting a fanout task worker.");
4129
- this.fanoutQueueStarted = true;
4130
- promises.push(this.fanoutQueue.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
4746
+ for (const [role, target] of targets) {
4747
+ if (target == null || !(queue == null || queue === role)) continue;
4748
+ if (this.startedQueues.has(target)) continue;
4749
+ this.startedQueues.add(target);
4750
+ logger.debug("Starting a {role} queue worker.", { role });
4751
+ promises.push(target.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
4131
4752
  }
4132
4753
  await Promise.all(promises);
4133
4754
  }
4134
4755
  processQueuedTask(contextData, message) {
4135
- const tracer = this._getTracer();
4136
4756
  const extractedContext = propagation.extract(context.active(), message.traceContext);
4137
- const meter = getFederationMetrics(this.meterProvider);
4138
4757
  return withContext({ messageId: message.id }, async () => {
4139
4758
  if (message.type === "fanout") {
4140
4759
  const common = {
@@ -4142,117 +4761,108 @@ var FederationImpl = class extends FederationBuilderImpl {
4142
4761
  queue: this.fanoutQueue,
4143
4762
  activityType: message.activityType
4144
4763
  };
4145
- await tracer.startActiveSpan("activitypub.fanout", {
4146
- kind: SpanKind.CONSUMER,
4147
- attributes: { "activitypub.activity.type": message.activityType }
4148
- }, extractedContext, async (span) => {
4149
- const spanCtx = span.spanContext();
4150
- return await withContext({
4151
- traceId: spanCtx.traceId,
4152
- spanId: spanCtx.spanId
4153
- }, async () => {
4154
- if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
4155
- meter.recordQueueTaskStarted(common);
4156
- meter.incrementQueueTaskInFlight(common);
4157
- const startedAt = performance.now();
4158
- let outcome = "completed";
4159
- try {
4160
- await this.#listenFanoutMessage(contextData, message);
4161
- } catch (e) {
4162
- const aborted = isAbortError(e);
4163
- outcome = aborted ? "aborted" : "failed";
4164
- if (!aborted) span.setStatus({
4165
- code: SpanStatusCode.ERROR,
4166
- message: String(e)
4167
- });
4168
- throw e;
4169
- } finally {
4170
- meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
4171
- meter.decrementQueueTaskInFlight(common);
4172
- span.end();
4173
- }
4174
- });
4175
- });
4764
+ await this.#runWorkerSpan("activitypub.fanout", { "activitypub.activity.type": message.activityType }, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
4765
+ if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
4766
+ await this.#listenFanoutMessage(contextData, message);
4767
+ return { outcome: "completed" };
4768
+ }, classifyAbortableError));
4176
4769
  } else if (message.type === "outbox") {
4177
4770
  const common = {
4178
4771
  role: "outbox",
4179
4772
  queue: this.outboxQueue,
4180
4773
  activityType: message.activityType
4181
4774
  };
4182
- await tracer.startActiveSpan("activitypub.outbox", {
4183
- kind: SpanKind.CONSUMER,
4184
- attributes: {
4185
- "activitypub.activity.type": message.activityType,
4186
- "activitypub.activity.retries": message.attempt
4187
- }
4188
- }, extractedContext, async (span) => {
4189
- const spanCtx = span.spanContext();
4190
- return await withContext({
4191
- traceId: spanCtx.traceId,
4192
- spanId: spanCtx.spanId
4193
- }, async () => {
4194
- if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
4195
- meter.recordQueueTaskStarted(common);
4196
- meter.incrementQueueTaskInFlight(common);
4197
- const startedAt = performance.now();
4198
- let outcome = "completed";
4199
- try {
4200
- await this.#listenOutboxMessage(contextData, message, span);
4201
- } catch (e) {
4202
- const aborted = isAbortError(e);
4203
- outcome = aborted ? "aborted" : "failed";
4204
- if (!aborted) span.setStatus({
4205
- code: SpanStatusCode.ERROR,
4206
- message: String(e)
4207
- });
4208
- throw e;
4209
- } finally {
4210
- meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
4211
- meter.decrementQueueTaskInFlight(common);
4212
- span.end();
4213
- }
4214
- });
4215
- });
4775
+ await this.#runWorkerSpan("activitypub.outbox", {
4776
+ "activitypub.activity.type": message.activityType,
4777
+ "activitypub.activity.retries": message.attempt
4778
+ }, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
4779
+ if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
4780
+ await this.#listenOutboxMessage(contextData, message, span);
4781
+ return { outcome: "completed" };
4782
+ }, classifyAbortableError));
4216
4783
  } else if (message.type === "inbox") {
4217
4784
  const common = {
4218
4785
  role: "inbox",
4219
4786
  queue: this.inboxQueue
4220
4787
  };
4221
- await tracer.startActiveSpan("activitypub.inbox", {
4222
- kind: SpanKind.CONSUMER,
4223
- attributes: { "activitypub.shared_inbox": message.identifier == null }
4224
- }, extractedContext, async (span) => {
4225
- const spanCtx = span.spanContext();
4226
- return await withContext({
4227
- traceId: spanCtx.traceId,
4228
- spanId: spanCtx.spanId
4229
- }, async () => {
4230
- meter.recordQueueTaskStarted(common);
4231
- meter.incrementQueueTaskInFlight(common);
4232
- const startedAt = performance.now();
4233
- let outcome = "completed";
4234
- try {
4235
- await this.#listenInboxMessage(contextData, message, span, (activityType) => {
4236
- common.activityType = activityType;
4237
- });
4238
- } catch (e) {
4239
- const aborted = isAbortError(e);
4240
- outcome = aborted ? "aborted" : "failed";
4241
- if (!aborted) span.setStatus({
4242
- code: SpanStatusCode.ERROR,
4243
- message: String(e)
4244
- });
4245
- throw e;
4246
- } finally {
4247
- meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
4248
- meter.decrementQueueTaskInFlight(common);
4249
- span.end();
4250
- }
4788
+ await this.#runWorkerSpan("activitypub.inbox", { "activitypub.shared_inbox": message.identifier == null }, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
4789
+ await this.#listenInboxMessage(contextData, message, span, (activityType) => {
4790
+ common.activityType = activityType;
4251
4791
  });
4252
- });
4792
+ return { outcome: "completed" };
4793
+ }, classifyAbortableError));
4794
+ } else if (message.type === "task") {
4795
+ const registered = this.taskDefinitions.get(message.taskName) != null;
4796
+ const common = {
4797
+ role: "task",
4798
+ queue: this.resolveTaskQueue(message.taskName),
4799
+ taskName: registered ? message.taskName : void 0
4800
+ };
4801
+ await this.#runWorkerSpan("fedify.task", {
4802
+ "fedify.task.name": message.taskName,
4803
+ "fedify.task.attempt": message.attempt
4804
+ }, extractedContext, this.#instrumentWorkerBody(common, () => this.#listenTaskMessage(contextData, message), classifyTaskError));
4253
4805
  }
4254
4806
  });
4255
4807
  }
4808
+ /**
4809
+ * Opens a CONSUMER span for a queue worker and scopes the extracted trace
4810
+ * context around its body. The body—typically built by
4811
+ * {@link FederationImpl.instrumentWorkerBody}—receives the span and returns
4812
+ * the context-scoped worker callback.
4813
+ * @param spanName The consumer span name (for example, `"fedify.task"`).
4814
+ * @param spanAttributes The initial span attributes.
4815
+ * @param extractedContext The trace context extracted from the message.
4816
+ * @param instrumentedBody Builds the context-scoped worker callback for the
4817
+ * opened span.
4818
+ */
4819
+ async #runWorkerSpan(spanName, spanAttributes, extractedContext, instrumentedBody) {
4820
+ await this._getTracer().startActiveSpan(spanName, {
4821
+ kind: SpanKind.CONSUMER,
4822
+ attributes: spanAttributes
4823
+ }, extractedContext, async (span) => {
4824
+ const spanCtx = span.spanContext();
4825
+ return await withContext({
4826
+ traceId: spanCtx.traceId,
4827
+ spanId: spanCtx.spanId
4828
+ }, instrumentedBody(span));
4829
+ });
4830
+ }
4831
+ /**
4832
+ * Wraps a worker body with the shared `fedify.queue.task.*` boundary
4833
+ * telemetry, returning the instrumented body keyed by its span. The
4834
+ * instrumentation records the started, outcome, and duration metrics, pairs
4835
+ * the in-flight increment with its decrement, sets the span's error status
4836
+ * from the terminal outcome, and ends the span. The fanout, outbox, inbox,
4837
+ * and task workers differ only in `common`, the body, and how a thrown error
4838
+ * maps to an outcome.
4839
+ * @param common The shared queue-task metric attributes for this worker.
4840
+ * @param run Runs the worker body and resolves to its terminal outcome.
4841
+ * @param classifyError Maps a thrown error to a terminal outcome.
4842
+ */
4843
+ #instrumentWorkerBody = (common, run, classifyError) => (span) => async () => {
4844
+ this.metrics.recordQueueTaskStarted(common);
4845
+ this.metrics.incrementQueueTaskInFlight(common);
4846
+ const startedAt = performance.now();
4847
+ let result = { outcome: "completed" };
4848
+ try {
4849
+ result = await run(span);
4850
+ } catch (e) {
4851
+ result = classifyError(e);
4852
+ throw e;
4853
+ } finally {
4854
+ if (result.outcome === "failed") {
4855
+ if (result.failureReason != null) span.setAttribute("fedify.task.failure_reason", result.failureReason);
4856
+ span.setStatus({
4857
+ code: SpanStatusCode.ERROR,
4858
+ ...result.error == null ? {} : { message: String(result.error) }
4859
+ });
4860
+ }
4861
+ this.metrics.recordQueueTaskOutcome(common, result.outcome, getDurationMs(startedAt), result.outcome === "failed" ? result.failureReason : void 0);
4862
+ this.metrics.decrementQueueTaskInFlight(common);
4863
+ span.end();
4864
+ }
4865
+ };
4256
4866
  async #listenFanoutMessage(data, message) {
4257
4867
  getLogger([
4258
4868
  "fedify",
@@ -4342,7 +4952,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4342
4952
  delay: clampNegativeDelay(delay),
4343
4953
  orderingKey: message.orderingKey
4344
4954
  });
4345
- getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
4955
+ this.metrics.recordQueueTaskEnqueued({
4346
4956
  role: "outbox",
4347
4957
  queue: outboxQueue,
4348
4958
  activityType: heldMessage.activityType
@@ -4521,7 +5131,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4521
5131
  return;
4522
5132
  }
4523
5133
  if (isPermanentFailure) {
4524
- getFederationMetrics(this.meterProvider).recordPermanentFailure(error.inbox, error.statusCode);
5134
+ this.metrics.recordPermanentFailure(error.inbox, error.statusCode);
4525
5135
  logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not retrying.", {
4526
5136
  ...logData,
4527
5137
  status: error.statusCode
@@ -4581,7 +5191,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4581
5191
  delay: clampNegativeDelay(delay),
4582
5192
  orderingKey: message.orderingKey
4583
5193
  });
4584
- getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
5194
+ this.metrics.recordQueueTaskEnqueued({
4585
5195
  role: "outbox",
4586
5196
  queue: outboxQueue,
4587
5197
  activityType: retryMessage.activityType
@@ -4671,7 +5281,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4671
5281
  };
4672
5282
  await this.inboxQueue.enqueue(retryMessage, { delay: clampNegativeDelay(delay) });
4673
5283
  if (activityType != null) {
4674
- getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
5284
+ this.metrics.recordQueueTaskEnqueued({
4675
5285
  role: "inbox",
4676
5286
  queue: this.inboxQueue,
4677
5287
  activityType
@@ -4798,7 +5408,7 @@ var FederationImpl = class extends FederationBuilderImpl {
4798
5408
  try {
4799
5409
  await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, activityType), activity);
4800
5410
  } finally {
4801
- getFederationMetrics(this.meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
5411
+ this.metrics.recordInboxProcessingDuration(activityType, getDurationMs(started));
4802
5412
  }
4803
5413
  recordInboxActivity(this.meterProvider, "processed", activityType);
4804
5414
  } catch (error) {
@@ -4814,6 +5424,105 @@ var FederationImpl = class extends FederationBuilderImpl {
4814
5424
  listenerSpan.end();
4815
5425
  });
4816
5426
  }
5427
+ async #listenTaskMessage(contextData, message) {
5428
+ const logger = getLogger([
5429
+ "fedify",
5430
+ "federation",
5431
+ "task"
5432
+ ]);
5433
+ const def = this.taskDefinitions.get(message.taskName);
5434
+ if (def == null) {
5435
+ logger.warn("Received a custom task {taskName} with no registered handler; dropping.", { taskName: message.taskName });
5436
+ return {
5437
+ outcome: "failed",
5438
+ failureReason: "unknown_task"
5439
+ };
5440
+ }
5441
+ const context = this.#createContext(new URL(message.baseUrl), contextData);
5442
+ const data = await context.codec.decode(def.schema, message.data);
5443
+ if (!data.ok) {
5444
+ if (data.phase === "deserialization") logger.error("Custom task {taskName} payload could not be deserialized; dropping:\n{error}", {
5445
+ taskName: message.taskName,
5446
+ error: data.error
5447
+ });
5448
+ else logger.error("Custom task {taskName} payload failed schema validation; dropping:\n{error}", {
5449
+ taskName: message.taskName,
5450
+ error: data.error
5451
+ });
5452
+ return {
5453
+ outcome: "failed",
5454
+ failureReason: data.phase
5455
+ };
5456
+ }
5457
+ try {
5458
+ await def.handler(context, data.value);
5459
+ return { outcome: "completed" };
5460
+ } catch (error) {
5461
+ if (def.onError != null) try {
5462
+ await def.onError(context, error, data.value);
5463
+ } catch (onErrorError) {
5464
+ logger.error("onError for custom task {taskName} threw:\n{error}", {
5465
+ taskName: message.taskName,
5466
+ error: onErrorError
5467
+ });
5468
+ }
5469
+ const queue = this.resolveTaskQueue(def.name);
5470
+ if (queue?.nativeRetrial) throw error;
5471
+ const retryPolicy = def.retryPolicy ?? this.taskRetryPolicy;
5472
+ let elapsedTime = Temporal.Duration.from({ seconds: 0 });
5473
+ try {
5474
+ elapsedTime = Temporal.Instant.from(message.started).until(Temporal.Now.instant());
5475
+ } catch (parseError) {
5476
+ logger.error("Custom task {taskName} has an unparsable started time {started}; treating elapsedTime as zero:\n{error}", {
5477
+ taskName: message.taskName,
5478
+ started: message.started,
5479
+ error: parseError
5480
+ });
5481
+ }
5482
+ const delay = retryPolicy({
5483
+ elapsedTime,
5484
+ attempts: message.attempt
5485
+ });
5486
+ if (delay != null && queue != null) {
5487
+ logger.error("Custom task {taskName} failed (attempt #{attempt}); retry...:\n{error}", {
5488
+ taskName: message.taskName,
5489
+ attempt: message.attempt,
5490
+ error
5491
+ });
5492
+ const retryMessage = {
5493
+ ...message,
5494
+ attempt: message.attempt + 1
5495
+ };
5496
+ try {
5497
+ await queue.enqueue(retryMessage, {
5498
+ delay: clampNegativeDelay(delay),
5499
+ orderingKey: message.orderingKey
5500
+ });
5501
+ } catch (enqueueError) {
5502
+ logger.error("Custom task {taskName} could not be re-enqueued for a retry (attempt #{attempt}):\n{error}", {
5503
+ taskName: message.taskName,
5504
+ attempt: retryMessage.attempt,
5505
+ error: enqueueError
5506
+ });
5507
+ throw new TaskRetryEnqueueError(error);
5508
+ }
5509
+ this.metrics.recordQueueTaskEnqueued({
5510
+ role: "task",
5511
+ queue,
5512
+ taskName: message.taskName
5513
+ }, retryMessage.attempt);
5514
+ return { outcome: "completed" };
5515
+ } else logger.error("Custom task {taskName} failed after {attempt} attempts; giving up:\n{error}", {
5516
+ taskName: message.taskName,
5517
+ attempt: message.attempt,
5518
+ error
5519
+ });
5520
+ return isAbortError(error) ? { outcome: "aborted" } : {
5521
+ outcome: "failed",
5522
+ failureReason: "handler"
5523
+ };
5524
+ }
5525
+ }
4817
5526
  startQueue(contextData, options = {}) {
4818
5527
  return this._startQueueInternal(contextData, options.signal, options.queue);
4819
5528
  }
@@ -5047,7 +5756,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5047
5756
  });
5048
5757
  if (acceptsJsonLd(request)) response.headers.set("Vary", "Accept");
5049
5758
  } catch (error) {
5050
- getFederationMetrics(this.meterProvider).recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), { routeTemplate: metricState.routeTemplate });
5759
+ this.metrics.recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), { routeTemplate: metricState.routeTemplate });
5051
5760
  span.setStatus({
5052
5761
  code: SpanStatusCode.ERROR,
5053
5762
  message: `${error}`
@@ -5060,7 +5769,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5060
5769
  });
5061
5770
  throw error;
5062
5771
  }
5063
- getFederationMetrics(this.meterProvider).recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), {
5772
+ this.metrics.recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), {
5064
5773
  statusCode: response.status,
5065
5774
  routeTemplate: metricState.routeTemplate
5066
5775
  });
@@ -5398,6 +6107,7 @@ var ContextImpl = class ContextImpl {
5398
6107
  documentLoader;
5399
6108
  contextLoader;
5400
6109
  invokedFromActorKeyPairsDispatcher;
6110
+ #codec;
5401
6111
  constructor({ url, federation, data, documentLoader, contextLoader, invokedFromActorKeyPairsDispatcher }) {
5402
6112
  this.url = url;
5403
6113
  this.federation = federation;
@@ -5406,6 +6116,18 @@ var ContextImpl = class ContextImpl {
5406
6116
  this.contextLoader = contextLoader;
5407
6117
  this.invokedFromActorKeyPairsDispatcher = invokedFromActorKeyPairsDispatcher;
5408
6118
  }
6119
+ /**
6120
+ * A {@link TaskCodec} bound to this context's loaders, used to encode
6121
+ * and decode custom task payloads. Lazily created and cached so a context
6122
+ * that never enqueues or dispatches a task pays nothing.
6123
+ * @internal
6124
+ */
6125
+ get codec() {
6126
+ return this.#codec ??= new TaskCodec(this);
6127
+ }
6128
+ get #enqueueTasks() {
6129
+ return enqueueTasks(this);
6130
+ }
5409
6131
  clone(data) {
5410
6132
  return new ContextImpl({
5411
6133
  url: this.url,
@@ -5724,6 +6446,12 @@ var ContextImpl = class ContextImpl {
5724
6446
  allowPrivateAddress: this.federation.allowPrivateAddress
5725
6447
  });
5726
6448
  }
6449
+ async enqueueTask(task, data, options = {}) {
6450
+ await this.#enqueueTasks(task, [data], options);
6451
+ }
6452
+ async enqueueTaskMany(task, payloads, options = {}) {
6453
+ await this.#enqueueTasks(task, payloads, options);
6454
+ }
5727
6455
  sendActivity(sender, recipients, activity, options = {}) {
5728
6456
  return this.tracerProvider.getTracer(name, version).startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
5729
6457
  kind: this.federation.outboxQueue == null || options.immediate ? SpanKind.CLIENT : SpanKind.PRODUCER,
@@ -6434,4 +7162,4 @@ function getRequestId(request) {
6434
7162
  return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
6435
7163
  }
6436
7164
  //#endregion
6437
- export { SendActivityError as a, buildCollectionSynchronizationHeader as c, normalizeCircuitBreakerOptions as d, parseCircuitBreakerKvState as f, createExponentialBackoffPolicy as i, digest as l, middleware_exports as n, respondWithObject as o, createFederationBuilder as p, handleWebFinger as r, respondWithObjectIfAcceptable as s, createFederation as t, CircuitBreaker as u };
7165
+ export { ParallelMessageQueue as a, respondWithObject as c, digest as d, CircuitBreaker as f, createFederationBuilder as h, InProcessMessageQueue as i, respondWithObjectIfAcceptable as l, parseCircuitBreakerKvState as m, middleware_exports as n, createExponentialBackoffPolicy as o, normalizeCircuitBreakerOptions as p, handleWebFinger as r, SendActivityError as s, createFederation as t, buildCollectionSynchronizationHeader as u };