veryfront 0.1.815 → 0.1.817
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/deno.js +1 -1
- package/esm/src/html/hydration-script-builder/templates/router.d.ts.map +1 -1
- package/esm/src/html/hydration-script-builder/templates/router.js +32 -5
- package/esm/src/proxy/handler.d.ts +2 -0
- package/esm/src/proxy/handler.d.ts.map +1 -1
- package/esm/src/proxy/handler.js +9 -5
- package/esm/src/proxy/main.js +1 -1
- package/esm/src/proxy/proxy-token-resolution.d.ts +1 -0
- package/esm/src/proxy/proxy-token-resolution.d.ts.map +1 -1
- package/esm/src/proxy/proxy-token-resolution.js +7 -1
- package/esm/src/security/http/response/cache-handler.d.ts.map +1 -1
- package/esm/src/security/http/response/cache-handler.js +3 -0
- package/esm/src/security/http/response/types.d.ts +1 -0
- package/esm/src/security/http/response/types.d.ts.map +1 -1
- package/esm/src/server/handlers/request/module/page-data-endpoint-handler.d.ts.map +1 -1
- package/esm/src/server/handlers/request/module/page-data-endpoint-handler.js +96 -15
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +1 -1
package/esm/deno.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../../../../src/src/html/hydration-script-builder/templates/router.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../../../../src/src/html/hydration-script-builder/templates/router.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,cAmvB3B,CAAC"}
|
|
@@ -33,6 +33,7 @@ export const getRouterScript = () => `
|
|
|
33
33
|
const MAX_RETRIES = 2;
|
|
34
34
|
const MAX_CACHE_SIZE = 50;
|
|
35
35
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
36
|
+
const BACKGROUND_REFRESH_INTERVAL_MS = 30 * 1000;
|
|
36
37
|
const PREFETCH_DELAY_MS = 100;
|
|
37
38
|
const MAX_PREFETCH_PATHS = 100;
|
|
38
39
|
|
|
@@ -126,6 +127,8 @@ export const getRouterScript = () => `
|
|
|
126
127
|
// LRU Cache with TTL (single Map to prevent sync issues)
|
|
127
128
|
// ============================================
|
|
128
129
|
const pageDataCache = new Map();
|
|
130
|
+
const pendingPageDataFetches = new Map();
|
|
131
|
+
const backgroundRefreshTimestamps = new Map();
|
|
129
132
|
|
|
130
133
|
function getCachedPageData(path) {
|
|
131
134
|
const entry = pageDataCache.get(path);
|
|
@@ -134,13 +137,17 @@ export const getRouterScript = () => `
|
|
|
134
137
|
if (Date.now() - entry.timestamp < CACHE_TTL_MS) return entry.data;
|
|
135
138
|
|
|
136
139
|
pageDataCache.delete(path);
|
|
140
|
+
backgroundRefreshTimestamps.delete(path);
|
|
137
141
|
return null;
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
function setCachedPageData(path, data) {
|
|
141
145
|
if (pageDataCache.size >= MAX_CACHE_SIZE) {
|
|
142
146
|
const oldest = pageDataCache.keys().next().value;
|
|
143
|
-
if (oldest)
|
|
147
|
+
if (oldest) {
|
|
148
|
+
pageDataCache.delete(oldest);
|
|
149
|
+
backgroundRefreshTimestamps.delete(oldest);
|
|
150
|
+
}
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
pageDataCache.set(path, { data, timestamp: Date.now() });
|
|
@@ -291,13 +298,33 @@ export const getRouterScript = () => `
|
|
|
291
298
|
return data;
|
|
292
299
|
}
|
|
293
300
|
|
|
301
|
+
function fetchPageDataDeduped(path) {
|
|
302
|
+
const pending = pendingPageDataFetches.get(path);
|
|
303
|
+
if (pending) return pending;
|
|
304
|
+
|
|
305
|
+
const refreshPromise = fetchPageDataFresh(path, null).finally(() => {
|
|
306
|
+
pendingPageDataFetches.delete(path);
|
|
307
|
+
});
|
|
308
|
+
pendingPageDataFetches.set(path, refreshPromise);
|
|
309
|
+
return refreshPromise;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function refreshPageDataInBackground(path) {
|
|
313
|
+
const lastRefreshAt = backgroundRefreshTimestamps.get(path) || 0;
|
|
314
|
+
const now = Date.now();
|
|
315
|
+
if (now - lastRefreshAt < BACKGROUND_REFRESH_INTERVAL_MS) return;
|
|
316
|
+
|
|
317
|
+
backgroundRefreshTimestamps.set(path, now);
|
|
318
|
+
fetchPageDataDeduped(path).catch((error) => {
|
|
319
|
+
logBackgroundFetchFailure('Stale page data refresh', path, error);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
294
323
|
async function fetchPageDataForNavigation(path, signal) {
|
|
295
324
|
const cached = getCachedPageData(path);
|
|
296
325
|
if (cached) {
|
|
297
326
|
log('Using cached page data:', path);
|
|
298
|
-
|
|
299
|
-
logBackgroundFetchFailure('Stale page data refresh', path, error);
|
|
300
|
-
});
|
|
327
|
+
refreshPageDataInBackground(path);
|
|
301
328
|
return cached;
|
|
302
329
|
}
|
|
303
330
|
|
|
@@ -306,7 +333,7 @@ export const getRouterScript = () => `
|
|
|
306
333
|
|
|
307
334
|
async function fetchPageDataForPrefetch(path) {
|
|
308
335
|
if (getCachedPageData(path)) return;
|
|
309
|
-
return
|
|
336
|
+
return fetchPageDataDeduped(path)
|
|
310
337
|
.then((data) => preloadModulesForPageData(data, path))
|
|
311
338
|
.catch((error) => {
|
|
312
339
|
logBackgroundFetchFailure('Page data prefetch', path, error);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ParsedDomain } from "../server/utils/domain-parser.js";
|
|
2
2
|
import type { TokenCache } from "./cache/types.js";
|
|
3
|
+
import { type ProxyServerTiming } from "./server-timing.js";
|
|
3
4
|
export { __resetCachedAuthProviderForTests } from "./proxy-access-control.js";
|
|
4
5
|
export declare const INTERNAL_PROXY_HEADERS: readonly ["x-token", "x-project-slug", "x-environment", "x-environment-id", "x-content-source-id", "x-forwarded-host", "x-project-path", "x-project-id", "x-release-id", "x-branch-id", "x-branch-name"];
|
|
5
6
|
export interface ProxyConfig {
|
|
@@ -45,6 +46,7 @@ export interface ProxyHandlerOptions {
|
|
|
45
46
|
}
|
|
46
47
|
export interface ProxyRequestOptions {
|
|
47
48
|
url?: URL;
|
|
49
|
+
timing?: ProxyServerTiming;
|
|
48
50
|
}
|
|
49
51
|
export declare function createProxyHandler(options: ProxyHandlerOptions): {
|
|
50
52
|
processRequest: (req: Request, options?: ProxyRequestOptions) => Promise<ProxyContext>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/src/proxy/handler.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,YAAY,EAAsB,MAAM,kCAAkC,CAAC;AACzF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/src/proxy/handler.ts"],"names":[],"mappings":"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"}
|
package/esm/src/proxy/handler.js
CHANGED
|
@@ -6,6 +6,7 @@ import { checkProtectedProxyAccess } from "./proxy-access-control.js";
|
|
|
6
6
|
import { createLocalProjectResolver } from "./local-project-resolver.js";
|
|
7
7
|
import { isMissingCustomDomainProjectError, resolveProxyRequestToken, } from "./proxy-token-resolution.js";
|
|
8
8
|
import { createProjectNotFoundProxyContext, createProxyErrorContext, createReleaseNotFoundProxyContext, } from "./proxy-error-context.js";
|
|
9
|
+
import { profileProxyServerTimingPhase } from "./server-timing.js";
|
|
9
10
|
export { __resetCachedAuthProviderForTests } from "./proxy-access-control.js";
|
|
10
11
|
export const INTERNAL_PROXY_HEADERS = [
|
|
11
12
|
"x-token",
|
|
@@ -106,6 +107,9 @@ export function createProxyHandler(options) {
|
|
|
106
107
|
previewApiClientId: config.previewApiClientId,
|
|
107
108
|
previewApiClientSecret: config.previewApiClientSecret,
|
|
108
109
|
}, { cache });
|
|
110
|
+
async function resolveProjectLookup(lookupKey, token, timing) {
|
|
111
|
+
return await profileProxyServerTimingPhase(timing ?? { enabled: false, startedAt: 0, phases: new Map() }, "proxy.project_lookup", () => lookupProjectByDomain(lookupKey, config.apiBaseUrl, token, logger));
|
|
112
|
+
}
|
|
109
113
|
function validateConfig() {
|
|
110
114
|
const missing = [];
|
|
111
115
|
if (!config.apiClientId)
|
|
@@ -114,8 +118,8 @@ export function createProxyHandler(options) {
|
|
|
114
118
|
missing.push("VERYFRONT_PROXY_API_CLIENT_SECRET");
|
|
115
119
|
return missing;
|
|
116
120
|
}
|
|
117
|
-
async function resolveReleaseAndProtection(req, url, token, userToken, lookupKey, envMatcher, logContext) {
|
|
118
|
-
const lookupResult = await
|
|
121
|
+
async function resolveReleaseAndProtection(req, url, token, userToken, lookupKey, envMatcher, timing, logContext) {
|
|
122
|
+
const lookupResult = await resolveProjectLookup(lookupKey, token, timing);
|
|
119
123
|
if (!lookupResult)
|
|
120
124
|
return { projectId: undefined, releaseId: undefined };
|
|
121
125
|
const matchingEnv = lookupResult.environments?.find(envMatcher);
|
|
@@ -232,7 +236,7 @@ export function createProxyHandler(options) {
|
|
|
232
236
|
token,
|
|
233
237
|
});
|
|
234
238
|
}
|
|
235
|
-
const lookupResult = await
|
|
239
|
+
const lookupResult = await resolveProjectLookup(host, token, options.timing);
|
|
236
240
|
if (!lookupResult) {
|
|
237
241
|
logger?.info("Custom domain not found", { domain: host });
|
|
238
242
|
return createProxyErrorContext(base, {
|
|
@@ -276,7 +280,7 @@ export function createProxyHandler(options) {
|
|
|
276
280
|
}
|
|
277
281
|
else if (projectSlug && scope === "production" && token && parsedDomain.environment) {
|
|
278
282
|
const targetEnv = parsedDomain.environment.toLowerCase();
|
|
279
|
-
const resolved = await resolveReleaseAndProtection(req, url, token, userToken, projectSlug, (env) => env.name.toLowerCase() === targetEnv, { projectSlug });
|
|
283
|
+
const resolved = await resolveReleaseAndProtection(req, url, token, userToken, projectSlug, (env) => env.name.toLowerCase() === targetEnv, options.timing, { projectSlug });
|
|
280
284
|
if ("error" in resolved) {
|
|
281
285
|
return createProxyErrorContext(base, {
|
|
282
286
|
status: resolved.error.status,
|
|
@@ -308,7 +312,7 @@ export function createProxyHandler(options) {
|
|
|
308
312
|
else if (projectSlug && scope === "preview" && token) {
|
|
309
313
|
// Preview uses branch-based content (no releaseId needed), but must
|
|
310
314
|
// still enforce the environment's `protected` flag like other scopes.
|
|
311
|
-
const resolved = await resolveReleaseAndProtection(req, url, token, userToken, projectSlug, (env) => env.name.toLowerCase() === "preview", { projectSlug });
|
|
315
|
+
const resolved = await resolveReleaseAndProtection(req, url, token, userToken, projectSlug, (env) => env.name.toLowerCase() === "preview", options.timing, { projectSlug });
|
|
312
316
|
if ("error" in resolved) {
|
|
313
317
|
return createProxyErrorContext(base, {
|
|
314
318
|
status: resolved.error.status,
|
package/esm/src/proxy/main.js
CHANGED
|
@@ -238,7 +238,7 @@ function forwardToServer(req, url) {
|
|
|
238
238
|
const host = req.headers.get("host") || "";
|
|
239
239
|
const execute = async (lifecycle) => {
|
|
240
240
|
try {
|
|
241
|
-
const ctx = await profileProxyServerTimingPhase(proxyTiming, "proxy.resolve_request", () => proxyHandler.processRequest(req, { url }));
|
|
241
|
+
const ctx = await profileProxyServerTimingPhase(proxyTiming, "proxy.resolve_request", () => proxyHandler.processRequest(req, { url, timing: proxyTiming }));
|
|
242
242
|
return runWithProxyRequestContext({
|
|
243
243
|
requestId,
|
|
244
244
|
projectSlug: ctx.projectSlug,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy-token-resolution.d.ts","sourceRoot":"","sources":["../../../src/src/proxy/proxy-token-resolution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CACN,KAAK,EAAE,UAAU,EACjB,WAAW,CAAC,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC9D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC7D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC7D,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC9E;AAED,MAAM,WAAW,+BAA+B;IAC9C,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,GAAG,CAAC;IACT,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,MAAM,EAAE,0BAA0B,CAAC;IACnC,YAAY,EAAE,iBAAiB,CAAC;IAChC,MAAM,CAAC,EAAE,0BAA0B,CAAC;IACpC,oCAAoC,CAAC,EAAE,OAAO,CAAC;IAC/C,iCAAiC,CAAC,EAAE,OAAO,CAAC;IAC5C,sBAAsB,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGzE;AAID,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGzE;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,
|
|
1
|
+
{"version":3,"file":"proxy-token-resolution.d.ts","sourceRoot":"","sources":["../../../src/src/proxy/proxy-token-resolution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CACN,KAAK,EAAE,UAAU,EACjB,WAAW,CAAC,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC9D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC7D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC7D,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC9E;AAED,MAAM,WAAW,+BAA+B;IAC9C,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,GAAG,CAAC;IACT,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,MAAM,EAAE,0BAA0B,CAAC;IACnC,YAAY,EAAE,iBAAiB,CAAC;IAChC,MAAM,CAAC,EAAE,0BAA0B,CAAC;IACpC,oCAAoC,CAAC,EAAE,OAAO,CAAC;IAC/C,iCAAiC,CAAC,EAAE,OAAO,CAAC;IAC5C,sBAAsB,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGzE;AAID,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGzE;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,CA0DpC"}
|
|
@@ -14,9 +14,12 @@ export async function resolveProxyRequestToken(options) {
|
|
|
14
14
|
const useSignedInternalControlPlaneToken = options.allowSignedInternalControlPlaneToken &&
|
|
15
15
|
options.signedInternalControlPlaneRequest;
|
|
16
16
|
let token;
|
|
17
|
+
let tokenSource;
|
|
17
18
|
let tokenFetchError;
|
|
18
19
|
if (useSignedInternalControlPlaneToken) {
|
|
19
20
|
token = req.headers.get("x-token") ?? undefined;
|
|
21
|
+
if (token)
|
|
22
|
+
tokenSource = "signed-internal";
|
|
20
23
|
logger?.debug("Using signed control-plane token for internal request", {
|
|
21
24
|
pathname: url.pathname,
|
|
22
25
|
scope,
|
|
@@ -24,6 +27,7 @@ export async function resolveProxyRequestToken(options) {
|
|
|
24
27
|
}
|
|
25
28
|
else if (scope === "preview" && userToken) {
|
|
26
29
|
token = userToken;
|
|
30
|
+
tokenSource = "user";
|
|
27
31
|
logger?.debug("Using user auth token for preview");
|
|
28
32
|
}
|
|
29
33
|
if (!token && config.apiClientId && config.apiClientSecret) {
|
|
@@ -31,6 +35,7 @@ export async function resolveProxyRequestToken(options) {
|
|
|
31
35
|
if (projectSlug || customDomain) {
|
|
32
36
|
try {
|
|
33
37
|
token = await tokenManager.getToken(scope, projectSlug, customDomain);
|
|
38
|
+
tokenSource = "service";
|
|
34
39
|
}
|
|
35
40
|
catch (error) {
|
|
36
41
|
tokenFetchError = error;
|
|
@@ -45,7 +50,8 @@ export async function resolveProxyRequestToken(options) {
|
|
|
45
50
|
}
|
|
46
51
|
if (!token && config.apiToken) {
|
|
47
52
|
token = config.apiToken;
|
|
53
|
+
tokenSource = "static";
|
|
48
54
|
logger?.debug("Using static API token fallback");
|
|
49
55
|
}
|
|
50
|
-
return { token, userToken, tokenFetchError };
|
|
56
|
+
return { token, tokenSource, userToken, tokenFetchError };
|
|
51
57
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/security/http/response/cache-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAahD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"cache-handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/security/http/response/cache-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAahD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAuBjE"}
|
|
@@ -23,5 +23,8 @@ export function buildCacheControl(strategy) {
|
|
|
23
23
|
if (strategy.mustRevalidate) {
|
|
24
24
|
parts.push("must-revalidate");
|
|
25
25
|
}
|
|
26
|
+
if (typeof strategy.staleWhileRevalidate === "number") {
|
|
27
|
+
parts.push(`stale-while-revalidate=${strategy.staleWhileRevalidate}`);
|
|
28
|
+
}
|
|
26
29
|
return parts.join(", ");
|
|
27
30
|
}
|
|
@@ -10,6 +10,7 @@ export type CacheStrategy = "no-cache" | "no-store" | "short" | "medium" | "long
|
|
|
10
10
|
public?: boolean;
|
|
11
11
|
immutable?: boolean;
|
|
12
12
|
mustRevalidate?: boolean;
|
|
13
|
+
staleWhileRevalidate?: number;
|
|
13
14
|
};
|
|
14
15
|
export interface ResponseBuilderConfig {
|
|
15
16
|
securityConfig?: SecurityConfig | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/src/security/http/response/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC9D,YAAY,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D,MAAM,MAAM,aAAa,GACrB,UAAU,GACV,UAAU,GACV,OAAO,GACP,QAAQ,GACR,MAAM,GACN,WAAW,GACX,MAAM,GACN;IACA,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/src/security/http/response/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC9D,YAAY,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D,MAAM,MAAM,aAAa,GACrB,UAAU,GACV,UAAU,GACV,OAAO,GACP,QAAQ,GACR,MAAM,GACN,WAAW,GACX,MAAM,GACN;IACA,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEJ,MAAM,WAAW,qBAAqB;IACpC,cAAc,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACvC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,CAAC,EAAE,OAAO,oCAAoC,EAAE,cAAc,CAAC;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page-data-endpoint-handler.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/request/module/page-data-endpoint-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"page-data-endpoint-handler.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/request/module/page-data-endpoint-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AA0EhE,wBAAgB,oCAAoC,IAAI,IAAI,CAE3D;AAED,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,cAAc,EACnB,qBAAqB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,eAAe,EAC/D,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,aAAa,EAC9C,eAAe,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,GAC1C,OAAO,CAAC,aAAa,CAAC,CA6GxB"}
|
|
@@ -8,11 +8,44 @@ import { HTTP_GATEWAY_TIMEOUT } from "../../../../utils/constants/http.js";
|
|
|
8
8
|
import { serverLogger } from "../../../../utils/index.js";
|
|
9
9
|
import { Singleflight } from "../../../../utils/singleflight.js";
|
|
10
10
|
import { requestHasCacheSensitiveState } from "../../../../cache/request-cacheability.js";
|
|
11
|
+
import { getEnv } from "../../../../platform/compat/process.js";
|
|
11
12
|
const PAGE_DATA_TIMEOUT_MS = 25_000;
|
|
12
|
-
const PAGE_DATA_CACHE_TTL_MS = 60_000;
|
|
13
|
-
const
|
|
13
|
+
const PAGE_DATA_CACHE_TTL_MS = readPositiveIntegerEnv("VERYFRONT_PAGE_DATA_CACHE_TTL_MS", 60_000);
|
|
14
|
+
const PAGE_DATA_CACHE_STALE_MS = readPositiveIntegerEnv("VERYFRONT_PAGE_DATA_CACHE_STALE_MS", 30 * 60_000);
|
|
15
|
+
const PAGE_DATA_CACHE_MAX_ENTRIES = readPositiveIntegerEnv("VERYFRONT_PAGE_DATA_CACHE_MAX_ENTRIES", 500);
|
|
16
|
+
const PAGE_DATA_CACHE_MAX_AGE_SECONDS = Math.max(0, Math.floor(PAGE_DATA_CACHE_TTL_MS / 1000));
|
|
17
|
+
const PAGE_DATA_CACHE_STALE_SECONDS = Math.max(0, Math.floor(PAGE_DATA_CACHE_STALE_MS / 1000));
|
|
14
18
|
const pageDataCache = new Map();
|
|
15
19
|
const pageDataFlight = new Singleflight();
|
|
20
|
+
const RELEASE_PAGE_DATA_CACHE_POLICY = {
|
|
21
|
+
ttlMs: PAGE_DATA_CACHE_TTL_MS,
|
|
22
|
+
staleMs: PAGE_DATA_CACHE_STALE_MS,
|
|
23
|
+
maxAgeSeconds: PAGE_DATA_CACHE_MAX_AGE_SECONDS,
|
|
24
|
+
staleSeconds: PAGE_DATA_CACHE_STALE_SECONDS,
|
|
25
|
+
};
|
|
26
|
+
function readPositiveIntegerEnv(name, fallback) {
|
|
27
|
+
const raw = getEnv(name);
|
|
28
|
+
if (!raw)
|
|
29
|
+
return fallback;
|
|
30
|
+
const value = Number(raw);
|
|
31
|
+
if (!Number.isFinite(value) || value < 0)
|
|
32
|
+
return fallback;
|
|
33
|
+
return Math.floor(value);
|
|
34
|
+
}
|
|
35
|
+
function getPageDataCachePolicy(ctx) {
|
|
36
|
+
const environment = ctx.resolvedEnvironment ?? ctx.requestContext?.mode ?? "preview";
|
|
37
|
+
const isReleaseBackedProduction = environment === "production" && !!ctx.releaseId;
|
|
38
|
+
if (isReleaseBackedProduction)
|
|
39
|
+
return RELEASE_PAGE_DATA_CACHE_POLICY;
|
|
40
|
+
return {
|
|
41
|
+
...RELEASE_PAGE_DATA_CACHE_POLICY,
|
|
42
|
+
staleMs: 0,
|
|
43
|
+
staleSeconds: 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function isPageDataCacheEnabled() {
|
|
47
|
+
return PAGE_DATA_CACHE_MAX_ENTRIES > 0;
|
|
48
|
+
}
|
|
16
49
|
export function __clearPageDataEndpointCacheForTests() {
|
|
17
50
|
pageDataCache.clear();
|
|
18
51
|
}
|
|
@@ -24,13 +57,22 @@ export function handlePageDataEndpoint(req, pathname, ctx, createResponseBuilder
|
|
|
24
57
|
.replace(/\.json$/, "") || "";
|
|
25
58
|
const url = new URL(req.url);
|
|
26
59
|
const renderer = await getRendererForProject(ctx);
|
|
27
|
-
const cacheKey = requestHasCacheSensitiveState(req)
|
|
60
|
+
const cacheKey = !isPageDataCacheEnabled() || requestHasCacheSensitiveState(req)
|
|
28
61
|
? null
|
|
29
62
|
: buildPageDataCacheKey(ctx, slug, url);
|
|
63
|
+
const cachePolicy = cacheKey ? getPageDataCachePolicy(ctx) : null;
|
|
30
64
|
const payload = cacheKey
|
|
31
|
-
? await resolveCachedPageData(cacheKey, () => withTimeoutThrow(renderer.resolvePageData(slug, { request: req, url }), PAGE_DATA_TIMEOUT_MS, `resolvePageData for ${slug}`))
|
|
65
|
+
? await resolveCachedPageData(cacheKey, () => withTimeoutThrow(renderer.resolvePageData(slug, { request: req, url }), PAGE_DATA_TIMEOUT_MS, `resolvePageData for ${slug}`), cachePolicy)
|
|
32
66
|
: await resolveUncachedPageData(() => withTimeoutThrow(renderer.resolvePageData(slug, { request: req, url }), PAGE_DATA_TIMEOUT_MS, `resolvePageData for ${slug}`));
|
|
33
|
-
const cacheStrategy = cacheKey
|
|
67
|
+
const cacheStrategy = cacheKey
|
|
68
|
+
? {
|
|
69
|
+
maxAge: cachePolicy.maxAgeSeconds,
|
|
70
|
+
public: true,
|
|
71
|
+
...(cachePolicy.staleSeconds > 0
|
|
72
|
+
? { staleWhileRevalidate: cachePolicy.staleSeconds }
|
|
73
|
+
: {}),
|
|
74
|
+
}
|
|
75
|
+
: "no-cache";
|
|
34
76
|
const builder = createResponseBuilder(ctx).withCORS(req, ctx.securityConfig?.cors);
|
|
35
77
|
if (hasMatchingEtag(req, payload.etag)) {
|
|
36
78
|
return respond(builder.notModified(payload.etag));
|
|
@@ -78,25 +120,35 @@ export function handlePageDataEndpoint(req, pathname, ctx, createResponseBuilder
|
|
|
78
120
|
"module.pageData.projectSlug": ctx.projectSlug || "unknown",
|
|
79
121
|
});
|
|
80
122
|
}
|
|
81
|
-
async function resolveCachedPageData(cacheKey, resolve) {
|
|
123
|
+
async function resolveCachedPageData(cacheKey, resolve, cachePolicy) {
|
|
82
124
|
const cached = getCachedPageData(cacheKey);
|
|
83
|
-
if (cached) {
|
|
125
|
+
if (cached?.state === "fresh") {
|
|
84
126
|
markRequestProfilePhase("page_data.cache_hit");
|
|
85
|
-
return cached;
|
|
127
|
+
return cached.entry;
|
|
128
|
+
}
|
|
129
|
+
if (cached?.state === "stale") {
|
|
130
|
+
markRequestProfilePhase("page_data.cache_stale");
|
|
131
|
+
refreshStalePageData(cacheKey, resolve, cachePolicy);
|
|
132
|
+
return cached.entry;
|
|
86
133
|
}
|
|
87
134
|
markRequestProfilePhase("page_data.cache_miss");
|
|
88
135
|
return await pageDataFlight.do(cacheKey, async () => {
|
|
89
136
|
const concurrentCached = getCachedPageData(cacheKey);
|
|
90
|
-
if (concurrentCached) {
|
|
137
|
+
if (concurrentCached?.state === "fresh") {
|
|
91
138
|
markRequestProfilePhase("page_data.cache_hit_after_wait");
|
|
92
|
-
return concurrentCached;
|
|
139
|
+
return concurrentCached.entry;
|
|
140
|
+
}
|
|
141
|
+
if (concurrentCached?.state === "stale") {
|
|
142
|
+
markRequestProfilePhase("page_data.cache_stale_after_wait");
|
|
143
|
+
refreshStalePageData(cacheKey, resolve, cachePolicy);
|
|
144
|
+
return concurrentCached.entry;
|
|
93
145
|
}
|
|
94
|
-
const payload = await resolveUncachedPageData(resolve);
|
|
146
|
+
const payload = await resolveUncachedPageData(resolve, cachePolicy);
|
|
95
147
|
setCachedPageData(cacheKey, payload);
|
|
96
148
|
return payload;
|
|
97
149
|
});
|
|
98
150
|
}
|
|
99
|
-
async function resolveUncachedPageData(resolve) {
|
|
151
|
+
async function resolveUncachedPageData(resolve, cachePolicy = RELEASE_PAGE_DATA_CACHE_POLICY) {
|
|
100
152
|
const startedAt = performance.now();
|
|
101
153
|
const pageData = await resolve();
|
|
102
154
|
markRequestProfilePhase("page_data.resolve", performance.now() - startedAt);
|
|
@@ -107,20 +159,32 @@ async function resolveUncachedPageData(resolve) {
|
|
|
107
159
|
return {
|
|
108
160
|
body,
|
|
109
161
|
etag,
|
|
110
|
-
expiresAt: Date.now() +
|
|
162
|
+
expiresAt: Date.now() + cachePolicy.ttlMs,
|
|
163
|
+
staleUntil: Date.now() + cachePolicy.ttlMs + cachePolicy.staleMs,
|
|
111
164
|
};
|
|
112
165
|
}
|
|
113
166
|
function getCachedPageData(cacheKey) {
|
|
114
167
|
const entry = pageDataCache.get(cacheKey);
|
|
115
168
|
if (!entry)
|
|
116
169
|
return null;
|
|
117
|
-
|
|
118
|
-
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
if (now <= entry.expiresAt) {
|
|
172
|
+
pageDataCache.delete(cacheKey);
|
|
173
|
+
pageDataCache.set(cacheKey, entry);
|
|
174
|
+
return { entry, state: "fresh" };
|
|
175
|
+
}
|
|
176
|
+
if (now <= entry.staleUntil) {
|
|
177
|
+
pageDataCache.delete(cacheKey);
|
|
178
|
+
pageDataCache.set(cacheKey, entry);
|
|
179
|
+
return { entry, state: "stale" };
|
|
180
|
+
}
|
|
119
181
|
pageDataCache.delete(cacheKey);
|
|
120
182
|
markRequestProfilePhase("page_data.cache_expired");
|
|
121
183
|
return null;
|
|
122
184
|
}
|
|
123
185
|
function setCachedPageData(cacheKey, entry) {
|
|
186
|
+
if (!isPageDataCacheEnabled())
|
|
187
|
+
return;
|
|
124
188
|
if (pageDataCache.size >= PAGE_DATA_CACHE_MAX_ENTRIES && !pageDataCache.has(cacheKey)) {
|
|
125
189
|
const oldestKey = pageDataCache.keys().next().value;
|
|
126
190
|
if (oldestKey)
|
|
@@ -128,6 +192,23 @@ function setCachedPageData(cacheKey, entry) {
|
|
|
128
192
|
}
|
|
129
193
|
pageDataCache.set(cacheKey, entry);
|
|
130
194
|
}
|
|
195
|
+
function refreshStalePageData(cacheKey, resolve, cachePolicy) {
|
|
196
|
+
void pageDataFlight.do(cacheKey, async () => {
|
|
197
|
+
const payload = await resolveUncachedPageData(resolve, cachePolicy);
|
|
198
|
+
setCachedPageData(cacheKey, payload);
|
|
199
|
+
return payload;
|
|
200
|
+
}).then(() => {
|
|
201
|
+
markRequestProfilePhase("page_data.refresh_background");
|
|
202
|
+
}, (error) => {
|
|
203
|
+
serverLogger.warn("[page-data] Background refresh failed", {
|
|
204
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
205
|
+
});
|
|
206
|
+
const stale = pageDataCache.get(cacheKey);
|
|
207
|
+
if (stale && Date.now() > stale.staleUntil) {
|
|
208
|
+
pageDataCache.delete(cacheKey);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
131
212
|
function buildPageDataCacheKey(ctx, slug, url) {
|
|
132
213
|
const projectKey = ctx.projectId ?? ctx.projectSlug ?? ctx.projectDir;
|
|
133
214
|
const environment = ctx.resolvedEnvironment ?? ctx.requestContext?.mode ?? "preview";
|