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