Skip to content

Commit 56ee718

Browse files
committed
Docs: One-shot recipients gathering
1 parent ac4f177 commit 56ee718

File tree

4 files changed

+156
-21
lines changed

4 files changed

+156
-21
lines changed

docs/bun.lockb

32.1 KB
Binary file not shown.

docs/manual/collections.md

+129-3
Original file line numberDiff line numberDiff line change
@@ -826,9 +826,9 @@ interface GetFollowersByUserIdOptions {
826826
*/
827827
cursor?: string | null;
828828
/**
829-
* The number of items per page.
829+
* The number of items per page. If `null`, the entire collection is returned.
830830
*/
831-
limit: number;
831+
limit?: number | null;
832832
}
833833
/**
834834
* A hypothetical function that returns the actors that are following an actor.
@@ -839,7 +839,7 @@ interface GetFollowersByUserIdOptions {
839839
*/
840840
async function getFollowersByUserId(
841841
userId: string,
842-
options: GetFollowersByUserIdOptions,
842+
options: GetFollowersByUserIdOptions = {},
843843
): Promise<ResultSet> {
844844
return { users: [], nextCursor: null, last: true };
845845
}
@@ -874,6 +874,132 @@ federation
874874
> Every `Actor` object is also a `Recipient` object, so you can use the `Actor`
875875
> object as the `Recipient` object.
876876
877+
### One-shot followers collection for gathering recipients
878+
879+
When you invoke `Context.sendActivity()` method with setting the `recipients`
880+
parameter to `"followers"`, Fedify automatically gathers the recipients from
881+
the followers collection. In this case, the followers collection dispatcher
882+
is not called by remote servers, but it's called in the same process.
883+
Therefore, you don't have much merit to paginate the followers collection,
884+
but instead you would want to gather all the followers at once.
885+
886+
Under the hood, the `Context.sendActivity()` method tries to gather the
887+
recipients by calling the followers collection dispatcher with the `cursor`
888+
parameter set to `null`. However, if the followers collection dispatcher
889+
returns `null`, the method treats it as a signal that the followers collection
890+
is always paginated, and it gather the recipients by paginating the followers
891+
collection with multiple invocation of the followers collection dispatcher.
892+
If the followers collection dispatcher returns an object that contains
893+
the entire followers collection, the method gathers the recipients at once.
894+
895+
Therefore, if you use `"followers"` as the `recipients` parameter of
896+
the `Context.sendActivity()` method, you should return the entire followers
897+
collection when the `cursor` parameter is `null`:
898+
899+
~~~~ typescript{5-17} twoslash
900+
import type { Federation, Recipient } from "@fedify/fedify";
901+
const federation = null as unknown as Federation<void>;
902+
/**
903+
* A hypothetical type that represents an actor in the database.
904+
*/
905+
interface User {
906+
/**
907+
* The URI of the actor.
908+
*/
909+
uri: string;
910+
/**
911+
* The inbox URI of the actor.
912+
*/
913+
inboxUri: string;
914+
}
915+
/**
916+
* A hypothetical type that represents the result set of the actors that
917+
* are following an actor.
918+
*/
919+
interface ResultSet {
920+
/**
921+
* The actors that are following the actor.
922+
*/
923+
users: User[];
924+
/**
925+
* The next cursor that represents the position of the next page.
926+
*/
927+
nextCursor: string | null;
928+
/**
929+
* Whether the current page is the last page.
930+
*/
931+
last: boolean;
932+
}
933+
/**
934+
* A hypothetical type that represents the options for
935+
* the `getFollowersByUserId` function.
936+
*/
937+
interface GetFollowersByUserIdOptions {
938+
/**
939+
* The cursor that represents the position of the current page.
940+
*/
941+
cursor?: string | null;
942+
/**
943+
* The number of items per page. If `null`, the entire collection is returned.
944+
*/
945+
limit?: number | null;
946+
}
947+
/**
948+
* A hypothetical function that returns the actors that are following an actor.
949+
* @param userId The actor's identifier.
950+
* @param options The options for the query.
951+
* @returns The actors that are following the actor, the next cursor, and
952+
* whether the current page is the last page.
953+
*/
954+
async function getFollowersByUserId(
955+
userId: string,
956+
options: GetFollowersByUserIdOptions = {},
957+
): Promise<ResultSet> {
958+
return { users: [], nextCursor: null, last: true };
959+
}
960+
// ---cut-before---
961+
federation
962+
.setFollowersDispatcher(
963+
"/users/{identifier}/followers",
964+
async (ctx, identifier, cursor) => {
965+
// If a whole collection is requested, returns the entire collection
966+
// instead of paginating it, as we prefer one-shot gathering:
967+
if (cursor == null) {
968+
// Work with the database to find the actors that are following the actor
969+
// (the below `getFollowersByUserId` is a hypothetical function):
970+
const { users } = await getFollowersByUserId(identifier);
971+
return {
972+
items: users.map(actor => ({
973+
id: new URL(actor.uri),
974+
inboxId: new URL(actor.inboxUri),
975+
})),
976+
};
977+
}
978+
const { users, nextCursor, last } = await getFollowersByUserId(
979+
identifier,
980+
cursor === "" ? { limit: 10 } : { cursor, limit: 10 }
981+
);
982+
// Turn the users into `Recipient` objects:
983+
const items: Recipient[] = users.map(actor => ({
984+
id: new URL(actor.uri),
985+
inboxId: new URL(actor.inboxUri),
986+
}));
987+
return { items, nextCursor: last ? null : nextCursor };
988+
}
989+
)
990+
// The first cursor is an empty string:
991+
.setFirstCursor(async (ctx, identifier) => "");
992+
~~~~
993+
994+
> [!CAUTION]
995+
> The common pitfall is that the followers collection dispatcher returns
996+
> the first page of the followers collection when the `cursor` parameter is
997+
> `null`. If the followers collection dispatcher returns only the first page
998+
> when the `cursor` parameter is `null`, the `Context.sendActivity()` method
999+
> will treat it as the entire followers collection, and it will not gather
1000+
> the rest of the followers collection. Therefore, it will send the activity
1001+
> only to the followers in the first page. Watch out for this pitfall.
1002+
8771003
### Filtering by server
8781004

8791005
*This API is available since Fedify 0.8.0.*

docs/manual/send.md

+9
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,15 @@ the digest of the followers collection in the payload.
388388
> the `PUBLIC_COLLECTION`, the activity is visible to everyone regardless of
389389
> the recipients parameter.
390390
391+
> [!TIP]
392+
> Does the `Context.sendActivity()` method takes quite a long time to complete
393+
> even if you configured the [`queue`](./federation.md#queue)? It might be
394+
> because the followers collection is large and the method under the hood
395+
> invokes your [followers collection dispatcher](./collections.md#followers)
396+
> multiple times to paginate the collection. To improve the performance,
397+
> you should implement the [one-short followers collection for gathering
398+
> recipients](./collections.md#one-shot-followers-collection-for-gathering-recipients).
399+
391400
[FEP-8fcf]: https://w3id.org/fep/8fcf
392401

393402

docs/package.json

+18-18
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
{
22
"devDependencies": {
3-
"@braintree/sanitize-url": "^7.1.0",
4-
"@deno/kv": "^0.8.2",
5-
"@fedify/amqp": "0.1.0-dev.8",
6-
"@fedify/fedify": "^1.3.1",
3+
"@braintree/sanitize-url": "^7.1.1",
4+
"@deno/kv": "^0.8.4",
5+
"@fedify/amqp": "0.1.0",
6+
"@fedify/fedify": "^1.3.2",
77
"@fedify/postgres": "0.2.2",
8-
"@fedify/redis": "0.2.0-dev.10",
9-
"@hono/node-server": "^1.12.2",
8+
"@fedify/redis": "0.3.0",
9+
"@hono/node-server": "^1.13.7",
1010
"@js-temporal/polyfill": "^0.4.4",
1111
"@logtape/logtape": "^0.8.0",
12-
"@opentelemetry/exporter-trace-otlp-proto": "^0.55.0",
13-
"@opentelemetry/sdk-node": "^0.55.0",
14-
"@sentry/node": "^8.40.0",
15-
"@shikijs/vitepress-twoslash": "^1.17.6",
16-
"@teidesu/deno-types": "^1.46.3",
17-
"@types/amqplib": "^0.10.5",
18-
"@types/better-sqlite3": "^7.6.11",
19-
"@types/bun": "^1.1.9",
20-
"amqplib": "^0.10.4",
12+
"@opentelemetry/exporter-trace-otlp-proto": "^0.57.0",
13+
"@opentelemetry/sdk-node": "^0.57.0",
14+
"@sentry/node": "^8.47.0",
15+
"@shikijs/vitepress-twoslash": "^1.24.4",
16+
"@teidesu/deno-types": "^2.1.4",
17+
"@types/amqplib": "^0.10.6",
18+
"@types/better-sqlite3": "^7.6.12",
19+
"@types/bun": "^1.1.14",
20+
"amqplib": "^0.10.5",
2121
"dayjs": "^1.11.13",
22-
"hono": "^4.6.1",
23-
"ioredis": "^5.4.1",
22+
"hono": "^4.6.14",
23+
"ioredis": "^5.4.2",
2424
"markdown-it-abbr": "^2.0.0",
2525
"markdown-it-deflist": "^3.0.0",
2626
"markdown-it-footnote": "^4.0.0",
2727
"markdown-it-jsr-ref": "0.4.1",
28-
"mermaid": "^10.9.1",
28+
"mermaid": "^11.4.1",
2929
"postgres": "^3.4.5",
3030
"stringify-entities": "^4.0.4",
3131
"vitepress": "^1.5.0",

0 commit comments

Comments
 (0)