mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-15 07:05:18 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5356cd9e10 | |||
| 0af066bed8 | |||
| 26b2a6917d | |||
| 82cf22449f | |||
| 13eed34ff7 | |||
| 6733ac3fa0 | |||
| 497c5d325a | |||
| 264f4cc939 | |||
| 528d4a2475 | |||
| ad1e0e52af | |||
| d7b3aa97cd | |||
| 4acb8538f8 |
@@ -21,59 +21,59 @@ outputs:
|
||||
description: 'Deployment URL'
|
||||
value: ${{ steps.deploy_middleware.outputs.deployment-url }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- id: wrangler_status
|
||||
name: Check wrangler deployment status
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
using: 'composite'
|
||||
steps:
|
||||
- id: wrangler_status
|
||||
name: Check wrangler deployment status
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
# This step is used to get the version ID that is currently deployed to Cloudflare.
|
||||
- id: extract_current_version
|
||||
- id: extract_current_version
|
||||
name: Extract current version
|
||||
shell: bash
|
||||
run: |
|
||||
version_id=$(echo "${{ steps.wrangler_status.outputs.command-output }}" | grep -A 3 "(100%)" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: deploy_server
|
||||
name: Deploy server to Cloudflare at 0%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
- id: deploy_server
|
||||
name: Deploy server to Cloudflare at 0%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
|
||||
- id: deploy_middleware
|
||||
name: Deploy middleware to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
|
||||
- id: deploy_middleware
|
||||
name: Deploy middleware to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
- name: Deploy server to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
- name: Deploy server to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
|
||||
@@ -67,19 +67,32 @@ runs:
|
||||
run: bun run turbo build:cloudflare
|
||||
env:
|
||||
GITBOOK_RUNTIME: cloudflare
|
||||
VERCEL_TARGET_ENV: ${{ inputs.environment }}
|
||||
GITBOOK_BLOCK_SEARCH_INDEXATION: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
shell: bash
|
||||
|
||||
- id: extract_deployment_id
|
||||
name: Extract Next deployment ID
|
||||
shell: bash
|
||||
run: |
|
||||
deployment_id=$(bun -e '
|
||||
const buildOutput = await Bun.file("packages/gitbook/.open-next/server-functions/default/packages/gitbook/.next/required-server-files.json").json();
|
||||
const deploymentId = buildOutput.config?.deploymentId;
|
||||
if (!deploymentId) throw new Error("Next build did not emit a deployment ID");
|
||||
console.log(deploymentId);
|
||||
')
|
||||
echo "deployment_id=$deployment_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload the DO worker
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deploy --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc
|
||||
command: ${{ format('deploy --var OPEN_NEXT_BUILD_ID:{0} --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc', steps.extract_deployment_id.outputs.deployment_id) }}
|
||||
|
||||
- id: upload_server
|
||||
name: Upload server to Cloudflare
|
||||
@@ -88,7 +101,7 @@ runs:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
@@ -111,7 +124,7 @@ runs:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"version": "0.27.2",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "catalog:",
|
||||
"@cloudflare/workers-types": "^4.20251011.0",
|
||||
"@cloudflare/workers-types": "^5.20260716.1",
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/browser-types": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
@@ -223,7 +223,7 @@
|
||||
"ts-essentials": "^10.0.1",
|
||||
"typescript": "catalog:",
|
||||
"vercel": "50.37.3",
|
||||
"wrangler": "^4.79.0",
|
||||
"wrangler": "^4.122.0",
|
||||
},
|
||||
},
|
||||
"packages/icons": {
|
||||
@@ -580,21 +580,21 @@
|
||||
|
||||
"@chevrotain/utils": ["@chevrotain/utils@11.1.1", "", {}, "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ=="],
|
||||
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="],
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="],
|
||||
|
||||
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg=="],
|
||||
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260401.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZSmceM70jH6k+/62VkEcmMNzrpr4kSctkX5Lsgqv38KktfhPY/hsh75y1lRoPWS3H3kgMa4p2pUSlidZR1u2hw=="],
|
||||
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260811.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-i5jqz+ywtOefr0AJbiAc8qxBLfSim/B0WJG7aW3B+pWnoVfMJdUQvi+BWcFKZJ0MoCci3KadTx6g31VfuEEqpQ=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260401.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7UKWF+IUZ3NXMVPsDg8Cjg0r58b+uYlfvs5Yt8bvtU+geCtW4P2MxRHmRSEo8SryckXOJjb/b8tcncgCykFu8g=="],
|
||||
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260811.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NoOUM/nvaDdm2Onlnz33FikWjtatzulNtvwvy4xs0IrHaTCHwC0c8NwIt6s+AI13FkDs02/vm2I3GTPLCT9+hQ=="],
|
||||
|
||||
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260401.1", "", { "os": "linux", "cpu": "x64" }, "sha512-MDWUH/0bvL/l9aauN8zEddyYOXId1OueqrUCXXENNJ95R/lSmF6OgGVuXaYhoIhxQkNiEJ/0NOlnVYj9mJq4dw=="],
|
||||
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260811.1", "", { "os": "linux", "cpu": "x64" }, "sha512-sdYq2jL1AD1supa3fsi5O4zTB28wSjvTHj7Migh6/ts8EROPdvrSwv+rdGHhv8HJNAz/wbIAY3wZsi1Rw4uUIg=="],
|
||||
|
||||
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260401.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-UgkzpMzVWM/bwbo3vjCTg2aoKfGcUhiEoQoDdo6RGWvbHRJyLVZ4VQCG9ZcISiztkiS2ICCoYOtPy6M/lV6Gcw=="],
|
||||
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260811.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RIRv4shbu1kg05sD+DHTpSFCNnb5Dl2SkPDMUykqZa508tkPqe7VVw7gO0Q5msTBGyL0FfFrLuRxwwfA8u5Sow=="],
|
||||
|
||||
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260401.1", "", { "os": "win32", "cpu": "x64" }, "sha512-HBLzcQF5iF4Qv20tQ++pG7xs3OsCnaIbc+GAi6fmhUKZhvmzvml/jwrQzLJ+MPm0cQo41K5OO/U3T4S8tvJetQ=="],
|
||||
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260811.1", "", { "os": "win32", "cpu": "x64" }, "sha512-g6VquwjASlYAibcNW/0E6Zszht4qLkmnXOGwIjjRHl2A0Qz48kVeMcGvyH6eA0G9U3OzZojjYFpP+YeyQmmdjw=="],
|
||||
|
||||
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251011.0", "", {}, "sha512-gQpih+pbq3sP4uXltUeCSbPgZxTNp2gQd8639SaIbQMwgA6oJNHLhIART1fWy6DQACngiRzDVULA2x0ohmkGTQ=="],
|
||||
"@cloudflare/workers-types": ["@cloudflare/workers-types@5.20260716.1", "", {}, "sha512-LqQPmGAvdpQxzZGAMlDI6fnCsTlr8nRQibWsREaERqD0PucwFl25aXpUskSob70uSY2n6K1sp6Te9xkmzSFgaw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -2612,7 +2612,7 @@
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"miniflare": ["miniflare@4.20260401.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.4", "workerd": "1.20260401.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-lngHPzZFN9sxYG/mhzvnWiBMNVAN5MsO/7g32ttJ07rymtiK/ZBalODTKb8Od+BQdlU5DOR4CjVt9NydjnUyYg=="],
|
||||
"miniflare": ["miniflare@5.20260811.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260811.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-sypXsD5fjY88fZNedPqnwrwR1dwfnfbfW7MfvMyIfPJdtRiCCOpUnjWGeFVYYZ+0fQVICye6Juu+vZgzTEx8XA=="],
|
||||
|
||||
"minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
@@ -3260,9 +3260,9 @@
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"workerd": ["workerd@1.20260401.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260401.1", "@cloudflare/workerd-darwin-arm64": "1.20260401.1", "@cloudflare/workerd-linux-64": "1.20260401.1", "@cloudflare/workerd-linux-arm64": "1.20260401.1", "@cloudflare/workerd-windows-64": "1.20260401.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-mUYCd+ohaWJWF5nhDzxugWaAD/DM8Dw0ze3B7bu8JaA7S70+XQJXcvcvwE8C4qGcxSdCyqjsrFzqxKubECDwzg=="],
|
||||
"workerd": ["workerd@1.20260811.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260811.1", "@cloudflare/workerd-darwin-arm64": "1.20260811.1", "@cloudflare/workerd-linux-64": "1.20260811.1", "@cloudflare/workerd-linux-arm64": "1.20260811.1", "@cloudflare/workerd-windows-64": "1.20260811.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kh+FFm55JQ4ssxhHZV9VPdMQq3D1nHxNJgwxMtWGD4dGppJvLySdguTRDKgeNTvgq6heSz+6TTXyPSDGj8Yllw=="],
|
||||
|
||||
"wrangler": ["wrangler@4.80.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.16.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260401.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260401.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260401.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-2ZKF7uPeOZy65BGk3YfvqBCPo/xH1MrAlMmH9mVP+tCNBrTUMnwOHSj1HrZHgR8LttkAqhko0fGz+I4ax1rzyQ=="],
|
||||
"wrangler": ["wrangler@4.122.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260811.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260811.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260811.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-qkskzgQ76Y1qvVe5JARgvc3RISq6BC2rPoxQhFoKH1dKIwQc3GDFttQ/7m2OfeQ+tmQRzynv2dy/DXnxFCj2Lw=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="],
|
||||
|
||||
@@ -3272,7 +3272,7 @@
|
||||
|
||||
"write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="],
|
||||
|
||||
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||
|
||||
"xdg-app-paths": ["xdg-app-paths@5.1.0", "", { "dependencies": { "xdg-portable": "^7.0.0" } }, "sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA=="],
|
||||
|
||||
@@ -3830,7 +3830,9 @@
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"miniflare/undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="],
|
||||
"miniflare/sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="],
|
||||
|
||||
"miniflare/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
|
||||
|
||||
"mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
@@ -4308,6 +4310,62 @@
|
||||
|
||||
"micro/raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"miniflare/sharp/@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.1" }, "os": "darwin", "cpu": "x64" }, "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "os": "freebsd" }, "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.1", "", { "os": "linux", "cpu": "none" }, "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.1" }, "os": "linux", "cpu": "arm" }, "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.1" }, "os": "linux", "cpu": "ppc64" }, "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.1" }, "os": "linux", "cpu": "none" }, "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.1" }, "os": "linux", "cpu": "s390x" }, "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "cpu": "none" }, "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.2", "", { "os": "win32", "cpu": "x64" }, "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ=="],
|
||||
|
||||
"miniflare/sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"miniflare/sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"next/postcss/nanoid": ["nanoid@3.3.7", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="],
|
||||
|
||||
"next/postcss/picocolors": ["picocolors@1.1.0", "", {}, "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="],
|
||||
@@ -4440,6 +4498,10 @@
|
||||
|
||||
"micro/raw-body/http-errors/toidentifier": ["toidentifier@1.0.0", "", {}, "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="],
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
"rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
@@ -4474,6 +4536,10 @@
|
||||
|
||||
"@zenuml/core/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-freebsd-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-webcontainers-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"@opennextjs/aws/express/body-parser/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
@@ -21,11 +21,7 @@ const allowedDevOrigins =
|
||||
]
|
||||
: undefined;
|
||||
|
||||
// We don't use the deployment ID yet on 2c, we need to remove it because of https://github.com/opennextjs/opennextjs-aws/issues/1136
|
||||
let deploymentId =
|
||||
process.env.GITBOOK_RUNTIME === 'cloudflare'
|
||||
? undefined
|
||||
: process.env.GITBOOK_HEAD_SHA || process.env.GITHUB_SHA || Date.now().toString(); // Needed because we use a custom deployment method i.e. https://vercel.com/docs/skew-protection#custom-deployment-id
|
||||
let deploymentId = process.env.GITBOOK_HEAD_SHA || process.env.GITHUB_SHA || Date.now().toString(); // Needed because we use a custom deployment method i.e. https://vercel.com/docs/skew-protection#custom-deployment-id
|
||||
|
||||
const { VERCEL_TARGET_ENV } = process.env;
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ export default {
|
||||
proxyExternalRequest: 'fetch',
|
||||
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
|
||||
incrementalCache: () =>
|
||||
import('./openNext/incrementalCache/server').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
|
||||
import('./openNext/incrementalCache/cacheWorkerClient').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/cacheWorkerClient').then((m) => m.default),
|
||||
},
|
||||
},
|
||||
middleware: {
|
||||
@@ -20,8 +20,8 @@ export default {
|
||||
proxyExternalRequest: 'fetch',
|
||||
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
|
||||
incrementalCache: () =>
|
||||
import('./openNext/incrementalCache/middleware').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
|
||||
import('./openNext/incrementalCache/cacheWorkerClient').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/cacheWorkerClient').then((m) => m.default),
|
||||
},
|
||||
},
|
||||
dangerous: {
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-dev",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-dev",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
@@ -70,6 +74,10 @@
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-preview",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-preview",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
@@ -106,6 +114,10 @@
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-staging",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-staging",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
@@ -150,6 +162,10 @@
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-production",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-production",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
// This worker only purposes it to host the different DO that we will need in the other workers.
|
||||
import { DurableObject } from 'cloudflare:workers';
|
||||
|
||||
// `use cache` could cause multiple writes to the same key to happen concurrently, there is a limit of 1 write per key/second
|
||||
// so we need to buffer writes to the R2 bucket to avoid hitting this limit.
|
||||
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 { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
|
||||
|
||||
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||
|
||||
export default {
|
||||
async fetch() {
|
||||
// This worker does not handle any requests, it only provides Durable Objects
|
||||
return new Response('This worker is not meant to handle requests directly', {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
|
||||
const runWithCloudflareRequestContext = mock(
|
||||
async <T>(_: Request, __: unknown, ___: unknown, operation: () => Promise<T>) => operation()
|
||||
);
|
||||
const get = mock();
|
||||
const getTagsFromValue = mock();
|
||||
const hasBeenRevalidated = mock();
|
||||
|
||||
mock.module('cloudflare:workers', () => ({
|
||||
DurableObject: class {},
|
||||
WorkerEntrypoint: class {},
|
||||
}));
|
||||
mock.module('../../.open-next/cloudflare/init.js', () => ({ runWithCloudflareRequestContext }));
|
||||
mock.module('../incrementalCache/incrementalCache', () => ({
|
||||
GitbookIncrementalCache: class {
|
||||
get = get;
|
||||
},
|
||||
}));
|
||||
mock.module('@opennextjs/aws/utils/cache.js', () => ({ getTagsFromValue }));
|
||||
mock.module('../tagCache/middleware', () => ({
|
||||
default: { hasBeenRevalidated },
|
||||
}));
|
||||
|
||||
const { default: IncrementalCacheWorker } = await import('./do');
|
||||
|
||||
const CACHE_CONTROL = 'public, s-maxage=3600, stale-while-revalidate=86400';
|
||||
const NO_STORE_CACHE_CONTROL = 'private, no-store, max-age=0, must-revalidate';
|
||||
|
||||
const cacheValue = {
|
||||
type: 'page' as const,
|
||||
html: '<p>cached</p>',
|
||||
json: {},
|
||||
revalidate: 60,
|
||||
};
|
||||
|
||||
describe('IncrementalCacheWorker fetch', () => {
|
||||
const selfFetch = mock();
|
||||
|
||||
beforeEach(() => {
|
||||
selfFetch.mockReset();
|
||||
get.mockReset();
|
||||
getTagsFromValue.mockReset();
|
||||
hasBeenRevalidated.mockReset();
|
||||
getTagsFromValue.mockReturnValue(['space:1']);
|
||||
hasBeenRevalidated.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
const fetch = (request: Request) =>
|
||||
IncrementalCacheWorker.prototype.fetch.call(
|
||||
{
|
||||
env: { WORKER_SELF_REFERENCE: { fetch: selfFetch } },
|
||||
ctx: {},
|
||||
},
|
||||
request
|
||||
);
|
||||
|
||||
it('forwards cache reads to the internal endpoint and restores cache metadata', async () => {
|
||||
selfFetch.mockResolvedValue(
|
||||
Response.json(
|
||||
{ value: cacheValue, lastModified: 123 },
|
||||
{
|
||||
headers: {
|
||||
'x-gitbook-cache-control': CACHE_CONTROL,
|
||||
'x-gitbook-cache-tag': 'incremental-cache:entry,space:1',
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const response = await fetch(
|
||||
new Request('https://incremental-cache.internal/?key=entry&cacheType=cache')
|
||||
);
|
||||
|
||||
expect(response.headers.get('cache-control')).toBe(CACHE_CONTROL);
|
||||
expect(response.headers.get('cache-tag')).toBe('incremental-cache:entry,space:1');
|
||||
const forwardedRequest = selfFetch.mock.calls[0]?.[0] as Request;
|
||||
const forwardedURL = new URL(forwardedRequest.url);
|
||||
expect(forwardedURL.pathname).toBe('/internal');
|
||||
expect(forwardedURL.searchParams.get('key')).toBe('entry');
|
||||
expect(forwardedURL.searchParams.get('cacheType')).toBe('cache');
|
||||
});
|
||||
|
||||
it('reads and annotates a cache hit only on the internal endpoint', async () => {
|
||||
get.mockResolvedValue({ value: cacheValue, lastModified: Date.now() });
|
||||
|
||||
const response = await fetch(
|
||||
new Request('https://incremental-cache.internal/internal?key=entry&cacheType=cache')
|
||||
);
|
||||
|
||||
expect(get).toHaveBeenCalledWith('entry', 'cache');
|
||||
expect(selfFetch).not.toHaveBeenCalled();
|
||||
expect(response.headers.get('cache-control')).toBe(CACHE_CONTROL);
|
||||
expect(response.headers.get('x-gitbook-cache-control')).toBe(CACHE_CONTROL);
|
||||
expect(response.headers.get('cache-tag')).toBe('incremental-cache:entry,space:1');
|
||||
expect(response.headers.get('x-gitbook-cache-tag')).toBe('incremental-cache:entry,space:1');
|
||||
});
|
||||
|
||||
it('keeps cache misses out of the worker cache', async () => {
|
||||
get.mockResolvedValue(null);
|
||||
|
||||
const response = await fetch(
|
||||
new Request('https://incremental-cache.internal/internal?key=missing')
|
||||
);
|
||||
|
||||
expect(response.headers.get('cache-control')).toBe(NO_STORE_CACHE_CONTROL);
|
||||
expect(response.headers.get('x-gitbook-cache-control')).toBe(NO_STORE_CACHE_CONTROL);
|
||||
expect(await response.json()).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps revalidated and stale entries out of the worker cache', async () => {
|
||||
get.mockResolvedValueOnce({ value: cacheValue, lastModified: Date.now() });
|
||||
hasBeenRevalidated.mockResolvedValueOnce(true);
|
||||
const revalidatedResponse = await fetch(
|
||||
new Request('https://incremental-cache.internal/internal?key=revalidated')
|
||||
);
|
||||
|
||||
get.mockResolvedValueOnce({
|
||||
value: { ...cacheValue, revalidate: 0 },
|
||||
lastModified: Date.now() - 1_000,
|
||||
});
|
||||
const staleResponse = await fetch(
|
||||
new Request('https://incremental-cache.internal/internal?key=stale')
|
||||
);
|
||||
|
||||
expect(revalidatedResponse.headers.get('cache-control')).toBe(NO_STORE_CACHE_CONTROL);
|
||||
expect(revalidatedResponse.headers.get('x-gitbook-cache-revalidated')).toBe('true');
|
||||
expect(staleResponse.headers.get('cache-control')).toBe(NO_STORE_CACHE_CONTROL);
|
||||
expect(staleResponse.headers.get('x-gitbook-cache-revalidated')).toBe('true');
|
||||
});
|
||||
|
||||
it('rejects invalid internal requests and does not forward non-GET requests', async () => {
|
||||
const invalidResponse = await fetch(
|
||||
new Request('https://incremental-cache.internal/internal?key=entry&cacheType=invalid')
|
||||
);
|
||||
const methodResponse = await fetch(
|
||||
new Request('https://incremental-cache.internal/?key=entry', { method: 'POST' })
|
||||
);
|
||||
|
||||
expect(invalidResponse.status).toBe(400);
|
||||
expect(methodResponse.status).toBe(405);
|
||||
expect(selfFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
|
||||
import type {
|
||||
CacheEntryType,
|
||||
CacheValue,
|
||||
NextModeTagCacheWriteInput,
|
||||
} from '@opennextjs/aws/types/overrides.js';
|
||||
import { getTagsFromValue } from '@opennextjs/aws/utils/cache.js';
|
||||
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
|
||||
|
||||
// @ts-ignore Generated by the Cloudflare build.
|
||||
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
|
||||
import { GitbookIncrementalCache } from '../incrementalCache/incrementalCache';
|
||||
import tagCache from '../tagCache/middleware';
|
||||
|
||||
type CacheWorkerEnv = {
|
||||
NEXT_INC_CACHE_R2_BUCKET: {
|
||||
put(key: string, value: string): Promise<unknown>;
|
||||
};
|
||||
WORKER_SELF_REFERENCE: {
|
||||
fetch(request: Request): Promise<Response>;
|
||||
};
|
||||
};
|
||||
|
||||
//@ts-ignore - Just to avoid tag cache crashing
|
||||
globalThis.openNextConfig = {
|
||||
dangerous: {
|
||||
enableCacheInterception: true,
|
||||
},
|
||||
};
|
||||
|
||||
const cacheEntryTypes = new Set<CacheEntryType>(['cache', 'fetch', 'composable']);
|
||||
|
||||
const isCacheEntryType = (value: string | null): value is CacheEntryType =>
|
||||
value !== null && cacheEntryTypes.has(value as CacheEntryType);
|
||||
|
||||
const NO_STORE_CACHE_CONTROL = 'private, no-store, max-age=0, must-revalidate';
|
||||
const INTERNAL_PATH = '/internal';
|
||||
const CACHE_CONTROL_HEADER = 'x-gitbook-cache-control';
|
||||
const CACHE_TAG_HEADER = 'x-gitbook-cache-tag';
|
||||
|
||||
const getCacheHeaders = (cacheControl: string, cacheTag?: string): HeadersInit => ({
|
||||
'Cache-Control': cacheControl,
|
||||
[CACHE_CONTROL_HEADER]: cacheControl,
|
||||
...(cacheTag
|
||||
? {
|
||||
'Cache-Tag': cacheTag,
|
||||
[CACHE_TAG_HEADER]: cacheTag,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
const nullCacheResponse = (hasBeenRevalidated = false): Response =>
|
||||
Response.json(null, {
|
||||
headers: {
|
||||
...getCacheHeaders(NO_STORE_CACHE_CONTROL),
|
||||
...(hasBeenRevalidated ? { 'x-gitbook-cache-revalidated': 'true' } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const restoreCacheHeaders = (response: Response): Response => {
|
||||
const headers = new Headers(response.headers);
|
||||
const cacheControl = headers.get(CACHE_CONTROL_HEADER);
|
||||
const cacheTag = headers.get(CACHE_TAG_HEADER);
|
||||
|
||||
if (cacheControl) {
|
||||
headers.set('Cache-Control', cacheControl);
|
||||
}
|
||||
if (cacheTag) {
|
||||
headers.set('Cache-Tag', cacheTag);
|
||||
}
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
};
|
||||
|
||||
const isTimeStale = (value: CacheValue<CacheEntryType>, lastModified?: number): boolean => {
|
||||
const revalidate = value.revalidate;
|
||||
if (typeof revalidate !== 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Date.now() >= (lastModified ?? Date.now()) + revalidate * 1000;
|
||||
};
|
||||
|
||||
const getTagName = (tag: NextModeTagCacheWriteInput): string =>
|
||||
typeof tag === 'string' ? tag : tag.tag;
|
||||
|
||||
// `use cache` can write the same key concurrently, but R2 accepts one write per key per second.
|
||||
export class R2WriteBuffer extends DurableObject<CacheWorkerEnv> {
|
||||
private writePromise: Promise<unknown> | undefined;
|
||||
|
||||
async write(cacheKey: string, value: string): Promise<void> {
|
||||
if (this.writePromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
const writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
|
||||
this.writePromise = writePromise;
|
||||
this.ctx.waitUntil(
|
||||
writePromise.finally(() => {
|
||||
this.writePromise = undefined;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore Generated by the Cloudflare build.
|
||||
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
|
||||
|
||||
// @ts-ignore Generated by the Cloudflare build.
|
||||
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||
|
||||
export default class IncrementalCacheWorker extends WorkerEntrypoint<CacheWorkerEnv> {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
if (request.method !== 'GET') {
|
||||
return new Response('Method not allowed', { status: 405 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname !== INTERNAL_PATH) {
|
||||
url.pathname = INTERNAL_PATH;
|
||||
const response = await this.env.WORKER_SELF_REFERENCE.fetch(new Request(url, request));
|
||||
return restoreCacheHeaders(response);
|
||||
}
|
||||
|
||||
const key = url.searchParams.get('key');
|
||||
const cacheType = url.searchParams.get('cacheType');
|
||||
if (!key || (cacheType !== null && !isCacheEntryType(cacheType))) {
|
||||
return new Response('Invalid cache request', { status: 400 });
|
||||
}
|
||||
|
||||
return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
|
||||
const value = await new GitbookIncrementalCache().get(key, cacheType ?? undefined);
|
||||
if (!value?.value) {
|
||||
return nullCacheResponse();
|
||||
}
|
||||
|
||||
const tags = getTagsFromValue(value?.value as CacheValue<'cache'> | undefined);
|
||||
if (await tagCache.hasBeenRevalidated(tags, value.lastModified)) {
|
||||
return nullCacheResponse(true);
|
||||
}
|
||||
|
||||
if (isTimeStale(value.value, value.lastModified)) {
|
||||
return nullCacheResponse(true);
|
||||
}
|
||||
|
||||
return Response.json(value, {
|
||||
headers: getCacheHeaders(
|
||||
// 1 hour cache, with a 1 day stale-while-revalidate.
|
||||
'public, s-maxage=3600, stale-while-revalidate=86400',
|
||||
[`incremental-cache:${key}`, ...tags].join(',')
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async set<CacheType extends CacheEntryType>(
|
||||
key: string,
|
||||
value: CacheValue<CacheType>,
|
||||
cacheType?: CacheType
|
||||
): Promise<void> {
|
||||
await this.#runRpcOperation(async () => {
|
||||
await new GitbookIncrementalCache().set(key, value, cacheType);
|
||||
});
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
await this.#runRpcOperation(async () => {
|
||||
await new GitbookIncrementalCache().delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
async writeTags(tags: NextModeTagCacheWriteInput[]): Promise<void> {
|
||||
const tagsToWrite = tags.filter(softTagFilter);
|
||||
if (tagsToWrite.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.#runRpcOperation(async () => {
|
||||
await tagCache.writeTags(tagsToWrite);
|
||||
await this.ctx.cache?.purge({ tags: tagsToWrite.map(getTagName) });
|
||||
});
|
||||
}
|
||||
|
||||
async #runRpcOperation(operation: () => Promise<void>): Promise<void> {
|
||||
await runWithCloudflareRequestContext(
|
||||
new Request('https://incremental-cache.internal'),
|
||||
this.env,
|
||||
this.ctx,
|
||||
async () => {
|
||||
await operation();
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"main": "do.js",
|
||||
"main": "do.ts",
|
||||
"name": "gitbook-open-v2-do",
|
||||
"compatibility_date": "2025-04-14",
|
||||
"compatibility_date": "2026-06-14",
|
||||
"keep_names": false,
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
@@ -11,10 +11,17 @@
|
||||
"observability": {
|
||||
"enabled": false,
|
||||
},
|
||||
"cache": {
|
||||
"enabled": true,
|
||||
},
|
||||
"placement": {
|
||||
"region": "gcp:us-central1",
|
||||
},
|
||||
"env": {
|
||||
"preview": {
|
||||
"dev": {
|
||||
"vars": {
|
||||
"STAGE": "preview",
|
||||
"STAGE": "dev",
|
||||
"OPEN_NEXT_BUILD_ID": "local",
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||
},
|
||||
"r2_buckets": [
|
||||
@@ -26,7 +33,7 @@
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-preview",
|
||||
"service": "gitbook-open-v2-do-dev",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
@@ -52,6 +59,56 @@
|
||||
},
|
||||
],
|
||||
},
|
||||
"preview": {
|
||||
"vars": {
|
||||
"STAGE": "preview",
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview",
|
||||
},
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-do-preview",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
},
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
},
|
||||
],
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"],
|
||||
},
|
||||
],
|
||||
"observability": {
|
||||
"traces": {
|
||||
"enabled": true,
|
||||
"head_sampling_rate": 1,
|
||||
},
|
||||
"logs": {
|
||||
"enabled": true,
|
||||
"head_sampling_rate": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
"staging": {
|
||||
"vars": {
|
||||
"STAGE": "staging",
|
||||
@@ -71,7 +128,7 @@
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-staging",
|
||||
"service": "gitbook-open-v2-do-staging",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
@@ -120,7 +177,7 @@
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-production",
|
||||
"service": "gitbook-open-v2-do-production",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
|
||||
@@ -23,9 +23,6 @@
|
||||
"vars": {
|
||||
"STAGE": "dev",
|
||||
"NEXT_PRIVATE_DEBUG_CACHE": "true",
|
||||
// When deployed locally, we don't have access to the tag cache here,
|
||||
// we should just bypass the cache to go to the server directly
|
||||
"SHOULD_BYPASS_CACHE": "true",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true",
|
||||
"GITBOOK_URL": "http://localhost:8771",
|
||||
},
|
||||
@@ -44,6 +41,10 @@
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-dev",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-dev",
|
||||
},
|
||||
],
|
||||
},
|
||||
"preview": {
|
||||
@@ -67,6 +68,10 @@
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-preview",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-preview",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
@@ -87,6 +92,16 @@
|
||||
},
|
||||
],
|
||||
},
|
||||
"observability": {
|
||||
"traces": {
|
||||
"enabled": true,
|
||||
"head_sampling_rate": 1,
|
||||
},
|
||||
"logs": {
|
||||
"enabled": true,
|
||||
"head_sampling_rate": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
"staging": {
|
||||
"vars": {
|
||||
@@ -119,6 +134,10 @@
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-staging",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-staging",
|
||||
},
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
@@ -188,6 +207,10 @@
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-production",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-production",
|
||||
},
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
|
||||
const getCloudflareContext = mock();
|
||||
mock.module('@opennextjs/cloudflare', () => ({ getCloudflareContext }));
|
||||
|
||||
const { GitbookIncrementalCache } = await import('./cacheWorkerClient');
|
||||
|
||||
const cacheValue = {
|
||||
type: 'page' as const,
|
||||
html: '<p>cached</p>',
|
||||
json: {},
|
||||
};
|
||||
|
||||
describe('GitbookIncrementalCache cache worker client', () => {
|
||||
const fetch = mock();
|
||||
const set = mock();
|
||||
const remove = mock();
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
beforeEach(() => {
|
||||
fetch.mockReset();
|
||||
set.mockReset();
|
||||
remove.mockReset();
|
||||
getCloudflareContext.mockReturnValue({
|
||||
env: {
|
||||
NEXT_INC_CACHE_WORKER: { fetch, set, delete: remove },
|
||||
},
|
||||
});
|
||||
console.error = mock();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
it('gets cache entries through the service binding', async () => {
|
||||
fetch.mockResolvedValue(
|
||||
Response.json({
|
||||
value: cacheValue,
|
||||
lastModified: 123,
|
||||
})
|
||||
);
|
||||
|
||||
const result = await new GitbookIncrementalCache().get('key with / characters', 'cache');
|
||||
|
||||
expect(result).toEqual({ value: cacheValue, lastModified: 123 });
|
||||
const request = fetch.mock.calls[0]?.[0] as Request;
|
||||
const url = new URL(request.url);
|
||||
expect(url.searchParams.get('key')).toBe('key with / characters');
|
||||
expect(url.searchParams.get('cacheType')).toBe('cache');
|
||||
});
|
||||
|
||||
it('returns null for cache misses and failed reads', async () => {
|
||||
fetch.mockResolvedValue(Response.json(null));
|
||||
expect(await new GitbookIncrementalCache().get('missing')).toBeNull();
|
||||
|
||||
fetch.mockResolvedValueOnce(new Response(null, { status: 503 }));
|
||||
expect(await new GitbookIncrementalCache().get('unavailable-response')).toBeNull();
|
||||
|
||||
fetch.mockRejectedValueOnce(new Error('service unavailable'));
|
||||
expect(await new GitbookIncrementalCache().get('unavailable')).toBeNull();
|
||||
});
|
||||
|
||||
it('uses RPC for writes and deletes', async () => {
|
||||
set.mockResolvedValue(undefined);
|
||||
remove.mockResolvedValue(undefined);
|
||||
const cache = new GitbookIncrementalCache();
|
||||
|
||||
await cache.set('entry', cacheValue, 'cache');
|
||||
await cache.delete('entry');
|
||||
|
||||
expect(set).toHaveBeenCalledWith('entry', cacheValue, 'cache');
|
||||
expect(remove).toHaveBeenCalledWith('entry');
|
||||
});
|
||||
|
||||
it('contains mutation failures', async () => {
|
||||
set.mockRejectedValue(new Error('service unavailable'));
|
||||
remove.mockRejectedValue(new Error('service unavailable'));
|
||||
const cache = new GitbookIncrementalCache();
|
||||
|
||||
await expect(cache.set('entry', cacheValue, 'cache')).resolves.toBeUndefined();
|
||||
await expect(cache.delete('entry')).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import type {
|
||||
CacheEntryType,
|
||||
CacheValue,
|
||||
IncrementalCache,
|
||||
WithLastModified,
|
||||
} from '@opennextjs/aws/types/overrides.js';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
|
||||
export const BINDING_NAME = 'NEXT_INC_CACHE_WORKER';
|
||||
|
||||
type CacheWorker = {
|
||||
fetch(request: Request): Promise<Response>;
|
||||
set<CacheType extends CacheEntryType>(
|
||||
key: string,
|
||||
value: CacheValue<CacheType>,
|
||||
cacheType?: CacheType
|
||||
): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
};
|
||||
|
||||
export class GitbookIncrementalCache implements IncrementalCache {
|
||||
name = 'GitbookIncrementalCache';
|
||||
|
||||
async get<CacheType extends CacheEntryType = 'cache'>(
|
||||
key: string,
|
||||
cacheType?: CacheType
|
||||
): Promise<WithLastModified<CacheValue<CacheType>> | null> {
|
||||
try {
|
||||
const url = new URL('https://incremental-cache.internal');
|
||||
url.searchParams.set('key', key);
|
||||
if (cacheType) {
|
||||
url.searchParams.set('cacheType', cacheType);
|
||||
}
|
||||
|
||||
const response = await this.getWorker().fetch(new Request(url));
|
||||
if (!response.ok) {
|
||||
console.error('Failed to get from cache worker', response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await response.json()) as WithLastModified<CacheValue<CacheType>> | null;
|
||||
} catch (error) {
|
||||
console.error('Failed to get from cache worker', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async set<CacheType extends CacheEntryType = 'cache'>(
|
||||
key: string,
|
||||
value: CacheValue<CacheType>,
|
||||
cacheType?: CacheType
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.getWorker().set(key, value, cacheType);
|
||||
} catch (error) {
|
||||
console.error('Failed to set to cache worker', error);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
try {
|
||||
await this.getWorker().delete(key);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete from cache worker', error);
|
||||
}
|
||||
}
|
||||
|
||||
private getWorker(): CacheWorker {
|
||||
const env = getCloudflareContext().env as Record<string, unknown>;
|
||||
const worker = env[BINDING_NAME] as CacheWorker | undefined;
|
||||
if (!worker) {
|
||||
throw new Error(`Missing ${BINDING_NAME} service binding`);
|
||||
}
|
||||
return worker;
|
||||
}
|
||||
}
|
||||
|
||||
export default new GitbookIncrementalCache();
|
||||
@@ -0,0 +1,51 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import { DEFAULT_PREFIX, GitbookIncrementalCache } from './incrementalCache';
|
||||
|
||||
const environmentKeys = ['OPEN_NEXT_BUILD_ID', 'DEPLOYMENT_ID', 'NEXT_BUILD_ID'] as const;
|
||||
const originalEnvironment = Object.fromEntries(
|
||||
environmentKeys.map((key) => [key, process.env[key]])
|
||||
);
|
||||
|
||||
const hash = (key: string) => createHash('sha256').update(key).digest('hex');
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of environmentKeys) {
|
||||
const value = originalEnvironment[key];
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('GitbookIncrementalCache cache keys', () => {
|
||||
it('uses the deployment-aware OpenNext build ID', () => {
|
||||
process.env.OPEN_NEXT_BUILD_ID = 'deployment-id';
|
||||
process.env.NEXT_BUILD_ID = 'legacy-build-id';
|
||||
|
||||
expect(new GitbookIncrementalCache().getR2Key('entry')).toBe(
|
||||
`${DEFAULT_PREFIX}/deployment-id/${hash('entry')}.cache`
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the Cloudflare deployment ID when the worker build ID is unavailable', () => {
|
||||
delete process.env.OPEN_NEXT_BUILD_ID;
|
||||
process.env.DEPLOYMENT_ID = 'runtime-deployment-id';
|
||||
|
||||
expect(new GitbookIncrementalCache().getR2Key('entry')).toBe(
|
||||
`${DEFAULT_PREFIX}/runtime-deployment-id/${hash('entry')}.cache`
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes composable cache keys before applying the deployment namespace', () => {
|
||||
process.env.OPEN_NEXT_BUILD_ID = 'deployment-id';
|
||||
const key = JSON.stringify(['next-build-id', 'cache-key']);
|
||||
|
||||
expect(new GitbookIncrementalCache().getR2Key(key, 'composable')).toBe(
|
||||
`${DEFAULT_PREFIX}/dataCache/${hash(JSON.stringify(['cache-key']))}.composable`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -128,7 +128,8 @@ export class GitbookIncrementalCache implements IncrementalCache {
|
||||
}
|
||||
|
||||
const hash = createHash('sha256').update(key).digest('hex');
|
||||
return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? process.env?.NEXT_BUILD_ID : 'dataCache'}/${hash}.${cacheType}`.replace(
|
||||
const buildId = process.env.OPEN_NEXT_BUILD_ID ?? process.env.DEPLOYMENT_ID;
|
||||
return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? buildId : 'dataCache'}/${hash}.${cacheType}`.replace(
|
||||
/\/+/g,
|
||||
'/'
|
||||
);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
|
||||
// import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
|
||||
import { GitbookIncrementalCache } from './cacheWorkerClient';
|
||||
|
||||
import { GitbookIncrementalCache } from './incrementalCache';
|
||||
// export default withRegionalCache(new GitbookIncrementalCache(), {
|
||||
// mode: 'long-lived',
|
||||
// // We can do it because we use our own logic to invalidate the cache
|
||||
// bypassTagCacheOnCacheHit: true,
|
||||
// //TODO: bump it again once I figured out the race condition
|
||||
// defaultLongLivedTtlSec: 5 * 60, // 5 minutes
|
||||
// // We don't want to update the cache entry on every cache hit
|
||||
// shouldLazilyUpdateOnCacheHit: false,
|
||||
// });
|
||||
|
||||
export default withRegionalCache(new GitbookIncrementalCache(), {
|
||||
mode: 'long-lived',
|
||||
// We can do it because we use our own logic to invalidate the cache
|
||||
bypassTagCacheOnCacheHit: true,
|
||||
//TODO: bump it again once I figured out the race condition
|
||||
defaultLongLivedTtlSec: 5 * 60, // 5 minutes
|
||||
// We don't want to update the cache entry on every cache hit
|
||||
shouldLazilyUpdateOnCacheHit: false,
|
||||
});
|
||||
export default new GitbookIncrementalCache();
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
|
||||
// import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
|
||||
import { GitbookIncrementalCache } from './cacheWorkerClient';
|
||||
|
||||
import { GitbookIncrementalCache } from './incrementalCache';
|
||||
// // We cannot have regional cache only in the middleware, otherwise it will override things on cache miss
|
||||
// // and cause race conditions. This will be fixed in a future release of OpenNext
|
||||
// export default withRegionalCache(new GitbookIncrementalCache(), {
|
||||
// mode: 'long-lived',
|
||||
// // Because of a race condition, the middleware may have populated the cache entry before `cache.match` had time to run on the server.
|
||||
// // TODO: We should bypass the incremental cache entirely when the interceptor has caught the request. Should be done in OpenNext.
|
||||
// bypassTagCacheOnCacheHit: false,
|
||||
// //TODO: remove, reducing cache ttl of regional cache to help debugging
|
||||
// defaultLongLivedTtlSec: 5 * 60 /* 5 minutes */,
|
||||
// // We don't want to update the cache entry on every cache hit
|
||||
// shouldLazilyUpdateOnCacheHit: false,
|
||||
// });
|
||||
|
||||
// We cannot have regional cache only in the middleware, otherwise it will override things on cache miss
|
||||
// and cause race conditions. This will be fixed in a future release of OpenNext
|
||||
export default withRegionalCache(new GitbookIncrementalCache(), {
|
||||
mode: 'long-lived',
|
||||
// Because of a race condition, the middleware may have populated the cache entry before `cache.match` had time to run on the server.
|
||||
// TODO: We should bypass the incremental cache entirely when the interceptor has caught the request. Should be done in OpenNext.
|
||||
bypassTagCacheOnCacheHit: false,
|
||||
//TODO: remove, reducing cache ttl of regional cache to help debugging
|
||||
defaultLongLivedTtlSec: 5 * 60 /* 5 minutes */,
|
||||
// We don't want to update the cache entry on every cache hit
|
||||
shouldLazilyUpdateOnCacheHit: false,
|
||||
});
|
||||
export default new GitbookIncrementalCache();
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
|
||||
const getCloudflareContext = mock();
|
||||
mock.module('@opennextjs/cloudflare', () => ({ getCloudflareContext }));
|
||||
|
||||
const { default: tagCache } = await import('./cacheWorkerClient');
|
||||
|
||||
describe('GitbookTagCache cache worker client', () => {
|
||||
const writeTags = mock();
|
||||
|
||||
beforeEach(() => {
|
||||
writeTags.mockReset();
|
||||
getCloudflareContext.mockReturnValue({
|
||||
env: {
|
||||
NEXT_INC_CACHE_WORKER: { writeTags },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getCloudflareContext.mockReset();
|
||||
});
|
||||
|
||||
it('writes hard tags through the cache worker service binding', async () => {
|
||||
writeTags.mockResolvedValue(undefined);
|
||||
|
||||
await tagCache.writeTags([
|
||||
'content',
|
||||
{ tag: 'with-duration', stale: 100, expire: 200 },
|
||||
'_N_T_/soft-tag',
|
||||
]);
|
||||
|
||||
expect(writeTags).toHaveBeenCalledWith([
|
||||
'content',
|
||||
{ tag: 'with-duration', stale: 100, expire: 200 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not call the worker when all tags are soft tags', async () => {
|
||||
await tagCache.writeTags(['_N_T_/soft-tag']);
|
||||
|
||||
expect(writeTags).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { NextModeTagCache, NextModeTagCacheWriteInput } from '@opennextjs/aws/types/overrides';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
|
||||
|
||||
const BINDING_NAME = 'NEXT_INC_CACHE_WORKER';
|
||||
|
||||
type CacheWorker = {
|
||||
writeTags(tags: NextModeTagCacheWriteInput[]): Promise<void>;
|
||||
};
|
||||
|
||||
const getWorker = (): CacheWorker => {
|
||||
const env = getCloudflareContext().env as Record<string, unknown>;
|
||||
const worker = env[BINDING_NAME] as CacheWorker | undefined;
|
||||
if (!worker) {
|
||||
throw new Error(`Missing ${BINDING_NAME} service binding`);
|
||||
}
|
||||
return worker;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'GitbookTagCache',
|
||||
mode: 'nextMode',
|
||||
// Do nothing.
|
||||
getLastRevalidated: async () => {
|
||||
return 0;
|
||||
},
|
||||
// Return false, everything handled at the incremental cache level in the do worker.
|
||||
hasBeenRevalidated: async () => {
|
||||
return false;
|
||||
},
|
||||
writeTags: async (tags: NextModeTagCacheWriteInput[]) => {
|
||||
const tagsToWrite = tags.filter(softTagFilter);
|
||||
if (tagsToWrite.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await getWorker().writeTags(tagsToWrite);
|
||||
},
|
||||
} satisfies NextModeTagCache;
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { NextModeTagCache } from '@opennextjs/aws/types/overrides.js';
|
||||
import type {
|
||||
NextModeTagCache,
|
||||
NextModeTagCacheWriteInput,
|
||||
} from '@opennextjs/aws/types/overrides.js';
|
||||
import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
|
||||
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
|
||||
|
||||
@@ -48,7 +51,7 @@ export default {
|
||||
return false; // In case of error, return false
|
||||
}
|
||||
},
|
||||
writeTags: async (tags: string[]) => {
|
||||
writeTags: async (tags: NextModeTagCacheWriteInput[]) => {
|
||||
const tagsToWrite = tags.filter(softTagFilter);
|
||||
if (tagsToWrite.length === 0) {
|
||||
const logger = getLogger().subLogger('gitbookTagCache');
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@base-ui/react": "catalog:",
|
||||
"@cloudflare/workers-types": "^4.20251011.0",
|
||||
"@cloudflare/workers-types": "^5.20260716.1",
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/browser-types": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
@@ -113,7 +113,7 @@
|
||||
"ts-essentials": "^10.0.1",
|
||||
"typescript": "catalog:",
|
||||
"vercel": "50.37.3",
|
||||
"wrangler": "^4.79.0",
|
||||
"wrangler": "^4.122.0",
|
||||
"rss-parser": "^3.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -131,6 +131,7 @@
|
||||
"dev:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
|
||||
"dev:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
|
||||
"profile:cf:memory": "bun run build:cloudflare && bun ./scripts/profile-opennext-memory.ts",
|
||||
"dev:cf:cache": "wrangler dev --env dev --config ./openNext/customWorkers/doWrangler.jsonc",
|
||||
"e2e": "playwright test e2e/internal.spec.ts e2e/cookie-banner.spec.ts e2e/pdf.spec.ts e2e/select.spec.ts --project=chromium",
|
||||
"e2e-customers": "playwright test e2e/customers.spec.ts --project=chromium",
|
||||
"e2e-style-perf": "playwright test e2e/style-perf.spec.ts --project=chromium --reporter=list",
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Usage:
|
||||
* bun packages/gitbook/scripts/test-cache-revalidation.ts benchmark
|
||||
* bun packages/gitbook/scripts/test-cache-revalidation.ts revalidate
|
||||
*/
|
||||
|
||||
import { createHmac } from 'node:crypto';
|
||||
|
||||
// Edit this section for the local site under test. `bun dev` exposes published
|
||||
// sites at /url/<published-site-host>/<path>.
|
||||
const PAGE_URLS = [
|
||||
'http://localhost:3000/url/example.gitbook.io/docs',
|
||||
'http://localhost:3000/url/example.gitbook.io/docs/getting-started',
|
||||
];
|
||||
|
||||
// The revalidation route must use the same /url/<published-site-host>/ prefix.
|
||||
// Leave REVALIDATE_SECRET empty when the deployment does not set GITBOOK_SECRET.
|
||||
const REVALIDATE_URL = 'http://localhost:3000/url/example.gitbook.io/~gitbook/revalidate';
|
||||
const REVALIDATE_SECRET = '';
|
||||
const REVALIDATE_BODY = '{"tags":["space:YOUR_SPACE_ID"]}';
|
||||
|
||||
// Next sets x-nextjs-cache to HIT or MISS. Change this when testing through a
|
||||
// proxy that exposes the result under a different header.
|
||||
const CACHE_STATUS_HEADER = 'x-nextjs-cache';
|
||||
const BENCHMARK_RUNS = 3;
|
||||
const WARMUP_REQUESTS = 2;
|
||||
const WAIT_AFTER_REVALIDATION_MS = 1_000;
|
||||
|
||||
type RequestResult = {
|
||||
cacheStatus: string | null;
|
||||
status: number;
|
||||
totalMs: number;
|
||||
ttfbMs: number;
|
||||
url: string;
|
||||
};
|
||||
|
||||
function printTableHeader() {
|
||||
console.log(
|
||||
`${'request'.padEnd(13)} ${'HTTP'.padEnd(4)} ${CACHE_STATUS_HEADER.padEnd(16)} ${'TTFB / total'.padEnd(22)} URL`
|
||||
);
|
||||
}
|
||||
|
||||
function printResult(label: string, result: RequestResult) {
|
||||
const cacheStatus = result.cacheStatus ?? '<missing>';
|
||||
const timings = `${result.ttfbMs.toFixed(1)} / ${result.totalMs.toFixed(1)} ms`;
|
||||
console.log(
|
||||
`${label.padEnd(13)} ${String(result.status).padEnd(4)} ${cacheStatus.padEnd(16)} ${timings.padEnd(22)} ${result.url}`
|
||||
);
|
||||
}
|
||||
|
||||
async function requestPage(url: string): Promise<RequestResult> {
|
||||
const start = performance.now();
|
||||
const response = await fetch(url);
|
||||
const ttfbMs = performance.now() - start;
|
||||
|
||||
// Consume the response body so totalMs measures the entire transfer.
|
||||
await response.arrayBuffer();
|
||||
|
||||
return {
|
||||
cacheStatus: response.headers.get(CACHE_STATUS_HEADER),
|
||||
status: response.status,
|
||||
totalMs: performance.now() - start,
|
||||
ttfbMs,
|
||||
url: response.url,
|
||||
};
|
||||
}
|
||||
|
||||
function isSuccessful(result: RequestResult) {
|
||||
return result.status >= 200 && result.status < 400;
|
||||
}
|
||||
|
||||
async function benchmark(): Promise<number> {
|
||||
let failures = 0;
|
||||
printTableHeader();
|
||||
|
||||
for (let run = 1; run <= BENCHMARK_RUNS; run += 1) {
|
||||
for (const url of PAGE_URLS) {
|
||||
try {
|
||||
const result = await requestPage(url);
|
||||
printResult(`benchmark-${run}`, result);
|
||||
if (!isSuccessful(result)) {
|
||||
failures += 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`benchmark-${run} ERR ${url}: ${String(error)}`);
|
||||
failures += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
|
||||
function validateRevalidationConfig() {
|
||||
if (
|
||||
REVALIDATE_URL.includes('example.gitbook.io') ||
|
||||
REVALIDATE_BODY.includes('YOUR_') ||
|
||||
PAGE_URLS.some((url) => url.includes('example.gitbook.io'))
|
||||
) {
|
||||
throw new Error(
|
||||
'Set PAGE_URLS, REVALIDATE_URL, and REVALIDATE_BODY before revalidate mode.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createSignature() {
|
||||
if (!REVALIDATE_SECRET) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return createHmac('sha256', REVALIDATE_SECRET).update(REVALIDATE_BODY).digest('hex');
|
||||
}
|
||||
|
||||
async function revalidate(): Promise<number> {
|
||||
validateRevalidationConfig();
|
||||
let failures = 0;
|
||||
|
||||
console.log(`Warming each page (${WARMUP_REQUESTS} request(s) each)...`);
|
||||
printTableHeader();
|
||||
for (let run = 1; run <= WARMUP_REQUESTS; run += 1) {
|
||||
for (const url of PAGE_URLS) {
|
||||
try {
|
||||
const result = await requestPage(url);
|
||||
printResult(`warmup-${run}`, result);
|
||||
if (!isSuccessful(result)) {
|
||||
failures += 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`warmup-${run} ERR ${url}: ${String(error)}`);
|
||||
failures += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const headers = new Headers({ 'content-type': 'application/json' });
|
||||
const signature = createSignature();
|
||||
if (signature) {
|
||||
headers.set('x-gitbook-signature', signature);
|
||||
}
|
||||
|
||||
console.log(`Revalidating ${REVALIDATE_BODY}...`);
|
||||
const revalidationResponse = await fetch(REVALIDATE_URL, {
|
||||
body: REVALIDATE_BODY,
|
||||
headers,
|
||||
method: 'POST',
|
||||
});
|
||||
const revalidationResponseBody = await revalidationResponse.text();
|
||||
if (!revalidationResponse.ok) {
|
||||
throw new Error(
|
||||
`Revalidation failed with ${revalidationResponse.status}: ${revalidationResponseBody}`
|
||||
);
|
||||
}
|
||||
console.log(revalidationResponseBody);
|
||||
|
||||
await Bun.sleep(WAIT_AFTER_REVALIDATION_MS);
|
||||
|
||||
console.log('Checking that the next response is a MISS...');
|
||||
printTableHeader();
|
||||
for (const url of PAGE_URLS) {
|
||||
try {
|
||||
const result = await requestPage(url);
|
||||
printResult('revalidated', result);
|
||||
if (!isSuccessful(result) || result.cacheStatus?.toUpperCase() !== 'MISS') {
|
||||
failures += 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`revalidated ERR ${url}: ${String(error)}`);
|
||||
failures += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const mode = process.argv[2] ?? 'benchmark';
|
||||
let failures: number;
|
||||
|
||||
try {
|
||||
switch (mode) {
|
||||
case 'benchmark':
|
||||
failures = await benchmark();
|
||||
break;
|
||||
case 'revalidate':
|
||||
failures = await revalidate();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Usage: bun ${process.argv[1]} {benchmark|revalidate}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(String(error));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (failures > 0) {
|
||||
console.error(`${failures} request(s) failed.`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void main().then((exitCode) => {
|
||||
process.exitCode = exitCode;
|
||||
});
|
||||
Reference in New Issue
Block a user