@opennextjs/cloudflare 0.5.12 → 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 +7 -9
- 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
package/README.md
CHANGED
|
@@ -19,13 +19,13 @@ Run the following commands to preview the production build of your application l
|
|
|
19
19
|
- build the app and adapt it for Cloudflare
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npx opennextjs-cloudflare
|
|
22
|
+
npx opennextjs-cloudflare build
|
|
23
23
|
# or
|
|
24
|
-
pnpm opennextjs-cloudflare
|
|
24
|
+
pnpm opennextjs-cloudflare build
|
|
25
25
|
# or
|
|
26
|
-
yarn opennextjs-cloudflare
|
|
26
|
+
yarn opennextjs-cloudflare build
|
|
27
27
|
# or
|
|
28
|
-
bun opennextjs-cloudflare
|
|
28
|
+
bun opennextjs-cloudflare build
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
- Preview the app in Wrangler
|
|
@@ -47,11 +47,11 @@ Deploy your application to production with the following:
|
|
|
47
47
|
- build the app and adapt it for Cloudflare
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
npx opennextjs-cloudflare && npx
|
|
50
|
+
npx opennextjs-cloudflare build && npx opennextjs-cloudflare deploy
|
|
51
51
|
# or
|
|
52
|
-
pnpm opennextjs-cloudflare && pnpm
|
|
52
|
+
pnpm opennextjs-cloudflare build && pnpm opennextjs-cloudflare deploy
|
|
53
53
|
# or
|
|
54
|
-
yarn opennextjs-cloudflare && yarn
|
|
54
|
+
yarn opennextjs-cloudflare build && yarn opennextjs-cloudflare deploy
|
|
55
55
|
# or
|
|
56
|
-
bun opennextjs-cloudflare && bun
|
|
56
|
+
bun opennextjs-cloudflare build && bun opennextjs-cloudflare deploy
|
|
57
57
|
```
|
|
@@ -1,11 +1,22 @@
|
|
|
1
|
+
import type { DurableObjectQueueHandler } from "./durable-objects/queue";
|
|
2
|
+
import { DOShardedTagCache } from "./durable-objects/sharded-tag-cache";
|
|
1
3
|
declare global {
|
|
2
4
|
interface CloudflareEnv {
|
|
3
|
-
NEXT_CACHE_WORKERS_KV?: KVNamespace;
|
|
4
|
-
NEXT_CACHE_D1?: D1Database;
|
|
5
|
-
NEXT_CACHE_D1_TAGS_TABLE?: string;
|
|
6
|
-
NEXT_CACHE_D1_REVALIDATIONS_TABLE?: string;
|
|
7
|
-
NEXT_CACHE_REVALIDATION_WORKER?: Service;
|
|
8
5
|
ASSETS?: Fetcher;
|
|
6
|
+
NEXTJS_ENV?: string;
|
|
7
|
+
WORKER_SELF_REFERENCE?: Service;
|
|
8
|
+
NEXT_INC_CACHE_KV?: KVNamespace;
|
|
9
|
+
NEXT_INC_CACHE_R2_BUCKET?: R2Bucket;
|
|
10
|
+
NEXT_INC_CACHE_R2_PREFIX?: string;
|
|
11
|
+
NEXT_TAG_CACHE_D1?: D1Database;
|
|
12
|
+
NEXT_TAG_CACHE_DO_SHARDED?: DurableObjectNamespace<DOShardedTagCache>;
|
|
13
|
+
NEXT_TAG_CACHE_DO_SHARDED_DLQ?: Queue;
|
|
14
|
+
NEXT_CACHE_DO_QUEUE?: DurableObjectNamespace<DurableObjectQueueHandler>;
|
|
15
|
+
NEXT_CACHE_DO_QUEUE_MAX_REVALIDATION?: string;
|
|
16
|
+
NEXT_CACHE_DO_QUEUE_REVALIDATION_TIMEOUT_MS?: string;
|
|
17
|
+
NEXT_CACHE_DO_QUEUE_RETRY_INTERVAL_MS?: string;
|
|
18
|
+
NEXT_CACHE_DO_QUEUE_MAX_NUM_REVALIDATIONS?: string;
|
|
19
|
+
NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE?: string;
|
|
9
20
|
}
|
|
10
21
|
}
|
|
11
22
|
export type CloudflareContext<CfProperties extends Record<string, unknown> = IncomingRequestCfProperties, Context = ExecutionContext> = {
|
package/dist/api/config.d.ts
CHANGED
|
@@ -1,56 +1,29 @@
|
|
|
1
|
-
import { OpenNextConfig } from "@opennextjs/aws/types/open-next";
|
|
1
|
+
import { BaseOverride, LazyLoadedOverride, OpenNextConfig } from "@opennextjs/aws/types/open-next";
|
|
2
2
|
import type { IncrementalCache, Queue, TagCache } from "@opennextjs/aws/types/overrides";
|
|
3
|
-
export type
|
|
3
|
+
export type Override<T extends BaseOverride> = "dummy" | T | LazyLoadedOverride<T>;
|
|
4
|
+
/**
|
|
5
|
+
* Cloudflare specific overrides.
|
|
6
|
+
*
|
|
7
|
+
* See the [Caching documentation](https://opennext.js.org/cloudflare/caching))
|
|
8
|
+
*/
|
|
9
|
+
export type CloudflareOverrides = {
|
|
4
10
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `@opennextjs/cloudflare` offers a kv incremental cache implementation ready
|
|
8
|
-
* to use which can be imported from `"@opennextjs/cloudflare/kv-cache"`
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* import { defineCloudflareConfig } from "@opennextjs/cloudflare";
|
|
12
|
-
* import kvIncrementalCache from "@opennextjs/cloudflare/kv-cache";
|
|
13
|
-
*
|
|
14
|
-
* export default defineCloudflareConfig({
|
|
15
|
-
* incrementalCache: kvIncrementalCache,
|
|
16
|
-
* });
|
|
11
|
+
* Sets the incremental cache implementation.
|
|
17
12
|
*/
|
|
18
|
-
incrementalCache?:
|
|
13
|
+
incrementalCache?: Override<IncrementalCache>;
|
|
19
14
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `@opennextjs/cloudflare` offers a d1 tag cache implementation ready
|
|
23
|
-
* to use which can be imported from `"@opennextjs/cloudflare/d1-tag-cache"`
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* import { defineCloudflareConfig } from "@opennextjs/cloudflare";
|
|
27
|
-
* import d1TagCache from "@opennextjs/cloudflare/d1-tag-cache";
|
|
28
|
-
*
|
|
29
|
-
* export default defineCloudflareConfig({
|
|
30
|
-
* tagCache: d1TagCache,
|
|
31
|
-
* });
|
|
15
|
+
* Sets the tag cache implementation.
|
|
32
16
|
*/
|
|
33
|
-
tagCache?:
|
|
17
|
+
tagCache?: Override<TagCache>;
|
|
34
18
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* `@opennextjs/cloudflare` offers an in memory queue implementation ready
|
|
38
|
-
* to use which can be imported from `"@opennextjs/cloudflare/memory-queue"`
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* import { defineCloudflareConfig } from "@opennextjs/cloudflare";
|
|
42
|
-
* import memoryQueue from "@opennextjs/cloudflare/memory-queue";
|
|
43
|
-
*
|
|
44
|
-
* export default defineCloudflareConfig({
|
|
45
|
-
* queue: memoryQueue,
|
|
46
|
-
* });
|
|
19
|
+
* Sets the revalidation queue implementation
|
|
47
20
|
*/
|
|
48
|
-
queue?: "
|
|
21
|
+
queue?: "direct" | Override<Queue>;
|
|
49
22
|
};
|
|
50
23
|
/**
|
|
51
24
|
* Defines the OpenNext configuration that targets the Cloudflare adapter
|
|
52
25
|
*
|
|
53
|
-
* @param
|
|
26
|
+
* @param config options that enabled you to configure the application's behavior
|
|
54
27
|
* @returns the OpenNext configuration object
|
|
55
28
|
*/
|
|
56
|
-
export declare function defineCloudflareConfig(
|
|
29
|
+
export declare function defineCloudflareConfig(config?: CloudflareOverrides): OpenNextConfig;
|
package/dist/api/config.js
CHANGED
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Defines the OpenNext configuration that targets the Cloudflare adapter
|
|
3
3
|
*
|
|
4
|
-
* @param
|
|
4
|
+
* @param config options that enabled you to configure the application's behavior
|
|
5
5
|
* @returns the OpenNext configuration object
|
|
6
6
|
*/
|
|
7
|
-
export function defineCloudflareConfig(
|
|
8
|
-
const { incrementalCache, tagCache, queue } =
|
|
7
|
+
export function defineCloudflareConfig(config = {}) {
|
|
8
|
+
const { incrementalCache, tagCache, queue } = config;
|
|
9
9
|
return {
|
|
10
10
|
default: {
|
|
11
11
|
override: {
|
|
12
12
|
wrapper: "cloudflare-node",
|
|
13
13
|
converter: "edge",
|
|
14
|
-
incrementalCache: resolveOverride(incrementalCache),
|
|
15
|
-
tagCache: resolveOverride(tagCache),
|
|
16
|
-
queue: resolveOverride(queue),
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
middleware: {
|
|
20
|
-
external: true,
|
|
21
|
-
override: {
|
|
22
|
-
wrapper: "cloudflare-edge",
|
|
23
|
-
converter: "edge",
|
|
24
14
|
proxyExternalRequest: "fetch",
|
|
15
|
+
incrementalCache: resolveIncrementalCache(incrementalCache),
|
|
16
|
+
tagCache: resolveTagCache(tagCache),
|
|
17
|
+
queue: resolveQueue(queue),
|
|
25
18
|
},
|
|
26
19
|
},
|
|
27
20
|
};
|
|
28
21
|
}
|
|
29
|
-
function
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
22
|
+
function resolveIncrementalCache(value = "dummy") {
|
|
23
|
+
if (typeof value === "string") {
|
|
24
|
+
return value;
|
|
32
25
|
}
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
return typeof value === "function" ? value : () => value;
|
|
27
|
+
}
|
|
28
|
+
function resolveTagCache(value = "dummy") {
|
|
29
|
+
if (typeof value === "string") {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
return typeof value === "function" ? value : () => value;
|
|
33
|
+
}
|
|
34
|
+
function resolveQueue(value = "dummy") {
|
|
35
|
+
if (typeof value === "string") {
|
|
36
|
+
return value;
|
|
35
37
|
}
|
|
36
|
-
return
|
|
38
|
+
return typeof value === "function" ? value : () => value;
|
|
37
39
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { QueueMessage } from "@opennextjs/aws/types/overrides";
|
|
2
|
+
import { DurableObject } from "cloudflare:workers";
|
|
3
|
+
interface FailedState {
|
|
4
|
+
msg: QueueMessage;
|
|
5
|
+
retryCount: number;
|
|
6
|
+
nextAlarmMs: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class DurableObjectQueueHandler extends DurableObject<CloudflareEnv> {
|
|
9
|
+
ongoingRevalidations: Map<string, Promise<void>>;
|
|
10
|
+
sql: SqlStorage;
|
|
11
|
+
routeInFailedState: Map<string, FailedState>;
|
|
12
|
+
service: NonNullable<CloudflareEnv["WORKER_SELF_REFERENCE"]>;
|
|
13
|
+
readonly maxRevalidations: number;
|
|
14
|
+
readonly revalidationTimeout: number;
|
|
15
|
+
readonly revalidationRetryInterval: number;
|
|
16
|
+
readonly maxRevalidationAttempts: number;
|
|
17
|
+
readonly disableSQLite: boolean;
|
|
18
|
+
constructor(ctx: DurableObjectState, env: CloudflareEnv);
|
|
19
|
+
revalidate(msg: QueueMessage): Promise<void>;
|
|
20
|
+
executeRevalidation(msg: QueueMessage): Promise<void>;
|
|
21
|
+
alarm(): Promise<void>;
|
|
22
|
+
addToFailedState(msg: QueueMessage): Promise<void>;
|
|
23
|
+
addAlarm(): Promise<void>;
|
|
24
|
+
initState(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
*
|
|
27
|
+
* @param msg
|
|
28
|
+
* @returns `true` if the route has been revalidated since the lastModified from the message, `false` otherwise
|
|
29
|
+
*/
|
|
30
|
+
checkSyncTable(msg: QueueMessage): boolean;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { debug, error } from "@opennextjs/aws/adapters/logger.js";
|
|
2
|
+
import { FatalError, IgnorableError, isOpenNextError, RecoverableError, } from "@opennextjs/aws/utils/error.js";
|
|
3
|
+
import { DurableObject } from "cloudflare:workers";
|
|
4
|
+
const DEFAULT_MAX_REVALIDATION = 5;
|
|
5
|
+
const DEFAULT_REVALIDATION_TIMEOUT_MS = 10_000;
|
|
6
|
+
const DEFAULT_RETRY_INTERVAL_MS = 2_000;
|
|
7
|
+
const DEFAULT_MAX_NUM_REVALIDATIONS = 6;
|
|
8
|
+
export class DurableObjectQueueHandler extends DurableObject {
|
|
9
|
+
// Ongoing revalidations are deduped by the deduplication id
|
|
10
|
+
// Since this is running in waitUntil, we expect the durable object state to persist this during the duration of the revalidation
|
|
11
|
+
// TODO: handle incremental cache with only eventual consistency (i.e. KV or R2/D1 with the optional cache layer on top)
|
|
12
|
+
ongoingRevalidations = new Map();
|
|
13
|
+
sql;
|
|
14
|
+
routeInFailedState = new Map();
|
|
15
|
+
service;
|
|
16
|
+
// Configurable params
|
|
17
|
+
maxRevalidations;
|
|
18
|
+
revalidationTimeout;
|
|
19
|
+
revalidationRetryInterval;
|
|
20
|
+
maxRevalidationAttempts;
|
|
21
|
+
disableSQLite;
|
|
22
|
+
constructor(ctx, env) {
|
|
23
|
+
super(ctx, env);
|
|
24
|
+
this.service = env.WORKER_SELF_REFERENCE;
|
|
25
|
+
// If there is no service binding, we throw an error because we can't revalidate without it
|
|
26
|
+
if (!this.service)
|
|
27
|
+
throw new IgnorableError("No service binding for cache revalidation worker");
|
|
28
|
+
this.sql = ctx.storage.sql;
|
|
29
|
+
this.maxRevalidations = env.NEXT_CACHE_DO_QUEUE_MAX_REVALIDATION
|
|
30
|
+
? parseInt(env.NEXT_CACHE_DO_QUEUE_MAX_REVALIDATION)
|
|
31
|
+
: DEFAULT_MAX_REVALIDATION;
|
|
32
|
+
this.revalidationTimeout = env.NEXT_CACHE_DO_QUEUE_REVALIDATION_TIMEOUT_MS
|
|
33
|
+
? parseInt(env.NEXT_CACHE_DO_QUEUE_REVALIDATION_TIMEOUT_MS)
|
|
34
|
+
: DEFAULT_REVALIDATION_TIMEOUT_MS;
|
|
35
|
+
this.revalidationRetryInterval = env.NEXT_CACHE_DO_QUEUE_RETRY_INTERVAL_MS
|
|
36
|
+
? parseInt(env.NEXT_CACHE_DO_QUEUE_RETRY_INTERVAL_MS)
|
|
37
|
+
: DEFAULT_RETRY_INTERVAL_MS;
|
|
38
|
+
this.maxRevalidationAttempts = env.NEXT_CACHE_DO_QUEUE_MAX_NUM_REVALIDATIONS
|
|
39
|
+
? parseInt(env.NEXT_CACHE_DO_QUEUE_MAX_NUM_REVALIDATIONS)
|
|
40
|
+
: DEFAULT_MAX_NUM_REVALIDATIONS;
|
|
41
|
+
this.disableSQLite = env.NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE === "true";
|
|
42
|
+
// We restore the state
|
|
43
|
+
ctx.blockConcurrencyWhile(async () => {
|
|
44
|
+
debug(`Restoring the state of the durable object`);
|
|
45
|
+
await this.initState();
|
|
46
|
+
});
|
|
47
|
+
debug(`Durable object initialized`);
|
|
48
|
+
}
|
|
49
|
+
async revalidate(msg) {
|
|
50
|
+
// If there is already an ongoing revalidation, we don't need to revalidate again
|
|
51
|
+
if (this.ongoingRevalidations.has(msg.MessageDeduplicationId))
|
|
52
|
+
return;
|
|
53
|
+
// The route is already in a failed state, it will be retried later
|
|
54
|
+
if (this.routeInFailedState.has(msg.MessageDeduplicationId))
|
|
55
|
+
return;
|
|
56
|
+
// If the last success is newer than the last modified, it's likely that the regional cache is out of date
|
|
57
|
+
// We don't need to revalidate in this case
|
|
58
|
+
if (this.checkSyncTable(msg))
|
|
59
|
+
return;
|
|
60
|
+
if (this.ongoingRevalidations.size >= this.maxRevalidations) {
|
|
61
|
+
debug(`The maximum number of revalidations (${this.maxRevalidations}) is reached. Blocking until one of the revalidations finishes.`);
|
|
62
|
+
const ongoingRevalidations = this.ongoingRevalidations.values();
|
|
63
|
+
// When there is more than the max revalidations, we block concurrency until one of the revalidations finishes
|
|
64
|
+
// We still await the promise to ensure the revalidation is completed
|
|
65
|
+
// This is fine because the queue itself run inside a waitUntil
|
|
66
|
+
await this.ctx.blockConcurrencyWhile(async () => {
|
|
67
|
+
debug(`Waiting for one of the revalidations to finish`);
|
|
68
|
+
await Promise.race(ongoingRevalidations);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const revalidationPromise = this.executeRevalidation(msg);
|
|
72
|
+
// We store the promise to dedupe the revalidation
|
|
73
|
+
this.ongoingRevalidations.set(msg.MessageDeduplicationId, revalidationPromise);
|
|
74
|
+
// TODO: check if the object stays up during waitUntil so that the internal state is maintained
|
|
75
|
+
this.ctx.waitUntil(revalidationPromise);
|
|
76
|
+
}
|
|
77
|
+
async executeRevalidation(msg) {
|
|
78
|
+
try {
|
|
79
|
+
debug(`Revalidating ${msg.MessageBody.host}${msg.MessageBody.url}`);
|
|
80
|
+
const { MessageBody: { host, url }, } = msg;
|
|
81
|
+
const protocol = host.includes("localhost") ? "http" : "https";
|
|
82
|
+
const response = await this.service.fetch(`${protocol}://${host}${url}`, {
|
|
83
|
+
method: "HEAD",
|
|
84
|
+
headers: {
|
|
85
|
+
// This is defined during build
|
|
86
|
+
"x-prerender-revalidate": process.env.__NEXT_PREVIEW_MODE_ID,
|
|
87
|
+
"x-isr": "1",
|
|
88
|
+
},
|
|
89
|
+
signal: AbortSignal.timeout(this.revalidationTimeout),
|
|
90
|
+
});
|
|
91
|
+
// Now we need to handle errors from the fetch
|
|
92
|
+
if (response.status === 200 && response.headers.get("x-nextjs-cache") !== "REVALIDATED") {
|
|
93
|
+
this.routeInFailedState.delete(msg.MessageDeduplicationId);
|
|
94
|
+
throw new FatalError(`The revalidation for ${host}${url} cannot be done. This error should never happen.`);
|
|
95
|
+
}
|
|
96
|
+
else if (response.status === 404) {
|
|
97
|
+
// The page is not found, we should not revalidate it
|
|
98
|
+
// We remove the route from the failed state because it might be expected (i.e. a route that was deleted)
|
|
99
|
+
this.routeInFailedState.delete(msg.MessageDeduplicationId);
|
|
100
|
+
throw new IgnorableError(`The revalidation for ${host}${url} cannot be done because the page is not found. It's either expected or an error in user code itself`);
|
|
101
|
+
}
|
|
102
|
+
else if (response.status === 500) {
|
|
103
|
+
// A server error occurred, we should retry
|
|
104
|
+
await this.addToFailedState(msg);
|
|
105
|
+
throw new IgnorableError(`Something went wrong while revalidating ${host}${url}`);
|
|
106
|
+
}
|
|
107
|
+
else if (response.status !== 200) {
|
|
108
|
+
// TODO: check if we need to handle cloudflare specific status codes/errors
|
|
109
|
+
// An unknown error occurred, most likely from something in user code like missing auth in the middleware
|
|
110
|
+
// We probably want to retry in this case as well
|
|
111
|
+
await this.addToFailedState(msg);
|
|
112
|
+
throw new RecoverableError(`An unknown error occurred while revalidating ${host}${url}`);
|
|
113
|
+
}
|
|
114
|
+
// Everything went well, we can update the sync table
|
|
115
|
+
// We use unixepoch here,it also works with Date.now()/1000, but not with Date.now() alone.
|
|
116
|
+
// TODO: This needs to be investigated
|
|
117
|
+
if (!this.disableSQLite) {
|
|
118
|
+
this.sql.exec("INSERT OR REPLACE INTO sync (id, lastSuccess, buildId) VALUES (?, unixepoch(), ?)",
|
|
119
|
+
// We cannot use the deduplication id because it's not unique per route - every time a route is revalidated, the deduplication id is different.
|
|
120
|
+
`${host}${url}`, process.env.__NEXT_BUILD_ID);
|
|
121
|
+
}
|
|
122
|
+
// If everything went well, we can remove the route from the failed state
|
|
123
|
+
this.routeInFailedState.delete(msg.MessageDeduplicationId);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
// Do we want to propagate the error to the calling worker?
|
|
127
|
+
if (!isOpenNextError(e)) {
|
|
128
|
+
await this.addToFailedState(msg);
|
|
129
|
+
}
|
|
130
|
+
error(e);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
this.ongoingRevalidations.delete(msg.MessageDeduplicationId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async alarm() {
|
|
137
|
+
const currentDateTime = Date.now();
|
|
138
|
+
// We fetch the first event that needs to be retried or if the date is expired
|
|
139
|
+
const nextEventToRetry = Array.from(this.routeInFailedState.values())
|
|
140
|
+
.filter(({ nextAlarmMs }) => nextAlarmMs > currentDateTime)
|
|
141
|
+
.sort(({ nextAlarmMs: a }, { nextAlarmMs: b }) => a - b)[0];
|
|
142
|
+
// We also have to check if there are expired events, if the revalidation takes too long, or if the
|
|
143
|
+
const expiredEvents = Array.from(this.routeInFailedState.values()).filter(({ nextAlarmMs }) => nextAlarmMs <= currentDateTime);
|
|
144
|
+
const allEventsToRetry = nextEventToRetry ? [nextEventToRetry, ...expiredEvents] : expiredEvents;
|
|
145
|
+
for (const event of allEventsToRetry) {
|
|
146
|
+
debug(`Retrying revalidation for ${event.msg.MessageBody.host}${event.msg.MessageBody.url}`);
|
|
147
|
+
await this.executeRevalidation(event.msg);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async addToFailedState(msg) {
|
|
151
|
+
debug(`Adding ${msg.MessageBody.host}${msg.MessageBody.url} to the failed state`);
|
|
152
|
+
const existingFailedState = this.routeInFailedState.get(msg.MessageDeduplicationId);
|
|
153
|
+
let updatedFailedState;
|
|
154
|
+
if (existingFailedState) {
|
|
155
|
+
if (existingFailedState.retryCount >= this.maxRevalidationAttempts) {
|
|
156
|
+
// We give up after 6 retries and log the error
|
|
157
|
+
error(`The revalidation for ${msg.MessageBody.host}${msg.MessageBody.url} has failed after 6 retries. It will not be tried again, but subsequent ISR requests will retry.`);
|
|
158
|
+
this.routeInFailedState.delete(msg.MessageDeduplicationId);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const nextAlarmMs = Date.now() + Math.pow(2, existingFailedState.retryCount + 1) * this.revalidationRetryInterval;
|
|
162
|
+
updatedFailedState = {
|
|
163
|
+
...existingFailedState,
|
|
164
|
+
retryCount: existingFailedState.retryCount + 1,
|
|
165
|
+
nextAlarmMs,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
updatedFailedState = {
|
|
170
|
+
msg,
|
|
171
|
+
retryCount: 1,
|
|
172
|
+
nextAlarmMs: Date.now() + 2_000,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
this.routeInFailedState.set(msg.MessageDeduplicationId, updatedFailedState);
|
|
176
|
+
if (!this.disableSQLite) {
|
|
177
|
+
this.sql.exec("INSERT OR REPLACE INTO failed_state (id, data, buildId) VALUES (?, ?, ?)", msg.MessageDeduplicationId, JSON.stringify(updatedFailedState), process.env.__NEXT_BUILD_ID);
|
|
178
|
+
}
|
|
179
|
+
// We probably want to do something if routeInFailedState is becoming too big, at least log it
|
|
180
|
+
await this.addAlarm();
|
|
181
|
+
}
|
|
182
|
+
async addAlarm() {
|
|
183
|
+
const existingAlarm = await this.ctx.storage.getAlarm({ allowConcurrency: false });
|
|
184
|
+
if (existingAlarm)
|
|
185
|
+
return;
|
|
186
|
+
if (this.routeInFailedState.size === 0)
|
|
187
|
+
return;
|
|
188
|
+
let nextAlarmToSetup = Math.min(...Array.from(this.routeInFailedState.values()).map(({ nextAlarmMs }) => nextAlarmMs));
|
|
189
|
+
if (nextAlarmToSetup < Date.now()) {
|
|
190
|
+
// We don't want to set an alarm in the past
|
|
191
|
+
nextAlarmToSetup = Date.now() + this.revalidationRetryInterval;
|
|
192
|
+
}
|
|
193
|
+
await this.ctx.storage.setAlarm(nextAlarmToSetup);
|
|
194
|
+
}
|
|
195
|
+
// This function is used to restore the state of the durable object
|
|
196
|
+
// We don't restore the ongoing revalidations because we cannot know in which state they are
|
|
197
|
+
// We only restore the failed state and the alarm
|
|
198
|
+
async initState() {
|
|
199
|
+
if (this.disableSQLite)
|
|
200
|
+
return;
|
|
201
|
+
// We store the failed state as a blob, we don't want to do anything with it anyway besides restoring
|
|
202
|
+
this.sql.exec("CREATE TABLE IF NOT EXISTS failed_state (id TEXT PRIMARY KEY, data TEXT, buildId TEXT)");
|
|
203
|
+
// We create the sync table to handle eventually consistent incremental cache
|
|
204
|
+
this.sql.exec("CREATE TABLE IF NOT EXISTS sync (id TEXT PRIMARY KEY, lastSuccess INTEGER, buildId TEXT)");
|
|
205
|
+
// Before doing anything else, we clear the DB for any potential old data
|
|
206
|
+
this.sql.exec("DELETE FROM failed_state WHERE buildId != ?", process.env.__NEXT_BUILD_ID);
|
|
207
|
+
this.sql.exec("DELETE FROM sync WHERE buildId != ?", process.env.__NEXT_BUILD_ID);
|
|
208
|
+
const failedStateCursor = this.sql.exec("SELECT * FROM failed_state");
|
|
209
|
+
for (const row of failedStateCursor) {
|
|
210
|
+
this.routeInFailedState.set(row.id, JSON.parse(row.data));
|
|
211
|
+
}
|
|
212
|
+
// Now that we have restored the failed state, we can restore the alarm as well
|
|
213
|
+
await this.addAlarm();
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
*
|
|
217
|
+
* @param msg
|
|
218
|
+
* @returns `true` if the route has been revalidated since the lastModified from the message, `false` otherwise
|
|
219
|
+
*/
|
|
220
|
+
checkSyncTable(msg) {
|
|
221
|
+
try {
|
|
222
|
+
if (this.disableSQLite)
|
|
223
|
+
return false;
|
|
224
|
+
const numNewer = this.sql
|
|
225
|
+
.exec("SELECT COUNT(*) as numNewer FROM sync WHERE id = ? AND lastSuccess > ? LIMIT 1", `${msg.MessageBody.host}${msg.MessageBody.url}`, Math.round(msg.MessageBody.lastModified / 1000))
|
|
226
|
+
.one().numNewer;
|
|
227
|
+
return numNewer > 0;
|
|
228
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|