Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An initial implementation of the cache API #428

Merged
merged 3 commits into from
Nov 9, 2022
Merged

Conversation

penalosa
Copy link
Contributor

@penalosa penalosa commented Nov 8, 2022

To test this, build workerd on the expose-cache-api branch, copy the binary into the right directory within node_modules for your system, and run miniflare with cache: true. The following test worker should demonstrate the cache behaviour:

test-cache.worker.js
export default {
  async fetch(request) {
    let res = "Running test cases...\n";
    const assert = (cond, message) => {
      if (!cond) {
        res += "❌ " + message + "\n";
      } else {
        res += "✅ " + message + "\n";
      }
    };
    const put = async (
      str,
      { df = null, reqHeaders = {}, resHeaders = {}, url = "" } = {}
    ) => {
      await (df == null ? caches.default : await caches.open(df)).put(
        new Request("https://example.com" + url, { headers: reqHeaders }),
        new Response(str, { headers: resHeaders })
      );
    };
    const del = async ({ df = null, reqHeaders = {}, url = "" } = {}) => {
      await (df == null ? caches.default : await caches.open(df)).delete(
        new Request("https://example.com" + url, { headers: reqHeaders })
      );
    };
    const get = async ({ df = null, reqHeaders = {}, url = "" } = {}) => {
      return await (df == null ? caches.default : await caches.open(df)).match(
        new Request("https://example.com" + url, { headers: reqHeaders })
      );
    };
    const test = async (name, cb) => {
      let incr = 1;
      let assertions = [];
      await cb((cond) => assertions.push(cond));
      if (assertions.length === 1) {
        assert(assertions[0], name);
      } else {
        assertions.forEach((cond) => {
          assert(cond, `(#${incr++}) ${name}`);
        });
      }
    };
    // Tests
    await test("Caches are not shared between namespaces", async (assert) => {
      await put("cached", { url: "/ns" });
      assert((await get({ url: "/ns", df: "test" })) === undefined);
    });

    await test("Cache item persisted", async (assert) => {
      await put("test-cached");
      assert((await get().then((r) => r.text())) === "test-cached");
    });

    await test("Etag is persisted", async (assert) => {
      await put("test-etag", { resHeaders: { ETag: "abc" } });
      assert((await get()).headers.get("ETag") === "abc");
    });

    await test("Expires is persisted", async (assert) => {
      await put("expires", { resHeaders: { Expires: "abc" } });
      assert((await get()).headers.get("Expires") === "abc");
    });

    await test("Last-Modified is persisted", async (assert) => {
      await put("last-modified", { resHeaders: { "Last-Modified": "abc" } });
      assert((await get()).headers.get("Last-Modified") === "abc");
    });

    await test("Cache-Control is persisted", async (assert) => {
      await put("cache-control", { resHeaders: { "Cache-Control": "abc" } });
      assert((await get()).headers.get("Cache-Control") === "abc");
    });

    await test("Set-Cookie causes no storage", async (assert) => {
      await put("set-cookie", {
        url: "/set-cookie",
        resHeaders: { "Set-Cookie": "abc=cde" },
      });
      assert(!(await get({ url: "/set-cookie" })));
    });

    await test("Set-Cookie is filtered with private=Set-Cookie", async (assert) => {
      await put("set-cookie-filter", {
        url: "/set-cookie-filter",
        resHeaders: {
          "Set-Cookie": "abc=cde",
          "Cache-Control": "private=Set-Cookie",
        },
      });
      assert(
        (await get({ url: "/set-cookie-filter" }).then((r) => r.text())) ===
          "set-cookie-filter"
      );
    });

    await test("Etag If-None-Match matches", async (assert) => {
      await put("test-etag", {
        url: "/etag-if-none-match",
        resHeaders: { ETag: '"abc"' },
      });
      assert(
        (
          await get({
            url: "/etag-if-none-match",
            reqHeaders: {
              "If-None-Match": '"abc"',
            },
          })
        ).status === 304
      );
    });

    await test("Etag If-None-Match doesn't match", async (assert) => {
      await put("test-etag", {
        url: "/etag-if-none-match-mismatch",
        resHeaders: { ETag: '"abc"' },
      });

      assert(
        (
          await get({
            url: "/etag-if-none-match-mismatch",
            reqHeaders: {
              "If-None-Match": '"cde"',
            },
          })
        ).headers.get("Cf-Cache-Status") == "HIT"
      );
    });

    await test("Last-Modified If-Modified-Since too late", async (assert) => {
      await put("last-modified", {
        url: "/last-modified-too-late",
        resHeaders: { "Last-Modified": "Wed, 21 Oct 2015 07:28:00 GMT" },
      });

      assert(
        (
          await get({
            url: "/last-modified-too-late",
            reqHeaders: {
              "If-Modified-Since": "Wed, 21 Oct 2016 07:28:00 GMT",
            },
          })
        ).status == 304
      );
    });
    await test("Last-Modified If-Modified-Since in time", async (assert) => {
      await put("last-modified", {
        url: "/last-modified-in-time",
        resHeaders: { "Last-Modified": "Wed, 21 Oct 2015 07:28:00 GMT" },
      });

      assert(
        (
          await get({
            url: "/last-modified-in-time",
            reqHeaders: {
              "If-Modified-Since": "Wed, 21 Oct 2014 07:28:00 GMT",
            },
          })
        ).headers.get("Cf-Cache-Status") == "HIT"
      );
    });

    await test("Range 1-", async (assert) => {
      await put("range", {
        url: "/range",
      });

      assert(
        (await get({
          url: "/range",
          reqHeaders: {
            Range: "bytes=1-",
          },
        }).then((r) => r.text())) === "ange"
      );
    });

    await test("Cache item is gone after purge ", async (assert) => {
      await put("test-cached-del");
      assert((await get().then((r) => r.text())) === "test-cached-del");
      await del();
      assert((await get()) === undefined);
    });

    return new Response(res);
  },
};

@changeset-bot
Copy link

changeset-bot bot commented Nov 8, 2022

⚠️ No Changeset found

Latest commit: 4fecda3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@penalosa penalosa changed the base branch from master to tre November 8, 2022 19:15
@penalosa penalosa requested a review from mrbbot November 8, 2022 19:16
Copy link
Contributor

@mrbbot mrbbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! 😃 Added some comments, but looks really good, and especially like the README for the cache plugin.

packages/tre/package.json Outdated Show resolved Hide resolved
packages/tre/src/plugins/cache/README.md Show resolved Hide resolved
}
}

export class CacheError extends Error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do this for R2Error too, but this should probably extend MiniflareError (or HttpError?), and use code instead of status. MiniflareError will automatically set the name correctly too based off new.target.

Copy link
Contributor Author

@penalosa penalosa Nov 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied this over from R2Error, but the reason it uses status rather than code is because it is the http status code. R2Error has another property v4Code (or v4code depending on context, the API is maddeningly inconsistent), so using code there would be a bit confusing, I think. For consistency, it might make sense to keep this as status. I'll have a look at extending from MiniflareError though

packages/tre/src/plugins/cache/errors.ts Outdated Show resolved Hide resolved
import { Clock } from "../../shared";
import crypto from "crypto";
import { AddressInfo } from "net";
import http from "node:http";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nit-picky, but given we're not using node: prefixes throughout the rest of the codebase (we probably should tbh), could we change this to just http? Whatever we end up deciding, probably best to add an eslint rule.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about adding an eslint rule to enforce it? (In a separate PR, so as not to mess up the diff of this one?) I'll remove it for this PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that sounds good, I'd make the switch to node: in that PR too. 👍

packages/tre/src/plugins/cache/index.ts Outdated Show resolved Hide resolved
packages/tre/src/plugins/cache/router.ts Outdated Show resolved Hide resolved
packages/tre/src/plugins/cache/router.ts Outdated Show resolved Hide resolved
packages/tre/src/plugins/cache/router.ts Show resolved Hide resolved
packages/tre/src/runtime/config/workerd.capnp Show resolved Hide resolved
@penalosa penalosa requested a review from mrbbot November 9, 2022 13:37
packages/tre/src/plugins/cache/gateway.ts Outdated Show resolved Hide resolved
packages/tre/src/plugins/cache/gateway.ts Outdated Show resolved Hide resolved
@penalosa penalosa requested a review from mrbbot November 9, 2022 14:58
@penalosa
Copy link
Contributor Author

penalosa commented Nov 9, 2022

@mrbbot is this good to go?

Copy link
Contributor

@mrbbot mrbbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep! 😊 LGTM!

@mrbbot mrbbot merged commit be72411 into tre Nov 9, 2022
@mrbbot mrbbot deleted the penalosa/support-cache-api branch November 9, 2022 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants