@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.
Files changed (110) hide show
  1. package/README.md +8 -8
  2. package/dist/api/cloudflare-context.d.ts +16 -5
  3. package/dist/api/config.d.ts +16 -43
  4. package/dist/api/config.js +21 -19
  5. package/dist/api/durable-objects/queue.d.ts +32 -0
  6. package/dist/api/durable-objects/queue.js +234 -0
  7. package/dist/api/durable-objects/queue.spec.js +290 -0
  8. package/dist/api/durable-objects/sharded-tag-cache.d.ts +7 -0
  9. package/dist/api/durable-objects/sharded-tag-cache.js +22 -0
  10. package/dist/api/durable-objects/sharded-tag-cache.spec.js +37 -0
  11. package/dist/api/overrides/incremental-cache/internal.d.ts +5 -0
  12. package/dist/api/{kv-cache.d.ts → overrides/incremental-cache/kv-incremental-cache.d.ts} +1 -1
  13. package/dist/api/{kv-cache.js → overrides/incremental-cache/kv-incremental-cache.js} +5 -5
  14. package/dist/api/overrides/incremental-cache/r2-incremental-cache.d.ts +17 -0
  15. package/dist/api/overrides/incremental-cache/r2-incremental-cache.js +61 -0
  16. package/dist/api/overrides/incremental-cache/regional-cache.d.ts +51 -0
  17. package/dist/api/overrides/incremental-cache/regional-cache.js +111 -0
  18. package/dist/api/overrides/queue/do-queue.d.ts +6 -0
  19. package/dist/api/overrides/queue/do-queue.js +15 -0
  20. package/dist/api/{memory-queue.d.ts → overrides/queue/memory-queue.d.ts} +3 -3
  21. package/dist/api/{memory-queue.js → overrides/queue/memory-queue.js} +18 -14
  22. package/dist/api/overrides/queue/memory-queue.spec.d.ts +1 -0
  23. package/dist/api/{memory-queue.spec.js → overrides/queue/memory-queue.spec.js} +20 -14
  24. package/dist/api/overrides/tag-cache/d1-next-tag-cache.d.ts +13 -0
  25. package/dist/api/overrides/tag-cache/d1-next-tag-cache.js +61 -0
  26. package/dist/api/{d1-tag-cache.d.ts → overrides/tag-cache/d1-tag-cache.d.ts} +3 -5
  27. package/dist/api/{d1-tag-cache.js → overrides/tag-cache/d1-tag-cache.js} +22 -29
  28. package/dist/api/overrides/tag-cache/do-sharded-tag-cache.d.ts +122 -0
  29. package/dist/api/overrides/tag-cache/do-sharded-tag-cache.js +247 -0
  30. package/dist/api/overrides/tag-cache/do-sharded-tag-cache.spec.d.ts +1 -0
  31. package/dist/api/overrides/tag-cache/do-sharded-tag-cache.spec.js +322 -0
  32. package/dist/cli/args.d.ts +13 -2
  33. package/dist/cli/args.js +44 -29
  34. package/dist/cli/build/build.d.ts +5 -1
  35. package/dist/cli/build/build.js +9 -19
  36. package/dist/cli/build/bundle-server.js +5 -13
  37. package/dist/cli/build/open-next/compile-cache-assets-manifest.d.ts +1 -1
  38. package/dist/cli/build/open-next/compile-cache-assets-manifest.js +4 -6
  39. package/dist/cli/build/open-next/compileDurableObjects.d.ts +2 -0
  40. package/dist/cli/build/open-next/compileDurableObjects.js +30 -0
  41. package/dist/cli/build/open-next/copyCacheAssets.js +1 -1
  42. package/dist/cli/build/open-next/createServerBundle.d.ts +9 -1
  43. package/dist/cli/build/open-next/createServerBundle.js +28 -9
  44. package/dist/cli/build/patches/ast/patch-vercel-og-library.js +1 -1
  45. package/dist/cli/build/patches/ast/vercel-og.d.ts +5 -5
  46. package/dist/cli/build/patches/ast/vercel-og.js +1 -1
  47. package/dist/cli/build/patches/ast/vercel-og.spec.js +1 -1
  48. package/dist/cli/build/patches/ast/webpack-runtime.js +1 -1
  49. package/dist/cli/build/patches/ast/webpack-runtime.spec.js +1 -1
  50. package/dist/cli/build/patches/plugins/build-id.d.ts +2 -2
  51. package/dist/cli/build/patches/plugins/build-id.js +12 -5
  52. package/dist/cli/build/patches/plugins/build-id.spec.js +1 -1
  53. package/dist/cli/build/patches/plugins/dynamic-requires.d.ts +1 -2
  54. package/dist/cli/build/patches/plugins/dynamic-requires.js +21 -11
  55. package/dist/cli/build/patches/plugins/eval-manifest.d.ts +2 -2
  56. package/dist/cli/build/patches/plugins/eval-manifest.js +12 -5
  57. package/dist/cli/build/patches/plugins/find-dir.d.ts +2 -2
  58. package/dist/cli/build/patches/plugins/find-dir.js +10 -5
  59. package/dist/cli/build/patches/plugins/instrumentation.d.ts +2 -5
  60. package/dist/cli/build/patches/plugins/instrumentation.js +19 -3
  61. package/dist/cli/build/patches/plugins/instrumentation.spec.js +1 -1
  62. package/dist/cli/build/patches/plugins/load-manifest.d.ts +2 -2
  63. package/dist/cli/build/patches/plugins/load-manifest.js +12 -5
  64. package/dist/cli/build/patches/plugins/next-minimal.d.ts +4 -7
  65. package/dist/cli/build/patches/plugins/next-minimal.js +31 -15
  66. package/dist/cli/build/patches/plugins/next-minimal.spec.js +1 -1
  67. package/dist/cli/build/patches/plugins/patch-depd-deprecations.d.ts +2 -2
  68. package/dist/cli/build/patches/plugins/patch-depd-deprecations.js +10 -2
  69. package/dist/cli/build/patches/plugins/patch-depd-deprecations.spec.js +1 -1
  70. package/dist/cli/build/patches/plugins/require.d.ts +2 -2
  71. package/dist/cli/build/patches/plugins/require.js +43 -35
  72. package/dist/cli/build/patches/plugins/res-revalidate.d.ts +3 -0
  73. package/dist/cli/build/patches/plugins/res-revalidate.js +77 -0
  74. package/dist/cli/build/patches/plugins/res-revalidate.spec.d.ts +1 -0
  75. package/dist/cli/build/patches/plugins/res-revalidate.spec.js +141 -0
  76. package/dist/cli/build/utils/create-config-files.d.ts +2 -2
  77. package/dist/cli/build/utils/create-config-files.js +3 -3
  78. package/dist/cli/build/utils/ensure-cf-config.js +3 -13
  79. package/dist/cli/commands/deploy.d.ts +5 -0
  80. package/dist/cli/commands/deploy.js +9 -0
  81. package/dist/cli/commands/populate-cache.d.ts +7 -0
  82. package/dist/cli/commands/populate-cache.js +78 -0
  83. package/dist/cli/commands/preview.d.ts +5 -0
  84. package/dist/cli/commands/preview.js +9 -0
  85. package/dist/cli/index.js +36 -9
  86. package/dist/cli/project-options.d.ts +5 -1
  87. package/dist/cli/templates/worker.d.ts +3 -4
  88. package/dist/cli/templates/worker.js +30 -18
  89. package/dist/cli/utils/run-wrangler.d.ts +18 -0
  90. package/dist/cli/utils/run-wrangler.js +41 -0
  91. package/package.json +7 -9
  92. package/templates/open-next.config.ts +1 -1
  93. package/templates/wrangler.jsonc +2 -2
  94. package/dist/api/kvCache.d.ts +0 -5
  95. package/dist/api/kvCache.js +0 -5
  96. package/dist/cli/build/patches/ast/util.d.ts +0 -50
  97. package/dist/cli/build/patches/ast/util.js +0 -65
  98. package/dist/cli/build/patches/ast/util.spec.js +0 -43
  99. package/dist/cli/build/patches/plugins/content-updater.d.ts +0 -44
  100. package/dist/cli/build/patches/plugins/content-updater.js +0 -55
  101. package/dist/cli/build/patches/plugins/fetch-cache-wait-until.d.ts +0 -14
  102. package/dist/cli/build/patches/plugins/fetch-cache-wait-until.js +0 -40
  103. package/dist/cli/build/patches/plugins/fetch-cache-wait-until.spec.js +0 -453
  104. package/dist/cli/templates/shims/node-fs.d.ts +0 -17
  105. package/dist/cli/templates/shims/node-fs.js +0 -51
  106. package/dist/cli/templates/shims/throw.d.ts +0 -0
  107. package/dist/cli/templates/shims/throw.js +0 -2
  108. /package/dist/api/{memory-queue.spec.d.ts → durable-objects/queue.spec.d.ts} +0 -0
  109. /package/dist/{cli/build/patches/ast/util.spec.d.ts → api/durable-objects/sharded-tag-cache.spec.d.ts} +0 -0
  110. /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 wrangler deploy
50
+ npx opennextjs-cloudflare build && npx opennextjs-cloudflare deploy
51
51
  # or
52
- pnpm opennextjs-cloudflare && pnpm wrangler deploy
52
+ pnpm opennextjs-cloudflare build && pnpm opennextjs-cloudflare deploy
53
53
  # or
54
- yarn opennextjs-cloudflare && yarn wrangler deploy
54
+ yarn opennextjs-cloudflare build && yarn opennextjs-cloudflare deploy
55
55
  # or
56
- bun opennextjs-cloudflare && bun wrangler deploy
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> = {
@@ -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 CloudflareConfigOptions = {
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
- * The incremental cache implementation to use, for more details see the [Caching documentation](https://opennext.js.org/cloudflare/caching))
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?: "dummy" | IncrementalCache | (() => IncrementalCache | Promise<IncrementalCache>);
13
+ incrementalCache?: Override<IncrementalCache>;
19
14
  /**
20
- * The tag cache implementation to use, for more details see the [Caching documentation](https://opennext.js.org/cloudflare/caching))
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?: "dummy" | TagCache | (() => TagCache | Promise<TagCache>);
17
+ tagCache?: Override<TagCache>;
34
18
  /**
35
- * The revalidation queue implementation to use, for more details see the [Caching documentation](https://opennext.js.org/cloudflare/caching))
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?: "dummy" | "direct" | Queue | (() => Queue | Promise<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 options options that enabled you to configure the application's behavior
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(options?: CloudflareConfigOptions): OpenNextConfig;
29
+ export declare function defineCloudflareConfig(config?: CloudflareOverrides): OpenNextConfig;
@@ -1,37 +1,39 @@
1
1
  /**
2
2
  * Defines the OpenNext configuration that targets the Cloudflare adapter
3
3
  *
4
- * @param options options that enabled you to configure the application's behavior
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(options = {}) {
8
- const { incrementalCache, tagCache, queue } = options;
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 resolveOverride(value) {
30
- if (!value || value === "dummy") {
31
- return "dummy";
22
+ function resolveIncrementalCache(value = "dummy") {
23
+ if (typeof value === "string") {
24
+ return value;
32
25
  }
33
- if (value === "direct") {
34
- return "direct";
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 (typeof value === "function" ? value : () => value);
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
+ }