@fedify/fedify 2.4.0-dev.1490 → 2.4.0-dev.1508

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 (109) hide show
  1. package/README.md +7 -5
  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-CwwQRArg.mjs → builder-DlWCek6Z.mjs} +23 -3
  9. package/dist/codec-CmlPqjNX.mjs +103 -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-BU6jSQdo.d.ts → context-BF5B4ZDk.d.ts} +529 -259
  17. package/dist/{context-BBVLF7lx.d.cts → context-CYF8X0ft.d.cts} +530 -259
  18. package/dist/{deno-Yohf1ISx.mjs → deno-nexucTAX.mjs} +1 -1
  19. package/dist/{docloader-B6DFWEOx.mjs → docloader-DAdEN549.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 -13
  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 +200 -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 +292 -0
  41. package/dist/federation/tasks/enqueue.test.d.mts +2 -0
  42. package/dist/federation/tasks/enqueue.test.mjs +992 -0
  43. package/dist/federation/tasks/tasks.test.d.mts +2 -0
  44. package/dist/federation/tasks/tasks.test.mjs +555 -0
  45. package/dist/federation/temporal.test.mjs +4 -4
  46. package/dist/federation/webfinger.test.mjs +6 -6
  47. package/dist/{http-C3eJp_Ha.cjs → http-BCj9dlA_.cjs} +1 -1
  48. package/dist/{http-BqSm-f2G.js → http-Dv3147TL.js} +1 -1
  49. package/dist/{http-BeDlAH97.mjs → http-inQqS3yx.mjs} +4 -4
  50. package/dist/{key-DNBTcjuV.mjs → key-CGrr6-I2.mjs} +2 -2
  51. package/dist/{kv-cache-Crq8nvSV.js → kv-cache-C2oTEXyb.js} +1 -1
  52. package/dist/{kv-cache-b2HaCLC-.mjs → kv-cache-C3qkFkbt.mjs} +1 -1
  53. package/dist/{kv-cache-CGJMNSs-.cjs → kv-cache-c8X6svwE.cjs} +1 -1
  54. package/dist/{ld-CQlFhsU6.mjs → ld-fnWaNT8y.mjs} +3 -3
  55. package/dist/{metrics-B6RKtTLt.mjs → metrics-C5hMINAa.mjs} +1 -1
  56. package/dist/{middleware-CQygcRYE.mjs → middleware-CQlq6ykI.mjs} +1 -1
  57. package/dist/{middleware-CBM9vGYg.cjs → middleware-DeVzbg1I.cjs} +690 -30
  58. package/dist/{middleware-CsxoclmN.mjs → middleware-Dl2nyVgB.mjs} +306 -46
  59. package/dist/{middleware-B3vFLcCh.js → middleware-vQ_ZpgcX.js} +679 -31
  60. package/dist/{mod-vPYVoa5n.d.ts → mod-4WGAuR6X.d.ts} +1 -1
  61. package/dist/{mod-C0F6kvgS.d.cts → mod-BHEB3xhE.d.cts} +1 -1
  62. package/dist/mod.cjs +6 -6
  63. package/dist/mod.d.cts +5 -5
  64. package/dist/mod.d.ts +5 -5
  65. package/dist/mod.js +5 -5
  66. package/dist/mq-03vS-C9P.mjs +276 -0
  67. package/dist/{mq-D8uSFzxe.d.cts → mq-B5_hohs7.d.ts} +34 -0
  68. package/dist/{mq-D-nlpY04.d.ts → mq-DXMt_qCY.d.cts} +34 -0
  69. package/dist/nodeinfo/client.test.mjs +3 -3
  70. package/dist/nodeinfo/handler.test.mjs +5 -5
  71. package/dist/nodeinfo/types.test.mjs +4 -4
  72. package/dist/otel/exporter.test.mjs +3 -3
  73. package/dist/{outgoing-jsonld-L_DbOaFe.mjs → outgoing-jsonld-BNL8AC14.mjs} +1 -1
  74. package/dist/{owner-B1FnbZ1w.mjs → owner-Bq2eZyO_.mjs} +2 -2
  75. package/dist/{proof-ChmZL5rY.cjs → proof-Buz7I8wj.cjs} +1 -1
  76. package/dist/{proof-BhNTn0HE.js → proof-CTG9azxN.js} +1 -1
  77. package/dist/{proof-BUuvq0ia.mjs → proof-DZCjcFCd.mjs} +5 -5
  78. package/dist/{send-Dq-Ofquk.mjs → send-B253S5gN.mjs} +3 -3
  79. package/dist/sig/accept.test.mjs +1 -1
  80. package/dist/sig/http.test.mjs +8 -8
  81. package/dist/sig/key.test.mjs +6 -6
  82. package/dist/sig/ld.test.mjs +7 -7
  83. package/dist/sig/mod.cjs +2 -2
  84. package/dist/sig/mod.js +2 -2
  85. package/dist/sig/owner.test.mjs +6 -6
  86. package/dist/sig/proof.test.mjs +8 -8
  87. package/dist/{std__assert-BBjXFNOb.mjs → std__assert-Dh6uLf_q.mjs} +4 -4
  88. package/dist/{temporal-C-vrfOld.mjs → temporal-d6TshGjq.mjs} +1 -1
  89. package/dist/testing/mod.d.mts +1037 -8
  90. package/dist/testing/mod.mjs +3 -2
  91. package/dist/testing-Dy-WhqpN.mjs +77 -0
  92. package/dist/utils/docloader.test.mjs +6 -6
  93. package/dist/utils/kv-cache.test.mjs +1 -1
  94. package/dist/utils/mod.cjs +1 -1
  95. package/dist/utils/mod.js +1 -1
  96. package/package.json +9 -7
  97. /package/dist/{accept-CPkZzmGN.mjs → accept-CgDcxvjV.mjs} +0 -0
  98. /package/dist/{activity-listener-tztVvlNb.mjs → activity-listener-BeTGV3wc.mjs} +0 -0
  99. /package/dist/{assert_equals-C-ZRDbaf.mjs → assert_equals-PBVKNMJd.mjs} +0 -0
  100. /package/dist/{circuit-breaker-CSWsyoef.mjs → circuit-breaker-hJBB6jwA.mjs} +0 -0
  101. /package/dist/{client-ByXmQhYD.mjs → client-B_A6mfn3.mjs} +0 -0
  102. /package/dist/{collection-Cc3DVAhE.mjs → collection-Dp5ky45w.mjs} +0 -0
  103. /package/dist/{keycache-BeU0LCII.mjs → keycache-CaOR6NYg.mjs} +0 -0
  104. /package/dist/{keys-DGu1NFwu.mjs → keys-CSYsOMFG.mjs} +0 -0
  105. /package/dist/{kv-rV3vodCc.mjs → kv-CIvR3Non.mjs} +0 -0
  106. /package/dist/{negotiation-DDstyBvc.mjs → negotiation-m_L2nyS3.mjs} +0 -0
  107. /package/dist/{public-audience-Cvbr2Gzt.mjs → public-audience-c9zmYKgA.mjs} +0 -0
  108. /package/dist/{retry-CXg_MBI-.mjs → retry-DI4O-zYl.mjs} +0 -0
  109. /package/dist/{types-J53Kw7so.mjs → types-BFowWFTT.mjs} +0 -0
@@ -1,39 +1,41 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-Yohf1ISx.mjs";
5
- import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-B6RKtTLt.mjs";
6
- import { t as formatAcceptSignature } from "./accept-CPkZzmGN.mjs";
7
- import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-DNBTcjuV.mjs";
8
- import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-BeDlAH97.mjs";
9
- import { t as getAuthenticatedDocumentLoader } from "./docloader-B6DFWEOx.mjs";
10
- import { n as kvCache } from "./kv-cache-b2HaCLC-.mjs";
11
- import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-CQlFhsU6.mjs";
12
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-B1FnbZ1w.mjs";
13
- import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-L_DbOaFe.mjs";
14
- import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-BUuvq0ia.mjs";
15
- import { t as getNodeInfo } from "./client-ByXmQhYD.mjs";
16
- import { t as nodeInfoToJson } from "./types-J53Kw7so.mjs";
17
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-Dq-Ofquk.mjs";
18
- import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-CwwQRArg.mjs";
19
- import { t as CircuitBreaker } from "./circuit-breaker-CSWsyoef.mjs";
20
- import { t as buildCollectionSynchronizationHeader } from "./collection-Cc3DVAhE.mjs";
21
- import { t as KvKeyCache } from "./keycache-BeU0LCII.mjs";
22
- import { t as acceptsJsonLd } from "./negotiation-DDstyBvc.mjs";
23
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-C-vrfOld.mjs";
24
- import { t as createExponentialBackoffPolicy } from "./retry-CXg_MBI-.mjs";
4
+ import { n as version, t as name } from "./deno-nexucTAX.mjs";
5
+ import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-C5hMINAa.mjs";
6
+ import { t as formatAcceptSignature } from "./accept-CgDcxvjV.mjs";
7
+ import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-CGrr6-I2.mjs";
8
+ import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-inQqS3yx.mjs";
9
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-DAdEN549.mjs";
10
+ import { n as kvCache } from "./kv-cache-C3qkFkbt.mjs";
11
+ import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-fnWaNT8y.mjs";
12
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-Bq2eZyO_.mjs";
13
+ import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-BNL8AC14.mjs";
14
+ import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-DZCjcFCd.mjs";
15
+ import { t as getNodeInfo } from "./client-B_A6mfn3.mjs";
16
+ import { t as nodeInfoToJson } from "./types-BFowWFTT.mjs";
17
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-B253S5gN.mjs";
18
+ import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-DlWCek6Z.mjs";
19
+ import { t as CircuitBreaker } from "./circuit-breaker-hJBB6jwA.mjs";
20
+ import { t as buildCollectionSynchronizationHeader } from "./collection-Dp5ky45w.mjs";
21
+ import { t as KvKeyCache } from "./keycache-CaOR6NYg.mjs";
22
+ import { t as acceptsJsonLd } from "./negotiation-m_L2nyS3.mjs";
23
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-d6TshGjq.mjs";
24
+ import { t as createExponentialBackoffPolicy } from "./retry-DI4O-zYl.mjs";
25
+ import { t as TaskCodec } from "./codec-CmlPqjNX.mjs";
26
+ import { n as ParallelMessageQueue } from "./mq-03vS-C9P.mjs";
25
27
  import { getLogger, withContext } from "@logtape/logtape";
26
28
  import { RouterError } from "@fedify/uri-template";
27
29
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
28
30
  import { lookupWebFinger } from "@fedify/webfinger";
29
31
  import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
32
+ import { uniq } from "es-toolkit";
30
33
  import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
31
34
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
32
- import { uniq } from "es-toolkit";
33
35
  import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
34
36
  import { domainToASCII } from "node:url";
35
37
  //#region src/compat/transformers.ts
36
- const logger$1 = getLogger([
38
+ const logger$2 = getLogger([
37
39
  "fedify",
38
40
  "compat",
39
41
  "transformers"
@@ -59,7 +61,7 @@ const logger$1 = getLogger([
59
61
  function autoIdAssigner(activity, context) {
60
62
  if (activity.id != null) return activity;
61
63
  const id = new URL(`/#${activity.constructor.name}/${crypto.randomUUID()}`, context.origin);
62
- logger$1.warn("As the activity to send does not have an id, a new id {id} has been generated for it. However, it is recommended to explicitly set the id for the activity.", { id: id.href });
64
+ logger$2.warn("As the activity to send does not have an id, a new id {id} has been generated for it. However, it is recommended to explicitly set the id for the activity.", { id: id.href });
63
65
  return activity.clone({ id });
64
66
  }
65
67
  /**
@@ -2093,6 +2095,160 @@ const MIN_COMPONENTS = [
2093
2095
  "@authority"
2094
2096
  ];
2095
2097
  //#endregion
2098
+ //#region src/federation/tasks/enqueue.ts
2099
+ /**
2100
+ * The enqueue pipeline for custom background tasks. `ContextImpl.enqueueTask`
2101
+ * and `ContextImpl.enqueueTaskMany` delegate to {@link enqueueTasks} so the
2102
+ * handle validation, deduplication planning, payload encoding, and queue
2103
+ * dispatch live in one cohesive place instead of one oversized method.
2104
+ *
2105
+ * @module
2106
+ */
2107
+ /**
2108
+ * Validates the task handle, plans deduplication, encodes every payload, then
2109
+ * dispatches the resulting messages to the queue. A single item flows through
2110
+ * the same pipeline as a batch, so {@link Context.enqueueTask} and
2111
+ * {@link Context.enqueueTaskMany} share one implementation.
2112
+ * @template TContextData The context data to pass to the {@link Context}.
2113
+ * @template TData The type of the task payload, inferred from the task's schema.
2114
+ * @param ctx The enqueueing dependencies assembled by `ContextImpl`.
2115
+ * @param task The handle returned by `defineTask()`.
2116
+ * @param items The payloads to enqueue, in order.
2117
+ * @param options The enqueue options governing delay, ordering, and dedup.
2118
+ * @internal
2119
+ */
2120
+ const enqueueTasks = (ctx) => async function(task, items, options) {
2121
+ const def = ctx.federation.taskDefinitions.get(task.name);
2122
+ 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().`);
2123
+ const queue = ctx.federation.resolveTaskQueue(task.name);
2124
+ if (queue == null) throw new TypeError("No message queue is configured for tasks; pass `queue` to createFederation() or to defineTask().");
2125
+ if (items.length < 1) return;
2126
+ const plan = planDeduplication(ctx, queue, task.name, options, items.length);
2127
+ const messages = await Promise.all(items.map(encodeTaskMessage(ctx.codec, ctx.origin, task, options)));
2128
+ const claim = await claimDeduplication(ctx, plan, task.name);
2129
+ if (!claim.proceed) return;
2130
+ if (!ctx.federation.manuallyStartQueue) ctx.federation._startQueueInternal(ctx.data);
2131
+ try {
2132
+ await dispatch(queue, messages, {
2133
+ delay: getDurationIfDefined(options.delay),
2134
+ orderingKey: options.orderingKey,
2135
+ deduplicationKey: claim.forwardedDeduplicationKey
2136
+ });
2137
+ } catch (error) {
2138
+ if (claim.rollback != null) try {
2139
+ await claim.rollback();
2140
+ } catch (rollbackError) {
2141
+ logger$1.warn("Failed to roll back the deduplication marker for task {taskName} after a failed enqueue; it will expire by TTL. {rollbackError}", {
2142
+ taskName: task.name,
2143
+ rollbackError
2144
+ });
2145
+ }
2146
+ throw error;
2147
+ }
2148
+ };
2149
+ const getDurationIfDefined = (item) => item == null ? void 0 : Temporal.Duration.from(item);
2150
+ /**
2151
+ * Decides how a `deduplicationKey` (if any) is honored: forwarded to a native
2152
+ * queue, claimed via `cas`, or—when neither is available—dropped or rejected
2153
+ * per the federation's `taskDeduplicationFallback`. Throws the fail-fast
2154
+ * `TypeError`s so they precede the encode.
2155
+ */
2156
+ function planDeduplication(ctx, queue, taskName, options, itemCount) {
2157
+ if (options.deduplicationKey == null) return { kind: "none" };
2158
+ const key = options.deduplicationKey;
2159
+ const native = queue.nativeDeduplication === true;
2160
+ const canCas = ctx.federation.kv.cas != null;
2161
+ const canBatchAtomically = queue.enqueueMany != null && !(queue instanceof ParallelMessageQueue && queue.queue.enqueueMany == null);
2162
+ 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().`);
2163
+ if (native) return {
2164
+ kind: "native",
2165
+ key
2166
+ };
2167
+ if (canCas) return {
2168
+ kind: "cas",
2169
+ key
2170
+ };
2171
+ 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.");
2172
+ return {
2173
+ kind: "open",
2174
+ key
2175
+ };
2176
+ }
2177
+ /**
2178
+ * Executes the planned deduplication once the payloads are encoded. A native
2179
+ * plan forwards its key to the queue; a `cas` plan claims the marker and stops
2180
+ * the enqueue when it loses the race; an `open` plan logs and proceeds.
2181
+ * @returns Whether to proceed, and the key (if any) to forward to the queue.
2182
+ */
2183
+ async function claimDeduplication(ctx, plan, taskName) {
2184
+ switch (plan.kind) {
2185
+ case "native": return {
2186
+ proceed: true,
2187
+ forwardedDeduplicationKey: plan.key
2188
+ };
2189
+ case "cas": {
2190
+ const cacheKey = [...ctx.federation.kvPrefixes.taskDeduplication, plan.key];
2191
+ const token = crypto.randomUUID();
2192
+ if (!await ctx.federation.kv.cas(cacheKey, void 0, token, { ttl: ctx.federation.taskDeduplicationTtl })) return { proceed: false };
2193
+ return {
2194
+ proceed: true,
2195
+ rollback: async () => {
2196
+ await ctx.federation.kv.cas(cacheKey, token, void 0);
2197
+ }
2198
+ };
2199
+ }
2200
+ 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\").", {
2201
+ deduplicationKey: plan.key,
2202
+ taskName
2203
+ });
2204
+ case "none": return { proceed: true };
2205
+ default: throw new TypeError(`Unknown deduplication plan: ${JSON.stringify(plan)}`);
2206
+ }
2207
+ }
2208
+ /**
2209
+ * Sends the encoded messages to the queue, picking the bulk path when the
2210
+ * queue implements `enqueueMany` and otherwise fanning out parallel single
2211
+ * enqueues. The fan-out drops `deduplicationKey`, which is only ever set for a
2212
+ * native plan that the bulk paths already cover.
2213
+ */
2214
+ async function dispatch(queue, messages, options) {
2215
+ if (messages.length === 1) await queue.enqueue(messages[0], options);
2216
+ else if (queue.enqueueMany != null) await queue.enqueueMany(messages, options);
2217
+ else {
2218
+ const fanoutOptions = {
2219
+ delay: options.delay,
2220
+ orderingKey: options.orderingKey
2221
+ };
2222
+ await Promise.all(messages.map((m) => queue.enqueue(m, fanoutOptions)));
2223
+ }
2224
+ }
2225
+ /**
2226
+ * Builds the per-payload encoder: validates and serializes the payload, then
2227
+ * stamps the message envelope with a fresh id, the context's origin, and the
2228
+ * active trace context. Curried so the batch encode reuses one bound encoder.
2229
+ */
2230
+ const encodeTaskMessage = (codec, origin, task, options) => async (data) => {
2231
+ const encoded = await codec.encode(task.schema, data);
2232
+ const carrier = {};
2233
+ propagation.inject(context.active(), carrier);
2234
+ return {
2235
+ type: "task",
2236
+ id: crypto.randomUUID(),
2237
+ baseUrl: origin,
2238
+ taskName: task.name,
2239
+ data: encoded,
2240
+ started: Temporal.Now.instant().toString(),
2241
+ attempt: 0,
2242
+ orderingKey: options.orderingKey,
2243
+ traceContext: carrier
2244
+ };
2245
+ };
2246
+ const logger$1 = getLogger([
2247
+ "fedify",
2248
+ "federation",
2249
+ "task"
2250
+ ]);
2251
+ //#endregion
2096
2252
  //#region src/federation/webfinger.ts
2097
2253
  const logger = getLogger([
2098
2254
  "fedify",
@@ -2418,9 +2574,8 @@ var FederationImpl = class extends FederationBuilderImpl {
2418
2574
  inboxQueue;
2419
2575
  outboxQueue;
2420
2576
  fanoutQueue;
2421
- inboxQueueStarted;
2422
- outboxQueueStarted;
2423
- fanoutQueueStarted;
2577
+ taskQueue;
2578
+ startedQueues;
2424
2579
  manuallyStartQueue;
2425
2580
  origin;
2426
2581
  documentLoaderFactory;
@@ -2434,6 +2589,10 @@ var FederationImpl = class extends FederationBuilderImpl {
2434
2589
  skipSignatureVerification;
2435
2590
  outboxRetryPolicy;
2436
2591
  inboxRetryPolicy;
2592
+ taskRetryPolicy;
2593
+ taskQueueResolution;
2594
+ taskDeduplicationTtl;
2595
+ taskDeduplicationFallback;
2437
2596
  circuitBreaker;
2438
2597
  activityTransformers;
2439
2598
  _tracerProvider;
@@ -2473,20 +2632,24 @@ var FederationImpl = class extends FederationBuilderImpl {
2473
2632
  httpMessageSignaturesSpec: ["_fedify", "httpMessageSignaturesSpec"],
2474
2633
  acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
2475
2634
  circuitBreaker: ["_fedify", "circuit"],
2635
+ taskDeduplication: ["_fedify", "taskDeduplication"],
2476
2636
  ...options.kvPrefixes ?? {}
2477
2637
  };
2478
2638
  if (options.queue == null) {
2479
2639
  this.inboxQueue = void 0;
2480
2640
  this.outboxQueue = void 0;
2481
2641
  this.fanoutQueue = void 0;
2642
+ this.taskQueue = void 0;
2482
2643
  } else if ("enqueue" in options.queue && "listen" in options.queue) {
2483
2644
  this.inboxQueue = options.queue;
2484
2645
  this.outboxQueue = options.queue;
2485
2646
  this.fanoutQueue = options.queue;
2647
+ this.taskQueue = void 0;
2486
2648
  } else {
2487
2649
  this.inboxQueue = options.queue.inbox;
2488
2650
  this.outboxQueue = options.queue.outbox;
2489
2651
  this.fanoutQueue = options.queue.fanout;
2652
+ this.taskQueue = options.queue.task;
2490
2653
  }
2491
2654
  if (options.circuitBreaker !== false && this.outboxQueue != null) {
2492
2655
  this.circuitBreaker = new CircuitBreaker({
@@ -2507,9 +2670,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2507
2670
  ]).warn("The configured key-value store does not support CAS; outbound delivery circuit breaker updates may race under concurrent workers.");
2508
2671
  }
2509
2672
  }
2510
- this.inboxQueueStarted = false;
2511
- this.outboxQueueStarted = false;
2512
- this.fanoutQueueStarted = false;
2673
+ this.startedQueues = /* @__PURE__ */ new Set();
2513
2674
  this.manuallyStartQueue = options.manuallyStartQueue ?? false;
2514
2675
  if (options.origin != null) if (typeof options.origin === "string") {
2515
2676
  if (!URL.canParse(options.origin) || !options.origin.match(/^https?:\/\//)) throw new TypeError(`Invalid origin: ${JSON.stringify(options.origin)}`);
@@ -2591,6 +2752,10 @@ var FederationImpl = class extends FederationBuilderImpl {
2591
2752
  this.inboxChallengePolicy = options.inboxChallengePolicy;
2592
2753
  this.outboxRetryPolicy = options.outboxRetryPolicy ?? createExponentialBackoffPolicy();
2593
2754
  this.inboxRetryPolicy = options.inboxRetryPolicy ?? createExponentialBackoffPolicy();
2755
+ this.taskRetryPolicy = options.taskRetryPolicy ?? createExponentialBackoffPolicy();
2756
+ this.taskQueueResolution = options.taskQueueResolution ?? "fallback";
2757
+ this.taskDeduplicationTtl = Temporal.Duration.from(options.taskDeduplicationTtl ?? { hours: 1 });
2758
+ this.taskDeduplicationFallback = options.taskDeduplicationFallback ?? "open";
2594
2759
  this.activityTransformers = options.activityTransformers ?? getDefaultActivityTransformers();
2595
2760
  this._tracerProvider = options.tracerProvider;
2596
2761
  if (benchmarkMode) {
@@ -2639,28 +2804,33 @@ var FederationImpl = class extends FederationBuilderImpl {
2639
2804
  _getTracer() {
2640
2805
  return this.tracerProvider.getTracer(name, version);
2641
2806
  }
2807
+ resolveTaskQueue(taskName) {
2808
+ const resolved = this.taskDefinitions.get(taskName)?.queue ?? this.taskQueue;
2809
+ if (resolved != null) return resolved;
2810
+ return this.taskQueueResolution === "strict" ? void 0 : this.outboxQueue;
2811
+ }
2642
2812
  async _startQueueInternal(ctxData, signal, queue) {
2643
- if (this.inboxQueue == null && this.outboxQueue == null) return;
2813
+ const taskQueue = this.taskQueue ?? (this.taskQueueResolution === "fallback" ? this.outboxQueue : void 0);
2814
+ const customQueues = this.taskDefinitions.values().map((def) => ["task", def.queue]);
2815
+ const targets = [
2816
+ ["inbox", this.inboxQueue],
2817
+ ["outbox", this.outboxQueue],
2818
+ ["fanout", this.fanoutQueue],
2819
+ ["task", taskQueue],
2820
+ ...customQueues
2821
+ ];
2644
2822
  const logger = getLogger([
2645
2823
  "fedify",
2646
2824
  "federation",
2647
2825
  "queue"
2648
2826
  ]);
2649
2827
  const promises = [];
2650
- if (this.inboxQueue != null && (queue == null || queue === "inbox") && !this.inboxQueueStarted) {
2651
- logger.debug("Starting an inbox task worker.");
2652
- this.inboxQueueStarted = true;
2653
- promises.push(this.inboxQueue.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
2654
- }
2655
- if (this.outboxQueue != null && this.outboxQueue !== this.inboxQueue && (queue == null || queue === "outbox") && !this.outboxQueueStarted) {
2656
- logger.debug("Starting an outbox task worker.");
2657
- this.outboxQueueStarted = true;
2658
- promises.push(this.outboxQueue.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
2659
- }
2660
- if (this.fanoutQueue != null && this.fanoutQueue !== this.inboxQueue && this.fanoutQueue !== this.outboxQueue && (queue == null || queue === "fanout") && !this.fanoutQueueStarted) {
2661
- logger.debug("Starting a fanout task worker.");
2662
- this.fanoutQueueStarted = true;
2663
- promises.push(this.fanoutQueue.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
2828
+ for (const [role, target] of targets) {
2829
+ if (target == null || !(queue == null || queue === role)) continue;
2830
+ if (this.startedQueues.has(target)) continue;
2831
+ this.startedQueues.add(target);
2832
+ logger.debug("Starting a {role} queue worker.", { role });
2833
+ promises.push(target.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
2664
2834
  }
2665
2835
  await Promise.all(promises);
2666
2836
  }
@@ -2783,7 +2953,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2783
2953
  }
2784
2954
  });
2785
2955
  });
2786
- }
2956
+ } else if (message.type === "task") await this.#listenTaskMessage(contextData, message);
2787
2957
  });
2788
2958
  }
2789
2959
  async #listenFanoutMessage(data, message) {
@@ -3347,6 +3517,77 @@ var FederationImpl = class extends FederationBuilderImpl {
3347
3517
  listenerSpan.end();
3348
3518
  });
3349
3519
  }
3520
+ async #listenTaskMessage(contextData, message) {
3521
+ const logger = getLogger([
3522
+ "fedify",
3523
+ "federation",
3524
+ "task"
3525
+ ]);
3526
+ const def = this.taskDefinitions.get(message.taskName);
3527
+ if (def == null) {
3528
+ logger.warn("Received a custom task {taskName} with no registered handler; dropping.", { taskName: message.taskName });
3529
+ return;
3530
+ }
3531
+ const context = this.#createContext(new URL(message.baseUrl), contextData);
3532
+ let data;
3533
+ try {
3534
+ data = await context.codec.decode(def.schema, message.data);
3535
+ } catch (error) {
3536
+ logger.error("Custom task {taskName} payload could not be decoded or validated; dropping:\n{error}", {
3537
+ taskName: message.taskName,
3538
+ error
3539
+ });
3540
+ return;
3541
+ }
3542
+ try {
3543
+ await def.handler(context, data);
3544
+ } catch (error) {
3545
+ if (def.onError != null) try {
3546
+ await def.onError(context, error, data);
3547
+ } catch (onErrorError) {
3548
+ logger.error("onError for custom task {taskName} threw:\n{error}", {
3549
+ taskName: message.taskName,
3550
+ error: onErrorError
3551
+ });
3552
+ }
3553
+ const queue = this.resolveTaskQueue(def.name);
3554
+ if (queue?.nativeRetrial) throw error;
3555
+ const retryPolicy = def.retryPolicy ?? this.taskRetryPolicy;
3556
+ let elapsedTime = Temporal.Duration.from({ seconds: 0 });
3557
+ try {
3558
+ elapsedTime = Temporal.Instant.from(message.started).until(Temporal.Now.instant());
3559
+ } catch (parseError) {
3560
+ logger.error("Custom task {taskName} has an unparsable started time {started}; treating elapsedTime as zero:\n{error}", {
3561
+ taskName: message.taskName,
3562
+ started: message.started,
3563
+ error: parseError
3564
+ });
3565
+ }
3566
+ const delay = retryPolicy({
3567
+ elapsedTime,
3568
+ attempts: message.attempt
3569
+ });
3570
+ if (delay != null && queue != null) {
3571
+ logger.error("Custom task {taskName} failed (attempt #{attempt}); retry...:\n{error}", {
3572
+ taskName: message.taskName,
3573
+ attempt: message.attempt,
3574
+ error
3575
+ });
3576
+ const retryMessage = {
3577
+ ...message,
3578
+ attempt: message.attempt + 1
3579
+ };
3580
+ await queue.enqueue(retryMessage, {
3581
+ delay: clampNegativeDelay(delay),
3582
+ orderingKey: message.orderingKey
3583
+ });
3584
+ } else logger.error("Custom task {taskName} failed after {attempt} attempts; giving up:\n{error}", {
3585
+ taskName: message.taskName,
3586
+ attempt: message.attempt,
3587
+ error
3588
+ });
3589
+ }
3590
+ }
3350
3591
  startQueue(contextData, options = {}) {
3351
3592
  return this._startQueueInternal(contextData, options.signal, options.queue);
3352
3593
  }
@@ -3931,6 +4172,7 @@ var ContextImpl = class ContextImpl {
3931
4172
  documentLoader;
3932
4173
  contextLoader;
3933
4174
  invokedFromActorKeyPairsDispatcher;
4175
+ #codec;
3934
4176
  constructor({ url, federation, data, documentLoader, contextLoader, invokedFromActorKeyPairsDispatcher }) {
3935
4177
  this.url = url;
3936
4178
  this.federation = federation;
@@ -3939,6 +4181,18 @@ var ContextImpl = class ContextImpl {
3939
4181
  this.contextLoader = contextLoader;
3940
4182
  this.invokedFromActorKeyPairsDispatcher = invokedFromActorKeyPairsDispatcher;
3941
4183
  }
4184
+ /**
4185
+ * A {@link TaskCodec} bound to this context's loaders, used to encode
4186
+ * and decode custom task payloads. Lazily created and cached so a context
4187
+ * that never enqueues or dispatches a task pays nothing.
4188
+ * @internal
4189
+ */
4190
+ get codec() {
4191
+ return this.#codec ??= new TaskCodec(this);
4192
+ }
4193
+ get #enqueueTasks() {
4194
+ return enqueueTasks(this);
4195
+ }
3942
4196
  clone(data) {
3943
4197
  return new ContextImpl({
3944
4198
  url: this.url,
@@ -4257,6 +4511,12 @@ var ContextImpl = class ContextImpl {
4257
4511
  allowPrivateAddress: this.federation.allowPrivateAddress
4258
4512
  });
4259
4513
  }
4514
+ async enqueueTask(task, data, options = {}) {
4515
+ await this.#enqueueTasks(task, [data], options);
4516
+ }
4517
+ async enqueueTaskMany(task, payloads, options = {}) {
4518
+ await this.#enqueueTasks(task, payloads, options);
4519
+ }
4260
4520
  sendActivity(sender, recipients, activity, options = {}) {
4261
4521
  return this.tracerProvider.getTracer(name, version).startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
4262
4522
  kind: this.federation.outboxQueue == null || options.immediate ? SpanKind.CLIENT : SpanKind.PRODUCER,