mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-15 23:25:16 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| efbc6ff3e5 | |||
| 0da3639888 | |||
| 4e64e8d984 | |||
| f745daaa25 | |||
| 335c7fe990 | |||
| 464f45a614 | |||
| 5d8682b5b9 | |||
| 4e51378ad6 | |||
| 3193611a09 |
@@ -59,7 +59,7 @@ runs:
|
||||
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
|
||||
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
|
||||
- name: Build worker
|
||||
run: bun run turbo build:v2:cloudflare
|
||||
run: bun run turbo build:v2:container
|
||||
env:
|
||||
GITBOOK_RUNTIME: cloudflare
|
||||
shell: bash
|
||||
@@ -70,32 +70,42 @@ runs:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
wranglerVersion: '4.21.1'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deploy --config ./packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
|
||||
|
||||
- id: upload_server
|
||||
name: Upload server to Cloudflare
|
||||
# - id: upload_server
|
||||
# name: Upload server to Cloudflare
|
||||
# uses: cloudflare/wrangler-action@v3.14.0
|
||||
# with:
|
||||
# apiToken: ${{ inputs.apiToken }}
|
||||
# accountId: ${{ inputs.accountId }}
|
||||
# workingDirectory: ./
|
||||
# wranglerVersion: '4.10.0'
|
||||
# environment: ${{ inputs.environment }}
|
||||
# command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
# - name: Extract server version worker ID
|
||||
# shell: bash
|
||||
# id: extract_server_version_id
|
||||
# run: |
|
||||
# version_id=$(echo '${{ steps.upload_server.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||
# echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
# - name: Run updateWrangler scripts
|
||||
# shell: bash
|
||||
# run: |
|
||||
# bun run ./packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||
|
||||
- id: deploy_container
|
||||
name: Deploy container to Cloudflare
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
- name: Extract server version worker ID
|
||||
shell: bash
|
||||
id: extract_server_version_id
|
||||
run: |
|
||||
version_id=$(echo '${{ steps.upload_server.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run updateWrangler scripts
|
||||
shell: bash
|
||||
run: |
|
||||
bun run ./packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||
wranglerVersion: '4.21.1'
|
||||
command: deploy --config ./packages/gitbook-v2/openNext/customWorkers/containerWrangler.jsonc
|
||||
|
||||
- id: upload_middleware
|
||||
name: Upload middleware to Cloudflare
|
||||
@@ -104,29 +114,29 @@ runs:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
wranglerVersion: '4.21.1'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
- name: Extract middleware version worker ID
|
||||
shell: bash
|
||||
id: extract_middleware_version_id
|
||||
run: |
|
||||
version_id=$(echo '${{ steps.upload_middleware.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Deploy server and middleware to Cloudflare
|
||||
if: ${{ inputs.deploy == 'true' }}
|
||||
uses: ./.github/actions/gradual-deploy-cloudflare
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
opServiceAccount: ${{ inputs.opServiceAccount }}
|
||||
opItem: ${{ inputs.opItem }}
|
||||
environment: ${{ inputs.environment }}
|
||||
serverVersionId: ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||
middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }}
|
||||
deploy: ${{ inputs.deploy }}
|
||||
# - name: Extract middleware version worker ID
|
||||
# shell: bash
|
||||
# id: extract_middleware_version_id
|
||||
# run: |
|
||||
# version_id=$(echo '${{ steps.upload_middleware.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||
# echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
# - name: Deploy server and middleware to Cloudflare
|
||||
# if: ${{ inputs.deploy == 'true' }}
|
||||
# uses: ./.github/actions/gradual-deploy-cloudflare
|
||||
# with:
|
||||
# apiToken: ${{ inputs.apiToken }}
|
||||
# accountId: ${{ inputs.accountId }}
|
||||
# opServiceAccount: ${{ inputs.opServiceAccount }}
|
||||
# opItem: ${{ inputs.opItem }}
|
||||
# environment: ${{ inputs.environment }}
|
||||
# serverVersionId: ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||
# middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }}
|
||||
# deploy: ${{ inputs.deploy }}
|
||||
|
||||
|
||||
- name: Outputs
|
||||
@@ -134,5 +144,4 @@ runs:
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||
run: |
|
||||
echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
|
||||
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
|
||||
echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY ./packages/gitbook-v2/.open-next/server-functions/default /app
|
||||
EXPOSE 3000
|
||||
CMD ["node", "index.mjs"]
|
||||
@@ -164,6 +164,8 @@
|
||||
"name": "gitbook-v2",
|
||||
"version": "0.3.0",
|
||||
"dependencies": {
|
||||
"@cloudflare/containers": "^0.0.8",
|
||||
"@cloudflare/workers-types": "^4.20250620.0",
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@opennextjs/cloudflare": "1.2.1",
|
||||
@@ -279,6 +281,7 @@
|
||||
"patchedDependencies": {
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch",
|
||||
"@opennextjs/aws@3.6.5": "patches/@opennextjs%2Faws@3.6.5.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
@@ -495,6 +498,8 @@
|
||||
|
||||
"@changesets/write": ["@changesets/write@0.3.2", "", { "dependencies": { "@changesets/types": "^6.0.0", "fs-extra": "^7.0.1", "human-id": "^1.0.2", "prettier": "^2.7.1" } }, "sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw=="],
|
||||
|
||||
"@cloudflare/containers": ["@cloudflare/containers@0.0.8", "", {}, "sha512-Uaw11AaUN0jVxRAd6PW33shAA4D4u0Yei3Xl/AOSijguYDhDGDMW4Dykx8vYUAl4tCbV9hGxvBgw9mAFCFXoJw=="],
|
||||
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="],
|
||||
|
||||
"@cloudflare/next-on-pages": ["@cloudflare/next-on-pages@1.13.12", "", { "dependencies": { "acorn": "^8.8.0", "ast-types": "^0.14.2", "chalk": "^5.2.0", "chokidar": "^3.5.3", "commander": "^11.1.0", "cookie": "^0.5.0", "esbuild": "^0.15.3", "js-yaml": "^4.1.0", "miniflare": "^3.20231218.1", "package-manager-manager": "^0.2.0", "pcre-to-regexp": "^1.1.0", "semver": "^7.5.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240208.0", "vercel": ">=30.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "next-on-pages": "bin/index.js" } }, "sha512-rPy7x9c2+0RDDdJ5o0TeRUwXJ1b7N1epnqF6qKSp5Wz1r9KHOyvaZh1ACoOC6Vu5k9su5WZOgy+8fPLIyrldMQ=="],
|
||||
@@ -511,7 +516,7 @@
|
||||
|
||||
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250409.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dK9I8zBX5rR7MtaaP2AhICQTEw3PVzHcsltN8o46w7JsbYlMvFOj27FfYH5dhs3IahgmIfw2e572QXW2o/dbpg=="],
|
||||
|
||||
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20241230.0", "", {}, "sha512-dtLD4jY35Lb750cCVyO1i/eIfdZJg2Z0i+B1RYX6BVeRPlgaHx/H18ImKAkYmy0g09Ow8R2jZy3hIxMgXun0WQ=="],
|
||||
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250620.0", "", {}, "sha512-EVvRB/DJEm6jhdKg+A4Qm4y/ry1cIvylSgSO3/f/Bv161vldDRxaXM2YoQQWFhLOJOw0qtrHsKOD51KYxV1XCw=="],
|
||||
|
||||
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.4", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA=="],
|
||||
|
||||
|
||||
+2
-1
@@ -41,6 +41,7 @@
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
|
||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch",
|
||||
"@opennextjs/aws@3.6.5": "patches/@opennextjs%2Faws@3.6.5.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import type { OpenNextConfig } from '@opennextjs/cloudflare';
|
||||
export default {
|
||||
default: {
|
||||
override: {
|
||||
wrapper: 'cloudflare-node',
|
||||
converter: 'edge',
|
||||
proxyExternalRequest: 'fetch',
|
||||
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
|
||||
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
|
||||
wrapper: 'node',
|
||||
converter: 'node',
|
||||
proxyExternalRequest: 'node',
|
||||
generateDockerfile: true,
|
||||
queue: 'dummy',
|
||||
incrementalCache: () => import('./openNext/serverCache').then((m) => m.default),
|
||||
tagCache: 'dummy',
|
||||
},
|
||||
},
|
||||
middleware: {
|
||||
@@ -26,4 +27,7 @@ export default {
|
||||
enableCacheInterception: true,
|
||||
},
|
||||
edgeExternals: ['node:crypto'],
|
||||
cloudflare: {
|
||||
dangerousDisableConfigValidation: true,
|
||||
},
|
||||
} satisfies OpenNextConfig;
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
globalThis.openNextDebug = false;globalThis.openNextVersion = "3.6.5";
|
||||
|
||||
// ../../node_modules/@opennextjs/cloudflare/dist/api/durable-objects/bucket-cache-purge.js
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
|
||||
// ../../node_modules/@opennextjs/cloudflare/dist/api/cloudflare-context.js
|
||||
var cloudflareContextSymbol = Symbol.for("__cloudflare-context__");
|
||||
|
||||
// ../../node_modules/@opennextjs/cloudflare/dist/api/overrides/internal.js
|
||||
var debugCache = (name, ...args) => {
|
||||
if (process.env.NEXT_PRIVATE_DEBUG_CACHE) {
|
||||
console.log(`[${name}] `, ...args);
|
||||
}
|
||||
};
|
||||
async function internalPurgeCacheByTags(env, tags) {
|
||||
if (!env.CACHE_PURGE_ZONE_ID && !env.CACHE_PURGE_API_TOKEN) {
|
||||
debugCache("purgeCacheByTags", "No cache zone ID or API token provided. Skipping cache purge.");
|
||||
return "missing-credentials";
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`https://api.cloudflare.com/client/v4/zones/${env.CACHE_PURGE_ZONE_ID}/purge_cache`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.CACHE_PURGE_API_TOKEN}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
tags
|
||||
})
|
||||
});
|
||||
if (response.status === 429) {
|
||||
debugCache("purgeCacheByTags", "Rate limit exceeded. Skipping cache purge.");
|
||||
return "rate-limit-exceeded";
|
||||
}
|
||||
const bodyResponse = await response.json();
|
||||
if (!bodyResponse.success) {
|
||||
debugCache("purgeCacheByTags", "Cache purge failed. Errors:", bodyResponse.errors.map((error) => `${error.code}: ${error.message}`));
|
||||
return "purge-failed";
|
||||
}
|
||||
debugCache("purgeCacheByTags", "Cache purged successfully for tags:", tags);
|
||||
return "purge-success";
|
||||
} catch (error) {
|
||||
console.error("Error purging cache by tags:", error);
|
||||
return "purge-failed";
|
||||
}
|
||||
}
|
||||
|
||||
// ../../node_modules/@opennextjs/cloudflare/dist/api/durable-objects/bucket-cache-purge.js
|
||||
var DEFAULT_BUFFER_TIME_IN_SECONDS = 5;
|
||||
var MAX_NUMBER_OF_TAGS_PER_PURGE = 100;
|
||||
var BucketCachePurge = class extends DurableObject {
|
||||
bufferTimeInSeconds;
|
||||
constructor(state, env) {
|
||||
super(state, env);
|
||||
this.bufferTimeInSeconds = env.NEXT_CACHE_DO_PURGE_BUFFER_TIME_IN_SECONDS ? parseInt(env.NEXT_CACHE_DO_PURGE_BUFFER_TIME_IN_SECONDS) : DEFAULT_BUFFER_TIME_IN_SECONDS;
|
||||
state.blockConcurrencyWhile(async () => {
|
||||
state.storage.sql.exec(`
|
||||
CREATE TABLE IF NOT EXISTS cache_purge (
|
||||
tag TEXT NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS tag_index ON cache_purge (tag);
|
||||
`);
|
||||
});
|
||||
}
|
||||
async purgeCacheByTags(tags) {
|
||||
for (const tag of tags) {
|
||||
this.ctx.storage.sql.exec(`
|
||||
INSERT OR REPLACE INTO cache_purge (tag)
|
||||
VALUES (?)`, [tag]);
|
||||
}
|
||||
const nextAlarm = await this.ctx.storage.getAlarm();
|
||||
if (!nextAlarm) {
|
||||
this.ctx.storage.setAlarm(Date.now() + this.bufferTimeInSeconds * 1e3);
|
||||
}
|
||||
}
|
||||
async alarm() {
|
||||
let tags = this.ctx.storage.sql.exec(`
|
||||
SELECT * FROM cache_purge LIMIT ${MAX_NUMBER_OF_TAGS_PER_PURGE}
|
||||
`).toArray();
|
||||
do {
|
||||
if (tags.length === 0) {
|
||||
return;
|
||||
}
|
||||
const result = await internalPurgeCacheByTags(this.env, tags.map((row) => row.tag));
|
||||
if (result === "rate-limit-exceeded") {
|
||||
throw new Error("Rate limit exceeded");
|
||||
}
|
||||
this.ctx.storage.sql.exec(`
|
||||
DELETE FROM cache_purge
|
||||
WHERE tag IN (${tags.map(() => "?").join(",")})
|
||||
`, tags.map((row) => row.tag));
|
||||
if (tags.length < MAX_NUMBER_OF_TAGS_PER_PURGE) {
|
||||
tags = [];
|
||||
} else {
|
||||
tags = this.ctx.storage.sql.exec(`
|
||||
SELECT * FROM cache_purge LIMIT ${MAX_NUMBER_OF_TAGS_PER_PURGE}
|
||||
`).toArray();
|
||||
}
|
||||
} while (tags.length >= 0);
|
||||
}
|
||||
};
|
||||
export {
|
||||
BucketCachePurge
|
||||
};
|
||||
@@ -0,0 +1,278 @@
|
||||
globalThis.openNextDebug = false;globalThis.openNextVersion = "3.6.5";
|
||||
|
||||
// ../../node_modules/@opennextjs/aws/dist/utils/error.js
|
||||
var IgnorableError = class extends Error {
|
||||
__openNextInternal = true;
|
||||
canIgnore = true;
|
||||
logLevel = 0;
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "IgnorableError";
|
||||
}
|
||||
};
|
||||
var RecoverableError = class extends Error {
|
||||
__openNextInternal = true;
|
||||
canIgnore = true;
|
||||
logLevel = 1;
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "RecoverableError";
|
||||
}
|
||||
};
|
||||
var FatalError = class extends Error {
|
||||
__openNextInternal = true;
|
||||
canIgnore = false;
|
||||
logLevel = 2;
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "FatalError";
|
||||
}
|
||||
};
|
||||
function isOpenNextError(e) {
|
||||
try {
|
||||
return "__openNextInternal" in e;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ../../node_modules/@opennextjs/aws/dist/adapters/logger.js
|
||||
function debug(...args) {
|
||||
if (globalThis.openNextDebug) {
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
function warn(...args) {
|
||||
console.warn(...args);
|
||||
}
|
||||
var DOWNPLAYED_ERROR_LOGS = [
|
||||
{
|
||||
clientName: "S3Client",
|
||||
commandName: "GetObjectCommand",
|
||||
errorName: "NoSuchKey"
|
||||
}
|
||||
];
|
||||
var isDownplayedErrorLog = (errorLog) => DOWNPLAYED_ERROR_LOGS.some((downplayedInput) => downplayedInput.clientName === errorLog?.clientName && downplayedInput.commandName === errorLog?.commandName && (downplayedInput.errorName === errorLog?.error?.name || downplayedInput.errorName === errorLog?.error?.Code));
|
||||
function error(...args) {
|
||||
if (args.some((arg) => isDownplayedErrorLog(arg))) {
|
||||
return debug(...args);
|
||||
}
|
||||
if (args.some((arg) => isOpenNextError(arg))) {
|
||||
const error2 = args.find((arg) => isOpenNextError(arg));
|
||||
if (error2.logLevel < getOpenNextErrorLogLevel()) {
|
||||
return;
|
||||
}
|
||||
if (error2.logLevel === 0) {
|
||||
return console.log(...args.map((arg) => isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg));
|
||||
}
|
||||
if (error2.logLevel === 1) {
|
||||
return warn(...args.map((arg) => isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg));
|
||||
}
|
||||
return console.error(...args);
|
||||
}
|
||||
console.error(...args);
|
||||
}
|
||||
function getOpenNextErrorLogLevel() {
|
||||
const strLevel = process.env.OPEN_NEXT_ERROR_LOG_LEVEL ?? "1";
|
||||
switch (strLevel.toLowerCase()) {
|
||||
case "debug":
|
||||
case "0":
|
||||
return 0;
|
||||
case "error":
|
||||
case "2":
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ../../node_modules/@opennextjs/cloudflare/dist/api/durable-objects/queue.js
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
var DEFAULT_MAX_REVALIDATION = 5;
|
||||
var DEFAULT_REVALIDATION_TIMEOUT_MS = 1e4;
|
||||
var DEFAULT_RETRY_INTERVAL_MS = 2e3;
|
||||
var DEFAULT_MAX_RETRIES = 6;
|
||||
var DOQueueHandler = class extends DurableObject {
|
||||
// Ongoing revalidations are deduped by the deduplication id
|
||||
// Since this is running in waitUntil, we expect the durable object state to persist this during the duration of the revalidation
|
||||
// TODO: handle incremental cache with only eventual consistency (i.e. KV or R2/D1 with the optional cache layer on top)
|
||||
ongoingRevalidations = /* @__PURE__ */ new Map();
|
||||
sql;
|
||||
routeInFailedState = /* @__PURE__ */ new Map();
|
||||
service;
|
||||
// Configurable params
|
||||
maxRevalidations;
|
||||
revalidationTimeout;
|
||||
revalidationRetryInterval;
|
||||
maxRetries;
|
||||
disableSQLite;
|
||||
constructor(ctx, env) {
|
||||
super(ctx, env);
|
||||
this.service = env.WORKER_SELF_REFERENCE;
|
||||
if (!this.service)
|
||||
throw new IgnorableError("No service binding for cache revalidation worker");
|
||||
this.sql = ctx.storage.sql;
|
||||
this.maxRevalidations = env.NEXT_CACHE_DO_QUEUE_MAX_REVALIDATION ? parseInt(env.NEXT_CACHE_DO_QUEUE_MAX_REVALIDATION) : DEFAULT_MAX_REVALIDATION;
|
||||
this.revalidationTimeout = env.NEXT_CACHE_DO_QUEUE_REVALIDATION_TIMEOUT_MS ? parseInt(env.NEXT_CACHE_DO_QUEUE_REVALIDATION_TIMEOUT_MS) : DEFAULT_REVALIDATION_TIMEOUT_MS;
|
||||
this.revalidationRetryInterval = env.NEXT_CACHE_DO_QUEUE_RETRY_INTERVAL_MS ? parseInt(env.NEXT_CACHE_DO_QUEUE_RETRY_INTERVAL_MS) : DEFAULT_RETRY_INTERVAL_MS;
|
||||
this.maxRetries = env.NEXT_CACHE_DO_QUEUE_MAX_RETRIES ? parseInt(env.NEXT_CACHE_DO_QUEUE_MAX_RETRIES) : DEFAULT_MAX_RETRIES;
|
||||
this.disableSQLite = env.NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE === "true";
|
||||
ctx.blockConcurrencyWhile(async () => {
|
||||
debug(`Restoring the state of the durable object`);
|
||||
await this.initState();
|
||||
});
|
||||
debug(`Durable object initialized`);
|
||||
}
|
||||
async revalidate(msg) {
|
||||
if (this.ongoingRevalidations.size > 2 * this.maxRevalidations) {
|
||||
warn(`Your durable object has 2 times the maximum number of revalidations (${this.maxRevalidations}) in progress. If this happens often, you should consider increasing the NEXT_CACHE_DO_QUEUE_MAX_REVALIDATION or the number of durable objects with the MAX_REVALIDATE_CONCURRENCY env var.`);
|
||||
}
|
||||
if (this.ongoingRevalidations.has(msg.MessageDeduplicationId))
|
||||
return;
|
||||
if (this.routeInFailedState.has(msg.MessageDeduplicationId))
|
||||
return;
|
||||
if (this.checkSyncTable(msg))
|
||||
return;
|
||||
if (this.ongoingRevalidations.size >= this.maxRevalidations) {
|
||||
debug(`The maximum number of revalidations (${this.maxRevalidations}) is reached. Blocking until one of the revalidations finishes.`);
|
||||
while (this.ongoingRevalidations.size >= this.maxRevalidations) {
|
||||
const ongoingRevalidations = this.ongoingRevalidations.values();
|
||||
debug(`Waiting for one of the revalidations to finish`);
|
||||
await Promise.race(ongoingRevalidations);
|
||||
}
|
||||
}
|
||||
const revalidationPromise = this.executeRevalidation(msg);
|
||||
this.ongoingRevalidations.set(msg.MessageDeduplicationId, revalidationPromise);
|
||||
this.ctx.waitUntil(revalidationPromise);
|
||||
}
|
||||
async executeRevalidation(msg) {
|
||||
try {
|
||||
debug(`Revalidating ${msg.MessageBody.host}${msg.MessageBody.url}`);
|
||||
const { MessageBody: { host, url } } = msg;
|
||||
const protocol = host.includes("localhost") ? "http" : "https";
|
||||
const response = await this.service.fetch(`${protocol}://${host}${url}`, {
|
||||
method: "HEAD",
|
||||
headers: {
|
||||
// This is defined during build
|
||||
"x-prerender-revalidate": "e7d1449a4136a34776a83dfe63a2d4ef",
|
||||
"x-isr": "1"
|
||||
},
|
||||
// This one is kind of problematic, it will always show the wall time of the revalidation to `this.revalidationTimeout`
|
||||
signal: AbortSignal.timeout(this.revalidationTimeout)
|
||||
});
|
||||
if (response.status === 200 && response.headers.get("x-nextjs-cache") !== "REVALIDATED") {
|
||||
this.routeInFailedState.delete(msg.MessageDeduplicationId);
|
||||
throw new FatalError(`The revalidation for ${host}${url} cannot be done. This error should never happen.`);
|
||||
} else if (response.status === 404) {
|
||||
this.routeInFailedState.delete(msg.MessageDeduplicationId);
|
||||
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`);
|
||||
} else if (response.status === 500) {
|
||||
await this.addToFailedState(msg);
|
||||
throw new IgnorableError(`Something went wrong while revalidating ${host}${url}`);
|
||||
} else if (response.status !== 200) {
|
||||
await this.addToFailedState(msg);
|
||||
throw new RecoverableError(`An unknown error occurred while revalidating ${host}${url}`);
|
||||
}
|
||||
if (!this.disableSQLite) {
|
||||
this.sql.exec(
|
||||
"INSERT OR REPLACE INTO sync (id, lastSuccess, buildId) VALUES (?, unixepoch(), ?)",
|
||||
// We cannot use the deduplication id because it's not unique per route - every time a route is revalidated, the deduplication id is different.
|
||||
`${host}${url}`,
|
||||
"FsLrCjhoJaCSdOYr51rb3"
|
||||
);
|
||||
}
|
||||
this.routeInFailedState.delete(msg.MessageDeduplicationId);
|
||||
} catch (e) {
|
||||
if (!isOpenNextError(e)) {
|
||||
await this.addToFailedState(msg);
|
||||
}
|
||||
error(e);
|
||||
} finally {
|
||||
this.ongoingRevalidations.delete(msg.MessageDeduplicationId);
|
||||
}
|
||||
}
|
||||
async alarm() {
|
||||
const currentDateTime = Date.now();
|
||||
const nextEventToRetry = Array.from(this.routeInFailedState.values()).filter(({ nextAlarmMs }) => nextAlarmMs > currentDateTime).sort(({ nextAlarmMs: a }, { nextAlarmMs: b }) => a - b)[0];
|
||||
const expiredEvents = Array.from(this.routeInFailedState.values()).filter(({ nextAlarmMs }) => nextAlarmMs <= currentDateTime);
|
||||
const allEventsToRetry = nextEventToRetry ? [nextEventToRetry, ...expiredEvents] : expiredEvents;
|
||||
for (const event of allEventsToRetry) {
|
||||
debug(`Retrying revalidation for ${event.msg.MessageBody.host}${event.msg.MessageBody.url}`);
|
||||
await this.executeRevalidation(event.msg);
|
||||
}
|
||||
}
|
||||
async addToFailedState(msg) {
|
||||
debug(`Adding ${msg.MessageBody.host}${msg.MessageBody.url} to the failed state`);
|
||||
const existingFailedState = this.routeInFailedState.get(msg.MessageDeduplicationId);
|
||||
let updatedFailedState;
|
||||
if (existingFailedState) {
|
||||
if (existingFailedState.retryCount >= this.maxRetries) {
|
||||
error(`The revalidation for ${msg.MessageBody.host}${msg.MessageBody.url} has failed after ${this.maxRetries} retries. It will not be tried again, but subsequent ISR requests will retry.`);
|
||||
this.routeInFailedState.delete(msg.MessageDeduplicationId);
|
||||
return;
|
||||
}
|
||||
const nextAlarmMs = Date.now() + Math.pow(2, existingFailedState.retryCount + 1) * this.revalidationRetryInterval;
|
||||
updatedFailedState = {
|
||||
...existingFailedState,
|
||||
retryCount: existingFailedState.retryCount + 1,
|
||||
nextAlarmMs
|
||||
};
|
||||
} else {
|
||||
updatedFailedState = {
|
||||
msg,
|
||||
retryCount: 1,
|
||||
nextAlarmMs: Date.now() + 2e3
|
||||
};
|
||||
}
|
||||
this.routeInFailedState.set(msg.MessageDeduplicationId, updatedFailedState);
|
||||
if (!this.disableSQLite) {
|
||||
this.sql.exec("INSERT OR REPLACE INTO failed_state (id, data, buildId) VALUES (?, ?, ?)", msg.MessageDeduplicationId, JSON.stringify(updatedFailedState), "FsLrCjhoJaCSdOYr51rb3");
|
||||
}
|
||||
await this.addAlarm();
|
||||
}
|
||||
async addAlarm() {
|
||||
const existingAlarm = await this.ctx.storage.getAlarm({ allowConcurrency: false });
|
||||
if (existingAlarm)
|
||||
return;
|
||||
if (this.routeInFailedState.size === 0)
|
||||
return;
|
||||
let nextAlarmToSetup = Math.min(...Array.from(this.routeInFailedState.values()).map(({ nextAlarmMs }) => nextAlarmMs));
|
||||
if (nextAlarmToSetup < Date.now()) {
|
||||
nextAlarmToSetup = Date.now() + this.revalidationRetryInterval;
|
||||
}
|
||||
await this.ctx.storage.setAlarm(nextAlarmToSetup);
|
||||
}
|
||||
// This function is used to restore the state of the durable object
|
||||
// We don't restore the ongoing revalidations because we cannot know in which state they are
|
||||
// We only restore the failed state and the alarm
|
||||
async initState() {
|
||||
if (this.disableSQLite)
|
||||
return;
|
||||
this.sql.exec("CREATE TABLE IF NOT EXISTS failed_state (id TEXT PRIMARY KEY, data TEXT, buildId TEXT)");
|
||||
this.sql.exec("CREATE TABLE IF NOT EXISTS sync (id TEXT PRIMARY KEY, lastSuccess INTEGER, buildId TEXT)");
|
||||
this.sql.exec("DELETE FROM failed_state WHERE buildId != ?", "FsLrCjhoJaCSdOYr51rb3");
|
||||
this.sql.exec("DELETE FROM sync WHERE buildId != ?", "FsLrCjhoJaCSdOYr51rb3");
|
||||
const failedStateCursor = this.sql.exec("SELECT * FROM failed_state");
|
||||
for (const row of failedStateCursor) {
|
||||
this.routeInFailedState.set(row.id, JSON.parse(row.data));
|
||||
}
|
||||
await this.addAlarm();
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param msg
|
||||
* @returns `true` if the route has been revalidated since the lastModified from the message, `false` otherwise
|
||||
*/
|
||||
checkSyncTable(msg) {
|
||||
try {
|
||||
if (this.disableSQLite)
|
||||
return false;
|
||||
return this.sql.exec("SELECT 1 FROM sync WHERE id = ? AND lastSuccess > ? LIMIT 1", `${msg.MessageBody.host}${msg.MessageBody.url}`, Math.round(msg.MessageBody.lastModified / 1e3)).toArray().length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
export {
|
||||
DOQueueHandler
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
globalThis.openNextDebug = false;globalThis.openNextVersion = "3.6.5";
|
||||
|
||||
// ../../node_modules/@opennextjs/cloudflare/dist/api/durable-objects/sharded-tag-cache.js
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
var DOShardedTagCache = class extends DurableObject {
|
||||
sql;
|
||||
constructor(state, env) {
|
||||
super(state, env);
|
||||
this.sql = state.storage.sql;
|
||||
state.blockConcurrencyWhile(async () => {
|
||||
this.sql.exec(`CREATE TABLE IF NOT EXISTS revalidations (tag TEXT PRIMARY KEY, revalidatedAt INTEGER)`);
|
||||
});
|
||||
}
|
||||
async getLastRevalidated(tags) {
|
||||
try {
|
||||
const result = this.sql.exec(`SELECT MAX(revalidatedAt) AS time FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")})`, ...tags).toArray();
|
||||
if (result.length === 0)
|
||||
return 0;
|
||||
return result[0]?.time;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
async hasBeenRevalidated(tags, lastModified) {
|
||||
return this.sql.exec(`SELECT 1 FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")}) AND revalidatedAt > ? LIMIT 1`, ...tags, lastModified ?? Date.now()).toArray().length > 0;
|
||||
}
|
||||
async writeTags(tags, lastModified) {
|
||||
tags.forEach((tag) => {
|
||||
this.sql.exec(`INSERT OR REPLACE INTO revalidations (tag, revalidatedAt) VALUES (?, ?)`, tag, lastModified);
|
||||
});
|
||||
}
|
||||
};
|
||||
export {
|
||||
DOShardedTagCache
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import process from "node:process";
|
||||
import stream from "node:stream";
|
||||
import * as nextEnvVars from "./next-env.mjs";
|
||||
const cloudflareContextALS = new AsyncLocalStorage();
|
||||
Object.defineProperty(globalThis, Symbol.for("__cloudflare-context__"), {
|
||||
get() {
|
||||
return cloudflareContextALS.getStore();
|
||||
}
|
||||
});
|
||||
async function runWithCloudflareRequestContext(request, env, ctx, handler) {
|
||||
init(request, env);
|
||||
return cloudflareContextALS.run({ env, ctx, cf: request.cf }, handler);
|
||||
}
|
||||
let initialized = false;
|
||||
function init(request, env) {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
const url = new URL(request.url);
|
||||
initRuntime();
|
||||
populateProcessEnv(url, env);
|
||||
}
|
||||
function initRuntime() {
|
||||
Object.assign(process, { version: process.version || "v22.14.0" });
|
||||
Object.assign(process.versions, { node: "22.14.0", ...process.versions });
|
||||
globalThis.__dirname ??= "";
|
||||
globalThis.__filename ??= "";
|
||||
import.meta.url ??= "file:///worker.js";
|
||||
const __original_fetch = globalThis.fetch;
|
||||
globalThis.fetch = (input, init2) => {
|
||||
if (init2) {
|
||||
delete init2.cache;
|
||||
}
|
||||
return __original_fetch(input, init2);
|
||||
};
|
||||
const CustomRequest = class extends globalThis.Request {
|
||||
constructor(input, init2) {
|
||||
if (init2) {
|
||||
delete init2.cache;
|
||||
Object.defineProperty(init2, "body", {
|
||||
// @ts-ignore
|
||||
value: init2.body instanceof stream.Readable ? ReadableStream.from(init2.body) : init2.body
|
||||
});
|
||||
}
|
||||
super(input, init2);
|
||||
}
|
||||
};
|
||||
Object.assign(globalThis, {
|
||||
Request: CustomRequest,
|
||||
__BUILD_TIMESTAMP_MS__: 1750516662934,
|
||||
__NEXT_BASE_PATH__: "",
|
||||
// The external middleware will use the convertTo function of the `edge` converter
|
||||
// by default it will try to fetch the request, but since we are running everything in the same worker
|
||||
// we need to use the request as is.
|
||||
__dangerous_ON_edge_converter_returns_request: true
|
||||
});
|
||||
}
|
||||
function populateProcessEnv(url, env) {
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (typeof value === "string") {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
const mode = env.NEXTJS_ENV ?? "production";
|
||||
if (nextEnvVars[mode]) {
|
||||
for (const key in nextEnvVars[mode]) {
|
||||
process.env[key] ??= nextEnvVars[mode][key];
|
||||
}
|
||||
}
|
||||
process.env.OPEN_NEXT_ORIGIN = JSON.stringify({
|
||||
default: {
|
||||
host: url.hostname,
|
||||
protocol: url.protocol.slice(0, -1),
|
||||
port: url.port
|
||||
}
|
||||
});
|
||||
process.env.__NEXT_PRIVATE_ORIGIN = url.origin;
|
||||
}
|
||||
export {
|
||||
runWithCloudflareRequestContext
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const production = {};
|
||||
export const development = {};
|
||||
export const test = {};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Container } from '@cloudflare/containers';
|
||||
import type { DurableObjectNamespace } from '@cloudflare/workers-types';
|
||||
|
||||
interface Env {
|
||||
STAGE: string;
|
||||
}
|
||||
|
||||
export class OpenNextContainer extends Container<Env> {
|
||||
defaultPort = 3000;
|
||||
sleepAfter = '10s';
|
||||
startManually = true;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(
|
||||
request: Request,
|
||||
env: { ON_CONTAINER: DurableObjectNamespace<OpenNextContainer> }
|
||||
) {
|
||||
const idOne = env.ON_CONTAINER.idFromName('foo');
|
||||
const containerInstance = env.ON_CONTAINER.get(idOne);
|
||||
|
||||
await containerInstance.start({
|
||||
envVars: {
|
||||
HOST: request.headers.get('x-host') || 'localhost:8771',
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return containerInstance.fetch(request);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "on-container-worker",
|
||||
"main": "container.ts",
|
||||
"compatibility_date": "2025-05-06",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"containers": [
|
||||
{
|
||||
"class_name": "OpenNextContainer",
|
||||
"image": "./Dockerfile",
|
||||
"max_instances": 1,
|
||||
"name": "container-open-next"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"class_name": "OpenNextContainer",
|
||||
"name": "ON_CONTAINER"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"new_sqlite_classes": ["OpenNextContainer"],
|
||||
"tag": "v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -21,9 +21,9 @@ export class R2WriteBuffer extends DurableObject {
|
||||
}
|
||||
}
|
||||
|
||||
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
|
||||
export { DOQueueHandler } from './cloudflare/durable-objects/queue.js';
|
||||
|
||||
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||
export { DOShardedTagCache } from './cloudflare/durable-objects/sharded-tag-cache.js';
|
||||
|
||||
export default {
|
||||
async fetch() {
|
||||
|
||||
@@ -1,42 +1,113 @@
|
||||
import { WorkerEntrypoint } from 'cloudflare:workers';
|
||||
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
|
||||
import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
|
||||
import { runWithCloudflareRequestContext } from './cloudflare/init.js';
|
||||
|
||||
import { handler as middlewareHandler } from '../../.open-next/middleware/handler.mjs';
|
||||
export { DOQueueHandler } from './cloudflare/durable-objects/queue.js';
|
||||
|
||||
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
|
||||
export { DOShardedTagCache } from './cloudflare/durable-objects/sharded-tag-cache.js';
|
||||
|
||||
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||
// Only needed to run locally, in prod we'll use the one from do.js
|
||||
export class R2WriteBuffer extends DurableObject {
|
||||
writePromise;
|
||||
|
||||
async write(cacheKey, value) {
|
||||
// We are already writing to this key
|
||||
if (this.writePromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
|
||||
this.ctx.waitUntil(
|
||||
this.writePromise.finally(() => {
|
||||
this.writePromise = undefined;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class extends WorkerEntrypoint {
|
||||
async fetch(request) {
|
||||
return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
|
||||
const { handler: middlewareHandler } = await import(
|
||||
'../../.open-next/middleware/handler.mjs'
|
||||
);
|
||||
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname.startsWith('/_internal/')) {
|
||||
// Incremental cache handling
|
||||
if (url.pathname.startsWith('/_internal/set')) {
|
||||
const { key, value, cacheType } = await request.json();
|
||||
globalThis.incrementalCache.set(key, value, cacheType);
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
// biome-ignore lint/style/noUselessElse: <explanation>
|
||||
} else if (url.pathname.startsWith('/_internal/get')) {
|
||||
const { key, cacheType } = await request.json();
|
||||
const value = globalThis.incrementalCache.get(key, cacheType);
|
||||
return new Response(JSON.stringify(value), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, this.env, this.ctx);
|
||||
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
|
||||
if (this.env.STAGE !== 'preview') {
|
||||
// https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/#version-affinity
|
||||
reqOrResp.headers.set(
|
||||
'Cloudflare-Workers-Version-Overrides',
|
||||
`gitbook-open-v2-${this.env.STAGE}="${this.env.WORKER_VERSION_ID}"`
|
||||
);
|
||||
return this.env.DEFAULT_WORKER?.fetch(reqOrResp, {
|
||||
if (this.env.STAGE === 'dev') {
|
||||
const modifiedUrl = new URL(reqOrResp.url);
|
||||
modifiedUrl.host = this.env.PREVIEW_HOSTNAME;
|
||||
const nextRequest = new Request(modifiedUrl, reqOrResp);
|
||||
nextRequest.headers.set('x-host', reqOrResp.host);
|
||||
return fetch(nextRequest, {
|
||||
cf: {
|
||||
cacheEverything: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
// If we are in preview mode, we need to send the request to the preview URL
|
||||
const modifiedUrl = new URL(reqOrResp.url);
|
||||
modifiedUrl.hostname = this.env.PREVIEW_HOSTNAME;
|
||||
const nextRequest = new Request(modifiedUrl, reqOrResp);
|
||||
return fetch(nextRequest, {
|
||||
cf: {
|
||||
cacheEverything: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.env.STAGE === 'preview') {
|
||||
// We just send the request to the container worker
|
||||
reqOrResp.headers.set(
|
||||
'x-host',
|
||||
reqOrResp.headers.get('host') || this.env.PREVIEW_HOSTNAME
|
||||
);
|
||||
return this.env.CONTAINER_WORKER?.fetch(reqOrResp, {
|
||||
cf: {
|
||||
cacheEverything: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// if (this.env.STAGE !== 'preview') {
|
||||
// // https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/#version-affinity
|
||||
// reqOrResp.headers.set(
|
||||
// 'Cloudflare-Workers-Version-Overrides',
|
||||
// `gitbook-open-v2-${this.env.STAGE}="${this.env.WORKER_VERSION_ID}"`
|
||||
// );
|
||||
// return this.env.DEFAULT_WORKER?.fetch(reqOrResp, {
|
||||
// cf: {
|
||||
// cacheEverything: false,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// // If we are in preview mode, we need to send the request to the preview URL
|
||||
// const modifiedUrl = new URL(reqOrResp.url);
|
||||
// modifiedUrl.hostname = this.env.PREVIEW_HOSTNAME;
|
||||
// const nextRequest = new Request(modifiedUrl, reqOrResp);
|
||||
// return fetch(nextRequest, {
|
||||
// cf: {
|
||||
// cacheEverything: false,
|
||||
// },
|
||||
// });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"dev": {
|
||||
"vars": {
|
||||
"STAGE": "dev",
|
||||
"NEXT_PRIVATE_DEBUG_CACHE": "true"
|
||||
"NEXT_PRIVATE_DEBUG_CACHE": "true",
|
||||
"PREVIEW_HOSTNAME": "localhost:3000"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
@@ -38,7 +39,15 @@
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-dev"
|
||||
}
|
||||
]
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"vars": {
|
||||
@@ -60,6 +69,10 @@
|
||||
{
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-preview"
|
||||
},
|
||||
{
|
||||
"binding": "CONTAINER_WORKER",
|
||||
"service": "on-container-worker"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type {
|
||||
CacheEntryType,
|
||||
CacheValue,
|
||||
IncrementalCache,
|
||||
WithLastModified,
|
||||
} from '@opennextjs/aws/types/overrides';
|
||||
|
||||
export default {
|
||||
get: async <CacheType extends CacheEntryType = 'cache'>(
|
||||
key: string,
|
||||
cacheType?: CacheType
|
||||
): Promise<WithLastModified<CacheValue<CacheType>> | null> => {
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`http://${process.env.HOST || 'localhost:8771'}/_internal/get`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ key, cacheType }),
|
||||
}
|
||||
);
|
||||
return resp.ok ? resp.json() : null;
|
||||
} catch (e) {
|
||||
console.error('Error fetching cache:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
set: async <CacheType extends CacheEntryType = 'cache'>(
|
||||
key: string,
|
||||
value: CacheValue<CacheType>,
|
||||
cacheType?: CacheType
|
||||
): Promise<void> => {
|
||||
await fetch(`http://${process.env.HOST || 'localhost:8771'}/_internal/set`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ key, value, cacheType }),
|
||||
});
|
||||
},
|
||||
delete: (_key: string): Promise<void> => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
name: 'ServerContainerCache',
|
||||
} satisfies IncrementalCache;
|
||||
@@ -3,6 +3,8 @@
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@cloudflare/containers": "^0.0.8",
|
||||
"@cloudflare/workers-types": "^4.20250620.0",
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@opennextjs/cloudflare": "1.2.1",
|
||||
@@ -10,12 +12,12 @@
|
||||
"assert-never": "^1.2.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "^15.3.2",
|
||||
"object-identity": "^0.1.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"rison": "^0.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"warn-once": "^0.1.1",
|
||||
"object-identity": "^0.1.2"
|
||||
"warn-once": "^0.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gitbook": "*",
|
||||
@@ -30,9 +32,11 @@
|
||||
"build:v2": "next build",
|
||||
"start": "next start",
|
||||
"build:v2:cloudflare": "opennextjs-cloudflare build",
|
||||
"build:v2:container": "open-next build",
|
||||
"dev:v2:cloudflare": "wrangler dev --port 8771 --env preview",
|
||||
"dev:v2:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
|
||||
"dev:v2:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
|
||||
"dev:v2:container": "node ./.open-next/server-functions/default/index.mjs",
|
||||
"unit": "bun test",
|
||||
"typecheck": "tsc --noEmit"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/dist/build.js b/dist/build.js
|
||||
index 0c6fee9d01db1ed9f9f0ceaa0a61242f624e7b46..9e727163f7cec8ba50627c2854f5071d9f076ad7 100644
|
||||
--- a/dist/build.js
|
||||
+++ b/dist/build.js
|
||||
@@ -35,7 +35,7 @@ export async function build(openNextConfigPath, nodeExternals) {
|
||||
// Compile cache.ts
|
||||
compileCache(options);
|
||||
// Compile middleware
|
||||
- await createMiddleware(options);
|
||||
+ await createMiddleware(options, { forceOnlyBuildOnce: true });
|
||||
createStaticAssets(options);
|
||||
if (config.dangerous?.disableIncrementalCache !== true) {
|
||||
const { useTagCache } = createCacheAssets(options);
|
||||
@@ -30,6 +30,10 @@
|
||||
"dependsOn": ["^build:v2:cloudflare", "^build:v2", "generate"],
|
||||
"outputs": [".next/**", "!.next/cache/**", "dist", ".open-next/**"]
|
||||
},
|
||||
"build:v2:container": {
|
||||
"dependsOn": ["^build:v2:cloudflare", "^build:v2", "generate"],
|
||||
"outputs": [".next/**", "!.next/cache/**", "dist", ".open-next/**"]
|
||||
},
|
||||
// Build the package for Cloudflare Pages
|
||||
"build:cloudflare": {
|
||||
"dependsOn": ["^build", "generate"]
|
||||
|
||||
Reference in New Issue
Block a user