@fedify/fedify 2.4.0-dev.1488 → 2.4.0-dev.1504
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/dist/{assert-OguE97r2.mjs → assert-DvVHoIZ0.mjs} +1 -1
- package/dist/{assert_instance_of-DBC5X09g.mjs → assert_instance_of-CxoC8bVq.mjs} +1 -1
- package/dist/{assert_not_equals-DkVK8oqV.mjs → assert_not_equals-C8vcTTA3.mjs} +1 -1
- package/dist/{assert_rejects-DN60FHPX.mjs → assert_rejects-DAdaaIWM.mjs} +2 -2
- package/dist/{assert_strict_equals-XEgZAlrj.mjs → assert_strict_equals-BzqyKuYL.mjs} +1 -1
- package/dist/{assert_throws-BOkhLGYc.mjs → assert_throws-zN79lgIC.mjs} +1 -1
- package/dist/{builder-Dcr6y6XD.mjs → builder-Ck0Fl_a5.mjs} +23 -3
- package/dist/codec-CmlPqjNX.mjs +103 -0
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/outgoing-jsonld.test.mjs +3 -3
- package/dist/compat/public-audience.test.mjs +3 -3
- package/dist/compat/transformers.test.mjs +5 -5
- package/dist/{context-DVoTs_wM.mjs → context-BBRTgkrs.mjs} +8 -2
- package/dist/{context-BU6jSQdo.d.ts → context-BF5B4ZDk.d.ts} +529 -259
- package/dist/{context-BBVLF7lx.d.cts → context-CYF8X0ft.d.cts} +530 -259
- package/dist/{deno-BCFGf8NR.mjs → deno-D3gb4X9v.mjs} +1 -1
- package/dist/{docloader-DWEwaGqF.mjs → docloader-DUKu0GzG.mjs} +3 -3
- package/dist/federation/builder.test.mjs +5 -5
- package/dist/federation/circuit-breaker.test.mjs +5 -5
- package/dist/federation/collection.test.mjs +3 -3
- package/dist/federation/handler.test.mjs +13 -13
- package/dist/federation/idempotency.test.mjs +6 -6
- package/dist/federation/inbox.test.mjs +3 -3
- package/dist/federation/keycache.test.mjs +5 -5
- package/dist/federation/kv.test.mjs +3 -3
- package/dist/federation/metrics.test.mjs +4 -4
- package/dist/federation/middleware.test.mjs +200 -19
- package/dist/federation/mod.cjs +3 -259
- package/dist/federation/mod.d.cts +4 -4
- package/dist/federation/mod.d.ts +4 -4
- package/dist/federation/mod.js +1 -257
- package/dist/federation/mq.test.mjs +103 -261
- package/dist/federation/negotiation.test.mjs +4 -4
- package/dist/federation/retry.test.mjs +3 -3
- package/dist/federation/router.test.mjs +5 -5
- package/dist/federation/send.test.mjs +10 -10
- package/dist/federation/tasks/codec.test.d.mts +2 -0
- package/dist/federation/tasks/codec.test.mjs +292 -0
- package/dist/federation/tasks/enqueue.test.d.mts +2 -0
- package/dist/federation/tasks/enqueue.test.mjs +992 -0
- package/dist/federation/tasks/tasks.test.d.mts +2 -0
- package/dist/federation/tasks/tasks.test.mjs +555 -0
- package/dist/federation/temporal.test.mjs +4 -4
- package/dist/federation/webfinger.test.mjs +6 -6
- package/dist/{http-RriziGDE.mjs → http-BNFQkiEZ.mjs} +4 -4
- package/dist/{http-DaVoLyyA.js → http-CrZzUAtV.js} +7 -17
- package/dist/{http-DLqxhQg1.cjs → http-D9p2_d2X.cjs} +7 -17
- package/dist/{key-Bkg8Zg36.mjs → key-BM_SRwBV.mjs} +8 -18
- package/dist/{kv-cache-BR--9V4L.mjs → kv-cache-BE1QIPdL.mjs} +1 -1
- package/dist/{kv-cache-DxlFLMiU.js → kv-cache-Ch_Zv5Yq.js} +1 -1
- package/dist/{kv-cache-Fw-iwKjy.cjs → kv-cache-Cna211RR.cjs} +1 -1
- package/dist/{ld-BjsmbqO7.mjs → ld-ubkf03eb.mjs} +3 -3
- package/dist/{metrics-BLoa2pab.mjs → metrics-BXR36qzu.mjs} +1 -1
- package/dist/{middleware-V-r1QH5Z.cjs → middleware-DUNeIkXN.cjs} +694 -51
- package/dist/{middleware-D3kuDjvb.mjs → middleware-iP9VRMus.mjs} +310 -67
- package/dist/{middleware-_9t5HVBi.js → middleware-lB4vtShP.js} +683 -52
- package/dist/{middleware-DNR3YBCi.mjs → middleware-pzCP79He.mjs} +1 -1
- package/dist/{mod-vPYVoa5n.d.ts → mod-4WGAuR6X.d.ts} +1 -1
- package/dist/{mod-C0F6kvgS.d.cts → mod-BHEB3xhE.d.cts} +1 -1
- package/dist/mod.cjs +6 -6
- package/dist/mod.d.cts +5 -5
- package/dist/mod.d.ts +5 -5
- package/dist/mod.js +5 -5
- package/dist/mq-03vS-C9P.mjs +276 -0
- package/dist/{mq-D8uSFzxe.d.cts → mq-B5_hohs7.d.ts} +34 -0
- package/dist/{mq-D-nlpY04.d.ts → mq-DXMt_qCY.d.cts} +34 -0
- package/dist/nodeinfo/client.test.mjs +3 -3
- package/dist/nodeinfo/handler.test.mjs +5 -5
- package/dist/nodeinfo/types.test.mjs +4 -4
- package/dist/otel/exporter.test.mjs +3 -3
- package/dist/{outgoing-jsonld-L_DbOaFe.mjs → outgoing-jsonld-BNL8AC14.mjs} +1 -1
- package/dist/{owner-NK2p1ihR.mjs → owner-Bf1KkncT.mjs} +2 -2
- package/dist/{proof-D7WVwQzs.js → proof-BMcsi8q_.js} +1 -1
- package/dist/{proof-CRXqo703.mjs → proof-Q781Ks5q.mjs} +5 -5
- package/dist/{proof-2WI6mMbC.cjs → proof-yeAU1MyT.cjs} +1 -1
- package/dist/{send-CTaPzl8L.mjs → send-DFV0S_n9.mjs} +3 -3
- package/dist/sig/accept.test.mjs +1 -1
- package/dist/sig/http.test.mjs +8 -8
- package/dist/sig/key.test.mjs +6 -56
- package/dist/sig/ld.test.mjs +7 -7
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +6 -6
- package/dist/sig/proof.test.mjs +8 -8
- package/dist/{std__assert-BBjXFNOb.mjs → std__assert-Dh6uLf_q.mjs} +4 -4
- package/dist/{temporal-C8M0Y7v9.mjs → temporal-CyOjsqPj.mjs} +1 -1
- package/dist/testing/mod.d.mts +1037 -8
- package/dist/testing/mod.mjs +3 -2
- package/dist/testing-Dy-WhqpN.mjs +77 -0
- package/dist/utils/docloader.test.mjs +6 -6
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +13 -8
- /package/dist/{accept-CPkZzmGN.mjs → accept-CgDcxvjV.mjs} +0 -0
- /package/dist/{activity-listener-tztVvlNb.mjs → activity-listener-BeTGV3wc.mjs} +0 -0
- /package/dist/{assert_equals-C-ZRDbaf.mjs → assert_equals-PBVKNMJd.mjs} +0 -0
- /package/dist/{circuit-breaker-CSWsyoef.mjs → circuit-breaker-hJBB6jwA.mjs} +0 -0
- /package/dist/{client-ByXmQhYD.mjs → client-B_A6mfn3.mjs} +0 -0
- /package/dist/{collection-Cc3DVAhE.mjs → collection-Dp5ky45w.mjs} +0 -0
- /package/dist/{keycache-BeU0LCII.mjs → keycache-CaOR6NYg.mjs} +0 -0
- /package/dist/{keys-DGu1NFwu.mjs → keys-CSYsOMFG.mjs} +0 -0
- /package/dist/{kv-rV3vodCc.mjs → kv-CIvR3Non.mjs} +0 -0
- /package/dist/{negotiation-DDstyBvc.mjs → negotiation-m_L2nyS3.mjs} +0 -0
- /package/dist/{public-audience-Cvbr2Gzt.mjs → public-audience-c9zmYKgA.mjs} +0 -0
- /package/dist/{retry-CXg_MBI-.mjs → retry-DI4O-zYl.mjs} +0 -0
- /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-
|
|
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-
|
|
6
|
-
import { t as formatAcceptSignature } from "./accept-
|
|
7
|
-
import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-
|
|
8
|
-
import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-
|
|
9
|
-
import { t as getAuthenticatedDocumentLoader } from "./docloader-
|
|
10
|
-
import { n as kvCache } from "./kv-cache-
|
|
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-
|
|
12
|
-
import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-
|
|
13
|
-
import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-
|
|
14
|
-
import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-
|
|
15
|
-
import { t as getNodeInfo } from "./client-
|
|
16
|
-
import { t as nodeInfoToJson } from "./types-
|
|
17
|
-
import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-
|
|
18
|
-
import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-
|
|
19
|
-
import { t as CircuitBreaker } from "./circuit-breaker-
|
|
20
|
-
import { t as buildCollectionSynchronizationHeader } from "./collection-
|
|
21
|
-
import { t as KvKeyCache } from "./keycache-
|
|
22
|
-
import { t as acceptsJsonLd } from "./negotiation-
|
|
23
|
-
import { t as hasMalformedKnownTemporalLiteral } from "./temporal-
|
|
24
|
-
import { t as createExponentialBackoffPolicy } from "./retry-
|
|
4
|
+
import { n as version, t as name } from "./deno-D3gb4X9v.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-BXR36qzu.mjs";
|
|
6
|
+
import { t as formatAcceptSignature } from "./accept-CgDcxvjV.mjs";
|
|
7
|
+
import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-BM_SRwBV.mjs";
|
|
8
|
+
import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-BNFQkiEZ.mjs";
|
|
9
|
+
import { t as getAuthenticatedDocumentLoader } from "./docloader-DUKu0GzG.mjs";
|
|
10
|
+
import { n as kvCache } from "./kv-cache-BE1QIPdL.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-ubkf03eb.mjs";
|
|
12
|
+
import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-Bf1KkncT.mjs";
|
|
13
|
+
import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-BNL8AC14.mjs";
|
|
14
|
+
import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-Q781Ks5q.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-DFV0S_n9.mjs";
|
|
18
|
+
import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-Ck0Fl_a5.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-CyOjsqPj.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$
|
|
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$
|
|
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",
|
|
@@ -2392,15 +2548,6 @@ function isPermanentRemoteContextError(error) {
|
|
|
2392
2548
|
function isPermanentInboxParseError(error) {
|
|
2393
2549
|
return error instanceof Error && (error.name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError(error) || error.name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure(error)) || error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
|
|
2394
2550
|
}
|
|
2395
|
-
function hasLinkedDataSignatureJsonLdProcessingError(error) {
|
|
2396
|
-
if (error.message.startsWith("Maximum deep iterations exceeded")) return true;
|
|
2397
|
-
const cause = error.cause instanceof Error ? error.cause : error.details?.cause;
|
|
2398
|
-
if (error.name === "jsonld.InvalidUrl" && error.details?.code === "loading remote context failed" && cause instanceof Error) return cause.message.startsWith("Maximum deep iterations exceeded") || cause.name.startsWith("jsonld.");
|
|
2399
|
-
return error.name !== "jsonld.InvalidUrl" && error.name.startsWith("jsonld.");
|
|
2400
|
-
}
|
|
2401
|
-
function isLinkedDataSignatureJsonLdProcessingError(error) {
|
|
2402
|
-
return error instanceof Error && hasLinkedDataSignatureJsonLdProcessingError(error);
|
|
2403
|
-
}
|
|
2404
2551
|
/**
|
|
2405
2552
|
* Create a new {@link Federation} instance.
|
|
2406
2553
|
* @param options Parameters for initializing the instance.
|
|
@@ -2418,9 +2565,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2418
2565
|
inboxQueue;
|
|
2419
2566
|
outboxQueue;
|
|
2420
2567
|
fanoutQueue;
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
fanoutQueueStarted;
|
|
2568
|
+
taskQueue;
|
|
2569
|
+
startedQueues;
|
|
2424
2570
|
manuallyStartQueue;
|
|
2425
2571
|
origin;
|
|
2426
2572
|
documentLoaderFactory;
|
|
@@ -2434,6 +2580,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2434
2580
|
skipSignatureVerification;
|
|
2435
2581
|
outboxRetryPolicy;
|
|
2436
2582
|
inboxRetryPolicy;
|
|
2583
|
+
taskRetryPolicy;
|
|
2584
|
+
taskQueueResolution;
|
|
2585
|
+
taskDeduplicationTtl;
|
|
2586
|
+
taskDeduplicationFallback;
|
|
2437
2587
|
circuitBreaker;
|
|
2438
2588
|
activityTransformers;
|
|
2439
2589
|
_tracerProvider;
|
|
@@ -2473,20 +2623,24 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2473
2623
|
httpMessageSignaturesSpec: ["_fedify", "httpMessageSignaturesSpec"],
|
|
2474
2624
|
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
|
|
2475
2625
|
circuitBreaker: ["_fedify", "circuit"],
|
|
2626
|
+
taskDeduplication: ["_fedify", "taskDeduplication"],
|
|
2476
2627
|
...options.kvPrefixes ?? {}
|
|
2477
2628
|
};
|
|
2478
2629
|
if (options.queue == null) {
|
|
2479
2630
|
this.inboxQueue = void 0;
|
|
2480
2631
|
this.outboxQueue = void 0;
|
|
2481
2632
|
this.fanoutQueue = void 0;
|
|
2633
|
+
this.taskQueue = void 0;
|
|
2482
2634
|
} else if ("enqueue" in options.queue && "listen" in options.queue) {
|
|
2483
2635
|
this.inboxQueue = options.queue;
|
|
2484
2636
|
this.outboxQueue = options.queue;
|
|
2485
2637
|
this.fanoutQueue = options.queue;
|
|
2638
|
+
this.taskQueue = void 0;
|
|
2486
2639
|
} else {
|
|
2487
2640
|
this.inboxQueue = options.queue.inbox;
|
|
2488
2641
|
this.outboxQueue = options.queue.outbox;
|
|
2489
2642
|
this.fanoutQueue = options.queue.fanout;
|
|
2643
|
+
this.taskQueue = options.queue.task;
|
|
2490
2644
|
}
|
|
2491
2645
|
if (options.circuitBreaker !== false && this.outboxQueue != null) {
|
|
2492
2646
|
this.circuitBreaker = new CircuitBreaker({
|
|
@@ -2507,9 +2661,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2507
2661
|
]).warn("The configured key-value store does not support CAS; outbound delivery circuit breaker updates may race under concurrent workers.");
|
|
2508
2662
|
}
|
|
2509
2663
|
}
|
|
2510
|
-
this.
|
|
2511
|
-
this.outboxQueueStarted = false;
|
|
2512
|
-
this.fanoutQueueStarted = false;
|
|
2664
|
+
this.startedQueues = /* @__PURE__ */ new Set();
|
|
2513
2665
|
this.manuallyStartQueue = options.manuallyStartQueue ?? false;
|
|
2514
2666
|
if (options.origin != null) if (typeof options.origin === "string") {
|
|
2515
2667
|
if (!URL.canParse(options.origin) || !options.origin.match(/^https?:\/\//)) throw new TypeError(`Invalid origin: ${JSON.stringify(options.origin)}`);
|
|
@@ -2591,6 +2743,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2591
2743
|
this.inboxChallengePolicy = options.inboxChallengePolicy;
|
|
2592
2744
|
this.outboxRetryPolicy = options.outboxRetryPolicy ?? createExponentialBackoffPolicy();
|
|
2593
2745
|
this.inboxRetryPolicy = options.inboxRetryPolicy ?? createExponentialBackoffPolicy();
|
|
2746
|
+
this.taskRetryPolicy = options.taskRetryPolicy ?? createExponentialBackoffPolicy();
|
|
2747
|
+
this.taskQueueResolution = options.taskQueueResolution ?? "fallback";
|
|
2748
|
+
this.taskDeduplicationTtl = Temporal.Duration.from(options.taskDeduplicationTtl ?? { hours: 1 });
|
|
2749
|
+
this.taskDeduplicationFallback = options.taskDeduplicationFallback ?? "open";
|
|
2594
2750
|
this.activityTransformers = options.activityTransformers ?? getDefaultActivityTransformers();
|
|
2595
2751
|
this._tracerProvider = options.tracerProvider;
|
|
2596
2752
|
if (benchmarkMode) {
|
|
@@ -2639,28 +2795,33 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2639
2795
|
_getTracer() {
|
|
2640
2796
|
return this.tracerProvider.getTracer(name, version);
|
|
2641
2797
|
}
|
|
2798
|
+
resolveTaskQueue(taskName) {
|
|
2799
|
+
const resolved = this.taskDefinitions.get(taskName)?.queue ?? this.taskQueue;
|
|
2800
|
+
if (resolved != null) return resolved;
|
|
2801
|
+
return this.taskQueueResolution === "strict" ? void 0 : this.outboxQueue;
|
|
2802
|
+
}
|
|
2642
2803
|
async _startQueueInternal(ctxData, signal, queue) {
|
|
2643
|
-
|
|
2804
|
+
const taskQueue = this.taskQueue ?? (this.taskQueueResolution === "fallback" ? this.outboxQueue : void 0);
|
|
2805
|
+
const customQueues = this.taskDefinitions.values().map((def) => ["task", def.queue]);
|
|
2806
|
+
const targets = [
|
|
2807
|
+
["inbox", this.inboxQueue],
|
|
2808
|
+
["outbox", this.outboxQueue],
|
|
2809
|
+
["fanout", this.fanoutQueue],
|
|
2810
|
+
["task", taskQueue],
|
|
2811
|
+
...customQueues
|
|
2812
|
+
];
|
|
2644
2813
|
const logger = getLogger([
|
|
2645
2814
|
"fedify",
|
|
2646
2815
|
"federation",
|
|
2647
2816
|
"queue"
|
|
2648
2817
|
]);
|
|
2649
2818
|
const promises = [];
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
this.
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
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 }));
|
|
2819
|
+
for (const [role, target] of targets) {
|
|
2820
|
+
if (target == null || !(queue == null || queue === role)) continue;
|
|
2821
|
+
if (this.startedQueues.has(target)) continue;
|
|
2822
|
+
this.startedQueues.add(target);
|
|
2823
|
+
logger.debug("Starting a {role} queue worker.", { role });
|
|
2824
|
+
promises.push(target.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
|
|
2664
2825
|
}
|
|
2665
2826
|
await Promise.all(promises);
|
|
2666
2827
|
}
|
|
@@ -2783,7 +2944,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2783
2944
|
}
|
|
2784
2945
|
});
|
|
2785
2946
|
});
|
|
2786
|
-
}
|
|
2947
|
+
} else if (message.type === "task") await this.#listenTaskMessage(contextData, message);
|
|
2787
2948
|
});
|
|
2788
2949
|
}
|
|
2789
2950
|
async #listenFanoutMessage(data, message) {
|
|
@@ -3347,6 +3508,77 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3347
3508
|
listenerSpan.end();
|
|
3348
3509
|
});
|
|
3349
3510
|
}
|
|
3511
|
+
async #listenTaskMessage(contextData, message) {
|
|
3512
|
+
const logger = getLogger([
|
|
3513
|
+
"fedify",
|
|
3514
|
+
"federation",
|
|
3515
|
+
"task"
|
|
3516
|
+
]);
|
|
3517
|
+
const def = this.taskDefinitions.get(message.taskName);
|
|
3518
|
+
if (def == null) {
|
|
3519
|
+
logger.warn("Received a custom task {taskName} with no registered handler; dropping.", { taskName: message.taskName });
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
const context = this.#createContext(new URL(message.baseUrl), contextData);
|
|
3523
|
+
let data;
|
|
3524
|
+
try {
|
|
3525
|
+
data = await context.codec.decode(def.schema, message.data);
|
|
3526
|
+
} catch (error) {
|
|
3527
|
+
logger.error("Custom task {taskName} payload could not be decoded or validated; dropping:\n{error}", {
|
|
3528
|
+
taskName: message.taskName,
|
|
3529
|
+
error
|
|
3530
|
+
});
|
|
3531
|
+
return;
|
|
3532
|
+
}
|
|
3533
|
+
try {
|
|
3534
|
+
await def.handler(context, data);
|
|
3535
|
+
} catch (error) {
|
|
3536
|
+
if (def.onError != null) try {
|
|
3537
|
+
await def.onError(context, error, data);
|
|
3538
|
+
} catch (onErrorError) {
|
|
3539
|
+
logger.error("onError for custom task {taskName} threw:\n{error}", {
|
|
3540
|
+
taskName: message.taskName,
|
|
3541
|
+
error: onErrorError
|
|
3542
|
+
});
|
|
3543
|
+
}
|
|
3544
|
+
const queue = this.resolveTaskQueue(def.name);
|
|
3545
|
+
if (queue?.nativeRetrial) throw error;
|
|
3546
|
+
const retryPolicy = def.retryPolicy ?? this.taskRetryPolicy;
|
|
3547
|
+
let elapsedTime = Temporal.Duration.from({ seconds: 0 });
|
|
3548
|
+
try {
|
|
3549
|
+
elapsedTime = Temporal.Instant.from(message.started).until(Temporal.Now.instant());
|
|
3550
|
+
} catch (parseError) {
|
|
3551
|
+
logger.error("Custom task {taskName} has an unparsable started time {started}; treating elapsedTime as zero:\n{error}", {
|
|
3552
|
+
taskName: message.taskName,
|
|
3553
|
+
started: message.started,
|
|
3554
|
+
error: parseError
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
const delay = retryPolicy({
|
|
3558
|
+
elapsedTime,
|
|
3559
|
+
attempts: message.attempt
|
|
3560
|
+
});
|
|
3561
|
+
if (delay != null && queue != null) {
|
|
3562
|
+
logger.error("Custom task {taskName} failed (attempt #{attempt}); retry...:\n{error}", {
|
|
3563
|
+
taskName: message.taskName,
|
|
3564
|
+
attempt: message.attempt,
|
|
3565
|
+
error
|
|
3566
|
+
});
|
|
3567
|
+
const retryMessage = {
|
|
3568
|
+
...message,
|
|
3569
|
+
attempt: message.attempt + 1
|
|
3570
|
+
};
|
|
3571
|
+
await queue.enqueue(retryMessage, {
|
|
3572
|
+
delay: clampNegativeDelay(delay),
|
|
3573
|
+
orderingKey: message.orderingKey
|
|
3574
|
+
});
|
|
3575
|
+
} else logger.error("Custom task {taskName} failed after {attempt} attempts; giving up:\n{error}", {
|
|
3576
|
+
taskName: message.taskName,
|
|
3577
|
+
attempt: message.attempt,
|
|
3578
|
+
error
|
|
3579
|
+
});
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3350
3582
|
startQueue(contextData, options = {}) {
|
|
3351
3583
|
return this._startQueueInternal(contextData, options.signal, options.queue);
|
|
3352
3584
|
}
|
|
@@ -3435,18 +3667,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3435
3667
|
privateKey: pair.privateKey
|
|
3436
3668
|
}))
|
|
3437
3669
|
});
|
|
3438
|
-
else
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
});
|
|
3443
|
-
} catch (error) {
|
|
3444
|
-
if (!isLinkedDataSignatureJsonLdProcessingError(error)) throw error;
|
|
3445
|
-
logger.warn("Failed to create a Linked Data signature for the activity {activityId}. The activity will be sent without a Linked Data signature.", {
|
|
3446
|
-
activityId,
|
|
3447
|
-
error
|
|
3448
|
-
});
|
|
3449
|
-
}
|
|
3670
|
+
else jsonLd = await signJsonLd(jsonLd, rsaKey.privateKey, rsaKey.keyId, {
|
|
3671
|
+
contextLoader,
|
|
3672
|
+
tracerProvider: this.tracerProvider
|
|
3673
|
+
});
|
|
3450
3674
|
if (!hasProof) logger.warn("No supported key found to create a proof for the activity {activityId}. The activity will be sent without a proof. In order to create a proof, at least one Ed25519 key must be provided.", {
|
|
3451
3675
|
activityId,
|
|
3452
3676
|
keys: keys.map((pair) => ({
|
|
@@ -3931,6 +4155,7 @@ var ContextImpl = class ContextImpl {
|
|
|
3931
4155
|
documentLoader;
|
|
3932
4156
|
contextLoader;
|
|
3933
4157
|
invokedFromActorKeyPairsDispatcher;
|
|
4158
|
+
#codec;
|
|
3934
4159
|
constructor({ url, federation, data, documentLoader, contextLoader, invokedFromActorKeyPairsDispatcher }) {
|
|
3935
4160
|
this.url = url;
|
|
3936
4161
|
this.federation = federation;
|
|
@@ -3939,6 +4164,18 @@ var ContextImpl = class ContextImpl {
|
|
|
3939
4164
|
this.contextLoader = contextLoader;
|
|
3940
4165
|
this.invokedFromActorKeyPairsDispatcher = invokedFromActorKeyPairsDispatcher;
|
|
3941
4166
|
}
|
|
4167
|
+
/**
|
|
4168
|
+
* A {@link TaskCodec} bound to this context's loaders, used to encode
|
|
4169
|
+
* and decode custom task payloads. Lazily created and cached so a context
|
|
4170
|
+
* that never enqueues or dispatches a task pays nothing.
|
|
4171
|
+
* @internal
|
|
4172
|
+
*/
|
|
4173
|
+
get codec() {
|
|
4174
|
+
return this.#codec ??= new TaskCodec(this);
|
|
4175
|
+
}
|
|
4176
|
+
get #enqueueTasks() {
|
|
4177
|
+
return enqueueTasks(this);
|
|
4178
|
+
}
|
|
3942
4179
|
clone(data) {
|
|
3943
4180
|
return new ContextImpl({
|
|
3944
4181
|
url: this.url,
|
|
@@ -4257,6 +4494,12 @@ var ContextImpl = class ContextImpl {
|
|
|
4257
4494
|
allowPrivateAddress: this.federation.allowPrivateAddress
|
|
4258
4495
|
});
|
|
4259
4496
|
}
|
|
4497
|
+
async enqueueTask(task, data, options = {}) {
|
|
4498
|
+
await this.#enqueueTasks(task, [data], options);
|
|
4499
|
+
}
|
|
4500
|
+
async enqueueTaskMany(task, payloads, options = {}) {
|
|
4501
|
+
await this.#enqueueTasks(task, payloads, options);
|
|
4502
|
+
}
|
|
4260
4503
|
sendActivity(sender, recipients, activity, options = {}) {
|
|
4261
4504
|
return this.tracerProvider.getTracer(name, version).startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
|
|
4262
4505
|
kind: this.federation.outboxQueue == null || options.immediate ? SpanKind.CLIENT : SpanKind.PRODUCER,
|