@fedify/fedify 1.5.0-dev.718 → 1.5.0-dev.730
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 +84 -1
- package/esm/deno.js +1 -1
- package/esm/federation/middleware.js +143 -40
- package/esm/vocab/vocab.js +176 -176
- package/esm/webfinger/handler.js +10 -4
- package/package.json +1 -1
- package/types/federation/context.d.ts +17 -0
- package/types/federation/context.d.ts.map +1 -1
- package/types/federation/federation.d.ts +3 -2
- package/types/federation/federation.d.ts.map +1 -1
- package/types/federation/middleware.d.ts +16 -2
- package/types/federation/middleware.d.ts.map +1 -1
- package/types/federation/queue.d.ts +16 -1
- package/types/federation/queue.d.ts.map +1 -1
package/CHANGES.md
CHANGED
@@ -8,6 +8,18 @@ Version 1.5.0
|
|
8
8
|
|
9
9
|
To be released.
|
10
10
|
|
11
|
+
- Improved activity delivery performance with large audiences through
|
12
|
+
a two-stage queuing system. Sending activities to many recipients
|
13
|
+
(e.g., accounts with many followers) is now significantly faster and uses
|
14
|
+
less memory. [[#220]]
|
15
|
+
|
16
|
+
- Added `FederationQueueOptions.fanout` option.
|
17
|
+
- Changed the type of `FederationStartQueueOptions.queue` option to
|
18
|
+
`"inbox" | "outbox" | "fanout" | undefined` (was `"inbox" | "outbox" |
|
19
|
+
undefined`).
|
20
|
+
- Added `SendActivityOptions.fanout` option.
|
21
|
+
- Added OpenTelemetry instrumented span `activitypub.fanout`.
|
22
|
+
|
11
23
|
- A `Federation` object now can have a canonical origin for web URLs and
|
12
24
|
a canonical host for fediverse handles. This affects the URLs constructed
|
13
25
|
by `Context` objects, and the WebFinger responses.
|
@@ -65,15 +77,30 @@ To be released.
|
|
65
77
|
- Added more log messages using the [LogTape] library. Currently the below
|
66
78
|
logger categories are used:
|
67
79
|
|
80
|
+
- `["fedify", "federation", "fanout"]`
|
68
81
|
- `["fedify", "federation", "object"]`
|
69
82
|
|
70
83
|
[#127]: https://github.com/fedify-dev/fedify/issues/127
|
71
84
|
[#209]: https://github.com/fedify-dev/fedify/issues/209
|
72
85
|
[#211]: https://github.com/fedify-dev/fedify/issues/211
|
73
86
|
[#215]: https://github.com/fedify-dev/fedify/pull/215
|
87
|
+
[#220]: https://github.com/fedify-dev/fedify/issues/220
|
74
88
|
[multibase]: https://github.com/multiformats/js-multibase
|
75
89
|
|
76
90
|
|
91
|
+
Version 1.4.7
|
92
|
+
-------------
|
93
|
+
|
94
|
+
Released on March 20, 2025.
|
95
|
+
|
96
|
+
- Fixed a bug of WebFinger handler where it had failed to match
|
97
|
+
`acct:` URIs with a host having a port number.
|
98
|
+
[[#218], [#219] by Revath S Kumar]
|
99
|
+
|
100
|
+
- Fixed a server error thrown when an invalid URL was passed to the `base-url`
|
101
|
+
parameter of the followers collection. [[#217]]
|
102
|
+
|
103
|
+
|
77
104
|
Version 1.4.6
|
78
105
|
-------------
|
79
106
|
|
@@ -237,6 +264,19 @@ Released on February 5, 2025.
|
|
237
264
|
[#195]: https://github.com/fedify-dev/fedify/issues/195
|
238
265
|
|
239
266
|
|
267
|
+
Version 1.3.14
|
268
|
+
--------------
|
269
|
+
|
270
|
+
Released on March 20, 2025.
|
271
|
+
|
272
|
+
- Fixed a bug of WebFinger handler where it had failed to match
|
273
|
+
`acct:` URIs with a host having a port number.
|
274
|
+
[[#218], [#219] by Revath S Kumar]
|
275
|
+
|
276
|
+
- Fixed a server error thrown when an invalid URL was passed to the `base-url`
|
277
|
+
parameter of the followers collection. [[#217]]
|
278
|
+
|
279
|
+
|
240
280
|
Version 1.3.13
|
241
281
|
--------------
|
242
282
|
|
@@ -516,6 +556,19 @@ Released on November 30, 2024.
|
|
516
556
|
[#193]: https://github.com/fedify-dev/fedify/issues/193
|
517
557
|
|
518
558
|
|
559
|
+
Version 1.2.18
|
560
|
+
--------------
|
561
|
+
|
562
|
+
Released on March 20, 2025.
|
563
|
+
|
564
|
+
- Fixed a bug of WebFinger handler where it had failed to match
|
565
|
+
`acct:` URIs with a host having a port number.
|
566
|
+
[[#218], [#219] by Revath S Kumar]
|
567
|
+
|
568
|
+
- Fixed a server error thrown when an invalid URL was passed to the `base-url`
|
569
|
+
parameter of the followers collection. [[#217]]
|
570
|
+
|
571
|
+
|
519
572
|
Version 1.2.17
|
520
573
|
--------------
|
521
574
|
|
@@ -844,6 +897,19 @@ Released on October 31, 2024.
|
|
844
897
|
[#118]: https://github.com/fedify-dev/fedify/issues/118
|
845
898
|
|
846
899
|
|
900
|
+
Version 1.1.18
|
901
|
+
--------------
|
902
|
+
|
903
|
+
Released on March 20, 2025.
|
904
|
+
|
905
|
+
- Fixed a bug of WebFinger handler where it had failed to match
|
906
|
+
`acct:` URIs with a host having a port number.
|
907
|
+
[[#218], [#219] by Revath S Kumar]
|
908
|
+
|
909
|
+
- Fixed a server error thrown when an invalid URL was passed to the `base-url`
|
910
|
+
parameter of the followers collection. [[#217]]
|
911
|
+
|
912
|
+
|
847
913
|
Version 1.1.17
|
848
914
|
--------------
|
849
915
|
|
@@ -1213,6 +1279,23 @@ Released on October 20, 2024.
|
|
1213
1279
|
[#150]: https://github.com/fedify-dev/fedify/issues/150
|
1214
1280
|
|
1215
1281
|
|
1282
|
+
Version 1.0.21
|
1283
|
+
--------------
|
1284
|
+
|
1285
|
+
Released on March 20, 2025.
|
1286
|
+
|
1287
|
+
- Fixed a bug of WebFinger handler where it had failed to match
|
1288
|
+
`acct:` URIs with a host having a port number.
|
1289
|
+
[[#218], [#219] by Revath S Kumar]
|
1290
|
+
|
1291
|
+
- Fixed a server error thrown when an invalid URL was passed to the `base-url`
|
1292
|
+
parameter of the followers collection. [[#217]]
|
1293
|
+
|
1294
|
+
[#217]: https://github.com/fedify-dev/fedify/issues/217
|
1295
|
+
[#218]: https://github.com/fedify-dev/fedify/issues/218
|
1296
|
+
[#219]: https://github.com/fedify-dev/fedify/pull/219
|
1297
|
+
|
1298
|
+
|
1216
1299
|
Version 1.0.20
|
1217
1300
|
--------------
|
1218
1301
|
|
@@ -3378,4 +3461,4 @@ Version 0.1.0
|
|
3378
3461
|
Initial release. Released on March 8, 2024.
|
3379
3462
|
|
3380
3463
|
<!-- cSpell: ignore Dogeon Fabien Wressell Emelia Fróði Karlsson -->
|
3381
|
-
<!-- cSpell: ignore Hana Heesun Kyunghee Jiyu -->
|
3464
|
+
<!-- cSpell: ignore Hana Heesun Kyunghee Jiyu Revath Kumar -->
|
package/esm/deno.js
CHANGED
@@ -37,8 +37,10 @@ export class FederationImpl {
|
|
37
37
|
kvPrefixes;
|
38
38
|
inboxQueue;
|
39
39
|
outboxQueue;
|
40
|
+
fanoutQueue;
|
40
41
|
inboxQueueStarted;
|
41
42
|
outboxQueueStarted;
|
43
|
+
fanoutQueueStarted;
|
42
44
|
manuallyStartQueue;
|
43
45
|
origin;
|
44
46
|
router;
|
@@ -83,17 +85,21 @@ export class FederationImpl {
|
|
83
85
|
if (options.queue == null) {
|
84
86
|
this.inboxQueue = undefined;
|
85
87
|
this.outboxQueue = undefined;
|
88
|
+
this.fanoutQueue = undefined;
|
86
89
|
}
|
87
90
|
else if ("enqueue" in options.queue && "listen" in options.queue) {
|
88
91
|
this.inboxQueue = options.queue;
|
89
92
|
this.outboxQueue = options.queue;
|
93
|
+
this.fanoutQueue = options.queue;
|
90
94
|
}
|
91
95
|
else {
|
92
96
|
this.inboxQueue = options.queue.inbox;
|
93
97
|
this.outboxQueue = options.queue.outbox;
|
98
|
+
this.fanoutQueue = options.queue.fanout;
|
94
99
|
}
|
95
100
|
this.inboxQueueStarted = false;
|
96
101
|
this.outboxQueueStarted = false;
|
102
|
+
this.fanoutQueueStarted = false;
|
97
103
|
this.manuallyStartQueue = options.manuallyStartQueue ?? false;
|
98
104
|
if (options.origin != null) {
|
99
105
|
if (typeof options.origin === "string") {
|
@@ -206,7 +212,7 @@ export class FederationImpl {
|
|
206
212
|
#getTracer() {
|
207
213
|
return this.tracerProvider.getTracer(metadata.name, metadata.version);
|
208
214
|
}
|
209
|
-
async
|
215
|
+
async _startQueueInternal(ctxData, signal, queue) {
|
210
216
|
if (this.inboxQueue == null && this.outboxQueue == null)
|
211
217
|
return;
|
212
218
|
const logger = getLogger(["fedify", "federation", "queue"]);
|
@@ -225,13 +231,47 @@ export class FederationImpl {
|
|
225
231
|
this.outboxQueueStarted = true;
|
226
232
|
promises.push(this.outboxQueue.listen((msg) => this.#listenQueue(ctxData, msg), { signal }));
|
227
233
|
}
|
234
|
+
if (this.fanoutQueue != null &&
|
235
|
+
this.fanoutQueue !== this.inboxQueue &&
|
236
|
+
this.fanoutQueue !== this.outboxQueue &&
|
237
|
+
(queue == null || queue === "fanout") &&
|
238
|
+
!this.fanoutQueueStarted) {
|
239
|
+
logger.debug("Starting a fanout task worker.");
|
240
|
+
this.fanoutQueueStarted = true;
|
241
|
+
promises.push(this.fanoutQueue.listen((msg) => this.#listenQueue(ctxData, msg), { signal }));
|
242
|
+
}
|
228
243
|
await Promise.all(promises);
|
229
244
|
}
|
230
245
|
#listenQueue(ctxData, message) {
|
231
246
|
const tracer = this.#getTracer();
|
232
247
|
const extractedContext = propagation.extract(context.active(), message.traceContext);
|
233
248
|
return withContext({ messageId: message.id }, async () => {
|
234
|
-
if (message.type === "
|
249
|
+
if (message.type === "fanout") {
|
250
|
+
await tracer.startActiveSpan("activitypub.fanout", {
|
251
|
+
kind: SpanKind.CONSUMER,
|
252
|
+
attributes: {
|
253
|
+
"activitypub.activity.type": message.activityType,
|
254
|
+
},
|
255
|
+
}, extractedContext, async (span) => {
|
256
|
+
if (message.activityId != null) {
|
257
|
+
span.setAttribute("activitypub.activity.id", message.activityId);
|
258
|
+
}
|
259
|
+
try {
|
260
|
+
await this.#listenFanoutMessage(ctxData, message);
|
261
|
+
}
|
262
|
+
catch (e) {
|
263
|
+
span.setStatus({
|
264
|
+
code: SpanStatusCode.ERROR,
|
265
|
+
message: String(e),
|
266
|
+
});
|
267
|
+
throw e;
|
268
|
+
}
|
269
|
+
finally {
|
270
|
+
span.end();
|
271
|
+
}
|
272
|
+
});
|
273
|
+
}
|
274
|
+
else if (message.type === "outbox") {
|
235
275
|
await tracer.startActiveSpan("activitypub.outbox", {
|
236
276
|
kind: SpanKind.CONSUMER,
|
237
277
|
attributes: {
|
@@ -281,6 +321,33 @@ export class FederationImpl {
|
|
281
321
|
}
|
282
322
|
});
|
283
323
|
}
|
324
|
+
async #listenFanoutMessage(data, message) {
|
325
|
+
const keys = await Promise.all(message.keys.map(async ({ keyId, privateKey }) => ({
|
326
|
+
keyId: new URL(keyId),
|
327
|
+
privateKey: await importJwk(privateKey, "private"),
|
328
|
+
})));
|
329
|
+
const activity = await Activity.fromJsonLd(message.activity, {
|
330
|
+
contextLoader: this.contextLoaderFactory({
|
331
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
332
|
+
userAgent: this.userAgent,
|
333
|
+
}),
|
334
|
+
documentLoader: this.documentLoaderFactory({
|
335
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
336
|
+
userAgent: this.userAgent,
|
337
|
+
}),
|
338
|
+
tracerProvider: this.tracerProvider,
|
339
|
+
});
|
340
|
+
const context = this.#createContext(new URL(message.baseUrl), data, {
|
341
|
+
documentLoader: this.documentLoaderFactory({
|
342
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
343
|
+
userAgent: this.userAgent,
|
344
|
+
}),
|
345
|
+
});
|
346
|
+
await this.sendActivity(keys, message.inboxes, activity, {
|
347
|
+
collectionSync: message.collectionSync,
|
348
|
+
context,
|
349
|
+
});
|
350
|
+
}
|
284
351
|
async #listenOutboxMessage(_, message, span) {
|
285
352
|
const logger = getLogger(["fedify", "federation", "outbox"]);
|
286
353
|
const logData = {
|
@@ -485,7 +552,7 @@ export class FederationImpl {
|
|
485
552
|
});
|
486
553
|
}
|
487
554
|
startQueue(contextData, options = {}) {
|
488
|
-
return this
|
555
|
+
return this._startQueueInternal(contextData, options.signal, options.queue);
|
489
556
|
}
|
490
557
|
createContext(urlOrRequest, contextData) {
|
491
558
|
return urlOrRequest instanceof Request
|
@@ -1118,33 +1185,9 @@ export class FederationImpl {
|
|
1118
1185
|
};
|
1119
1186
|
return setters;
|
1120
1187
|
}
|
1121
|
-
async sendActivity(keys,
|
1188
|
+
async sendActivity(keys, inboxes, activity, options) {
|
1122
1189
|
const logger = getLogger(["fedify", "federation", "outbox"]);
|
1123
|
-
const {
|
1124
|
-
if (keys.length < 1) {
|
1125
|
-
throw new TypeError("The sender's keys must not be empty.");
|
1126
|
-
}
|
1127
|
-
for (const { privateKey } of keys) {
|
1128
|
-
validateCryptoKey(privateKey, "private");
|
1129
|
-
}
|
1130
|
-
for (const activityTransformer of this.activityTransformers) {
|
1131
|
-
activity = activityTransformer(activity, ctx);
|
1132
|
-
}
|
1133
|
-
span?.setAttribute("activitypub.activity.id", activity?.id?.href ?? "");
|
1134
|
-
if (activity.actorId == null) {
|
1135
|
-
logger.error("Activity {activityId} to send does not have an actor.", { activity, activityId: activity?.id?.href });
|
1136
|
-
throw new TypeError("The activity to send must have at least one actor property.");
|
1137
|
-
}
|
1138
|
-
const inboxes = extractInboxes({
|
1139
|
-
recipients: Array.isArray(recipients) ? recipients : [recipients],
|
1140
|
-
preferSharedInbox,
|
1141
|
-
excludeBaseUris,
|
1142
|
-
});
|
1143
|
-
logger.debug("Sending activity {activityId} to inboxes:\n{inboxes}", {
|
1144
|
-
inboxes: globalThis.Object.keys(inboxes),
|
1145
|
-
activityId: activity.id?.href,
|
1146
|
-
activity,
|
1147
|
-
});
|
1190
|
+
const { immediate, collectionSync, context: ctx } = options;
|
1148
1191
|
if (activity.id == null) {
|
1149
1192
|
throw new TypeError("The activity to send must have an id.");
|
1150
1193
|
}
|
@@ -1238,7 +1281,7 @@ export class FederationImpl {
|
|
1238
1281
|
keyJwkPairs.push({ keyId: keyId.href, privateKey: privateKeyJwk });
|
1239
1282
|
}
|
1240
1283
|
if (!this.manuallyStartQueue)
|
1241
|
-
this
|
1284
|
+
this._startQueueInternal(ctx.data);
|
1242
1285
|
const carrier = {};
|
1243
1286
|
propagation.inject(context.active(), carrier);
|
1244
1287
|
const promises = [];
|
@@ -1441,7 +1484,7 @@ export class FederationImpl {
|
|
1441
1484
|
}
|
1442
1485
|
}
|
1443
1486
|
if (!this.manuallyStartQueue)
|
1444
|
-
this
|
1487
|
+
this._startQueueInternal(contextData);
|
1445
1488
|
return await handleInbox(request, {
|
1446
1489
|
recipient: route.values.identifier ?? route.values.handle ?? null,
|
1447
1490
|
context,
|
@@ -1472,8 +1515,13 @@ export class FederationImpl {
|
|
1472
1515
|
case "followers": {
|
1473
1516
|
let baseUrl = url.searchParams.get("base-url");
|
1474
1517
|
if (baseUrl != null) {
|
1475
|
-
|
1476
|
-
|
1518
|
+
try {
|
1519
|
+
baseUrl = `${new URL(baseUrl).origin}/`;
|
1520
|
+
}
|
1521
|
+
catch {
|
1522
|
+
// If base-url is invalid, set to null to behave as if it wasn't provided
|
1523
|
+
baseUrl = null;
|
1524
|
+
}
|
1477
1525
|
}
|
1478
1526
|
return await handleCollection(request, {
|
1479
1527
|
name: "followers",
|
@@ -1534,6 +1582,7 @@ export class FederationImpl {
|
|
1534
1582
|
}
|
1535
1583
|
}
|
1536
1584
|
}
|
1585
|
+
const FANOUT_THRESHOLD = 5;
|
1537
1586
|
export class ContextImpl {
|
1538
1587
|
url;
|
1539
1588
|
federation;
|
@@ -1941,7 +1990,9 @@ export class ContextImpl {
|
|
1941
1990
|
}
|
1942
1991
|
sendActivity(sender, recipients, activity, options = {}) {
|
1943
1992
|
const tracer = this.tracerProvider.getTracer(metadata.name, metadata.version);
|
1944
|
-
return tracer.startActiveSpan(
|
1993
|
+
return tracer.startActiveSpan(this.federation.outboxQueue == null || options.immediate
|
1994
|
+
? "activitypub.outbox"
|
1995
|
+
: "activitypub.fanout", {
|
1945
1996
|
kind: this.federation.outboxQueue == null || options.immediate
|
1946
1997
|
? SpanKind.CLIENT
|
1947
1998
|
: SpanKind.PRODUCER,
|
@@ -1969,6 +2020,7 @@ export class ContextImpl {
|
|
1969
2020
|
});
|
1970
2021
|
}
|
1971
2022
|
async sendActivityInternal(sender, recipients, activity, options = {}, span) {
|
2023
|
+
const logger = getLogger(["fedify", "federation", "outbox"]);
|
1972
2024
|
let keys;
|
1973
2025
|
let identifier = null;
|
1974
2026
|
if ("identifier" in sender || "username" in sender || "handle" in sender) {
|
@@ -1982,7 +2034,7 @@ export class ContextImpl {
|
|
1982
2034
|
}
|
1983
2035
|
else {
|
1984
2036
|
username = sender.handle;
|
1985
|
-
|
2037
|
+
logger.warn('The "handle" property for the sender parameter is deprecated; ' +
|
1986
2038
|
'use "identifier" or "username" instead.', { sender });
|
1987
2039
|
}
|
1988
2040
|
if (this.federation.actorCallbacks?.handleMapper == null) {
|
@@ -2011,10 +2063,13 @@ export class ContextImpl {
|
|
2011
2063
|
else {
|
2012
2064
|
keys = [sender];
|
2013
2065
|
}
|
2014
|
-
|
2015
|
-
|
2016
|
-
|
2017
|
-
}
|
2066
|
+
if (keys.length < 1) {
|
2067
|
+
throw new TypeError("The sender's keys must not be empty.");
|
2068
|
+
}
|
2069
|
+
for (const { privateKey } of keys) {
|
2070
|
+
validateCryptoKey(privateKey, "private");
|
2071
|
+
}
|
2072
|
+
const opts = { context: this };
|
2018
2073
|
let expandedRecipients;
|
2019
2074
|
if (Array.isArray(recipients)) {
|
2020
2075
|
expandedRecipients = recipients;
|
@@ -2037,7 +2092,55 @@ export class ContextImpl {
|
|
2037
2092
|
expandedRecipients = [recipients];
|
2038
2093
|
}
|
2039
2094
|
span.setAttribute("activitypub.inboxes", expandedRecipients.length);
|
2040
|
-
|
2095
|
+
for (const activityTransformer of this.federation.activityTransformers) {
|
2096
|
+
activity = activityTransformer(activity, this);
|
2097
|
+
}
|
2098
|
+
span?.setAttribute("activitypub.activity.id", activity?.id?.href ?? "");
|
2099
|
+
if (activity.actorId == null) {
|
2100
|
+
logger.error("Activity {activityId} to send does not have an actor.", { activity, activityId: activity?.id?.href });
|
2101
|
+
throw new TypeError("The activity to send must have at least one actor property.");
|
2102
|
+
}
|
2103
|
+
const inboxes = extractInboxes({
|
2104
|
+
recipients: expandedRecipients,
|
2105
|
+
preferSharedInbox: options.preferSharedInbox,
|
2106
|
+
excludeBaseUris: options.excludeBaseUris,
|
2107
|
+
});
|
2108
|
+
logger.debug("Sending activity {activityId} to inboxes:\n{inboxes}", {
|
2109
|
+
inboxes: globalThis.Object.keys(inboxes),
|
2110
|
+
activityId: activity.id?.href,
|
2111
|
+
activity,
|
2112
|
+
});
|
2113
|
+
if (this.federation.fanoutQueue == null || options.immediate ||
|
2114
|
+
options.fanout === "skip" || (options.fanout ?? "auto") === "auto" &&
|
2115
|
+
globalThis.Object.keys(inboxes).length < FANOUT_THRESHOLD) {
|
2116
|
+
await this.federation.sendActivity(keys, inboxes, activity, opts);
|
2117
|
+
return;
|
2118
|
+
}
|
2119
|
+
const keyJwkPairs = await Promise.all(keys.map(async ({ keyId, privateKey }) => ({
|
2120
|
+
keyId: keyId.href,
|
2121
|
+
privateKey: await exportJwk(privateKey),
|
2122
|
+
})));
|
2123
|
+
const carrier = {};
|
2124
|
+
propagation.inject(context.active(), carrier);
|
2125
|
+
const message = {
|
2126
|
+
type: "fanout",
|
2127
|
+
id: dntShim.crypto.randomUUID(),
|
2128
|
+
baseUrl: this.origin,
|
2129
|
+
keys: keyJwkPairs,
|
2130
|
+
inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox }]) => [k, { actorIds: [...actorIds], sharedInbox }])),
|
2131
|
+
activity: await activity.toJsonLd({
|
2132
|
+
format: "compact",
|
2133
|
+
contextLoader: this.contextLoader,
|
2134
|
+
}),
|
2135
|
+
activityId: activity.id?.href,
|
2136
|
+
activityType: getTypeId(activity).href,
|
2137
|
+
collectionSync: opts.collectionSync,
|
2138
|
+
traceContext: carrier,
|
2139
|
+
};
|
2140
|
+
if (!this.federation.manuallyStartQueue) {
|
2141
|
+
this.federation._startQueueInternal(this.data);
|
2142
|
+
}
|
2143
|
+
this.federation.fanoutQueue.enqueue(message);
|
2041
2144
|
}
|
2042
2145
|
async *getFollowers(identifier) {
|
2043
2146
|
if (this.federation.followersCallbacks == null) {
|