@opennextjs/cloudflare 0.5.11 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/api/cloudflare-context.d.ts +16 -5
- package/dist/api/config.d.ts +16 -43
- package/dist/api/config.js +21 -19
- package/dist/api/durable-objects/queue.d.ts +32 -0
- package/dist/api/durable-objects/queue.js +234 -0
- package/dist/api/durable-objects/queue.spec.js +290 -0
- package/dist/api/durable-objects/sharded-tag-cache.d.ts +7 -0
- package/dist/api/durable-objects/sharded-tag-cache.js +22 -0
- package/dist/api/durable-objects/sharded-tag-cache.spec.js +37 -0
- package/dist/api/overrides/incremental-cache/internal.d.ts +5 -0
- package/dist/api/{kv-cache.d.ts → overrides/incremental-cache/kv-incremental-cache.d.ts} +1 -1
- package/dist/api/{kv-cache.js → overrides/incremental-cache/kv-incremental-cache.js} +5 -5
- package/dist/api/overrides/incremental-cache/r2-incremental-cache.d.ts +17 -0
- package/dist/api/overrides/incremental-cache/r2-incremental-cache.js +61 -0
- package/dist/api/overrides/incremental-cache/regional-cache.d.ts +51 -0
- package/dist/api/overrides/incremental-cache/regional-cache.js +111 -0
- package/dist/api/overrides/queue/do-queue.d.ts +6 -0
- package/dist/api/overrides/queue/do-queue.js +15 -0
- package/dist/api/{memory-queue.d.ts → overrides/queue/memory-queue.d.ts} +3 -3
- package/dist/api/{memory-queue.js → overrides/queue/memory-queue.js} +18 -14
- package/dist/api/overrides/queue/memory-queue.spec.d.ts +1 -0
- package/dist/api/{memory-queue.spec.js → overrides/queue/memory-queue.spec.js} +20 -14
- package/dist/api/overrides/tag-cache/d1-next-tag-cache.d.ts +13 -0
- package/dist/api/overrides/tag-cache/d1-next-tag-cache.js +61 -0
- package/dist/api/{d1-tag-cache.d.ts → overrides/tag-cache/d1-tag-cache.d.ts} +3 -5
- package/dist/api/{d1-tag-cache.js → overrides/tag-cache/d1-tag-cache.js} +22 -29
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.d.ts +122 -0
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.js +247 -0
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.spec.d.ts +1 -0
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.spec.js +322 -0
- package/dist/cli/args.d.ts +13 -2
- package/dist/cli/args.js +44 -29
- package/dist/cli/build/build.d.ts +5 -1
- package/dist/cli/build/build.js +9 -19
- package/dist/cli/build/bundle-server.js +5 -13
- package/dist/cli/build/open-next/compile-cache-assets-manifest.d.ts +1 -1
- package/dist/cli/build/open-next/compile-cache-assets-manifest.js +4 -6
- package/dist/cli/build/open-next/compileDurableObjects.d.ts +2 -0
- package/dist/cli/build/open-next/compileDurableObjects.js +30 -0
- package/dist/cli/build/open-next/copyCacheAssets.js +1 -1
- package/dist/cli/build/open-next/createServerBundle.d.ts +9 -1
- package/dist/cli/build/open-next/createServerBundle.js +28 -9
- package/dist/cli/build/patches/ast/patch-vercel-og-library.js +1 -1
- package/dist/cli/build/patches/ast/vercel-og.d.ts +5 -5
- package/dist/cli/build/patches/ast/vercel-og.js +1 -1
- package/dist/cli/build/patches/ast/vercel-og.spec.js +1 -1
- package/dist/cli/build/patches/ast/webpack-runtime.js +1 -1
- package/dist/cli/build/patches/ast/webpack-runtime.spec.js +1 -1
- package/dist/cli/build/patches/plugins/build-id.d.ts +2 -2
- package/dist/cli/build/patches/plugins/build-id.js +12 -5
- package/dist/cli/build/patches/plugins/build-id.spec.js +1 -1
- package/dist/cli/build/patches/plugins/dynamic-requires.d.ts +1 -2
- package/dist/cli/build/patches/plugins/dynamic-requires.js +21 -11
- package/dist/cli/build/patches/plugins/eval-manifest.d.ts +2 -2
- package/dist/cli/build/patches/plugins/eval-manifest.js +12 -5
- package/dist/cli/build/patches/plugins/find-dir.d.ts +2 -2
- package/dist/cli/build/patches/plugins/find-dir.js +10 -5
- package/dist/cli/build/patches/plugins/instrumentation.d.ts +2 -5
- package/dist/cli/build/patches/plugins/instrumentation.js +19 -3
- package/dist/cli/build/patches/plugins/instrumentation.spec.js +1 -1
- package/dist/cli/build/patches/plugins/load-manifest.d.ts +2 -2
- package/dist/cli/build/patches/plugins/load-manifest.js +12 -5
- package/dist/cli/build/patches/plugins/next-minimal.d.ts +4 -7
- package/dist/cli/build/patches/plugins/next-minimal.js +31 -15
- package/dist/cli/build/patches/plugins/next-minimal.spec.js +1 -1
- package/dist/cli/build/patches/plugins/patch-depd-deprecations.d.ts +2 -2
- package/dist/cli/build/patches/plugins/patch-depd-deprecations.js +10 -2
- package/dist/cli/build/patches/plugins/patch-depd-deprecations.spec.js +1 -1
- package/dist/cli/build/patches/plugins/require.d.ts +2 -2
- package/dist/cli/build/patches/plugins/require.js +43 -35
- package/dist/cli/build/patches/plugins/res-revalidate.d.ts +3 -0
- package/dist/cli/build/patches/plugins/res-revalidate.js +77 -0
- package/dist/cli/build/patches/plugins/res-revalidate.spec.d.ts +1 -0
- package/dist/cli/build/patches/plugins/res-revalidate.spec.js +141 -0
- package/dist/cli/build/utils/create-config-files.d.ts +2 -2
- package/dist/cli/build/utils/create-config-files.js +3 -3
- package/dist/cli/build/utils/ensure-cf-config.js +3 -13
- package/dist/cli/commands/deploy.d.ts +5 -0
- package/dist/cli/commands/deploy.js +9 -0
- package/dist/cli/commands/populate-cache.d.ts +7 -0
- package/dist/cli/commands/populate-cache.js +78 -0
- package/dist/cli/commands/preview.d.ts +5 -0
- package/dist/cli/commands/preview.js +9 -0
- package/dist/cli/index.js +36 -9
- package/dist/cli/project-options.d.ts +5 -1
- package/dist/cli/templates/worker.d.ts +3 -4
- package/dist/cli/templates/worker.js +30 -18
- package/dist/cli/utils/run-wrangler.d.ts +18 -0
- package/dist/cli/utils/run-wrangler.js +41 -0
- package/package.json +8 -10
- package/templates/open-next.config.ts +1 -1
- package/templates/wrangler.jsonc +2 -2
- package/dist/api/kvCache.d.ts +0 -5
- package/dist/api/kvCache.js +0 -5
- package/dist/cli/build/patches/ast/util.d.ts +0 -50
- package/dist/cli/build/patches/ast/util.js +0 -65
- package/dist/cli/build/patches/ast/util.spec.js +0 -43
- package/dist/cli/build/patches/plugins/content-updater.d.ts +0 -44
- package/dist/cli/build/patches/plugins/content-updater.js +0 -55
- package/dist/cli/build/patches/plugins/fetch-cache-wait-until.d.ts +0 -14
- package/dist/cli/build/patches/plugins/fetch-cache-wait-until.js +0 -40
- package/dist/cli/build/patches/plugins/fetch-cache-wait-until.spec.js +0 -453
- package/dist/cli/templates/shims/node-fs.d.ts +0 -17
- package/dist/cli/templates/shims/node-fs.js +0 -51
- package/dist/cli/templates/shims/throw.d.ts +0 -0
- package/dist/cli/templates/shims/throw.js +0 -2
- /package/dist/api/{memory-queue.spec.d.ts → durable-objects/queue.spec.d.ts} +0 -0
- /package/dist/{cli/build/patches/ast/util.spec.d.ts → api/durable-objects/sharded-tag-cache.spec.d.ts} +0 -0
- /package/dist/{cli/build/patches/plugins/fetch-cache-wait-until.spec.d.ts → api/overrides/incremental-cache/internal.js} +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { IgnorableError } from "@opennextjs/aws/utils/error.js";
|
|
2
|
+
import { getCloudflareContext } from "../../cloudflare-context";
|
|
3
|
+
export default {
|
|
4
|
+
name: "do-queue",
|
|
5
|
+
send: async (msg) => {
|
|
6
|
+
const durableObject = getCloudflareContext().env.NEXT_CACHE_DO_QUEUE;
|
|
7
|
+
if (!durableObject)
|
|
8
|
+
throw new IgnorableError("No durable object binding for cache revalidation");
|
|
9
|
+
const id = durableObject.idFromName(msg.MessageGroupId);
|
|
10
|
+
const stub = durableObject.get(id);
|
|
11
|
+
await stub.revalidate({
|
|
12
|
+
...msg,
|
|
13
|
+
});
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -5,16 +5,16 @@ export declare const DEFAULT_REVALIDATION_TIMEOUT_MS = 10000;
|
|
|
5
5
|
*
|
|
6
6
|
* It offers basic support for in-memory de-duping per isolate.
|
|
7
7
|
*
|
|
8
|
-
* A service binding called `
|
|
8
|
+
* A service binding called `WORKER_SELF_REFERENCE` that points to your worker is required.
|
|
9
9
|
*/
|
|
10
10
|
export declare class MemoryQueue implements Queue {
|
|
11
11
|
private opts;
|
|
12
12
|
readonly name = "memory-queue";
|
|
13
|
-
revalidatedPaths:
|
|
13
|
+
revalidatedPaths: Set<string>;
|
|
14
14
|
constructor(opts?: {
|
|
15
15
|
revalidationTimeoutMs: number;
|
|
16
16
|
});
|
|
17
|
-
send({ MessageBody: { host, url },
|
|
17
|
+
send({ MessageBody: { host, url }, MessageDeduplicationId }: QueueMessage): Promise<void>;
|
|
18
18
|
}
|
|
19
19
|
declare const _default: MemoryQueue;
|
|
20
20
|
export default _default;
|
|
@@ -1,49 +1,53 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { debug, error } from "@opennextjs/aws/adapters/logger.js";
|
|
2
2
|
import { IgnorableError } from "@opennextjs/aws/utils/error.js";
|
|
3
|
-
import { getCloudflareContext } from "
|
|
3
|
+
import { getCloudflareContext } from "../../cloudflare-context";
|
|
4
4
|
export const DEFAULT_REVALIDATION_TIMEOUT_MS = 10_000;
|
|
5
5
|
/**
|
|
6
6
|
* The Memory Queue offers basic ISR revalidation by directly requesting a revalidation of a route.
|
|
7
7
|
*
|
|
8
8
|
* It offers basic support for in-memory de-duping per isolate.
|
|
9
9
|
*
|
|
10
|
-
* A service binding called `
|
|
10
|
+
* A service binding called `WORKER_SELF_REFERENCE` that points to your worker is required.
|
|
11
11
|
*/
|
|
12
12
|
export class MemoryQueue {
|
|
13
13
|
opts;
|
|
14
14
|
name = "memory-queue";
|
|
15
|
-
revalidatedPaths = new
|
|
15
|
+
revalidatedPaths = new Set();
|
|
16
16
|
constructor(opts = { revalidationTimeoutMs: DEFAULT_REVALIDATION_TIMEOUT_MS }) {
|
|
17
17
|
this.opts = opts;
|
|
18
18
|
}
|
|
19
|
-
async send({ MessageBody: { host, url },
|
|
20
|
-
const service = getCloudflareContext().env.
|
|
19
|
+
async send({ MessageBody: { host, url }, MessageDeduplicationId }) {
|
|
20
|
+
const service = getCloudflareContext().env.WORKER_SELF_REFERENCE;
|
|
21
21
|
if (!service)
|
|
22
22
|
throw new IgnorableError("No service binding for cache revalidation worker");
|
|
23
|
-
if (this.revalidatedPaths.has(
|
|
23
|
+
if (this.revalidatedPaths.has(MessageDeduplicationId))
|
|
24
24
|
return;
|
|
25
|
-
this.revalidatedPaths.
|
|
26
|
-
// force remove to allow new revalidations incase something went wrong
|
|
27
|
-
setTimeout(() => this.revalidatedPaths.delete(MessageGroupId), this.opts.revalidationTimeoutMs));
|
|
25
|
+
this.revalidatedPaths.add(MessageDeduplicationId);
|
|
28
26
|
try {
|
|
29
27
|
const protocol = host.includes("localhost") ? "http" : "https";
|
|
30
28
|
// TODO: Drop the import - https://github.com/opennextjs/opennextjs-cloudflare/issues/361
|
|
31
29
|
// @ts-ignore
|
|
32
30
|
const manifest = await import("./.next/prerender-manifest.json");
|
|
33
|
-
await service.fetch(`${protocol}://${host}${url}`, {
|
|
31
|
+
const response = await service.fetch(`${protocol}://${host}${url}`, {
|
|
34
32
|
method: "HEAD",
|
|
35
33
|
headers: {
|
|
36
34
|
"x-prerender-revalidate": manifest.preview.previewModeId,
|
|
37
35
|
"x-isr": "1",
|
|
38
36
|
},
|
|
37
|
+
// We want to timeout the revalidation to avoid hanging the queue
|
|
38
|
+
signal: AbortSignal.timeout(this.opts.revalidationTimeoutMs),
|
|
39
39
|
});
|
|
40
|
+
// Here we want at least to log when the revalidation was not successful
|
|
41
|
+
if (response.status !== 200 || response.headers.get("x-nextjs-cache") !== "REVALIDATED") {
|
|
42
|
+
error(`Revalidation failed for ${url} with status ${response.status}`);
|
|
43
|
+
}
|
|
44
|
+
debug(`Revalidation successful for ${url}`);
|
|
40
45
|
}
|
|
41
46
|
catch (e) {
|
|
42
|
-
|
|
47
|
+
error(e);
|
|
43
48
|
}
|
|
44
49
|
finally {
|
|
45
|
-
|
|
46
|
-
this.revalidatedPaths.delete(MessageGroupId);
|
|
50
|
+
this.revalidatedPaths.delete(MessageDeduplicationId);
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,11 +3,17 @@ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
|
3
3
|
import cache, { DEFAULT_REVALIDATION_TIMEOUT_MS } from "./memory-queue.js";
|
|
4
4
|
vi.mock("./.next/prerender-manifest.json", () => Promise.resolve({ preview: { previewModeId: "id" } }));
|
|
5
5
|
const mockServiceWorkerFetch = vi.fn();
|
|
6
|
-
vi.mock("
|
|
6
|
+
vi.mock("../../cloudflare-context", () => ({
|
|
7
7
|
getCloudflareContext: () => ({
|
|
8
|
-
env: {
|
|
8
|
+
env: { WORKER_SELF_REFERENCE: { fetch: mockServiceWorkerFetch } },
|
|
9
9
|
}),
|
|
10
10
|
}));
|
|
11
|
+
const generateMessageBody = ({ host, url }) => ({
|
|
12
|
+
host,
|
|
13
|
+
url,
|
|
14
|
+
eTag: "etag",
|
|
15
|
+
lastModified: Date.now(),
|
|
16
|
+
});
|
|
11
17
|
describe("MemoryQueue", () => {
|
|
12
18
|
beforeAll(() => {
|
|
13
19
|
vi.useFakeTimers();
|
|
@@ -16,17 +22,17 @@ describe("MemoryQueue", () => {
|
|
|
16
22
|
afterEach(() => vi.clearAllMocks());
|
|
17
23
|
it("should process revalidations for a path", async () => {
|
|
18
24
|
const firstRequest = cache.send({
|
|
19
|
-
MessageBody: { host: "test.local", url: "/test" },
|
|
25
|
+
MessageBody: generateMessageBody({ host: "test.local", url: "/test" }),
|
|
20
26
|
MessageGroupId: generateMessageGroupId("/test"),
|
|
21
|
-
MessageDeduplicationId: "",
|
|
27
|
+
MessageDeduplicationId: "/test",
|
|
22
28
|
});
|
|
23
29
|
vi.advanceTimersByTime(DEFAULT_REVALIDATION_TIMEOUT_MS);
|
|
24
30
|
await firstRequest;
|
|
25
31
|
expect(mockServiceWorkerFetch).toHaveBeenCalledTimes(1);
|
|
26
32
|
const secondRequest = cache.send({
|
|
27
|
-
MessageBody: { host: "test.local", url: "/test" },
|
|
33
|
+
MessageBody: generateMessageBody({ host: "test.local", url: "/test" }),
|
|
28
34
|
MessageGroupId: generateMessageGroupId("/test"),
|
|
29
|
-
MessageDeduplicationId: "",
|
|
35
|
+
MessageDeduplicationId: "/test",
|
|
30
36
|
});
|
|
31
37
|
vi.advanceTimersByTime(1);
|
|
32
38
|
await secondRequest;
|
|
@@ -34,17 +40,17 @@ describe("MemoryQueue", () => {
|
|
|
34
40
|
});
|
|
35
41
|
it("should process revalidations for multiple paths", async () => {
|
|
36
42
|
const firstRequest = cache.send({
|
|
37
|
-
MessageBody: { host: "test.local", url: "/test" },
|
|
43
|
+
MessageBody: generateMessageBody({ host: "test.local", url: "/test" }),
|
|
38
44
|
MessageGroupId: generateMessageGroupId("/test"),
|
|
39
|
-
MessageDeduplicationId: "",
|
|
45
|
+
MessageDeduplicationId: "/test",
|
|
40
46
|
});
|
|
41
47
|
vi.advanceTimersByTime(1);
|
|
42
48
|
await firstRequest;
|
|
43
49
|
expect(mockServiceWorkerFetch).toHaveBeenCalledTimes(1);
|
|
44
50
|
const secondRequest = cache.send({
|
|
45
|
-
MessageBody: { host: "test.local", url: "/test" },
|
|
51
|
+
MessageBody: generateMessageBody({ host: "test.local", url: "/test" }),
|
|
46
52
|
MessageGroupId: generateMessageGroupId("/other"),
|
|
47
|
-
MessageDeduplicationId: "",
|
|
53
|
+
MessageDeduplicationId: "/other",
|
|
48
54
|
});
|
|
49
55
|
vi.advanceTimersByTime(1);
|
|
50
56
|
await secondRequest;
|
|
@@ -53,14 +59,14 @@ describe("MemoryQueue", () => {
|
|
|
53
59
|
it("should de-dupe revalidations", async () => {
|
|
54
60
|
const requests = [
|
|
55
61
|
cache.send({
|
|
56
|
-
MessageBody: { host: "test.local", url: "/test" },
|
|
62
|
+
MessageBody: generateMessageBody({ host: "test.local", url: "/test" }),
|
|
57
63
|
MessageGroupId: generateMessageGroupId("/test"),
|
|
58
|
-
MessageDeduplicationId: "",
|
|
64
|
+
MessageDeduplicationId: "/test",
|
|
59
65
|
}),
|
|
60
66
|
cache.send({
|
|
61
|
-
MessageBody: { host: "test.local", url: "/test" },
|
|
67
|
+
MessageBody: generateMessageBody({ host: "test.local", url: "/test" }),
|
|
62
68
|
MessageGroupId: generateMessageGroupId("/test"),
|
|
63
|
-
MessageDeduplicationId: "",
|
|
69
|
+
MessageDeduplicationId: "/test",
|
|
64
70
|
}),
|
|
65
71
|
];
|
|
66
72
|
vi.advanceTimersByTime(1);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js";
|
|
2
|
+
export declare class D1NextModeTagCache implements NextModeTagCache {
|
|
3
|
+
readonly mode: "nextMode";
|
|
4
|
+
readonly name = "d1-next-mode-tag-cache";
|
|
5
|
+
hasBeenRevalidated(tags: string[], lastModified?: number): Promise<boolean>;
|
|
6
|
+
writeTags(tags: string[]): Promise<void>;
|
|
7
|
+
private getConfig;
|
|
8
|
+
protected removeBuildId(key: string): string;
|
|
9
|
+
protected getCacheKey(key: string): string;
|
|
10
|
+
protected getBuildId(): string;
|
|
11
|
+
}
|
|
12
|
+
declare const _default: D1NextModeTagCache;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { debug, error } from "@opennextjs/aws/adapters/logger.js";
|
|
2
|
+
import { RecoverableError } from "@opennextjs/aws/utils/error.js";
|
|
3
|
+
import { getCloudflareContext } from "../../cloudflare-context.js";
|
|
4
|
+
export class D1NextModeTagCache {
|
|
5
|
+
mode = "nextMode";
|
|
6
|
+
name = "d1-next-mode-tag-cache";
|
|
7
|
+
async hasBeenRevalidated(tags, lastModified) {
|
|
8
|
+
const { isDisabled, db } = this.getConfig();
|
|
9
|
+
if (isDisabled)
|
|
10
|
+
return false;
|
|
11
|
+
try {
|
|
12
|
+
const result = await db
|
|
13
|
+
.prepare(`SELECT COUNT(*) as cnt FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")}) AND revalidatedAt > ? LIMIT 1`)
|
|
14
|
+
.bind(...tags.map((tag) => this.getCacheKey(tag)), lastModified ?? Date.now())
|
|
15
|
+
.first();
|
|
16
|
+
if (!result)
|
|
17
|
+
throw new RecoverableError(`D1 select failed for ${tags}`);
|
|
18
|
+
return result.cnt > 0;
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
error(e);
|
|
22
|
+
// By default we don't want to crash here, so we return false
|
|
23
|
+
// We still log the error though so we can debug it
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async writeTags(tags) {
|
|
28
|
+
const { isDisabled, db } = this.getConfig();
|
|
29
|
+
if (isDisabled)
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
const result = await db.batch(tags.map((tag) => db
|
|
32
|
+
.prepare(`INSERT INTO revalidations (tag, revalidatedAt) VALUES (?, ?)`)
|
|
33
|
+
.bind(this.getCacheKey(tag), Date.now())));
|
|
34
|
+
if (!result)
|
|
35
|
+
throw new RecoverableError(`D1 insert failed for ${tags}`);
|
|
36
|
+
}
|
|
37
|
+
getConfig() {
|
|
38
|
+
const cfEnv = getCloudflareContext().env;
|
|
39
|
+
const db = cfEnv.NEXT_TAG_CACHE_D1;
|
|
40
|
+
if (!db)
|
|
41
|
+
debug("No D1 database found");
|
|
42
|
+
const isDisabled = !!globalThis.openNextConfig
|
|
43
|
+
.dangerous?.disableTagCache;
|
|
44
|
+
return !db || isDisabled
|
|
45
|
+
? { isDisabled: true }
|
|
46
|
+
: {
|
|
47
|
+
isDisabled: false,
|
|
48
|
+
db,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
removeBuildId(key) {
|
|
52
|
+
return key.replace(`${this.getBuildId()}/`, "");
|
|
53
|
+
}
|
|
54
|
+
getCacheKey(key) {
|
|
55
|
+
return `${this.getBuildId()}/${key}`.replaceAll("//", "/");
|
|
56
|
+
}
|
|
57
|
+
getBuildId() {
|
|
58
|
+
return process.env.NEXT_BUILD_ID ?? "no-build-id";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export default new D1NextModeTagCache();
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import type { OriginalTagCache } from "@opennextjs/aws/types/overrides.js";
|
|
2
2
|
/**
|
|
3
|
-
* An instance of the Tag Cache that uses a D1 binding (`
|
|
3
|
+
* An instance of the Tag Cache that uses a D1 binding (`NEXT_TAG_CACHE_D1`) as it's underlying data store.
|
|
4
4
|
*
|
|
5
5
|
* **Tag/path mappings table**
|
|
6
6
|
*
|
|
7
7
|
* Information about the relation between tags and paths is stored in a `tags` table that contains
|
|
8
|
-
* two columns; `tag`, and `path`.
|
|
9
|
-
* environment variable.
|
|
8
|
+
* two columns; `tag`, and `path`.
|
|
10
9
|
*
|
|
11
10
|
* This table should be populated using an SQL file that is generated during the build process.
|
|
12
11
|
*
|
|
13
12
|
* **Tag revalidations table**
|
|
14
13
|
*
|
|
15
14
|
* Revalidation times for tags are stored in a `revalidations` table that contains two columns; `tags`,
|
|
16
|
-
* and `revalidatedAt`.
|
|
17
|
-
* environment variable.
|
|
15
|
+
* and `revalidatedAt`.
|
|
18
16
|
*/
|
|
19
17
|
declare class D1TagCache implements OriginalTagCache {
|
|
20
18
|
readonly name = "d1-tag-cache";
|
|
@@ -1,33 +1,31 @@
|
|
|
1
1
|
import { debug, error } from "@opennextjs/aws/adapters/logger.js";
|
|
2
2
|
import { RecoverableError } from "@opennextjs/aws/utils/error.js";
|
|
3
|
-
import { getCloudflareContext } from "
|
|
3
|
+
import { getCloudflareContext } from "../../cloudflare-context.js";
|
|
4
4
|
/**
|
|
5
|
-
* An instance of the Tag Cache that uses a D1 binding (`
|
|
5
|
+
* An instance of the Tag Cache that uses a D1 binding (`NEXT_TAG_CACHE_D1`) as it's underlying data store.
|
|
6
6
|
*
|
|
7
7
|
* **Tag/path mappings table**
|
|
8
8
|
*
|
|
9
9
|
* Information about the relation between tags and paths is stored in a `tags` table that contains
|
|
10
|
-
* two columns; `tag`, and `path`.
|
|
11
|
-
* environment variable.
|
|
10
|
+
* two columns; `tag`, and `path`.
|
|
12
11
|
*
|
|
13
12
|
* This table should be populated using an SQL file that is generated during the build process.
|
|
14
13
|
*
|
|
15
14
|
* **Tag revalidations table**
|
|
16
15
|
*
|
|
17
16
|
* Revalidation times for tags are stored in a `revalidations` table that contains two columns; `tags`,
|
|
18
|
-
* and `revalidatedAt`.
|
|
19
|
-
* environment variable.
|
|
17
|
+
* and `revalidatedAt`.
|
|
20
18
|
*/
|
|
21
19
|
class D1TagCache {
|
|
22
20
|
name = "d1-tag-cache";
|
|
23
21
|
async getByPath(rawPath) {
|
|
24
|
-
const { isDisabled, db
|
|
22
|
+
const { isDisabled, db } = this.getConfig();
|
|
25
23
|
if (isDisabled)
|
|
26
24
|
return [];
|
|
27
25
|
const path = this.getCacheKey(rawPath);
|
|
28
26
|
try {
|
|
29
27
|
const { success, results } = await db
|
|
30
|
-
.prepare(`SELECT tag FROM
|
|
28
|
+
.prepare(`SELECT tag FROM tags WHERE path = ?`)
|
|
31
29
|
.bind(path)
|
|
32
30
|
.all();
|
|
33
31
|
if (!success)
|
|
@@ -42,13 +40,13 @@ class D1TagCache {
|
|
|
42
40
|
}
|
|
43
41
|
}
|
|
44
42
|
async getByTag(rawTag) {
|
|
45
|
-
const { isDisabled, db
|
|
43
|
+
const { isDisabled, db } = this.getConfig();
|
|
46
44
|
if (isDisabled)
|
|
47
45
|
return [];
|
|
48
46
|
const tag = this.getCacheKey(rawTag);
|
|
49
47
|
try {
|
|
50
48
|
const { success, results } = await db
|
|
51
|
-
.prepare(`SELECT path FROM
|
|
49
|
+
.prepare(`SELECT path FROM tags WHERE tag = ?`)
|
|
52
50
|
.bind(tag)
|
|
53
51
|
.all();
|
|
54
52
|
if (!success)
|
|
@@ -63,14 +61,14 @@ class D1TagCache {
|
|
|
63
61
|
}
|
|
64
62
|
}
|
|
65
63
|
async getLastModified(path, lastModified) {
|
|
66
|
-
const { isDisabled, db
|
|
64
|
+
const { isDisabled, db } = this.getConfig();
|
|
67
65
|
if (isDisabled)
|
|
68
66
|
return lastModified ?? Date.now();
|
|
69
67
|
try {
|
|
70
68
|
const { success, results } = await db
|
|
71
|
-
.prepare(`SELECT
|
|
72
|
-
INNER JOIN
|
|
73
|
-
WHERE
|
|
69
|
+
.prepare(`SELECT revalidations.tag FROM revalidations
|
|
70
|
+
INNER JOIN tags ON revalidations.tag = tags.tag
|
|
71
|
+
WHERE tags.path = ? AND revalidations.revalidatedAt > ?;`)
|
|
74
72
|
.bind(this.getCacheKey(path), lastModified ?? 0)
|
|
75
73
|
.all();
|
|
76
74
|
if (!success)
|
|
@@ -84,7 +82,7 @@ class D1TagCache {
|
|
|
84
82
|
}
|
|
85
83
|
}
|
|
86
84
|
async writeTags(tags) {
|
|
87
|
-
const { isDisabled, db
|
|
85
|
+
const { isDisabled, db } = this.getConfig();
|
|
88
86
|
if (isDisabled || tags.length === 0)
|
|
89
87
|
return;
|
|
90
88
|
try {
|
|
@@ -94,14 +92,14 @@ class D1TagCache {
|
|
|
94
92
|
if (revalidatedAt === 1) {
|
|
95
93
|
// new tag/path mapping from set
|
|
96
94
|
return db
|
|
97
|
-
.prepare(`INSERT INTO
|
|
95
|
+
.prepare(`INSERT INTO tags (tag, path) VALUES (?, ?)`)
|
|
98
96
|
.bind(this.getCacheKey(tag), this.getCacheKey(path));
|
|
99
97
|
}
|
|
100
98
|
if (!uniqueTags.has(tag) && revalidatedAt !== -1) {
|
|
101
99
|
// tag was revalidated
|
|
102
100
|
uniqueTags.add(tag);
|
|
103
101
|
return db
|
|
104
|
-
.prepare(`INSERT INTO
|
|
102
|
+
.prepare(`INSERT INTO revalidations (tag, revalidatedAt) VALUES (?, ?)`)
|
|
105
103
|
.bind(this.getCacheKey(tag), revalidatedAt ?? Date.now());
|
|
106
104
|
}
|
|
107
105
|
})
|
|
@@ -117,22 +115,17 @@ class D1TagCache {
|
|
|
117
115
|
}
|
|
118
116
|
getConfig() {
|
|
119
117
|
const cfEnv = getCloudflareContext().env;
|
|
120
|
-
const db = cfEnv.
|
|
118
|
+
const db = cfEnv.NEXT_TAG_CACHE_D1;
|
|
121
119
|
if (!db)
|
|
122
120
|
debug("No D1 database found");
|
|
123
121
|
const isDisabled = !!globalThis.openNextConfig
|
|
124
122
|
.dangerous?.disableTagCache;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
tables: {
|
|
132
|
-
tags: cfEnv.NEXT_CACHE_D1_TAGS_TABLE ?? "tags",
|
|
133
|
-
revalidations: cfEnv.NEXT_CACHE_D1_REVALIDATIONS_TABLE ?? "revalidations",
|
|
134
|
-
},
|
|
135
|
-
};
|
|
123
|
+
return !db || isDisabled
|
|
124
|
+
? { isDisabled: true }
|
|
125
|
+
: {
|
|
126
|
+
isDisabled: false,
|
|
127
|
+
db,
|
|
128
|
+
};
|
|
136
129
|
}
|
|
137
130
|
removeBuildId(key) {
|
|
138
131
|
return key.replace(`${this.getBuildId()}/`, "");
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js";
|
|
2
|
+
export declare const DEFAULT_SOFT_REPLICAS = 4;
|
|
3
|
+
export declare const DEFAULT_HARD_REPLICAS = 2;
|
|
4
|
+
export declare const DEFAULT_WRITE_RETRIES = 3;
|
|
5
|
+
export declare const DEFAULT_NUM_SHARDS = 4;
|
|
6
|
+
interface ShardedDOTagCacheOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The number of shards that will be used.
|
|
9
|
+
* 1 shards means 1 durable object instance.
|
|
10
|
+
* Soft (internal next tags used for `revalidatePath`) and hard tags (the one you define in your app) will be split in different shards.
|
|
11
|
+
* The number of requests made to Durable Objects will scale linearly with the number of shards.
|
|
12
|
+
* For example, a request involving 5 tags may access between 1 and 5 shards, with the upper limit being the lesser of the number of tags or the number of shards
|
|
13
|
+
* @default 4
|
|
14
|
+
*/
|
|
15
|
+
baseShardSize: number;
|
|
16
|
+
/**
|
|
17
|
+
* Whether to enable a regional cache on a per-shard basis
|
|
18
|
+
* Because of the way tags are implemented in Next.js, some shards will have more requests than others. For these cases, it is recommended to enable the regional cache.
|
|
19
|
+
* @default false
|
|
20
|
+
*/
|
|
21
|
+
regionalCache?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The TTL for the regional cache in seconds
|
|
24
|
+
* Increasing this value will reduce the number of requests to the Durable Object, but it could make `revalidateTags`/`revalidatePath` call being longer to take effect
|
|
25
|
+
* @default 5
|
|
26
|
+
*/
|
|
27
|
+
regionalCacheTtlSec?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Whether to enable shard replication
|
|
30
|
+
* Shard replication will duplicate each shards into N replicas to spread the load even more
|
|
31
|
+
* All replicas of the a shard contain the same value - write are sent to all of the replicas.
|
|
32
|
+
* This allows most frequent read operations to be sent to only one replica to spread the load.
|
|
33
|
+
* For example with N being 2, tag `tag1` could be read from 2 different durable object instance
|
|
34
|
+
* On read you only need to read from one of the shards, but on write you need to write to all shards
|
|
35
|
+
* @default false
|
|
36
|
+
*/
|
|
37
|
+
enableShardReplication?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The number of replicas that will be used for shard replication
|
|
40
|
+
* Soft shard replicas are more often accessed than hard shard replicas, so it is recommended to have more soft replicas than hard replicas
|
|
41
|
+
* Soft replicas are for internal next tags used for `revalidatePath` (i.e. `_N_T_/layout`, `_N_T_/page1`), hard replicas are the tags defined in your app
|
|
42
|
+
* @default { numberOfSoftReplicas: 4, numberOfHardReplicas: 2 }
|
|
43
|
+
*/
|
|
44
|
+
shardReplicationOptions?: {
|
|
45
|
+
numberOfSoftReplicas: number;
|
|
46
|
+
numberOfHardReplicas: number;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The number of retries to perform when writing tags
|
|
50
|
+
* @default 3
|
|
51
|
+
*/
|
|
52
|
+
maxWriteRetries?: number;
|
|
53
|
+
}
|
|
54
|
+
interface TagCacheDOIdOptions {
|
|
55
|
+
baseShardId: string;
|
|
56
|
+
numberOfReplicas: number;
|
|
57
|
+
shardType: "soft" | "hard";
|
|
58
|
+
replicaId?: number;
|
|
59
|
+
}
|
|
60
|
+
export declare class TagCacheDOId {
|
|
61
|
+
options: TagCacheDOIdOptions;
|
|
62
|
+
shardId: string;
|
|
63
|
+
replicaId: number;
|
|
64
|
+
constructor(options: TagCacheDOIdOptions);
|
|
65
|
+
private generateRandomNumberBetween;
|
|
66
|
+
get key(): string;
|
|
67
|
+
}
|
|
68
|
+
declare class ShardedDOTagCache implements NextModeTagCache {
|
|
69
|
+
private opts;
|
|
70
|
+
readonly mode: "nextMode";
|
|
71
|
+
readonly name = "do-sharded-tag-cache";
|
|
72
|
+
readonly numSoftReplicas: number;
|
|
73
|
+
readonly numHardReplicas: number;
|
|
74
|
+
readonly maxWriteRetries: number;
|
|
75
|
+
localCache?: Cache;
|
|
76
|
+
constructor(opts?: ShardedDOTagCacheOptions);
|
|
77
|
+
private getDurableObjectStub;
|
|
78
|
+
/**
|
|
79
|
+
* Generates a list of DO ids for the shards and replicas
|
|
80
|
+
* @param tags The tags to generate shards for
|
|
81
|
+
* @param shardType Whether to generate shards for soft or hard tags
|
|
82
|
+
* @param generateAllShards Whether to generate all shards or only one
|
|
83
|
+
* @returns An array of TagCacheDOId and tag
|
|
84
|
+
*/
|
|
85
|
+
private generateDOIdArray;
|
|
86
|
+
/**
|
|
87
|
+
* Same tags are guaranteed to be in the same shard
|
|
88
|
+
* @param tags
|
|
89
|
+
* @returns An array of DO ids and tags
|
|
90
|
+
*/
|
|
91
|
+
groupTagsByDO({ tags, generateAllReplicas }: {
|
|
92
|
+
tags: string[];
|
|
93
|
+
generateAllReplicas?: boolean;
|
|
94
|
+
}): {
|
|
95
|
+
doId: TagCacheDOId;
|
|
96
|
+
tags: string[];
|
|
97
|
+
}[];
|
|
98
|
+
private getConfig;
|
|
99
|
+
/**
|
|
100
|
+
* This function checks if the tags have been revalidated
|
|
101
|
+
* It is never supposed to throw and in case of error, it will return false
|
|
102
|
+
* @param tags
|
|
103
|
+
* @param lastModified default to `Date.now()`
|
|
104
|
+
* @returns
|
|
105
|
+
*/
|
|
106
|
+
hasBeenRevalidated(tags: string[], lastModified?: number): Promise<boolean>;
|
|
107
|
+
/**
|
|
108
|
+
* This function writes the tags to the cache
|
|
109
|
+
* Due to the way shards and regional cache are implemented, the regional cache may not be properly invalidated
|
|
110
|
+
* @param tags
|
|
111
|
+
* @returns
|
|
112
|
+
*/
|
|
113
|
+
writeTags(tags: string[]): Promise<void>;
|
|
114
|
+
performWriteTagsWithRetry(doId: TagCacheDOId, tags: string[], lastModified: number, retryNumber?: number): Promise<void>;
|
|
115
|
+
getCacheInstance(): Promise<Cache | undefined>;
|
|
116
|
+
getCacheKey(doId: TagCacheDOId, tags: string[]): Promise<Request<unknown, CfProperties<unknown>>>;
|
|
117
|
+
getFromRegionalCache(doId: TagCacheDOId, tags: string[]): Promise<Response | undefined>;
|
|
118
|
+
putToRegionalCache(doId: TagCacheDOId, tags: string[], hasBeenRevalidated: boolean): Promise<void>;
|
|
119
|
+
deleteRegionalCache(doId: TagCacheDOId, tags: string[]): Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
declare const _default: (opts?: ShardedDOTagCacheOptions) => ShardedDOTagCache;
|
|
122
|
+
export default _default;
|