veryfront 0.1.814 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.814",
3
+ "version": "0.1.817",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "minimumDependencyAge": {
@@ -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,cAwtB3B,CAAC"}
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) pageDataCache.delete(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
- fetchPageDataFresh(path, null).catch((error) => {
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 fetchPageDataFresh(path, null)
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;AAenD,OAAO,EAAE,iCAAiC,EAAE,MAAM,2BAA2B,CAAC;AAE9E,eAAO,MAAM,sBAAsB,0MAYzB,CAAC;AAyGX,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,SAAS,GAAG,YAAY,CAAC;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE;QACN,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,MAAM,WAAW,WAAW;IAC1B,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,mBAAmB;IAClC,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,GAAG,CAAC;CACX;AAgBD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB;0BA6DtD,OAAO,YACH,mBAAmB,KAC3B,OAAO,CAAC,YAAY,CAAC;0BAiRjB,OAAO,YACH,mBAAmB,KAC3B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;;;;;;;;0BAlUH,MAAM,EAAE;;EAwWpC;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEjE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAwB7E"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/src/proxy/handler.ts"],"names":[],"mappings":"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"}
@@ -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 lookupProjectByDomain(lookupKey, config.apiBaseUrl, token, logger);
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 lookupProjectByDomain(host, config.apiBaseUrl, token, logger);
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,
@@ -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,
@@ -28,6 +28,7 @@ export interface ResolveProxyRequestTokenOptions {
28
28
  }
29
29
  export interface ResolvedProxyRequestToken {
30
30
  token?: string;
31
+ tokenSource?: "signed-internal" | "user" | "service" | "static";
31
32
  userToken?: string;
32
33
  tokenFetchError?: unknown;
33
34
  }
@@ -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,CAqDpC"}
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,CAmBjE"}
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;CAC1B,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
+ {"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;AAwBhE,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,CAoGxB"}
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 PAGE_DATA_CACHE_MAX_ENTRIES = 500;
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 ? { maxAge: 60, public: true } : "no-cache";
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() + PAGE_DATA_CACHE_TTL_MS,
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
- if (Date.now() <= entry.expiresAt)
118
- return entry;
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";
@@ -1,3 +1,3 @@
1
1
  /** Shared version value. */
2
- export declare const VERSION = "0.1.814";
2
+ export declare const VERSION = "0.1.817";
3
3
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,4 +1,4 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
3
  /** Shared version value. */
4
- export const VERSION = "0.1.814";
4
+ export const VERSION = "0.1.817";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.814",
3
+ "version": "0.1.817",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",