Skip to content

Commit

Permalink
Docs: One-shot recipients gathering
Browse files Browse the repository at this point in the history
  • Loading branch information
dahlia committed Dec 23, 2024
1 parent ac4f177 commit 56ee718
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 21 deletions.
Binary file modified docs/bun.lockb
Binary file not shown.
132 changes: 129 additions & 3 deletions docs/manual/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -826,9 +826,9 @@ interface GetFollowersByUserIdOptions {
*/
cursor?: string | null;
/**
* The number of items per page.
* The number of items per page. If `null`, the entire collection is returned.
*/
limit: number;
limit?: number | null;
}
/**
* A hypothetical function that returns the actors that are following an actor.
Expand All @@ -839,7 +839,7 @@ interface GetFollowersByUserIdOptions {
*/
async function getFollowersByUserId(
userId: string,
options: GetFollowersByUserIdOptions,
options: GetFollowersByUserIdOptions = {},
): Promise<ResultSet> {
return { users: [], nextCursor: null, last: true };
}
Expand Down Expand Up @@ -874,6 +874,132 @@ federation
> Every `Actor` object is also a `Recipient` object, so you can use the `Actor`
> object as the `Recipient` object.
### One-shot followers collection for gathering recipients

When you invoke `Context.sendActivity()` method with setting the `recipients`
parameter to `"followers"`, Fedify automatically gathers the recipients from
the followers collection. In this case, the followers collection dispatcher
is not called by remote servers, but it's called in the same process.
Therefore, you don't have much merit to paginate the followers collection,
but instead you would want to gather all the followers at once.

Under the hood, the `Context.sendActivity()` method tries to gather the
recipients by calling the followers collection dispatcher with the `cursor`
parameter set to `null`. However, if the followers collection dispatcher
returns `null`, the method treats it as a signal that the followers collection
is always paginated, and it gather the recipients by paginating the followers
collection with multiple invocation of the followers collection dispatcher.
If the followers collection dispatcher returns an object that contains
the entire followers collection, the method gathers the recipients at once.

Therefore, if you use `"followers"` as the `recipients` parameter of
the `Context.sendActivity()` method, you should return the entire followers
collection when the `cursor` parameter is `null`:

~~~~ typescript{5-17} twoslash
import type { Federation, Recipient } from "@fedify/fedify";
const federation = null as unknown as Federation<void>;
/**
* A hypothetical type that represents an actor in the database.
*/
interface User {
/**
* The URI of the actor.
*/
uri: string;
/**
* The inbox URI of the actor.
*/
inboxUri: string;
}
/**
* A hypothetical type that represents the result set of the actors that
* are following an actor.
*/
interface ResultSet {
/**
* The actors that are following the actor.
*/
users: User[];
/**
* The next cursor that represents the position of the next page.
*/
nextCursor: string | null;
/**
* Whether the current page is the last page.
*/
last: boolean;
}
/**
* A hypothetical type that represents the options for
* the `getFollowersByUserId` function.
*/
interface GetFollowersByUserIdOptions {
/**
* The cursor that represents the position of the current page.
*/
cursor?: string | null;
/**
* The number of items per page. If `null`, the entire collection is returned.
*/
limit?: number | null;
}
/**
* A hypothetical function that returns the actors that are following an actor.
* @param userId The actor's identifier.
* @param options The options for the query.
* @returns The actors that are following the actor, the next cursor, and
* whether the current page is the last page.
*/
async function getFollowersByUserId(
userId: string,
options: GetFollowersByUserIdOptions = {},
): Promise<ResultSet> {
return { users: [], nextCursor: null, last: true };
}
// ---cut-before---
federation
.setFollowersDispatcher(
"/users/{identifier}/followers",
async (ctx, identifier, cursor) => {
// If a whole collection is requested, returns the entire collection
// instead of paginating it, as we prefer one-shot gathering:
if (cursor == null) {
// Work with the database to find the actors that are following the actor
// (the below `getFollowersByUserId` is a hypothetical function):
const { users } = await getFollowersByUserId(identifier);
return {
items: users.map(actor => ({
id: new URL(actor.uri),
inboxId: new URL(actor.inboxUri),
})),
};
}
const { users, nextCursor, last } = await getFollowersByUserId(
identifier,
cursor === "" ? { limit: 10 } : { cursor, limit: 10 }
);
// Turn the users into `Recipient` objects:
const items: Recipient[] = users.map(actor => ({
id: new URL(actor.uri),
inboxId: new URL(actor.inboxUri),
}));
return { items, nextCursor: last ? null : nextCursor };
}
)
// The first cursor is an empty string:
.setFirstCursor(async (ctx, identifier) => "");
~~~~

> [!CAUTION]
> The common pitfall is that the followers collection dispatcher returns
> the first page of the followers collection when the `cursor` parameter is
> `null`. If the followers collection dispatcher returns only the first page
> when the `cursor` parameter is `null`, the `Context.sendActivity()` method
> will treat it as the entire followers collection, and it will not gather
> the rest of the followers collection. Therefore, it will send the activity
> only to the followers in the first page. Watch out for this pitfall.
### Filtering by server

*This API is available since Fedify 0.8.0.*
Expand Down
9 changes: 9 additions & 0 deletions docs/manual/send.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,15 @@ the digest of the followers collection in the payload.
> the `PUBLIC_COLLECTION`, the activity is visible to everyone regardless of
> the recipients parameter.
> [!TIP]
> Does the `Context.sendActivity()` method takes quite a long time to complete
> even if you configured the [`queue`](./federation.md#queue)? It might be
> because the followers collection is large and the method under the hood
> invokes your [followers collection dispatcher](./collections.md#followers)
> multiple times to paginate the collection. To improve the performance,
> you should implement the [one-short followers collection for gathering
> recipients](./collections.md#one-shot-followers-collection-for-gathering-recipients).
[FEP-8fcf]: https://w3id.org/fep/8fcf


Expand Down
36 changes: 18 additions & 18 deletions docs/package.json
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
{
"devDependencies": {
"@braintree/sanitize-url": "^7.1.0",
"@deno/kv": "^0.8.2",
"@fedify/amqp": "0.1.0-dev.8",
"@fedify/fedify": "^1.3.1",
"@braintree/sanitize-url": "^7.1.1",
"@deno/kv": "^0.8.4",
"@fedify/amqp": "0.1.0",
"@fedify/fedify": "^1.3.2",
"@fedify/postgres": "0.2.2",
"@fedify/redis": "0.2.0-dev.10",
"@hono/node-server": "^1.12.2",
"@fedify/redis": "0.3.0",
"@hono/node-server": "^1.13.7",
"@js-temporal/polyfill": "^0.4.4",
"@logtape/logtape": "^0.8.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.55.0",
"@opentelemetry/sdk-node": "^0.55.0",
"@sentry/node": "^8.40.0",
"@shikijs/vitepress-twoslash": "^1.17.6",
"@teidesu/deno-types": "^1.46.3",
"@types/amqplib": "^0.10.5",
"@types/better-sqlite3": "^7.6.11",
"@types/bun": "^1.1.9",
"amqplib": "^0.10.4",
"@opentelemetry/exporter-trace-otlp-proto": "^0.57.0",
"@opentelemetry/sdk-node": "^0.57.0",
"@sentry/node": "^8.47.0",
"@shikijs/vitepress-twoslash": "^1.24.4",
"@teidesu/deno-types": "^2.1.4",
"@types/amqplib": "^0.10.6",
"@types/better-sqlite3": "^7.6.12",
"@types/bun": "^1.1.14",
"amqplib": "^0.10.5",
"dayjs": "^1.11.13",
"hono": "^4.6.1",
"ioredis": "^5.4.1",
"hono": "^4.6.14",
"ioredis": "^5.4.2",
"markdown-it-abbr": "^2.0.0",
"markdown-it-deflist": "^3.0.0",
"markdown-it-footnote": "^4.0.0",
"markdown-it-jsr-ref": "0.4.1",
"mermaid": "^10.9.1",
"mermaid": "^11.4.1",
"postgres": "^3.4.5",
"stringify-entities": "^4.0.4",
"vitepress": "^1.5.0",
Expand Down

0 comments on commit 56ee718

Please sign in to comment.