Unverified Commit 2250de95 authored by Hong Minhee's avatar Hong Minhee
Browse files

Merge pull request #315 from r-4bb1t/feat/51-use-signal-in-lookup

parents 3837a3a6 85fe2430
Loading
Loading
Loading
Loading
+14 −3
Original line number Diff line number Diff line
@@ -91,6 +91,16 @@ the versioning.
     -  Added `@fedify/nestjs` package.
     -  Added `FedifyModule` for integrating Fedify into NestJS applications.

 -  APIs making HTTP requests became able to optionally take `AbortSignal`.
    [[#51], [#315] by Hyunchae Kim]

     -  Added `DocumentLoaderOptions` interface.
     -  The `DocumentLoader` type became able to optionally take
        the second parameter.
     -  Added `LookupObjectOptions.signal` option.
     -  Added `LookupWebFingerOptions.signal` option.
     -  Added `DoubleKnockOptions.signal` option.

 -  Added `SqliteKvStore`, implementing `KvStore` using SQLite with the
    `@fedify/sqlite` package. Compatible with Bun, Deno, and Node.js.
    [[#274], [#318] by An Subin]
@@ -146,6 +156,7 @@ the versioning.
[#263]: https://github.com/fedify-dev/fedify/issues/263
[#267]: https://github.com/fedify-dev/fedify/issues/267
[#269]: https://github.com/fedify-dev/fedify/issues/269
[#274]: https://github.com/fedify-dev/fedify/issues/274
[#278]: https://github.com/fedify-dev/fedify/pull/278
[#281]: https://github.com/fedify-dev/fedify/pull/281
[#282]: https://github.com/fedify-dev/fedify/pull/282
@@ -154,10 +165,10 @@ the versioning.
[#298]: https://github.com/fedify-dev/fedify/pull/298
[#304]: https://github.com/fedify-dev/fedify/issues/304
[#309]: https://github.com/fedify-dev/fedify/pull/309
[#274]: https://github.com/fedify-dev/fedify/issues/274
[#318]: https://github.com/fedify-dev/fedify/pull/318
[#310]: https://github.com/fedify-dev/fedify/issues/310
[#311]: https://github.com/fedify-dev/fedify/issues/311
[#315]: https://github.com/fedify-dev/fedify/pull/315
[#318]: https://github.com/fedify-dev/fedify/pull/318
[#321]: https://github.com/fedify-dev/fedify/pull/321
[#328]: https://github.com/fedify-dev/fedify/pull/328
[#331]: https://github.com/fedify-dev/fedify/pull/331
@@ -165,6 +176,7 @@ the versioning.
[#341]: https://github.com/fedify-dev/fedify/pull/341
[#342]: https://github.com/fedify-dev/fedify/pull/342
[#348]: https://github.com/fedify-dev/fedify/pull/348
[#51]: https://github.com/fedify-dev/fedify/issues/51
[Kitty]: https://sw.kovidgoyal.net/kitty/
[WezTerm]: https://wezterm.org/
[Konsole]: https://konsole.kde.org/
@@ -173,7 +185,6 @@ the versioning.
[st]: https://st.suckless.org/
[iTerm]: https://iterm2.com/


Version 1.7.7
-------------

+66 −0
Original line number Diff line number Diff line
@@ -57,3 +57,69 @@ test("getAuthenticatedDocumentLoader()", async (t) => {
    assertRejects(() => loader("http://localhost"), UrlError);
  });
});

test("getAuthenticatedDocumentLoader() cancellation", {
  sanitizeResources: false,
  sanitizeOps: false,
}, async (t) => {
  fetchMock.spyGlobal();

  await t.step("document loader cancellation", async () => {
    fetchMock.get(
      "https://example.com/slow-object",
      () =>
        new Promise((resolve) => {
          setTimeout(() => {
            resolve({
              status: 200,
              headers: { "Content-Type": "application/activity+json" },
              body: {
                "@context": "https://www.w3.org/ns/activitystreams",
                type: "Note",
                content: "Slow response",
              },
            });
          }, 1000);
        }),
    );

    const loader = getAuthenticatedDocumentLoader({
      keyId: new URL("https://example.com/key2"),
      privateKey: rsaPrivateKey2,
    });

    const controller = new AbortController();
    const promise = loader("https://example.com/slow-object", {
      signal: controller.signal,
    });

    controller.abort();

    await assertRejects(
      () => promise,
      Error,
    );

    await assertRejects(
      () => loader("https://example.com/object", { signal: controller.signal }),
      Error,
    );
  });

  await t.step("immediate cancellation", async () => {
    const loader = getAuthenticatedDocumentLoader({
      keyId: new URL("https://example.com/key2"),
      privateKey: rsaPrivateKey2,
    });

    const controller = new AbortController();
    controller.abort();

    await assertRejects(
      () => loader("https://example.com/object", { signal: controller.signal }),
      Error,
    );
  });

  fetchMock.hardReset();
});
+11 −2
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@ import {
  createRequest,
  type DocumentLoader,
  type DocumentLoaderFactoryOptions,
  type DocumentLoaderOptions,
  getRemoteDocument,
  logRequest,
  type RemoteDocument,
@@ -58,7 +59,10 @@ export function getAuthenticatedDocumentLoader(
    GetAuthenticatedDocumentLoaderOptions = {},
): DocumentLoader {
  validateCryptoKey(identity.privateKey);
  async function load(url: string): Promise<RemoteDocument> {
  async function load(
    url: string,
    options?: DocumentLoaderOptions,
  ): Promise<RemoteDocument> {
    if (!allowPrivateAddress) {
      try {
        await validatePublicUrl(url);
@@ -73,7 +77,12 @@ export function getAuthenticatedDocumentLoader(
    const response = await doubleKnock(
      originalRequest,
      identity,
      { specDeterminer, log: logRequest, tracerProvider },
      {
        specDeterminer,
        log: logRequest,
        tracerProvider,
        signal: options?.signal,
      },
    );
    return getRemoteDocument(url, response, load);
  }
+47 −10
Original line number Diff line number Diff line
@@ -29,12 +29,28 @@ export interface RemoteDocument {
  documentUrl: string;
}

/**
 * Options for {@link DocumentLoader}.
 * @since 1.8.0
 */
export interface DocumentLoaderOptions {
  /**
   * An `AbortSignal` for cancellation.
   * @since 1.8.0
   */
  signal?: AbortSignal;
}

/**
 * A JSON-LD document loader that fetches documents from the Web.
 * @param url The URL of the document to load.
 * @param options The options for the document loader.
 * @returns The loaded remote document.
 */
export type DocumentLoader = (url: string) => Promise<RemoteDocument>;
export type DocumentLoader = (
  url: string,
  options?: DocumentLoaderOptions,
) => Promise<RemoteDocument>;

/**
 * A factory function that creates a {@link DocumentLoader} with options.
@@ -163,7 +179,10 @@ export function logRequest(request: Request) {
export async function getRemoteDocument(
  url: string,
  response: Response,
  fetch: (url: string) => Promise<RemoteDocument>,
  fetch: (
    url: string,
    options?: DocumentLoaderOptions,
  ) => Promise<RemoteDocument>,
): Promise<RemoteDocument> {
  const documentUrl = response.url === "" ? url : response.url;
  const docUrl = new URL(documentUrl);
@@ -311,7 +330,11 @@ export function getDocumentLoader(
  { allowPrivateAddress, skipPreloadedContexts, userAgent }:
    GetDocumentLoaderOptions = {},
): DocumentLoader {
  async function load(url: string): Promise<RemoteDocument> {
  async function load(
    url: string,
    options?: DocumentLoaderOptions,
  ): Promise<RemoteDocument> {
    options?.signal?.throwIfAborted();
    if (!skipPreloadedContexts && url in preloadedContexts) {
      logger.debug("Using preloaded context: {url}.", { url });
      return {
@@ -337,13 +360,14 @@ export function getDocumentLoader(
      // to work around it we specify `redirect: "manual"` here too:
      // https://github.com/oven-sh/bun/issues/10754
      redirect: "manual",
      signal: options?.signal,
    });
    // Follow redirects manually to get the final URL:
    if (
      response.status >= 300 && response.status < 400 &&
      response.headers.has("Location")
    ) {
      return load(response.headers.get("Location")!);
      return load(response.headers.get("Location")!, options);
    }
    return getRemoteDocument(url, response, load);
  }
@@ -375,15 +399,25 @@ const _fetchDocumentLoader_allowPrivateAddress = getDocumentLoader({
 */
export function fetchDocumentLoader(
  url: string,
  allowPrivateAddress: boolean = false,
  allowPrivateAddress?: boolean,
): Promise<RemoteDocument>;
export function fetchDocumentLoader(
  url: string,
  options?: DocumentLoaderOptions,
): Promise<RemoteDocument>;
export function fetchDocumentLoader(
  url: string,
  arg: boolean | DocumentLoaderOptions = false,
): Promise<RemoteDocument> {
  const allowPrivateAddress = typeof arg === "boolean" ? arg : false;
  logger.warn(
    "fetchDocumentLoader() function is deprecated.  " +
      "Use getDocumentLoader() function instead.",
  );
  return (allowPrivateAddress
  const loader = allowPrivateAddress
    ? _fetchDocumentLoader_allowPrivateAddress
    : _fetchDocumentLoader)(url);
    : _fetchDocumentLoader;
  return loader(url);
}

/**
@@ -455,9 +489,12 @@ export function kvCache(
    return null;
  }

  return async (url: string): Promise<RemoteDocument> => {
  return async (
    url: string,
    options?: DocumentLoaderOptions,
  ): Promise<RemoteDocument> => {
    const match = matchRule(url);
    if (match == null) return await loader(url);
    if (match == null) return await loader(url, options);
    const key: KvKey = [...keyPrefix, url];
    let cache: RemoteDocument | undefined = undefined;
    try {
@@ -471,7 +508,7 @@ export function kvCache(
      }
    }
    if (cache == null) {
      const remoteDoc = await loader(url);
      const remoteDoc = await loader(url, options);
      try {
        await kv.set(key, remoteDoc, { ttl: match });
      } catch (error) {
+70 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ import {
  assertEquals,
  assertExists,
  assertFalse,
  assertRejects,
  assertStringIncludes,
  assertThrows,
} from "@std/assert";
@@ -1975,3 +1976,72 @@ test("doubleKnock() regression test for redirect handling bug", async () => {

  fetchMock.hardReset();
});

test("signRequest() and verifyRequest() cancellation", {
  sanitizeResources: false,
  sanitizeOps: false,
}, async (t) => {
  fetchMock.spyGlobal();

  await t.step("doubleKnock cancellation", async () => {
    fetchMock.post(
      "https://example.com/slow-endpoint",
      () =>
        new Promise((resolve) => {
          setTimeout(() => {
            resolve(new Response("", { status: 202 }));
          }, 1000);
        }),
    );

    const request = new Request("https://example.com/slow-endpoint", {
      method: "POST",
      body: "Test message",
      headers: {
        "Content-Type": "text/plain",
      },
    });

    const controller = new AbortController();
    const promise = doubleKnock(
      request,
      {
        keyId: rsaPublicKey2.id!,
        privateKey: rsaPrivateKey2,
      },
      { signal: controller.signal },
    );

    controller.abort();

    await assertRejects(
      () => promise,
      Error,
    );
  });

  await t.step("doubleKnock immediate cancellation", async () => {
    const request = new Request("https://example.com/", {
      method: "POST",
      body: "Hello, world!",
      headers: {
        "Content-Type": "text/plain; charset=utf-8",
        Accept: "text/plain",
      },
    });

    const controller = new AbortController();
    controller.abort();

    await assertRejects(
      () =>
        doubleKnock(request, {
          keyId: rsaPublicKey2.id!,
          privateKey: rsaPrivateKey2,
        }, { signal: controller.signal }),
      Error,
    );
  });

  fetchMock.hardReset();
});
Loading