@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.
- package/CHANGES.md +35 -0
- package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/concat.js +1 -1
- package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/copy.js +2 -2
- package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/ends_with.js +1 -1
- package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/equals.js +1 -1
- package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/includes_needle.js +2 -2
- package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/index_of_needle.js +2 -2
- package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/last_index_of_needle.js +2 -2
- package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/mod.js +1 -1
- package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/repeat.js +2 -2
- package/esm/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/starts_with.js +1 -1
- package/esm/federation/handler.js +41 -21
- package/esm/federation/inbox.js +34 -0
- package/esm/federation/middleware.js +151 -34
- package/esm/federation/mod.js +1 -0
- package/esm/federation/mq.js +1 -1
- package/esm/federation/retry.js +34 -0
- package/esm/runtime/key.js +1 -1
- package/esm/sig/http.js +1 -1
- package/package.json +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/concat.d.ts +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/concat.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/copy.d.ts +2 -2
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/copy.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/ends_with.d.ts +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/ends_with.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/equals.d.ts +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/equals.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/includes_needle.d.ts +2 -2
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/includes_needle.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/index_of_needle.d.ts +2 -2
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/index_of_needle.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/last_index_of_needle.d.ts +2 -2
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/last_index_of_needle.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/mod.d.ts +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/mod.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/repeat.d.ts +2 -2
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/repeat.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/starts_with.d.ts +1 -1
- package/types/deps/jsr.io/@std/bytes/{1.0.0 → 1.0.1}/starts_with.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/build_message.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/diff.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/diff_str.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/format.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/mod.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/styles.d.ts.map +1 -1
- package/types/deps/jsr.io/@std/internal/{1.0.0 → 1.0.1}/types.d.ts.map +1 -1
- package/types/federation/callback.d.ts +4 -4
- package/types/federation/callback.d.ts.map +1 -1
- package/types/federation/handler.d.ts +7 -4
- package/types/federation/handler.d.ts.map +1 -1
- package/types/federation/inbox.d.ts +9 -0
- package/types/federation/inbox.d.ts.map +1 -0
- package/types/federation/inbox.test.d.ts.map +1 -0
- package/types/federation/middleware.d.ts +21 -5
- package/types/federation/middleware.d.ts.map +1 -1
- package/types/federation/mod.d.ts +1 -0
- package/types/federation/mod.d.ts.map +1 -1
- package/types/federation/mq.d.ts +2 -0
- package/types/federation/mq.d.ts.map +1 -1
- package/types/federation/queue.d.ts +11 -1
- package/types/federation/queue.d.ts.map +1 -1
- package/types/federation/retry.d.ts +64 -0
- package/types/federation/retry.d.ts.map +1 -0
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
#
|
|
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(
|
|
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 =
|
|
101
|
-
if (
|
|
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.#
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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", "
|
|
119
|
-
logger.debug("Starting
|
|
120
|
-
this.#queue?.listen(this.#listenQueue
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
}, {
|
|
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 {
|
|
180
|
-
"
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 = {
|
|
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;
|
package/esm/federation/mod.js
CHANGED
package/esm/federation/mq.js
CHANGED
|
@@ -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);
|