From 955552747dec501699ce78d720a0f95c37ab7a1d Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Fri, 25 Jul 2025 13:19:02 +0200 Subject: [PATCH 1/4] improvement to test cf locally --- packages/gitbook/openNext/customWorkers/default.js | 3 +++ .../gitbook/openNext/customWorkers/defaultWrangler.jsonc | 6 +++++- .../gitbook/openNext/customWorkers/middlewareWrangler.jsonc | 3 +++ packages/gitbook/openNext/incrementalCache.ts | 5 +++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/gitbook/openNext/customWorkers/default.js b/packages/gitbook/openNext/customWorkers/default.js index 535c218167..c7da9ee964 100644 --- a/packages/gitbook/openNext/customWorkers/default.js +++ b/packages/gitbook/openNext/customWorkers/default.js @@ -2,6 +2,9 @@ import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/ini import { DurableObject } from 'cloudflare:workers'; +//Only needed to run locally, in prod we'll use the one from do.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; diff --git a/packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc b/packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc index 77f0bfde6e..b7832a6743 100644 --- a/packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc +++ b/packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc @@ -36,13 +36,17 @@ { "name": "WRITE_BUFFER", "class_name": "R2WriteBuffer" + }, + { + "name": "NEXT_TAG_CACHE_DO_SHARDED", + "class_name": "DOShardedTagCache" } ] }, "migrations": [ { "tag": "v1", - "new_sqlite_classes": ["R2WriteBuffer"] + "new_sqlite_classes": ["R2WriteBuffer", "DOShardedTagCache"] } ] }, diff --git a/packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc b/packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc index d9c379809d..e9bf9bcf1a 100644 --- a/packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc +++ b/packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc @@ -22,6 +22,9 @@ "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" }, "r2_buckets": [ diff --git a/packages/gitbook/openNext/incrementalCache.ts b/packages/gitbook/openNext/incrementalCache.ts index f16a2b4e7e..6cd75b70b4 100644 --- a/packages/gitbook/openNext/incrementalCache.ts +++ b/packages/gitbook/openNext/incrementalCache.ts @@ -36,6 +36,11 @@ class GitbookIncrementalCache implements IncrementalCache { const r2 = getCloudflareContext().env[BINDING_NAME]; const localCache = await this.getCacheInstance(); if (!r2) throw new Error('No R2 bucket'); + if (process.env.SHOULD_BYPASS_CACHE === 'true') { + // We are in a local middleware environment, we should bypass the cache + // and go directly to the server. + return null; + } try { // Check local cache first if available const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey)); From 2df100f77e816403f5eeecf87e0e8bb754397dfe Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Fri, 25 Jul 2025 14:26:22 +0200 Subject: [PATCH 2/4] implement regional caching --- packages/gitbook/openNext/incrementalCache.ts | 34 +++-- .../gitbook/openNext/tagCache/middleware.ts | 130 ++++++++++++++++-- 2 files changed, 140 insertions(+), 24 deletions(-) diff --git a/packages/gitbook/openNext/incrementalCache.ts b/packages/gitbook/openNext/incrementalCache.ts index 6cd75b70b4..97cce7cafa 100644 --- a/packages/gitbook/openNext/incrementalCache.ts +++ b/packages/gitbook/openNext/incrementalCache.ts @@ -139,19 +139,27 @@ class GitbookIncrementalCache implements IncrementalCache { } async writeToR2(key: string, value: string): Promise { - const env = getCloudflareContext().env as { - WRITE_BUFFER: DurableObjectNamespace< - Rpc.DurableObjectBranded & { - write: (key: string, value: string) => Promise; - } - >; - }; - const id = env.WRITE_BUFFER.idFromName(key); - - // A stub is a client used to invoke methods on the Durable Object - const stub = env.WRITE_BUFFER.get(id); - - await stub.write(key, value); + try { + const env = getCloudflareContext().env as { + WRITE_BUFFER: DurableObjectNamespace< + Rpc.DurableObjectBranded & { + write: (key: string, value: string) => Promise; + } + >; + }; + const id = env.WRITE_BUFFER.idFromName(key); + + // A stub is a client used to invoke methods on the Durable Object + const stub = env.WRITE_BUFFER.get(id); + + await stub.write(key, value); + } catch { + // We fallback to writing directly to R2 + // it can fail locally because the limit is 1Mb per args + // It is 32Mb in production, so we should be fine + const r2 = getCloudflareContext().env[BINDING_NAME]; + r2?.put(key, value); + } } async getCacheInstance(): Promise { diff --git a/packages/gitbook/openNext/tagCache/middleware.ts b/packages/gitbook/openNext/tagCache/middleware.ts index 6ee480cda6..5a499a73f4 100644 --- a/packages/gitbook/openNext/tagCache/middleware.ts +++ b/packages/gitbook/openNext/tagCache/middleware.ts @@ -1,13 +1,15 @@ -import { getLogger } from '@/lib/logger'; +import { createLogger, getLogger } from '@/lib/logger'; +import { filterOutNullable } from '@/lib/typescript'; import type { NextModeTagCache } from '@opennextjs/aws/types/overrides.js'; +import { getCloudflareContext } from '@opennextjs/cloudflare'; import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache'; import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter'; const originalTagCache = doShardedTagCache({ baseShardSize: 12, - regionalCache: true, - // We can probably increase this value even further - regionalCacheTtlSec: 60, + // It is broken right now, need to be fixed in OpenNext - We might still not use it depending on how + // it will be implemented there. + regionalCache: false, shardReplication: { numberOfSoftReplicas: 2, numberOfHardReplicas: 1, @@ -17,24 +19,124 @@ const originalTagCache = doShardedTagCache({ }, }); +function deduplicateTags(tags: string[]): string[] { + return Array.from(new Set(tags)); +} + +function getCacheKey(tag: string) { + return `http://regional.cache/${tag}`; +} + +async function getFromRegionalCache(tags: string[]): Promise<(readonly [string, number])[]> { + try { + const cache = await caches.open('tag'); + + const responses = await Promise.all( + tags.map(async (tag) => { + const resp = await cache.match(getCacheKey(tag)); + if (!resp) { + return null; + } + return { tag, resp }; + }) + ); + + const result = responses + .filter(filterOutNullable) + .map( + async (response) => [response.tag, (await response.resp.json()) as number] as const + ); + + return Promise.all(result); + } catch { + //If we have an error here, we just fallback to the original tag cache + return []; + } +} + +async function updateRegionalCache(tags: string[]) { + const regionalCache = await caches.open('tag'); + for (const tag of tags) { + const cacheKey = getCacheKey(tag); + const lastRevalidated = (await originalTagCache.getLastRevalidated([tag])) || 0; + + await regionalCache.put( + cacheKey, + new Response(JSON.stringify(lastRevalidated), { + headers: { + 'Content-Type': 'application/json', + // We should be safe to cache this for a while. + 'Cache-Control': 'public, max-age=300', + 'Cache-Tag': tag, + }, + }) + ); + } +} + +async function deleteFromRegionalCache(tags: string[]) { + const regionalCache = await caches.open('tag'); + await Promise.all( + tags.map(async (tag) => { + const cacheKey = getCacheKey(tag); + await regionalCache.delete(cacheKey); + }) + ); +} + export default { name: 'GitbookTagCache', mode: 'nextMode', + // We don't really use this one, as of now it is only used for soft tags getLastRevalidated: async (tags: string[]) => { const tagsToCheck = tags.filter(softTagFilter); if (tagsToCheck.length === 0) { return 0; // If no tags to check, return 0 } + const deduplicatedTags = deduplicateTags(tagsToCheck); - return await originalTagCache.getLastRevalidated(tagsToCheck); + return await originalTagCache.getLastRevalidated(deduplicatedTags); }, hasBeenRevalidated: async (tags: string[], lastModified?: number) => { - const tagsToCheck = tags.filter(softTagFilter); - if (tagsToCheck.length === 0) { - return false; // If no tags to check, return false - } + try { + const tagsToCheck = tags.filter(softTagFilter); + if (tagsToCheck.length === 0) { + return false; // If no tags to check, return false + } + + const deduplicatedTags = deduplicateTags(tagsToCheck); + const regionalCacheResult = await getFromRegionalCache(deduplicatedTags); + if (regionalCacheResult.length > 0) { + // If we have results from the regional cache, check if any of them are newer than lastModified + const cacheRevalidated = regionalCacheResult.some( + ([_, timestamp]) => timestamp >= (lastModified ?? 0) + ); + if (cacheRevalidated) { + // If any tag is revalidated, we can return true + return true; + } + } - return await originalTagCache.hasBeenRevalidated(tagsToCheck, lastModified); + const remainingTags = deduplicatedTags.filter( + (tag) => !regionalCacheResult.some(([cachedTag]) => cachedTag === tag) + ); + if (remainingTags.length > 0) { + // If there are remaining tags, check their status in the original cache + const result = await originalTagCache.hasBeenRevalidated( + remainingTags, + lastModified + ); + getCloudflareContext().ctx.waitUntil(updateRegionalCache(remainingTags)); + return result; + } + return false; // If no tags were found in the regional cache and no remaining tags, return false + } catch (e) { + createLogger('gitbookTagCache', {}).error( + `hasBeenRevalidated - Error checking tags ${tags.join(', ')}`, + e + ); + return false; // In case of error, return false + } }, writeTags: async (tags: string[]) => { const tagsToWrite = tags.filter(softTagFilter); @@ -43,7 +145,13 @@ export default { logger.warn('writeTags - No valid tags to write'); return; // If no tags to write, exit early } + + const deduplicatedTags = deduplicateTags(tagsToWrite); + // Write only the filtered tags - await originalTagCache.writeTags(tagsToWrite); + await originalTagCache.writeTags(deduplicatedTags); + + // delete from regional cache + await deleteFromRegionalCache(deduplicatedTags); }, } satisfies NextModeTagCache; From 8a1a6605e17d1e8e58bc7e3928d28327c453195a Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Mon, 28 Jul 2025 09:10:24 +0200 Subject: [PATCH 3/4] comment --- packages/gitbook/openNext/tagCache/middleware.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/gitbook/openNext/tagCache/middleware.ts b/packages/gitbook/openNext/tagCache/middleware.ts index 5a499a73f4..90d8714929 100644 --- a/packages/gitbook/openNext/tagCache/middleware.ts +++ b/packages/gitbook/openNext/tagCache/middleware.ts @@ -27,6 +27,11 @@ function getCacheKey(tag: string) { return `http://regional.cache/${tag}`; } +/** + * Fetches the last revalidated timestamp for each tag from the regional cache. + * @param tags The tags to fetch from the cache. + * @returns A promise that resolves to an array of tuples containing the tag and its last revalidated timestamp. + */ async function getFromRegionalCache(tags: string[]): Promise<(readonly [string, number])[]> { try { const cache = await caches.open('tag'); @@ -49,11 +54,13 @@ async function getFromRegionalCache(tags: string[]): Promise<(readonly [string, return Promise.all(result); } catch { - //If we have an error here, we just fallback to the original tag cache return []; } } +/** + * It will populate the regional cache with the last revalidated timestamp for each tag. + */ async function updateRegionalCache(tags: string[]) { const regionalCache = await caches.open('tag'); for (const tag of tags) { @@ -151,7 +158,10 @@ export default { // Write only the filtered tags await originalTagCache.writeTags(deduplicatedTags); - // delete from regional cache + /** + * Delete from the regional cache. + * We don't update it with new value so that we keep the DO as the single source of truth. + */ await deleteFromRegionalCache(deduplicatedTags); }, } satisfies NextModeTagCache; From b245569fea338904b7557d0946d88b74fd243ba1 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Tue, 29 Jul 2025 11:18:52 +0200 Subject: [PATCH 4/4] bump OpenNext to latest --- bun.lock | 72 +++++++++-- .../gitbook/openNext/tagCache/middleware.ts | 122 +----------------- packages/gitbook/package.json | 8 +- 3 files changed, 74 insertions(+), 128 deletions(-) diff --git a/bun.lock b/bun.lock index 156aa4b34b..2df4437c17 100644 --- a/bun.lock +++ b/bun.lock @@ -48,7 +48,7 @@ }, "packages/gitbook": { "name": "gitbook", - "version": "0.14.0", + "version": "0.14.1", "dependencies": { "@gitbook/api": "catalog:", "@gitbook/cache-tags": "workspace:*", @@ -60,7 +60,7 @@ "@gitbook/react-contentkit": "workspace:*", "@gitbook/react-math": "workspace:*", "@gitbook/react-openapi": "workspace:*", - "@opennextjs/cloudflare": "^1.4.0", + "@opennextjs/cloudflare": "^1.6.2", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.1.12", "@radix-ui/react-navigation-menu": "^1.2.3", @@ -166,7 +166,7 @@ }, "packages/openapi-parser": { "name": "@gitbook/openapi-parser", - "version": "2.2.1", + "version": "2.2.2", "dependencies": { "@scalar/openapi-parser": "^0.18.0", "@scalar/openapi-types": "^0.1.9", @@ -213,7 +213,7 @@ }, "packages/react-openapi": { "name": "@gitbook/react-openapi", - "version": "1.3.3", + "version": "1.3.4", "dependencies": { "@gitbook/openapi-parser": "workspace:*", "@scalar/api-client-react": "^1.3.16", @@ -787,9 +787,9 @@ "@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="], - "@opennextjs/aws": ["@opennextjs/aws@3.7.0", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-JXUZice+CedEQW20hnBVqzBEj+jfr4Oe2LVYSE4RNKdfHVIeYG+WJAop14TxRJ+NugKWGcJx6opf944l+ZG7XQ=="], + "@opennextjs/aws": ["@opennextjs/aws@3.7.1", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-ryV5cQucSJDU0TF3+Jzqn9g0/+zhUZgjNopSnchPvH0SxSAbKkNaNH7SlnWBhykjusqHaFzQ8dfEjcjAq7TJSQ=="], - "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.4.0", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.7.0", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.19.1" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-nEtxmSo6/y83Qw2TSU8rBpljaGl3NUl5GenD/c71h6YqgbP4QURSxMh7ySe7KK+vtQbilD7WePXovqL4Y6D6dw=="], + "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.6.2", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.7.1", "cloudflare": "^4.4.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "wrangler": "^4.24.4" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-N96OTbp1970NwLP6dNkAzBDrxjthLQ3VXHcjKl8edqNbJ/iT75H60ZYHU/ZZ7gSZQeJluPvPb1n0ZPQ08EooCQ=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -1301,6 +1301,8 @@ "@types/node": ["@types/node@20.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA=="], + "@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="], + "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], "@types/object-hash": ["@types/object-hash@3.0.6", "", {}, "sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w=="], @@ -1407,6 +1409,8 @@ "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + "ai": ["ai@4.3.0", "", { "dependencies": { "@ai-sdk/provider": "1.1.0", "@ai-sdk/provider-utils": "2.2.4", "@ai-sdk/react": "1.2.6", "@ai-sdk/ui-utils": "1.2.5", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-PxyQYKhWaU3LiZEpeKRaekVonZIbWdKAwgnqm0CSAxy1MFufmYEC5SM5Mc9uiK2DoHcbAL3d1jyaQ2fSDAJL8w=="], "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -1549,8 +1553,12 @@ "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], + "cloudflare": ["cloudflare@4.5.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-fPcbPKx4zF45jBvQ0z7PCdgejVAPBBCZxwqk1k7krQNfpM07Cfj97/Q6wBzvYqlWXx/zt1S9+m8vnfCe06umbQ=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "code-block-writer": ["code-block-writer@10.1.1", "", {}, "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw=="], @@ -1823,8 +1831,12 @@ "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="], + "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], @@ -1851,6 +1863,10 @@ "generic-pool": ["generic-pool@3.4.2", "", {}, "sha512-H7cUpwCQSiJmAHM4c/aFu6fUfrhWXW1ncyh8ftxEPMu6AiYkHw9K8br720TGPZJbk5eOH2bynjZD1yPvdDAmag=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + "get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], @@ -1979,6 +1995,8 @@ "human-signals": ["human-signals@1.1.1", "", {}, "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + "humanize-url": ["humanize-url@2.1.1", "", { "dependencies": { "normalize-url": "^4.5.1" } }, "sha512-V4nxsPGNE7mPjr1qDp471YfW8nhBiTRWrG/4usZlpvFU8I7gsV7Jvrrzv/snbLm5dWO3dr1ennu2YqnhTWFmYA=="], "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], @@ -2331,6 +2349,8 @@ "next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + "node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], "node-forge": ["node-forge@0.10.0", "", {}, "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA=="], @@ -2897,6 +2917,8 @@ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], + "web-vitals": ["web-vitals@0.2.4", "", {}, "sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -2913,7 +2935,7 @@ "wrangler": ["wrangler@4.10.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.3.1", "blake3-wasm": "2.1.5", "esbuild": "0.24.2", "miniflare": "4.20250409.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.15", "workerd": "1.20250409.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250409.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-fTE4hZ79msEUt8+HEjl/8Q72haCyzPLu4PgrU3L81ysmjrMEdiYfUPqnvCkBUVtJvrDNdctTEimkufT1Y0ipNg=="], - "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "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=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -2929,10 +2951,14 @@ "xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], + "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], + "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], @@ -3613,6 +3639,8 @@ "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@lezer/css/@lezer/common": ["@lezer/common@1.2.2", "", {}, "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw=="], "@lezer/highlight/@lezer/common": ["@lezer/common@1.2.2", "", {}, "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw=="], @@ -4049,6 +4077,14 @@ "camelcase-keys/quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="], + "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "cloudflare/@types/node": ["@types/node@18.19.121", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ=="], + + "cloudflare/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "codemirror/@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA=="], "codemirror/@codemirror/commands": ["@codemirror/commands@6.7.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-+cduIZ2KbesDhbykV02K25A5xIVrquSPz4UxxYBemRlAT2aW8dhwUgLDwej7q/RJUHKk4nALYcR1puecDvbdqw=="], @@ -4287,10 +4323,14 @@ "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - "wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + "youch/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "@argos-ci/core/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], @@ -4769,6 +4809,8 @@ "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "@mapbox/node-pre-gyp/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], "@mapbox/node-pre-gyp/tar/minizlib": ["minizlib@3.0.1", "", { "dependencies": { "minipass": "^7.0.4", "rimraf": "^5.0.5" } }, "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg=="], @@ -4909,6 +4951,12 @@ "bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "cliui/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "cloudflare/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "codemirror/@codemirror/autocomplete/@lezer/common": ["@lezer/common@1.2.2", "", {}, "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw=="], "codemirror/@codemirror/commands/@lezer/common": ["@lezer/common@1.2.2", "", {}, "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw=="], @@ -5017,10 +5065,14 @@ "wrangler/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "yargs/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "@argos-ci/core/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.3.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw=="], "@aws-sdk/client-dynamodb/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -5181,6 +5233,8 @@ "wrangler/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.3.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw=="], + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "@argos-ci/core/sharp/@img/sharp-wasm32/@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw=="], diff --git a/packages/gitbook/openNext/tagCache/middleware.ts b/packages/gitbook/openNext/tagCache/middleware.ts index 90d8714929..e5a6fb4ad0 100644 --- a/packages/gitbook/openNext/tagCache/middleware.ts +++ b/packages/gitbook/openNext/tagCache/middleware.ts @@ -1,15 +1,14 @@ import { createLogger, getLogger } from '@/lib/logger'; -import { filterOutNullable } from '@/lib/typescript'; import type { NextModeTagCache } from '@opennextjs/aws/types/overrides.js'; -import { getCloudflareContext } from '@opennextjs/cloudflare'; import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache'; import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter'; const originalTagCache = doShardedTagCache({ baseShardSize: 12, - // It is broken right now, need to be fixed in OpenNext - We might still not use it depending on how - // it will be implemented there. - regionalCache: false, + regionalCache: true, + regionalCacheTtlSec: 60 * 5 /* 5 minutes */, + // Because we invalidate the Cache API on update, we can safely set this to true + regionalCacheDangerouslyPersistMissingTags: true, shardReplication: { numberOfSoftReplicas: 2, numberOfHardReplicas: 1, @@ -19,78 +18,6 @@ const originalTagCache = doShardedTagCache({ }, }); -function deduplicateTags(tags: string[]): string[] { - return Array.from(new Set(tags)); -} - -function getCacheKey(tag: string) { - return `http://regional.cache/${tag}`; -} - -/** - * Fetches the last revalidated timestamp for each tag from the regional cache. - * @param tags The tags to fetch from the cache. - * @returns A promise that resolves to an array of tuples containing the tag and its last revalidated timestamp. - */ -async function getFromRegionalCache(tags: string[]): Promise<(readonly [string, number])[]> { - try { - const cache = await caches.open('tag'); - - const responses = await Promise.all( - tags.map(async (tag) => { - const resp = await cache.match(getCacheKey(tag)); - if (!resp) { - return null; - } - return { tag, resp }; - }) - ); - - const result = responses - .filter(filterOutNullable) - .map( - async (response) => [response.tag, (await response.resp.json()) as number] as const - ); - - return Promise.all(result); - } catch { - return []; - } -} - -/** - * It will populate the regional cache with the last revalidated timestamp for each tag. - */ -async function updateRegionalCache(tags: string[]) { - const regionalCache = await caches.open('tag'); - for (const tag of tags) { - const cacheKey = getCacheKey(tag); - const lastRevalidated = (await originalTagCache.getLastRevalidated([tag])) || 0; - - await regionalCache.put( - cacheKey, - new Response(JSON.stringify(lastRevalidated), { - headers: { - 'Content-Type': 'application/json', - // We should be safe to cache this for a while. - 'Cache-Control': 'public, max-age=300', - 'Cache-Tag': tag, - }, - }) - ); - } -} - -async function deleteFromRegionalCache(tags: string[]) { - const regionalCache = await caches.open('tag'); - await Promise.all( - tags.map(async (tag) => { - const cacheKey = getCacheKey(tag); - await regionalCache.delete(cacheKey); - }) - ); -} - export default { name: 'GitbookTagCache', mode: 'nextMode', @@ -100,9 +27,8 @@ export default { if (tagsToCheck.length === 0) { return 0; // If no tags to check, return 0 } - const deduplicatedTags = deduplicateTags(tagsToCheck); - return await originalTagCache.getLastRevalidated(deduplicatedTags); + return await originalTagCache.getLastRevalidated(tagsToCheck); }, hasBeenRevalidated: async (tags: string[], lastModified?: number) => { try { @@ -111,32 +37,7 @@ export default { return false; // If no tags to check, return false } - const deduplicatedTags = deduplicateTags(tagsToCheck); - const regionalCacheResult = await getFromRegionalCache(deduplicatedTags); - if (regionalCacheResult.length > 0) { - // If we have results from the regional cache, check if any of them are newer than lastModified - const cacheRevalidated = regionalCacheResult.some( - ([_, timestamp]) => timestamp >= (lastModified ?? 0) - ); - if (cacheRevalidated) { - // If any tag is revalidated, we can return true - return true; - } - } - - const remainingTags = deduplicatedTags.filter( - (tag) => !regionalCacheResult.some(([cachedTag]) => cachedTag === tag) - ); - if (remainingTags.length > 0) { - // If there are remaining tags, check their status in the original cache - const result = await originalTagCache.hasBeenRevalidated( - remainingTags, - lastModified - ); - getCloudflareContext().ctx.waitUntil(updateRegionalCache(remainingTags)); - return result; - } - return false; // If no tags were found in the regional cache and no remaining tags, return false + return await originalTagCache.hasBeenRevalidated(tagsToCheck, lastModified); } catch (e) { createLogger('gitbookTagCache', {}).error( `hasBeenRevalidated - Error checking tags ${tags.join(', ')}`, @@ -153,15 +54,6 @@ export default { return; // If no tags to write, exit early } - const deduplicatedTags = deduplicateTags(tagsToWrite); - - // Write only the filtered tags - await originalTagCache.writeTags(deduplicatedTags); - - /** - * Delete from the regional cache. - * We don't update it with new value so that we keep the DO as the single source of truth. - */ - await deleteFromRegionalCache(deduplicatedTags); + await originalTagCache.writeTags(tagsToWrite); }, } satisfies NextModeTagCache; diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index ef1f3208f4..54f185db61 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -13,7 +13,7 @@ "@gitbook/react-contentkit": "workspace:*", "@gitbook/react-math": "workspace:*", "@gitbook/react-openapi": "workspace:*", - "@opennextjs/cloudflare": "^1.4.0", + "@opennextjs/cloudflare": "^1.6.2", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.1.12", "@radix-ui/react-navigation-menu": "^1.2.3", @@ -22,6 +22,7 @@ "@sindresorhus/fnv1a": "^3.1.0", "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/typography": "^0.5.16", + "@tusbar/cache-control": "^1.0.2", "ai": "^4.2.2", "assert-never": "^1.2.1", "bun-types": "^1.1.20", @@ -49,7 +50,7 @@ "object-identity": "^0.1.2", "openapi-types": "^12.1.3", "p-map": "^7.0.3", - "@tusbar/cache-control": "^1.0.2", + "quick-lru": "^7.0.1", "react-hotkeys-hook": "^4.4.1", "rehype-sanitize": "^6.0.0", "rehype-stringify": "^10.0.1", @@ -67,8 +68,7 @@ "url-join": "^5.0.0", "usehooks-ts": "^3.1.0", "warn-once": "^0.1.1", - "zustand": "^5.0.3", - "quick-lru": "^7.0.1" + "zustand": "^5.0.3" }, "devDependencies": { "@argos-ci/playwright": "^5.0.5",