Unverified Commit edee829b authored by Hong Minhee's avatar Hong Minhee
Browse files

Make followers collection synchronization optional and off by default

parent 8b01be35
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -31,6 +31,15 @@ To be released.
     -  Added `FederationOrigin` interface.
     -  Added `Context.canonicalOrigin` property.

 -  Followers collection synchronization ([FEP-8fcf]) is now turned off by
    default.

     -  Added `SendActivityOptionsForCollection` interface.
     -  The type of `Context.sendActivity({ identifier: string } | { username:
        string } | { handle: string }, "followers", Activity)` overload's
        fourth parameter became `SendActivityOptionsForCollection | undefined`
        (was `SendActivityOptions | undefined`).

 -  Fedify now accepts PEM-PKCS#1 besides PEM-SPKI for RSA public keys.
    [[#209]]

@@ -102,6 +111,7 @@ To be released.
[#220]: https://github.com/fedify-dev/fedify/issues/220
[#221]: https://github.com/fedify-dev/fedify/issues/221
[#223]: https://github.com/fedify-dev/fedify/pull/223
[FEP-8fcf]: https://w3id.org/fep/8fcf
[multibase]: https://github.com/multiformats/js-multibase


+29 −16
Original line number Diff line number Diff line
@@ -262,6 +262,15 @@ await ctx.sendActivity(
> an array of `SenderKeyPair` objects.  You need to specify the recipients
> manually in this case.

> [!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).


Specifying an activity
----------------------
@@ -593,7 +602,8 @@ async function sendNote(
Followers collection synchronization
------------------------------------

*This API is available since Fedify 0.8.0.*
*This API is available since Fedify 0.8.0, and it is optional since
Fedify 1.5.0.*

> [!NOTE]
> For efficiency, you should implement
@@ -612,7 +622,8 @@ so that the recipient server can check if it needs to resynchronize
the followers collection.  Fedify provides a way to include the digest
of the followers collection in the activity delivery request by specifying
the recipients parameter of the `~Context.sendActivity()` method as
the `"followers"` string:
the `"followers"` string and turning on
the `~SendActivityOptionsForCollection.syncCollection` option:

~~~~ typescript twoslash
import { type Context, Create, Note } from "@fedify/fedify";
@@ -630,13 +641,17 @@ await ctx.sendActivity(
      to: ctx.getFollowersUri(senderId),
    }),
  }),
  { preferSharedInbox: true },  // [!code highlight]
  {
    preferSharedInbox: true,  // [!code highlight]
    syncCollection: true,  // [!code highlight]
  },
);
~~~~

If you specify the `"followers"` string as the recipients parameter,
it automatically sends the activity to the sender's followers and includes
the digest of the followers collection in the payload.
The `~SendActivityOptionsForCollection.syncCollection` option is only available
when you specify the `"followers"` string as the recipients parameter.  With
turning on this option, it automatically sends the activity to the sender's
followers and includes the digest of the followers collection in the payload.

> [!NOTE]
> The `to` and `cc` properties of an `Activity` and its `object` should be set
@@ -645,14 +660,12 @@ 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).
> [!NOTE]
> Some history of this feature: The followers collection synchronization was
> first introduced in Fedify 0.8.0, but it was automatically turned on when
> the recipients parameter was set to the `"followers"` string then.
> Since Fedify 1.5.0, it is optional, and you need to explicitly turn on
> the `~SendActivityOptionsForCollection.syncCollection` option to use it.

[FEP-8fcf]: https://w3id.org/fep/8fcf

+15 −1
Original line number Diff line number Diff line
@@ -360,7 +360,7 @@ export interface Context<TContextData> {
    sender: { identifier: string } | { username: string } | { handle: string },
    recipients: "followers",
    activity: Activity,
    options?: SendActivityOptions,
    options?: SendActivityOptionsForCollection,
  ): Promise<void>;

  /**
@@ -697,6 +697,20 @@ export interface SendActivityOptions {
  excludeBaseUris?: URL[];
}

/**
 * Options for {@link Context.sendActivity} method when sending to a collection.
 * @since 1.5.0
 */
export interface SendActivityOptionsForCollection extends SendActivityOptions {
  /**
   * Whether to synchronize the collection using `Collection-Synchronization`
   * header ([FEP-8fcf]).
   *
   * [FEP-8fcf]: https://w3id.org/fep/8fcf
   */
  syncCollection?: boolean;
}

/**
 * Options for {@link InboxContext.forwardActivity} method.
 * @since 1.0.0
+64 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ import {
  assertEquals,
  assertFalse,
  assertInstanceOf,
  assertNotEquals,
  assertRejects,
  assertStrictEquals,
  assertThrows,
@@ -1456,9 +1457,11 @@ test("ContextImpl.sendActivity()", async (t) => {

  let verified: ("http" | "ld" | "proof")[] | null = null;
  let request: Request | null = null;
  let collectionSyncHeader: string | null = null;
  mf.mock("POST@/inbox", async (req) => {
    verified = [];
    request = req.clone();
    collectionSyncHeader = req.headers.get("Collection-Synchronization");
    const options = {
      async documentLoader(url: string) {
        const response = await federation.fetch(
@@ -1541,6 +1544,18 @@ test("ContextImpl.sendActivity()", async (t) => {
    })
    .mapHandle((_ctx, username) => username === "john" ? "1" : null);

  federation.setFollowersDispatcher(
    "/users/{identifier}/followers",
    () => ({
      items: [
        {
          id: new URL("https://example.com/recipient"),
          inboxId: new URL("https://example.com/inbox"),
        },
      ],
    }),
  );

  await t.step("success", async () => {
    const activity = new Create({
      actor: new URL("https://example.com/person"),
@@ -1816,6 +1831,55 @@ test("ContextImpl.sendActivity()", async (t) => {
      },
    ]);
  });

  collectionSyncHeader = null;

  await t.step("followers collection without syncCollection", async () => {
    const ctx = new ContextImpl({
      data: undefined,
      federation,
      url: new URL("https://example.com/"),
      documentLoader: fetchDocumentLoader,
      contextLoader: fetchDocumentLoader,
    });

    const activity = new Create({
      id: new URL("https://example.com/activity/1"),
      actor: ctx.getActorUri("1"),
      to: ctx.getFollowersUri("1"),
    });

    await ctx.sendActivity({ identifier: "1" }, "followers", activity);

    assertEquals(collectionSyncHeader, null);
  });

  collectionSyncHeader = null;

  await t.step("followers collection with syncCollection", async () => {
    const ctx = new ContextImpl({
      data: undefined,
      federation,
      url: new URL("https://example.com/"),
      documentLoader: fetchDocumentLoader,
      contextLoader: fetchDocumentLoader,
    });

    const activity = new Create({
      id: new URL("https://example.com/activity/2"),
      actor: ctx.getActorUri("1"),
      to: ctx.getFollowersUri("1"),
    });

    await ctx.sendActivity(
      { identifier: "1" },
      "followers",
      activity,
      { syncCollection: true, preferSharedInbox: true },
    );

    assertNotEquals(collectionSyncHeader, null);
  });
});

test("ContextImpl.routeActivity()", async () => {
+12 −10
Original line number Diff line number Diff line
@@ -83,7 +83,7 @@ import type {
  ParseUriResult,
  RequestContext,
  RouteActivityOptions,
  SendActivityOptions,
  SendActivityOptionsForCollection,
} from "./context.ts";
import type {
  ActorCallbackSetters,
@@ -3221,7 +3221,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
      | { handle: string },
    recipients: Recipient | Recipient[] | "followers",
    activity: Activity,
    options: SendActivityOptions = {},
    options: SendActivityOptionsForCollection = {},
  ): Promise<void> {
    const tracer = this.tracerProvider.getTracer(
      metadata.name,
@@ -3274,7 +3274,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
      | { handle: string },
    recipients: Recipient | Recipient[] | "followers",
    activity: Activity,
    options: SendActivityOptions = {},
    options: SendActivityOptionsForCollection,
    span: Span,
  ): Promise<void> {
    const logger = getLogger(["fedify", "federation", "outbox"]);
@@ -3350,6 +3350,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
      ) {
        expandedRecipients.push(recipient);
      }
      if (options.syncCollection) {
        const collectionId = this.federation.router.build(
          "followers",
          { identifier, handle: identifier },
@@ -3357,6 +3358,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
        opts.collectionSync = collectionId == null
          ? undefined
          : new URL(collectionId, this.canonicalOrigin).href;
      }
    } else {
      expandedRecipients = [recipients];
    }