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

Sign up to get free protection for your applications and to get access to all the features.
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);