@fedify/fedify 0.12.0-dev.263 → 0.12.0-dev.266

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGES.md +35 -0
  2. package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/concat.js +1 -1
  3. package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/copy.js +2 -2
  4. package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/ends_with.js +1 -1
  5. package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/equals.js +1 -1
  6. package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/includes_needle.js +2 -2
  7. package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/index_of_needle.js +2 -2
  8. package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/last_index_of_needle.js +2 -2
  9. package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/mod.js +1 -1
  10. package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/repeat.js +2 -2
  11. package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/starts_with.js +1 -1
  12. package/esm/federation/handler.js +41 -21
  13. package/esm/federation/inbox.js +34 -0
  14. package/esm/federation/middleware.js +151 -34
  15. package/esm/federation/mod.js +1 -0
  16. package/esm/federation/mq.js +1 -1
  17. package/esm/federation/retry.js +34 -0
  18. package/esm/runtime/key.js +1 -1
  19. package/esm/sig/http.js +1 -1
  20. package/package.json +1 -1
  21. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/concat.d.ts +1 -1
  22. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/concat.d.ts.map +1 -1
  23. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/copy.d.ts +2 -2
  24. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/copy.d.ts.map +1 -1
  25. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/ends_with.d.ts +1 -1
  26. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/ends_with.d.ts.map +1 -1
  27. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/equals.d.ts +1 -1
  28. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/equals.d.ts.map +1 -1
  29. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/includes_needle.d.ts +2 -2
  30. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/includes_needle.d.ts.map +1 -1
  31. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/index_of_needle.d.ts +2 -2
  32. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/index_of_needle.d.ts.map +1 -1
  33. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/last_index_of_needle.d.ts +2 -2
  34. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/last_index_of_needle.d.ts.map +1 -1
  35. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/mod.d.ts +1 -1
  36. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/mod.d.ts.map +1 -1
  37. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/repeat.d.ts +2 -2
  38. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/repeat.d.ts.map +1 -1
  39. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/starts_with.d.ts +1 -1
  40. package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/starts_with.d.ts.map +1 -1
  41. package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/build_message.d.ts.map +1 -1
  42. package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/diff.d.ts.map +1 -1
  43. package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/diff_str.d.ts.map +1 -1
  44. package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/format.d.ts.map +1 -1
  45. package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/mod.d.ts.map +1 -1
  46. package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/styles.d.ts.map +1 -1
  47. package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/types.d.ts.map +1 -1
  48. package/types/federation/callback.d.ts +4 -4
  49. package/types/federation/callback.d.ts.map +1 -1
  50. package/types/federation/handler.d.ts +7 -4
  51. package/types/federation/handler.d.ts.map +1 -1
  52. package/types/federation/inbox.d.ts +9 -0
  53. package/types/federation/inbox.d.ts.map +1 -0
  54. package/types/federation/inbox.test.d.ts.map +1 -0
  55. package/types/federation/middleware.d.ts +21 -5
  56. package/types/federation/middleware.d.ts.map +1 -1
  57. package/types/federation/mod.d.ts +1 -0
  58. package/types/federation/mod.d.ts.map +1 -1
  59. package/types/federation/mq.d.ts +2 -0
  60. package/types/federation/mq.d.ts.map +1 -1
  61. package/types/federation/queue.d.ts +11 -1
  62. package/types/federation/queue.d.ts.map +1 -1
  63. package/types/federation/retry.d.ts +64 -0
  64. package/types/federation/retry.d.ts.map +1 -0
  65. package/types/federation/retry.test.d.ts.map +1 -0
package/CHANGES.md CHANGED
@@ -8,6 +8,29 @@ Version 0.12.0
8
8
 
9
9
  To be released.
10
10
 
11
+ - Incoming activities are now queued before being dispatched to the inbox
12
+ listener if the `queue` option is provided to the `createFederation()`
13
+ function. [[#70]]
14
+
15
+ - The type of `InboxListener` callback type's first parameter became
16
+ `Context` (was `RequestContext`).
17
+ - The type of `InboxErrorHandler` callback type's first parameter became
18
+ `Context` (was `RequestContext`).
19
+ - The type of `SharedInboxKeyDispatcher` callback type's first parameter
20
+ became `Context` (was `RequestContext`).
21
+
22
+ - Implemented fully customizable retry policy for failed tasks in the task
23
+ queue. By default, the task queue retries the failed tasks with
24
+ an exponential backoff policy with decorrelated jitter.
25
+
26
+ - Added `outboxRetryPolicy` option to `CreateFederationOptions` interface.
27
+ - Added `inboxRetryPolicy` option to `CreateFederationOptions` interface.
28
+ [[#70]]
29
+ - Added `RetryPolicy` callback type.
30
+ - Added `RetryContext` interface.
31
+ - Added `createExponentialBackoffPolicy()` function.
32
+ - Added `CreateExponentialBackoffPolicyOptions` interface.
33
+
11
34
  - Added `ChatMessage` class to Activity Vocabulary API. [[#85]]
12
35
 
13
36
  - Improved multitenancy (virtual hosting) support. [[#66]]
@@ -18,7 +41,19 @@ To be released.
18
41
  - The type of `ActorKeyPairsDispatcher<TContextData>`'s first parameter
19
42
  became `Context` (was `TContextData`).
20
43
 
44
+ - Deprecated `Federation.sendActivity()` method. Use `Context.sendActivity()`
45
+ method instead.
46
+
47
+ - The last parameter of `Federation.sendActivity()` method is no longer
48
+ optional. Also, it now takes the required `contextData` option.
49
+
50
+ - Added more log messages using the [LogTape] library. Currently the below
51
+ logger categories are used:
52
+
53
+ - `["fedify", "federation", "queue"]`
54
+
21
55
  [#66]: https://github.com/dahlia/fedify/issues/66
56
+ [#70]: https://github.com/dahlia/fedify/issues/70
22
57
  [#85]: https://github.com/dahlia/fedify/issues/85
23
58
 
24
59
 
@@ -9,7 +9,7 @@
9
9
  * @example Basic usage
10
10
  * ```ts
11
11
  * import { concat } from "@std/bytes/concat";
12
- * import { assertEquals } from "@std/assert/assert-equals";
12
+ * import { assertEquals } from "@std/assert";
13
13
  *
14
14
  * const a = new Uint8Array([0, 1, 2]);
15
15
  * const b = new Uint8Array([3, 4, 5]);
@@ -16,7 +16,7 @@
16
16
  * @example Basic usage
17
17
  * ```ts
18
18
  * import { copy } from "@std/bytes/copy";
19
- * import { assertEquals } from "@std/assert/assert-equals";
19
+ * import { assertEquals } from "@std/assert";
20
20
  *
21
21
  * const src = new Uint8Array([9, 8, 7]);
22
22
  * const dst = new Uint8Array([0, 1, 2, 3, 4, 5]);
@@ -28,7 +28,7 @@
28
28
  * @example Copy with offset
29
29
  * ```ts
30
30
  * import { copy } from "@std/bytes/copy";
31
- * import { assertEquals } from "@std/assert/assert-equals";
31
+ * import { assertEquals } from "@std/assert";
32
32
  *
33
33
  * const src = new Uint8Array([1, 1, 1, 1]);
34
34
  * const dst = new Uint8Array([0, 0, 0, 0]);
@@ -14,7 +14,7 @@
14
14
  * @example Basic usage
15
15
  * ```ts
16
16
  * import { endsWith } from "@std/bytes/ends-with";
17
- * import { assertEquals } from "@std/assert/assert-equals";
17
+ * import { assertEquals } from "@std/assert";
18
18
  *
19
19
  * const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
20
20
  * const suffix = new Uint8Array([1, 2, 3]);
@@ -61,7 +61,7 @@ const THRESHOLD_32_BIT = 160;
61
61
  * @example Basic usage
62
62
  * ```ts
63
63
  * import { equals } from "@std/bytes/equals";
64
- * import { assertEquals } from "@std/assert/assert-equals";
64
+ * import { assertEquals } from "@std/assert";
65
65
  *
66
66
  * const a = new Uint8Array([1, 2, 3]);
67
67
  * const b = new Uint8Array([1, 2, 3]);
@@ -16,7 +16,7 @@ import { indexOfNeedle } from "./index_of_needle.js";
16
16
  * @example Basic usage
17
17
  * ```ts
18
18
  * import { includesNeedle } from "@std/bytes/includes-needle";
19
- * import { assertEquals } from "@std/assert/assert-equals";
19
+ * import { assertEquals } from "@std/assert";
20
20
  *
21
21
  * const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
22
22
  * const needle = new Uint8Array([1, 2]);
@@ -27,7 +27,7 @@ import { indexOfNeedle } from "./index_of_needle.js";
27
27
  * @example Start index
28
28
  * ```ts
29
29
  * import { includesNeedle } from "@std/bytes/includes-needle";
30
- * import { assertEquals } from "@std/assert/assert-equals";
30
+ * import { assertEquals } from "@std/assert";
31
31
  *
32
32
  * const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
33
33
  * const needle = new Uint8Array([1, 2]);
@@ -19,7 +19,7 @@
19
19
  * @example Basic usage
20
20
  * ```ts
21
21
  * import { indexOfNeedle } from "@std/bytes/index-of-needle";
22
- * import { assertEquals } from "@std/assert/assert-equals";
22
+ * import { assertEquals } from "@std/assert";
23
23
  *
24
24
  * const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
25
25
  * const needle = new Uint8Array([1, 2]);
@@ -32,7 +32,7 @@
32
32
  * @example Start index
33
33
  * ```ts
34
34
  * import { indexOfNeedle } from "@std/bytes/index-of-needle";
35
- * import { assertEquals } from "@std/assert/assert-equals";
35
+ * import { assertEquals } from "@std/assert";
36
36
  *
37
37
  * const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
38
38
  * const needle = new Uint8Array([1, 2]);
@@ -16,7 +16,7 @@
16
16
  * @example Basic usage
17
17
  * ```ts
18
18
  * import { lastIndexOfNeedle } from "@std/bytes/last-index-of-needle";
19
- * import { assertEquals } from "@std/assert/assert-equals";
19
+ * import { assertEquals } from "@std/assert";
20
20
  *
21
21
  * const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
22
22
  * const needle = new Uint8Array([1, 2]);
@@ -29,7 +29,7 @@
29
29
  * @example Start index
30
30
  * ```ts
31
31
  * import { lastIndexOfNeedle } from "@std/bytes/last-index-of-needle";
32
- * import { assertEquals } from "@std/assert/assert-equals";
32
+ * import { assertEquals } from "@std/assert";
33
33
  *
34
34
  * const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
35
35
  * const needle = new Uint8Array([1, 2]);
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * ```ts
9
9
  * import { concat, indexOfNeedle, endsWith } from "@std/bytes";
10
- * import { assertEquals } from "@std/assert/assert-equals";
10
+ * import { assertEquals } from "@std/assert";
11
11
  *
12
12
  * const a = new Uint8Array([0, 1, 2]);
13
13
  * const b = new Uint8Array([3, 4, 5]);
@@ -13,7 +13,7 @@ import { copy } from "./copy.js";
13
13
  * @example Basic usage
14
14
  * ```ts
15
15
  * import { repeat } from "@std/bytes/repeat";
16
- * import { assertEquals } from "@std/assert/assert-equals";
16
+ * import { assertEquals } from "@std/assert";
17
17
  *
18
18
  * const source = new Uint8Array([0, 1, 2]);
19
19
  *
@@ -23,7 +23,7 @@ import { copy } from "./copy.js";
23
23
  * @example Zero count
24
24
  * ```ts
25
25
  * import { repeat } from "@std/bytes/repeat";
26
- * import { assertEquals } from "@std/assert/assert-equals";
26
+ * import { assertEquals } from "@std/assert";
27
27
  *
28
28
  * const source = new Uint8Array([0, 1, 2]);
29
29
  *
@@ -14,7 +14,7 @@
14
14
  * @example Basic usage
15
15
  * ```ts
16
16
  * import { startsWith } from "@std/bytes/starts-with";
17
- * import { assertEquals } from "@std/assert/assert-equals";
17
+ * import { assertEquals } from "@std/assert";
18
18
  *
19
19
  * const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
20
20
  * const prefix = new Uint8Array([0, 1, 2]);
@@ -166,7 +166,7 @@ function filterCollectionItems(items, collectionName, filterPredicate) {
166
166
  }
167
167
  return result;
168
168
  }
169
- export async function handleInbox(request, { handle, context, kv, kvPrefix, actorDispatcher, inboxListeners, inboxErrorHandler, onNotFound, signatureTimeWindow, }) {
169
+ export async function handleInbox(request, { handle, context, kv, kvPrefix, queue, actorDispatcher, inboxListeners, inboxErrorHandler, onNotFound, signatureTimeWindow, }) {
170
170
  const logger = getLogger(["fedify", "federation", "inbox"]);
171
171
  if (actorDispatcher == null) {
172
172
  logger.error("Actor dispatcher is not set.", { handle });
@@ -185,7 +185,12 @@ export async function handleInbox(request, { handle, context, kv, kvPrefix, acto
185
185
  }
186
186
  catch (error) {
187
187
  logger.error("Failed to parse JSON:\n{error}", { handle, error });
188
- await inboxErrorHandler?.(context, error);
188
+ try {
189
+ await inboxErrorHandler?.(context, error);
190
+ }
191
+ catch (error) {
192
+ logger.error("An unexpected error occurred in inbox error handler:\n{error}", { error, activity: json });
193
+ }
189
194
  return new Response("Invalid JSON.", {
190
195
  status: 400,
191
196
  headers: { "Content-Type": "text/plain; charset=utf-8" },
@@ -197,7 +202,12 @@ export async function handleInbox(request, { handle, context, kv, kvPrefix, acto
197
202
  }
198
203
  catch (error) {
199
204
  logger.error("Failed to parse activity:\n{error}", { handle, json, error });
200
- await inboxErrorHandler?.(context, error);
205
+ try {
206
+ await inboxErrorHandler?.(context, error);
207
+ }
208
+ catch (error) {
209
+ logger.error("An unexpected error occurred in inbox error handler:\n{error}", { error, activity: json });
210
+ }
201
211
  return new Response("Invalid activity.", {
202
212
  status: 400,
203
213
  headers: { "Content-Type": "text/plain; charset=utf-8" },
@@ -256,29 +266,39 @@ export async function handleInbox(request, { handle, context, kv, kvPrefix, acto
256
266
  });
257
267
  return response;
258
268
  }
259
- // deno-lint-ignore no-explicit-any
260
- let cls = activity
261
- // deno-lint-ignore no-explicit-any
262
- .constructor;
263
- while (true) {
264
- if (inboxListeners.has(cls))
265
- break;
266
- if (cls === Activity) {
267
- logger.error("Unsupported activity type:\n{activity}", { activity: json });
268
- return new Response("", {
269
- status: 202,
270
- headers: { "Content-Type": "text/plain; charset=utf-8" },
271
- });
272
- }
273
- cls = globalThis.Object.getPrototypeOf(cls);
269
+ if (queue != null) {
270
+ await queue.enqueue({
271
+ type: "inbox",
272
+ baseUrl: request.url,
273
+ activity: json,
274
+ handle,
275
+ attempt: 0,
276
+ started: new Date().toISOString(),
277
+ });
278
+ return new Response("Activity is enqueued.", {
279
+ status: 202,
280
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
281
+ });
282
+ }
283
+ const listener = inboxListeners?.dispatch(activity);
284
+ if (listener == null) {
285
+ logger.error("Unsupported activity type:\n{activity}", { activity: json });
286
+ return new Response("", {
287
+ status: 202,
288
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
289
+ });
274
290
  }
275
- const listener = inboxListeners.get(cls);
276
291
  try {
277
292
  await listener(context, activity);
278
293
  }
279
294
  catch (error) {
280
- logger.error("Failed to process the activity:\n{error}", { error, activity: json });
281
- await inboxErrorHandler?.(context, error);
295
+ try {
296
+ await inboxErrorHandler?.(context, error);
297
+ }
298
+ catch (error) {
299
+ logger.error("An unexpected error occurred in inbox error handler:\n{error}", { error, activityId: activity.id?.href, activity: json });
300
+ }
301
+ logger.error("Failed to process the incoming activity {activityId}:\n{error}", { error, activityId: activity.id?.href, activity: json });
282
302
  return new Response("Internal server error.", {
283
303
  status: 500,
284
304
  headers: { "Content-Type": "text/plain; charset=utf-8" },
@@ -0,0 +1,34 @@
1
+ import { Activity } from "../vocab/vocab.js";
2
+ export class InboxListenerSet {
3
+ #listeners;
4
+ constructor() {
5
+ this.#listeners = new Map();
6
+ }
7
+ add(
8
+ // deno-lint-ignore no-explicit-any
9
+ type, listener) {
10
+ if (this.#listeners.has(type)) {
11
+ throw new TypeError("Listener already set for this type.");
12
+ }
13
+ this.#listeners.set(type, listener);
14
+ }
15
+ dispatch(activity) {
16
+ // deno-lint-ignore no-explicit-any
17
+ let cls = activity
18
+ // deno-lint-ignore no-explicit-any
19
+ .constructor;
20
+ const inboxListeners = this.#listeners;
21
+ if (inboxListeners == null) {
22
+ return null;
23
+ }
24
+ while (true) {
25
+ if (inboxListeners.has(cls))
26
+ break;
27
+ if (cls === Activity)
28
+ return null;
29
+ cls = globalThis.Object.getPrototypeOf(cls);
30
+ }
31
+ const listener = inboxListeners.get(cls);
32
+ return listener;
33
+ }
34
+ }
@@ -9,6 +9,8 @@ import { Activity, CryptographicKey, Multikey, } from "../vocab/vocab.js";
9
9
  import { handleWebFinger } from "../webfinger/handler.js";
10
10
  import { buildCollectionSynchronizationHeader } from "./collection.js";
11
11
  import { handleActor, handleCollection, handleInbox, handleObject, } from "./handler.js";
12
+ import { InboxListenerSet } from "./inbox.js";
13
+ import { createExponentialBackoffPolicy } from "./retry.js";
12
14
  import { Router, RouterError } from "./router.js";
13
15
  import { extractInboxes, sendActivity } from "./send.js";
14
16
  const invokedByCreateFederation = Symbol("invokedByCreateFederation");
@@ -25,6 +27,7 @@ export function createFederation(options) {
25
27
  [invokedByCreateFederation]: true,
26
28
  });
27
29
  }
30
+ const invokedByContext = Symbol("invokedByContext");
28
31
  /**
29
32
  * An object that registers federation-related business logic and dispatches
30
33
  * requests to the appropriate handlers.
@@ -59,13 +62,15 @@ export class Federation {
59
62
  #treatHttps;
60
63
  #onOutboxError;
61
64
  #signatureTimeWindow;
62
- #backoffSchedule;
65
+ #outboxRetryPolicy;
66
+ #inboxRetryPolicy;
63
67
  /**
64
68
  * Create a new {@link Federation} instance.
65
69
  * @param parameters Parameters for initializing the instance.
66
70
  * @deprecated Use {@link createFederation} method instead.
67
71
  */
68
- constructor(options) {
72
+ constructor(parameters) {
73
+ const options = parameters;
69
74
  const logger = getLogger(["fedify", "federation"]);
70
75
  // @ts-ignore: This is a private symbol.
71
76
  if (!options[invokedByCreateFederation]) {
@@ -97,37 +102,42 @@ export class Federation {
97
102
  options.authenticatedDocumentLoaderFactory ??
98
103
  getAuthenticatedDocumentLoader;
99
104
  this.#onOutboxError = options.onOutboxError;
100
- this.#treatHttps = options.treatHttps ?? false;
101
- if (options.treatHttps) {
105
+ this.#treatHttps = parameters.treatHttps ?? false;
106
+ if (parameters.treatHttps) {
102
107
  logger.warn("The treatHttps option is deprecated and will be removed in " +
103
108
  "a future release. Instead, use the x-forwarded-fetch library" +
104
109
  " to recognize the X-Forwarded-Host and X-Forwarded-Proto " +
105
110
  "headers. See also: <https://github.com/dahlia/x-forwarded-fetch>.");
106
111
  }
107
112
  this.#signatureTimeWindow = options.signatureTimeWindow ?? { minutes: 1 };
108
- this.#backoffSchedule = options.backoffSchedule ?? [
109
- 3000,
110
- 15000,
111
- 60000,
112
- 15 * 60000,
113
- 60 * 60000,
114
- ].map((ms) => dntShim.Temporal.Duration.from({ milliseconds: ms }));
113
+ this.#outboxRetryPolicy = options.outboxRetryPolicy ??
114
+ createExponentialBackoffPolicy();
115
+ this.#inboxRetryPolicy = options.inboxRetryPolicy ??
116
+ createExponentialBackoffPolicy();
115
117
  }
116
- #startQueue() {
118
+ #startQueue(ctxData) {
117
119
  if (this.#queue != null && !this.#queueStarted) {
118
- const logger = getLogger(["fedify", "federation", "outbox"]);
119
- logger.debug("Starting an outbox queue.");
120
- this.#queue?.listen(this.#listenQueue.bind(this));
120
+ const logger = getLogger(["fedify", "federation", "queue"]);
121
+ logger.debug("Starting a task queue.");
122
+ this.#queue?.listen((msg) => this.#listenQueue(ctxData, msg));
121
123
  this.#queueStarted = true;
122
124
  }
123
125
  }
124
- async #listenQueue(message) {
126
+ async #listenQueue(ctxData, message) {
127
+ if (message.type === "outbox") {
128
+ await this.#listenOutboxMessage(ctxData, message);
129
+ }
130
+ else if (message.type === "inbox") {
131
+ await this.#listenInboxMessage(ctxData, message);
132
+ }
133
+ }
134
+ async #listenOutboxMessage(_, message) {
125
135
  const logger = getLogger(["fedify", "federation", "outbox"]);
126
136
  const logData = {
127
137
  keyIds: message.keys.map((pair) => pair.keyId),
128
138
  inbox: message.inbox,
129
139
  activity: message.activity,
130
- trial: message.trial,
140
+ attempt: message.attempt,
131
141
  headers: message.headers,
132
142
  };
133
143
  let activity = null;
@@ -167,22 +177,120 @@ export class Federation {
167
177
  catch (error) {
168
178
  logger.error("An unexpected error occurred in onError handler:\n{error}", { ...logData, error, activityId: activity?.id?.href });
169
179
  }
170
- if (message.trial < this.#backoffSchedule.length) {
171
- logger.error("Failed to send activity {activityId} to {inbox} (trial #{trial})" +
172
- "; retry...:\n{error}", { ...logData, error, activityId: activity?.id?.href });
180
+ const delay = this.#outboxRetryPolicy({
181
+ elapsedTime: dntShim.Temporal.Instant.from(message.started).until(dntShim.Temporal.Now.instant()),
182
+ attempts: message.attempt,
183
+ });
184
+ if (delay != null) {
185
+ logger.error("Failed to send activity {activityId} to {inbox} (attempt " +
186
+ "#{attempt}); retry...:\n{error}", { ...logData, error, activityId: activity?.id?.href });
173
187
  this.#queue?.enqueue({
174
188
  ...message,
175
- trial: message.trial + 1,
176
- }, { delay: this.#backoffSchedule[message.trial] });
189
+ attempt: message.attempt + 1,
190
+ }, {
191
+ delay: dntShim.Temporal.Duration.compare(delay, { seconds: 0 }) < 0
192
+ ? dntShim.Temporal.Duration.from({ seconds: 0 })
193
+ : delay,
194
+ });
177
195
  }
178
196
  else {
179
- logger.error("Failed to send activity {activityId} to {inbox} after {trial} " +
180
- "trials; giving up:\n{error}", { ...logData, error, activityId: activity?.id?.href });
197
+ logger.error("Failed to send activity {activityId} to {inbox} after {attempt} " +
198
+ "attempts; giving up:\n{error}", { ...logData, error, activityId: activity?.id?.href });
181
199
  }
182
200
  return;
183
201
  }
184
202
  logger.info("Successfully sent activity {activityId} to {inbox}.", { ...logData, activityId: activity?.id?.href });
185
203
  }
204
+ async #listenInboxMessage(ctxData, message) {
205
+ const logger = getLogger(["fedify", "federation", "inbox"]);
206
+ const baseUrl = new URL(message.baseUrl);
207
+ let context = this.#createContext(baseUrl, ctxData);
208
+ if (message.handle) {
209
+ context = this.#createContext(baseUrl, ctxData, {
210
+ documentLoader: await context.getDocumentLoader({
211
+ handle: message.handle,
212
+ }),
213
+ });
214
+ }
215
+ else if (this.#sharedInboxKeyDispatcher != null) {
216
+ const identity = await this.#sharedInboxKeyDispatcher(context);
217
+ if (identity != null) {
218
+ context = this.#createContext(baseUrl, ctxData, {
219
+ documentLoader: "handle" in identity
220
+ ? await context.getDocumentLoader(identity)
221
+ : context.getDocumentLoader(identity),
222
+ });
223
+ }
224
+ }
225
+ const activity = await Activity.fromJsonLd(message.activity, context);
226
+ const cacheKey = activity.id == null ? null : [
227
+ ...this.#kvPrefixes.activityIdempotence,
228
+ activity.id.href,
229
+ ];
230
+ if (cacheKey != null) {
231
+ const cached = await this.#kv.get(cacheKey);
232
+ if (cached === true) {
233
+ logger.debug("Activity {activityId} has already been processed.", {
234
+ activityId: activity.id?.href,
235
+ activity: message.activity,
236
+ });
237
+ return;
238
+ }
239
+ }
240
+ const listener = this.#inboxListeners?.dispatch(activity);
241
+ if (listener == null) {
242
+ logger.error("Unsupported activity type:\n{activity}", { activity: message.activity, trial: message.attempt });
243
+ return;
244
+ }
245
+ try {
246
+ await listener(context, activity);
247
+ }
248
+ catch (error) {
249
+ try {
250
+ await this.#inboxErrorHandler?.(context, error);
251
+ }
252
+ catch (error) {
253
+ logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
254
+ error,
255
+ trial: message.attempt,
256
+ activityId: activity.id?.href,
257
+ activity: message.activity,
258
+ });
259
+ }
260
+ const delay = this.#inboxRetryPolicy({
261
+ elapsedTime: dntShim.Temporal.Instant.from(message.started).until(dntShim.Temporal.Now.instant()),
262
+ attempts: message.attempt,
263
+ });
264
+ if (delay != null) {
265
+ logger.error("Failed to process the incoming activity {activityId} (attempt " +
266
+ "#{attempt}); retry...:\n{error}", {
267
+ error,
268
+ attempt: message.attempt,
269
+ activityId: activity.id?.href,
270
+ activity: message.activity,
271
+ });
272
+ this.#queue?.enqueue({
273
+ ...message,
274
+ attempt: message.attempt + 1,
275
+ }, {
276
+ delay: dntShim.Temporal.Duration.compare(delay, { seconds: 0 }) < 0
277
+ ? dntShim.Temporal.Duration.from({ seconds: 0 })
278
+ : delay,
279
+ });
280
+ }
281
+ else {
282
+ logger.error("Failed to process the incoming activity {activityId} after " +
283
+ "{trial} attempts; giving up:\n{error}", { error, activityId: activity.id?.href, activity: message.activity });
284
+ }
285
+ return;
286
+ }
287
+ if (cacheKey != null) {
288
+ await this.#kv.set(cacheKey, true, {
289
+ ttl: dntShim.Temporal.Duration.from({ days: 1 }),
290
+ });
291
+ }
292
+ logger.info("Activity {activityId} has been processed.", { activityId: activity.id?.href, activity: message.activity });
293
+ }
186
294
  createContext(urlOrRequest, contextData) {
187
295
  return urlOrRequest instanceof Request
188
296
  ? this.#createContext(urlOrRequest, contextData)
@@ -827,15 +935,12 @@ export class Federation {
827
935
  throw new RouterError("Path for shared inbox must have no variables.");
828
936
  }
829
937
  }
830
- const listeners = this.#inboxListeners = new Map();
938
+ const listeners = this.#inboxListeners = new InboxListenerSet();
831
939
  const setters = {
832
940
  on(
833
941
  // deno-lint-ignore no-explicit-any
834
942
  type, listener) {
835
- if (listeners.has(type)) {
836
- throw new TypeError("Listener already set for this type.");
837
- }
838
- listeners.set(type, listener);
943
+ listeners.add(type, listener);
839
944
  return setters;
840
945
  },
841
946
  onError: (handler) => {
@@ -858,9 +963,15 @@ export class Federation {
858
963
  * @param activity The activity to send.
859
964
  * @param options Options for sending the activity.
860
965
  * @throws {TypeError} If the activity to send does not have an actor.
966
+ * @deprecated Use {@link Context.sendActivity} instead.
861
967
  */
862
- async sendActivity(keys, recipients, activity, { preferSharedInbox, immediate, excludeBaseUris, collectionSync } = {}) {
968
+ async sendActivity(keys, recipients, activity, options) {
863
969
  const logger = getLogger(["fedify", "federation", "outbox"]);
970
+ if (!(invokedByContext in options) || !options[invokedByContext]) {
971
+ logger.warn("The Federation.sendActivity() method is deprecated. Use " +
972
+ "Context.sendActivity() instead.");
973
+ }
974
+ const { preferSharedInbox, immediate, excludeBaseUris, collectionSync, contextData, } = options;
864
975
  if (keys.length < 1) {
865
976
  throw new TypeError("The sender's keys must not be empty.");
866
977
  }
@@ -871,7 +982,7 @@ export class Federation {
871
982
  logger.error("Activity {activityId} to send does not have an actor.", { activity, activityId: activity?.id?.href });
872
983
  throw new TypeError("The activity to send must have at least one actor property.");
873
984
  }
874
- this.#startQueue();
985
+ this.#startQueue(contextData);
875
986
  if (activity.id == null) {
876
987
  activity = activity.clone({
877
988
  id: new URL(`urn:uuid:${dntShim.crypto.randomUUID()}`),
@@ -925,7 +1036,8 @@ export class Federation {
925
1036
  keys: keyJwkPairs,
926
1037
  activity: activityJson,
927
1038
  inbox,
928
- trial: 0,
1039
+ started: new Date().toISOString(),
1040
+ attempt: 0,
929
1041
  headers: collectionSync == null ? {} : {
930
1042
  "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox]),
931
1043
  },
@@ -1067,7 +1179,7 @@ export class Federation {
1067
1179
  kv: this.#kv,
1068
1180
  kvPrefix: this.#kvPrefixes.activityIdempotence,
1069
1181
  actorDispatcher: this.#actorCallbacks?.dispatcher,
1070
- inboxListeners: this.#inboxListeners ?? new Map(),
1182
+ inboxListeners: this.#inboxListeners,
1071
1183
  inboxErrorHandler: this.#inboxErrorHandler,
1072
1184
  onNotFound,
1073
1185
  signatureTimeWindow: this.#signatureTimeWindow,
@@ -1451,7 +1563,12 @@ class ContextImpl {
1451
1563
  else {
1452
1564
  keys = [sender];
1453
1565
  }
1454
- const opts = { ...options };
1566
+ const opts = {
1567
+ contextData: this.data,
1568
+ ...options,
1569
+ // @ts-ignore: This is a private symbol
1570
+ [invokedByContext]: true,
1571
+ };
1455
1572
  let expandedRecipients;
1456
1573
  if (Array.isArray(recipients)) {
1457
1574
  expandedRecipients = recipients;
@@ -10,4 +10,5 @@ export { respondWithObject, respondWithObjectIfAcceptable, } from "./handler.js"
10
10
  export * from "./kv.js";
11
11
  export * from "./middleware.js";
12
12
  export * from "./mq.js";
13
+ export * from "./retry.js";
13
14
  export * from "./router.js";
@@ -13,7 +13,7 @@ export class InProcessMessageQueue {
13
13
  enqueue(message, options) {
14
14
  const delay = options?.delay == null
15
15
  ? 0
16
- : options.delay.total("millisecond");
16
+ : Math.max(options.delay.total("millisecond"), 0);
17
17
  setTimeout(() => {
18
18
  for (const handler of this.#handlers)
19
19
  handler(message);