@@ -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 */
840840async 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.*
0 commit comments