@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,290 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { DurableObjectQueueHandler } from "./queue";
|
|
3
|
+
vi.mock("cloudflare:workers", () => ({
|
|
4
|
+
DurableObject: class {
|
|
5
|
+
ctx;
|
|
6
|
+
env;
|
|
7
|
+
constructor(ctx, env) {
|
|
8
|
+
this.ctx = ctx;
|
|
9
|
+
this.env = env;
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
const createDurableObjectQueue = ({ fetchDuration, statusCode, headers, disableSQLite, }) => {
|
|
14
|
+
const mockState = {
|
|
15
|
+
waitUntil: vi.fn(),
|
|
16
|
+
blockConcurrencyWhile: vi.fn().mockImplementation(async (fn) => fn()),
|
|
17
|
+
storage: {
|
|
18
|
+
setAlarm: vi.fn(),
|
|
19
|
+
getAlarm: vi.fn(),
|
|
20
|
+
sql: {
|
|
21
|
+
exec: vi.fn().mockImplementation(() => ({
|
|
22
|
+
one: vi.fn(),
|
|
23
|
+
})),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
return new DurableObjectQueueHandler(mockState, {
|
|
29
|
+
WORKER_SELF_REFERENCE: {
|
|
30
|
+
fetch: vi.fn().mockReturnValue(new Promise((res) => setTimeout(() => res(new Response(null, {
|
|
31
|
+
status: statusCode,
|
|
32
|
+
headers: headers ?? new Headers([["x-nextjs-cache", "REVALIDATED"]]),
|
|
33
|
+
})), fetchDuration))),
|
|
34
|
+
connect: vi.fn(),
|
|
35
|
+
},
|
|
36
|
+
NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE: disableSQLite ? "true" : undefined,
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const createMessage = (dedupId, lastModified = Date.now()) => ({
|
|
40
|
+
MessageBody: { host: "test.local", url: "/test", eTag: "test", lastModified },
|
|
41
|
+
MessageGroupId: "test.local/test",
|
|
42
|
+
MessageDeduplicationId: dedupId,
|
|
43
|
+
previewModeId: "test",
|
|
44
|
+
});
|
|
45
|
+
describe("DurableObjectQueue", () => {
|
|
46
|
+
describe("successful revalidation", () => {
|
|
47
|
+
it("should process a single revalidation", async () => {
|
|
48
|
+
process.env.__NEXT_PREVIEW_MODE_ID = "test";
|
|
49
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
50
|
+
const firstRequest = await queue.revalidate(createMessage("id"));
|
|
51
|
+
expect(firstRequest).toBeUndefined();
|
|
52
|
+
expect(queue.ongoingRevalidations.size).toBe(1);
|
|
53
|
+
expect(queue.ongoingRevalidations.has("id")).toBe(true);
|
|
54
|
+
await queue.ongoingRevalidations.get("id");
|
|
55
|
+
expect(queue.ongoingRevalidations.size).toBe(0);
|
|
56
|
+
expect(queue.ongoingRevalidations.has("id")).toBe(false);
|
|
57
|
+
expect(queue.service.fetch).toHaveBeenCalledWith("https://test.local/test", {
|
|
58
|
+
method: "HEAD",
|
|
59
|
+
headers: {
|
|
60
|
+
"x-prerender-revalidate": "test",
|
|
61
|
+
"x-isr": "1",
|
|
62
|
+
},
|
|
63
|
+
signal: expect.any(AbortSignal),
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it("should dedupe revalidations", async () => {
|
|
67
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
68
|
+
await queue.revalidate(createMessage("id"));
|
|
69
|
+
await queue.revalidate(createMessage("id"));
|
|
70
|
+
expect(queue.ongoingRevalidations.size).toBe(1);
|
|
71
|
+
expect(queue.ongoingRevalidations.has("id")).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it("should block concurrency", async () => {
|
|
74
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
75
|
+
await queue.revalidate(createMessage("id"));
|
|
76
|
+
await queue.revalidate(createMessage("id2"));
|
|
77
|
+
await queue.revalidate(createMessage("id3"));
|
|
78
|
+
await queue.revalidate(createMessage("id4"));
|
|
79
|
+
await queue.revalidate(createMessage("id5"));
|
|
80
|
+
// the next one should block until one of the previous ones finishes
|
|
81
|
+
const blockedReq = queue.revalidate(createMessage("id6"));
|
|
82
|
+
expect(queue.ongoingRevalidations.size).toBe(queue.maxRevalidations);
|
|
83
|
+
expect(queue.ongoingRevalidations.has("id6")).toBe(false);
|
|
84
|
+
expect(Array.from(queue.ongoingRevalidations.keys())).toEqual(["id", "id2", "id3", "id4", "id5"]);
|
|
85
|
+
// BlockConcurrencyWhile is called twice here, first time during creation of the object and second time when we try to revalidate
|
|
86
|
+
// @ts-expect-error
|
|
87
|
+
expect(queue.ctx.blockConcurrencyWhile).toHaveBeenCalledTimes(2);
|
|
88
|
+
// Here we await the blocked request to ensure it's resolved
|
|
89
|
+
await blockedReq;
|
|
90
|
+
// We then need to await for the actual revalidation to finish
|
|
91
|
+
await Promise.all(Array.from(queue.ongoingRevalidations.values()));
|
|
92
|
+
expect(queue.ongoingRevalidations.size).toBe(0);
|
|
93
|
+
expect(queue.service.fetch).toHaveBeenCalledTimes(6);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe("failed revalidation", () => {
|
|
97
|
+
it("should not put it in failed state for an incorrect 200", async () => {
|
|
98
|
+
const queue = createDurableObjectQueue({
|
|
99
|
+
fetchDuration: 10,
|
|
100
|
+
statusCode: 200,
|
|
101
|
+
headers: new Headers([["x-nextjs-cache", "MISS"]]),
|
|
102
|
+
});
|
|
103
|
+
await queue.revalidate(createMessage("id"));
|
|
104
|
+
await queue.ongoingRevalidations.get("id");
|
|
105
|
+
expect(queue.routeInFailedState.size).toBe(0);
|
|
106
|
+
});
|
|
107
|
+
it("should not put it in failed state for a failed revalidation with 404", async () => {
|
|
108
|
+
const queue = createDurableObjectQueue({
|
|
109
|
+
fetchDuration: 10,
|
|
110
|
+
statusCode: 404,
|
|
111
|
+
});
|
|
112
|
+
await queue.revalidate(createMessage("id"));
|
|
113
|
+
await queue.ongoingRevalidations.get("id");
|
|
114
|
+
expect(queue.routeInFailedState.size).toBe(0);
|
|
115
|
+
expect(queue.service.fetch).toHaveBeenCalledTimes(1);
|
|
116
|
+
await queue.revalidate(createMessage("id"));
|
|
117
|
+
expect(queue.routeInFailedState.size).toBe(0);
|
|
118
|
+
expect(queue.service.fetch).toHaveBeenCalledTimes(2);
|
|
119
|
+
});
|
|
120
|
+
it("should put it in failed state if revalidation fails with 500", async () => {
|
|
121
|
+
const queue = createDurableObjectQueue({
|
|
122
|
+
fetchDuration: 10,
|
|
123
|
+
statusCode: 500,
|
|
124
|
+
});
|
|
125
|
+
await queue.revalidate(createMessage("id"));
|
|
126
|
+
await queue.ongoingRevalidations.get("id");
|
|
127
|
+
expect(queue.routeInFailedState.size).toBe(1);
|
|
128
|
+
expect(queue.routeInFailedState.has("id")).toBe(true);
|
|
129
|
+
expect(queue.service.fetch).toHaveBeenCalledTimes(1);
|
|
130
|
+
await queue.revalidate(createMessage("id"));
|
|
131
|
+
expect(queue.routeInFailedState.size).toBe(1);
|
|
132
|
+
expect(queue.service.fetch).toHaveBeenCalledTimes(1);
|
|
133
|
+
});
|
|
134
|
+
it("should put it in failed state if revalidation fetch throw", async () => {
|
|
135
|
+
const queue = createDurableObjectQueue({
|
|
136
|
+
fetchDuration: 10,
|
|
137
|
+
});
|
|
138
|
+
// @ts-expect-error - This is mocked above
|
|
139
|
+
queue.service.fetch.mockImplementationOnce(() => Promise.reject(new Error("fetch error")));
|
|
140
|
+
await queue.revalidate(createMessage("id"));
|
|
141
|
+
await queue.ongoingRevalidations.get("id");
|
|
142
|
+
expect(queue.routeInFailedState.size).toBe(1);
|
|
143
|
+
expect(queue.routeInFailedState.has("id")).toBe(true);
|
|
144
|
+
expect(queue.ongoingRevalidations.size).toBe(0);
|
|
145
|
+
expect(queue.service.fetch).toHaveBeenCalledTimes(1);
|
|
146
|
+
await queue.revalidate(createMessage("id"));
|
|
147
|
+
expect(queue.routeInFailedState.size).toBe(1);
|
|
148
|
+
expect(queue.service.fetch).toHaveBeenCalledTimes(1);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe("addAlarm", () => {
|
|
152
|
+
const getStorage = (queue) => {
|
|
153
|
+
// @ts-expect-error - ctx is a protected field
|
|
154
|
+
return queue.ctx.storage;
|
|
155
|
+
};
|
|
156
|
+
it("should not add an alarm if there are no failed states", async () => {
|
|
157
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
158
|
+
await queue.addAlarm();
|
|
159
|
+
expect(getStorage(queue).setAlarm).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
it("should add an alarm if there are failed states", async () => {
|
|
162
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
163
|
+
const nextAlarmMs = Date.now() + 1000;
|
|
164
|
+
queue.routeInFailedState.set("id", { msg: createMessage("id"), retryCount: 0, nextAlarmMs });
|
|
165
|
+
await queue.addAlarm();
|
|
166
|
+
expect(getStorage(queue).setAlarm).toHaveBeenCalledWith(nextAlarmMs);
|
|
167
|
+
});
|
|
168
|
+
it("should not add an alarm if there is already an alarm set", async () => {
|
|
169
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
170
|
+
queue.routeInFailedState.set("id", { msg: createMessage("id"), retryCount: 0, nextAlarmMs: 1000 });
|
|
171
|
+
// @ts-expect-error
|
|
172
|
+
queue.ctx.storage.getAlarm.mockResolvedValueOnce(1000);
|
|
173
|
+
await queue.addAlarm();
|
|
174
|
+
expect(getStorage(queue).setAlarm).not.toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
it("should set the alarm to the lowest nextAlarm", async () => {
|
|
177
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
178
|
+
const nextAlarmMs = Date.now() + 1000;
|
|
179
|
+
const firstAlarm = Date.now() + 500;
|
|
180
|
+
queue.routeInFailedState.set("id", { msg: createMessage("id"), retryCount: 0, nextAlarmMs });
|
|
181
|
+
queue.routeInFailedState.set("id2", {
|
|
182
|
+
msg: createMessage("id2"),
|
|
183
|
+
retryCount: 0,
|
|
184
|
+
nextAlarmMs: firstAlarm,
|
|
185
|
+
});
|
|
186
|
+
await queue.addAlarm();
|
|
187
|
+
expect(getStorage(queue).setAlarm).toHaveBeenCalledWith(firstAlarm);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe("addToFailedState", () => {
|
|
191
|
+
it("should add a failed state", async () => {
|
|
192
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
193
|
+
await queue.addToFailedState(createMessage("id"));
|
|
194
|
+
expect(queue.routeInFailedState.size).toBe(1);
|
|
195
|
+
expect(queue.routeInFailedState.has("id")).toBe(true);
|
|
196
|
+
expect(queue.routeInFailedState.get("id")?.retryCount).toBe(1);
|
|
197
|
+
});
|
|
198
|
+
it("should add a failed state with the correct nextAlarm", async () => {
|
|
199
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
200
|
+
await queue.addToFailedState(createMessage("id"));
|
|
201
|
+
expect(queue.routeInFailedState.get("id")?.nextAlarmMs).toBeGreaterThan(Date.now());
|
|
202
|
+
expect(queue.routeInFailedState.get("id")?.retryCount).toBe(1);
|
|
203
|
+
});
|
|
204
|
+
it("should add a failed state with the correct nextAlarm for a retry", async () => {
|
|
205
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
206
|
+
await queue.addToFailedState(createMessage("id"));
|
|
207
|
+
await queue.addToFailedState(createMessage("id"));
|
|
208
|
+
expect(queue.routeInFailedState.get("id")?.nextAlarmMs).toBeGreaterThan(Date.now());
|
|
209
|
+
expect(queue.routeInFailedState.get("id")?.retryCount).toBe(2);
|
|
210
|
+
});
|
|
211
|
+
it("should not add a failed state if it has been retried 6 times", async () => {
|
|
212
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
213
|
+
queue.routeInFailedState.set("id", { msg: createMessage("id"), retryCount: 6, nextAlarmMs: 1000 });
|
|
214
|
+
await queue.addToFailedState(createMessage("id"));
|
|
215
|
+
expect(queue.routeInFailedState.size).toBe(0);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe("alarm", () => {
|
|
219
|
+
it("should execute revalidations for expired events", async () => {
|
|
220
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
221
|
+
queue.routeInFailedState.set("id", {
|
|
222
|
+
msg: createMessage("id"),
|
|
223
|
+
retryCount: 0,
|
|
224
|
+
nextAlarmMs: Date.now() - 1000,
|
|
225
|
+
});
|
|
226
|
+
queue.routeInFailedState.set("id2", {
|
|
227
|
+
msg: createMessage("id2"),
|
|
228
|
+
retryCount: 0,
|
|
229
|
+
nextAlarmMs: Date.now() - 1000,
|
|
230
|
+
});
|
|
231
|
+
await queue.alarm();
|
|
232
|
+
expect(queue.routeInFailedState.size).toBe(0);
|
|
233
|
+
expect(queue.service.fetch).toHaveBeenCalledTimes(2);
|
|
234
|
+
});
|
|
235
|
+
it("should execute revalidations for the next event to retry", async () => {
|
|
236
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
237
|
+
queue.routeInFailedState.set("id", {
|
|
238
|
+
msg: createMessage("id"),
|
|
239
|
+
retryCount: 0,
|
|
240
|
+
nextAlarmMs: Date.now() + 1000,
|
|
241
|
+
});
|
|
242
|
+
queue.routeInFailedState.set("id2", {
|
|
243
|
+
msg: createMessage("id2"),
|
|
244
|
+
retryCount: 0,
|
|
245
|
+
nextAlarmMs: Date.now() + 500,
|
|
246
|
+
});
|
|
247
|
+
await queue.alarm();
|
|
248
|
+
expect(queue.routeInFailedState.size).toBe(1);
|
|
249
|
+
expect(queue.service.fetch).toHaveBeenCalledTimes(1);
|
|
250
|
+
expect(queue.routeInFailedState.has("id2")).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
it("should execute revalidations for the next event to retry and expired events", async () => {
|
|
253
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10 });
|
|
254
|
+
queue.routeInFailedState.set("id", {
|
|
255
|
+
msg: createMessage("id"),
|
|
256
|
+
retryCount: 0,
|
|
257
|
+
nextAlarmMs: Date.now() + 1000,
|
|
258
|
+
});
|
|
259
|
+
queue.routeInFailedState.set("id2", {
|
|
260
|
+
msg: createMessage("id2"),
|
|
261
|
+
retryCount: 0,
|
|
262
|
+
nextAlarmMs: Date.now() - 1000,
|
|
263
|
+
});
|
|
264
|
+
await queue.alarm();
|
|
265
|
+
expect(queue.routeInFailedState.size).toBe(0);
|
|
266
|
+
expect(queue.service.fetch).toHaveBeenCalledTimes(2);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
describe("disableSQLite", () => {
|
|
270
|
+
it("should not initialize the sqlite storage", async () => {
|
|
271
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10, disableSQLite: true });
|
|
272
|
+
expect(queue.sql.exec).not.toHaveBeenCalled();
|
|
273
|
+
});
|
|
274
|
+
it("should not write to the sqlite storage on failed state", async () => {
|
|
275
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10, disableSQLite: true });
|
|
276
|
+
await queue.addToFailedState(createMessage("id"));
|
|
277
|
+
expect(queue.sql.exec).not.toHaveBeenCalled();
|
|
278
|
+
});
|
|
279
|
+
it("should not read from the sqlite storage on checkSyncTable", async () => {
|
|
280
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10, disableSQLite: true });
|
|
281
|
+
queue.checkSyncTable(createMessage("id"));
|
|
282
|
+
expect(queue.sql.exec).not.toHaveBeenCalled();
|
|
283
|
+
});
|
|
284
|
+
it("should not write to sql on successful revalidation", async () => {
|
|
285
|
+
const queue = createDurableObjectQueue({ fetchDuration: 10, disableSQLite: true });
|
|
286
|
+
await queue.revalidate(createMessage("id"));
|
|
287
|
+
expect(queue.sql.exec).not.toHaveBeenCalled();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
export declare class DOShardedTagCache extends DurableObject<CloudflareEnv> {
|
|
3
|
+
sql: SqlStorage;
|
|
4
|
+
constructor(state: DurableObjectState, env: CloudflareEnv);
|
|
5
|
+
hasBeenRevalidated(tags: string[], lastModified?: number): Promise<boolean>;
|
|
6
|
+
writeTags(tags: string[], lastModified: number): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
export class DOShardedTagCache extends DurableObject {
|
|
3
|
+
sql;
|
|
4
|
+
constructor(state, env) {
|
|
5
|
+
super(state, env);
|
|
6
|
+
this.sql = state.storage.sql;
|
|
7
|
+
state.blockConcurrencyWhile(async () => {
|
|
8
|
+
this.sql.exec(`CREATE TABLE IF NOT EXISTS revalidations (tag TEXT PRIMARY KEY, revalidatedAt INTEGER)`);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
async hasBeenRevalidated(tags, lastModified) {
|
|
12
|
+
const result = this.sql
|
|
13
|
+
.exec(`SELECT COUNT(*) as cnt FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")}) AND revalidatedAt > ?`, ...tags, lastModified ?? Date.now())
|
|
14
|
+
.one();
|
|
15
|
+
return result.cnt > 0;
|
|
16
|
+
}
|
|
17
|
+
async writeTags(tags, lastModified) {
|
|
18
|
+
tags.forEach((tag) => {
|
|
19
|
+
this.sql.exec(`INSERT OR REPLACE INTO revalidations (tag, revalidatedAt) VALUES (?, ?)`, tag, lastModified);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { DOShardedTagCache } from "./sharded-tag-cache";
|
|
3
|
+
vi.mock("cloudflare:workers", () => ({
|
|
4
|
+
DurableObject: class {
|
|
5
|
+
ctx;
|
|
6
|
+
env;
|
|
7
|
+
constructor(ctx, env) {
|
|
8
|
+
this.ctx = ctx;
|
|
9
|
+
this.env = env;
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
const createDOShardedTagCache = () => {
|
|
14
|
+
const mockState = {
|
|
15
|
+
waitUntil: vi.fn(),
|
|
16
|
+
blockConcurrencyWhile: vi.fn().mockImplementation(async (fn) => fn()),
|
|
17
|
+
storage: {
|
|
18
|
+
setAlarm: vi.fn(),
|
|
19
|
+
getAlarm: vi.fn(),
|
|
20
|
+
sql: {
|
|
21
|
+
exec: vi.fn().mockImplementation(() => ({
|
|
22
|
+
one: vi.fn(),
|
|
23
|
+
})),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
return new DOShardedTagCache(mockState, {});
|
|
29
|
+
};
|
|
30
|
+
describe("DOShardedTagCache class", () => {
|
|
31
|
+
it("should block concurrency while creating the table", async () => {
|
|
32
|
+
const cache = createDOShardedTagCache();
|
|
33
|
+
// @ts-expect-error - testing private method
|
|
34
|
+
expect(cache.ctx.blockConcurrencyWhile).toHaveBeenCalled();
|
|
35
|
+
expect(cache.sql.exec).toHaveBeenCalledWith(`CREATE TABLE IF NOT EXISTS revalidations (tag TEXT PRIMARY KEY, revalidatedAt INTEGER)`);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -5,7 +5,7 @@ export declare const STATUS_DELETED = 1;
|
|
|
5
5
|
* Open Next cache based on cloudflare KV and Assets.
|
|
6
6
|
*
|
|
7
7
|
* Note: The class is instantiated outside of the request context.
|
|
8
|
-
* The cloudflare context and process.env are not
|
|
8
|
+
* The cloudflare context and process.env are not initialized yet
|
|
9
9
|
* when the constructor is called.
|
|
10
10
|
*/
|
|
11
11
|
declare class Cache implements IncrementalCache {
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { IgnorableError, RecoverableError } from "@opennextjs/aws/utils/error.js";
|
|
2
|
-
import { getCloudflareContext } from "
|
|
2
|
+
import { getCloudflareContext } from "../../cloudflare-context.js";
|
|
3
3
|
export const CACHE_ASSET_DIR = "cdn-cgi/_next_cache";
|
|
4
4
|
export const STATUS_DELETED = 1;
|
|
5
5
|
/**
|
|
6
6
|
* Open Next cache based on cloudflare KV and Assets.
|
|
7
7
|
*
|
|
8
8
|
* Note: The class is instantiated outside of the request context.
|
|
9
|
-
* The cloudflare context and process.env are not
|
|
9
|
+
* The cloudflare context and process.env are not initialized yet
|
|
10
10
|
* when the constructor is called.
|
|
11
11
|
*/
|
|
12
12
|
class Cache {
|
|
13
13
|
name = "cloudflare-kv";
|
|
14
14
|
async get(key, isFetch) {
|
|
15
15
|
const cfEnv = getCloudflareContext().env;
|
|
16
|
-
const kv = cfEnv.
|
|
16
|
+
const kv = cfEnv.NEXT_INC_CACHE_KV;
|
|
17
17
|
const assets = cfEnv.ASSETS;
|
|
18
18
|
if (!(kv || assets)) {
|
|
19
19
|
throw new IgnorableError(`No KVNamespace nor Fetcher`);
|
|
@@ -67,7 +67,7 @@ class Cache {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
async set(key, value, isFetch) {
|
|
70
|
-
const kv = getCloudflareContext().env.
|
|
70
|
+
const kv = getCloudflareContext().env.NEXT_INC_CACHE_KV;
|
|
71
71
|
if (!kv) {
|
|
72
72
|
throw new IgnorableError(`No KVNamespace`);
|
|
73
73
|
}
|
|
@@ -89,7 +89,7 @@ class Cache {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
async delete(key) {
|
|
92
|
-
const kv = getCloudflareContext().env.
|
|
92
|
+
const kv = getCloudflareContext().env.NEXT_INC_CACHE_KV;
|
|
93
93
|
if (!kv) {
|
|
94
94
|
throw new IgnorableError(`No KVNamespace`);
|
|
95
95
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js";
|
|
2
|
+
/**
|
|
3
|
+
* An instance of the Incremental Cache that uses an R2 bucket (`NEXT_INC_CACHE_R2_BUCKET`) as it's
|
|
4
|
+
* underlying data store.
|
|
5
|
+
*
|
|
6
|
+
* The directory that the cache entries are stored in can be configured with the `NEXT_INC_CACHE_R2_PREFIX`
|
|
7
|
+
* environment variable, and defaults to `incremental-cache`.
|
|
8
|
+
*/
|
|
9
|
+
declare class R2IncrementalCache implements IncrementalCache {
|
|
10
|
+
readonly name = "r2-incremental-cache";
|
|
11
|
+
get<IsFetch extends boolean = false>(key: string, isFetch?: IsFetch): Promise<WithLastModified<CacheValue<IsFetch>> | null>;
|
|
12
|
+
set<IsFetch extends boolean = false>(key: string, value: CacheValue<IsFetch>, isFetch?: IsFetch): Promise<void>;
|
|
13
|
+
delete(key: string): Promise<void>;
|
|
14
|
+
protected getR2Key(key: string, isFetch?: boolean): string;
|
|
15
|
+
}
|
|
16
|
+
declare const _default: R2IncrementalCache;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { debug, error } from "@opennextjs/aws/adapters/logger.js";
|
|
2
|
+
import { IgnorableError } from "@opennextjs/aws/utils/error.js";
|
|
3
|
+
import { getCloudflareContext } from "../../cloudflare-context.js";
|
|
4
|
+
/**
|
|
5
|
+
* An instance of the Incremental Cache that uses an R2 bucket (`NEXT_INC_CACHE_R2_BUCKET`) as it's
|
|
6
|
+
* underlying data store.
|
|
7
|
+
*
|
|
8
|
+
* The directory that the cache entries are stored in can be configured with the `NEXT_INC_CACHE_R2_PREFIX`
|
|
9
|
+
* environment variable, and defaults to `incremental-cache`.
|
|
10
|
+
*/
|
|
11
|
+
class R2IncrementalCache {
|
|
12
|
+
name = "r2-incremental-cache";
|
|
13
|
+
async get(key, isFetch) {
|
|
14
|
+
const r2 = getCloudflareContext().env.NEXT_INC_CACHE_R2_BUCKET;
|
|
15
|
+
if (!r2)
|
|
16
|
+
throw new IgnorableError("No R2 bucket");
|
|
17
|
+
debug(`Get ${key}`);
|
|
18
|
+
try {
|
|
19
|
+
const r2Object = await r2.get(this.getR2Key(key, isFetch));
|
|
20
|
+
if (!r2Object)
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
value: await r2Object.json(),
|
|
24
|
+
lastModified: r2Object.uploaded.getTime(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
error("Failed to get from cache", e);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async set(key, value, isFetch) {
|
|
33
|
+
const r2 = getCloudflareContext().env.NEXT_INC_CACHE_R2_BUCKET;
|
|
34
|
+
if (!r2)
|
|
35
|
+
throw new IgnorableError("No R2 bucket");
|
|
36
|
+
debug(`Set ${key}`);
|
|
37
|
+
try {
|
|
38
|
+
await r2.put(this.getR2Key(key, isFetch), JSON.stringify(value));
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
error("Failed to set to cache", e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async delete(key) {
|
|
45
|
+
const r2 = getCloudflareContext().env.NEXT_INC_CACHE_R2_BUCKET;
|
|
46
|
+
if (!r2)
|
|
47
|
+
throw new IgnorableError("No R2 bucket");
|
|
48
|
+
debug(`Delete ${key}`);
|
|
49
|
+
try {
|
|
50
|
+
await r2.delete(this.getR2Key(key));
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
error("Failed to delete from cache", e);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
getR2Key(key, isFetch) {
|
|
57
|
+
const directory = getCloudflareContext().env.NEXT_INC_CACHE_R2_PREFIX ?? "incremental-cache";
|
|
58
|
+
return `${directory}/${process.env.NEXT_BUILD_ID ?? "no-build-id"}/${key}.${isFetch ? "fetch" : "cache"}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export default new R2IncrementalCache();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js";
|
|
2
|
+
import { IncrementalCacheEntry } from "./internal.js";
|
|
3
|
+
type Options = {
|
|
4
|
+
/**
|
|
5
|
+
* The mode to use for the regional cache.
|
|
6
|
+
*
|
|
7
|
+
* - `short-lived`: Re-use a cache entry for up to a minute after it has been retrieved.
|
|
8
|
+
* - `long-lived`: Re-use a fetch cache entry until it is revalidated (per-region), or an ISR/SSG entry for up to 30 minutes.
|
|
9
|
+
*/
|
|
10
|
+
mode: "short-lived" | "long-lived";
|
|
11
|
+
/**
|
|
12
|
+
* Whether the regional cache entry should be updated in the background or not when it experiences
|
|
13
|
+
* a cache hit.
|
|
14
|
+
*
|
|
15
|
+
* Defaults to `false` for the `short-lived` mode, and `true` for the `long-lived` mode.
|
|
16
|
+
*/
|
|
17
|
+
shouldLazilyUpdateOnCacheHit?: boolean;
|
|
18
|
+
};
|
|
19
|
+
declare class RegionalCache implements IncrementalCache {
|
|
20
|
+
private store;
|
|
21
|
+
private opts;
|
|
22
|
+
name: string;
|
|
23
|
+
protected localCache: Cache | undefined;
|
|
24
|
+
constructor(store: IncrementalCache, opts: Options);
|
|
25
|
+
get<IsFetch extends boolean = false>(key: string, isFetch?: IsFetch): Promise<WithLastModified<CacheValue<IsFetch>> | null>;
|
|
26
|
+
set<IsFetch extends boolean = false>(key: string, value: CacheValue<IsFetch>, isFetch?: IsFetch): Promise<void>;
|
|
27
|
+
delete(key: string): Promise<void>;
|
|
28
|
+
protected getCacheInstance(): Promise<Cache>;
|
|
29
|
+
protected getCacheKey(key: string, isFetch?: boolean): Request<unknown, CfProperties<unknown>>;
|
|
30
|
+
protected putToCache(key: Request, entry: IncrementalCacheEntry<boolean>): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A regional cache will wrap an incremental cache and provide faster cache lookups for an entry
|
|
34
|
+
* when making requests within the region.
|
|
35
|
+
*
|
|
36
|
+
* The regional cache uses the Cache API.
|
|
37
|
+
*
|
|
38
|
+
* **WARNING:** If an entry is revalidated in one region, it will trigger an additional revalidation if
|
|
39
|
+
* a request is made to another region that has an entry stored in its regional cache.
|
|
40
|
+
*
|
|
41
|
+
* @param cache - Incremental cache instance.
|
|
42
|
+
* @param opts.mode - The mode to use for the regional cache.
|
|
43
|
+
* - `short-lived`: Re-use a cache entry for up to a minute after it has been retrieved.
|
|
44
|
+
* - `long-lived`: Re-use a fetch cache entry until it is revalidated (per-region), or an ISR/SSG entry for up to 30 minutes.
|
|
45
|
+
* @param opts.shouldLazilyUpdateOnCacheHit - Whether the regional cache entry should be updated in
|
|
46
|
+
* the background or not when it experiences a cache hit.
|
|
47
|
+
*
|
|
48
|
+
* Defaults to `false` for the `short-lived` mode, and `true` for the `long-lived` mode.
|
|
49
|
+
*/
|
|
50
|
+
export declare function withRegionalCache(cache: IncrementalCache, opts: Options): RegionalCache;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { debug, error } from "@opennextjs/aws/adapters/logger.js";
|
|
2
|
+
import { getCloudflareContext } from "../../cloudflare-context.js";
|
|
3
|
+
const ONE_MINUTE_IN_SECONDS = 60;
|
|
4
|
+
const THIRTY_MINUTES_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 30;
|
|
5
|
+
class RegionalCache {
|
|
6
|
+
store;
|
|
7
|
+
opts;
|
|
8
|
+
name;
|
|
9
|
+
localCache;
|
|
10
|
+
constructor(store, opts) {
|
|
11
|
+
this.store = store;
|
|
12
|
+
this.opts = opts;
|
|
13
|
+
this.name = this.store.name;
|
|
14
|
+
this.opts.shouldLazilyUpdateOnCacheHit ??= this.opts.mode === "long-lived";
|
|
15
|
+
}
|
|
16
|
+
async get(key, isFetch) {
|
|
17
|
+
try {
|
|
18
|
+
const cache = await this.getCacheInstance();
|
|
19
|
+
const localCacheKey = this.getCacheKey(key, isFetch);
|
|
20
|
+
// Check for a cached entry as this will be faster than the store response.
|
|
21
|
+
const cachedResponse = await cache.match(localCacheKey);
|
|
22
|
+
if (cachedResponse) {
|
|
23
|
+
debug("Get - cached response");
|
|
24
|
+
// Re-fetch from the store and update the regional cache in the background
|
|
25
|
+
if (this.opts.shouldLazilyUpdateOnCacheHit) {
|
|
26
|
+
getCloudflareContext().ctx.waitUntil(this.store.get(key, isFetch).then(async (rawEntry) => {
|
|
27
|
+
const { value, lastModified } = rawEntry ?? {};
|
|
28
|
+
if (value && typeof lastModified === "number") {
|
|
29
|
+
await this.putToCache(localCacheKey, { value, lastModified });
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
return cachedResponse.json();
|
|
34
|
+
}
|
|
35
|
+
const rawEntry = await this.store.get(key, isFetch);
|
|
36
|
+
const { value, lastModified } = rawEntry ?? {};
|
|
37
|
+
if (!value || typeof lastModified !== "number")
|
|
38
|
+
return null;
|
|
39
|
+
// Update the locale cache after retrieving from the store.
|
|
40
|
+
getCloudflareContext().ctx.waitUntil(this.putToCache(localCacheKey, { value, lastModified }));
|
|
41
|
+
return { value, lastModified };
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
error("Failed to get from regional cache", e);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async set(key, value, isFetch) {
|
|
49
|
+
try {
|
|
50
|
+
await this.store.set(key, value, isFetch);
|
|
51
|
+
await this.putToCache(this.getCacheKey(key, isFetch), {
|
|
52
|
+
value,
|
|
53
|
+
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
|
|
54
|
+
// See https://developers.cloudflare.com/workers/reference/security-model/
|
|
55
|
+
lastModified: Date.now(),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
error(`Failed to get from regional cache`, e);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async delete(key) {
|
|
63
|
+
try {
|
|
64
|
+
await this.store.delete(key);
|
|
65
|
+
const cache = await this.getCacheInstance();
|
|
66
|
+
await cache.delete(this.getCacheKey(key));
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
error("Failed to delete from regional cache", e);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async getCacheInstance() {
|
|
73
|
+
if (this.localCache)
|
|
74
|
+
return this.localCache;
|
|
75
|
+
this.localCache = await caches.open("incremental-cache");
|
|
76
|
+
return this.localCache;
|
|
77
|
+
}
|
|
78
|
+
getCacheKey(key, isFetch) {
|
|
79
|
+
return new Request(new URL(`${process.env.NEXT_BUILD_ID ?? "no-build-id"}/${key}.${isFetch ? "fetch" : "cache"}`, "http://cache.local"));
|
|
80
|
+
}
|
|
81
|
+
async putToCache(key, entry) {
|
|
82
|
+
const cache = await this.getCacheInstance();
|
|
83
|
+
const age = this.opts.mode === "short-lived"
|
|
84
|
+
? ONE_MINUTE_IN_SECONDS
|
|
85
|
+
: entry.value.revalidate || THIRTY_MINUTES_IN_SECONDS;
|
|
86
|
+
await cache.put(key, new Response(JSON.stringify(entry), {
|
|
87
|
+
headers: new Headers({ "cache-control": `max-age=${age}` }),
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A regional cache will wrap an incremental cache and provide faster cache lookups for an entry
|
|
93
|
+
* when making requests within the region.
|
|
94
|
+
*
|
|
95
|
+
* The regional cache uses the Cache API.
|
|
96
|
+
*
|
|
97
|
+
* **WARNING:** If an entry is revalidated in one region, it will trigger an additional revalidation if
|
|
98
|
+
* a request is made to another region that has an entry stored in its regional cache.
|
|
99
|
+
*
|
|
100
|
+
* @param cache - Incremental cache instance.
|
|
101
|
+
* @param opts.mode - The mode to use for the regional cache.
|
|
102
|
+
* - `short-lived`: Re-use a cache entry for up to a minute after it has been retrieved.
|
|
103
|
+
* - `long-lived`: Re-use a fetch cache entry until it is revalidated (per-region), or an ISR/SSG entry for up to 30 minutes.
|
|
104
|
+
* @param opts.shouldLazilyUpdateOnCacheHit - Whether the regional cache entry should be updated in
|
|
105
|
+
* the background or not when it experiences a cache hit.
|
|
106
|
+
*
|
|
107
|
+
* Defaults to `false` for the `short-lived` mode, and `true` for the `long-lived` mode.
|
|
108
|
+
*/
|
|
109
|
+
export function withRegionalCache(cache, opts) {
|
|
110
|
+
return new RegionalCache(cache, opts);
|
|
111
|
+
}
|