veryfront 0.0.80 → 0.0.82

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.
Files changed (54) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/cache/backend.d.ts +20 -0
  3. package/esm/src/cache/backend.d.ts.map +1 -1
  4. package/esm/src/cache/backend.js +57 -0
  5. package/esm/src/cache/hash.d.ts +107 -0
  6. package/esm/src/cache/hash.d.ts.map +1 -0
  7. package/esm/src/cache/hash.js +166 -0
  8. package/esm/src/cache/index.d.ts +3 -0
  9. package/esm/src/cache/index.d.ts.map +1 -1
  10. package/esm/src/cache/index.js +3 -0
  11. package/esm/src/cache/module-cache.d.ts +82 -0
  12. package/esm/src/cache/module-cache.d.ts.map +1 -0
  13. package/esm/src/cache/module-cache.js +214 -0
  14. package/esm/src/cache/multi-tier.d.ts +177 -0
  15. package/esm/src/cache/multi-tier.d.ts.map +1 -0
  16. package/esm/src/cache/multi-tier.js +352 -0
  17. package/esm/src/cli/templates/integration-loader.d.ts.map +1 -1
  18. package/esm/src/cli/templates/integration-loader.js +2 -4
  19. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
  20. package/esm/src/modules/react-loader/ssr-module-loader/loader.js +121 -14
  21. package/esm/src/observability/tracing/span-names.d.ts +2 -0
  22. package/esm/src/observability/tracing/span-names.d.ts.map +1 -1
  23. package/esm/src/observability/tracing/span-names.js +2 -0
  24. package/esm/src/rendering/orchestrator/module-loader/cache.d.ts +10 -2
  25. package/esm/src/rendering/orchestrator/module-loader/cache.d.ts.map +1 -1
  26. package/esm/src/rendering/orchestrator/module-loader/cache.js +11 -6
  27. package/esm/src/rendering/orchestrator/module-loader/index.d.ts.map +1 -1
  28. package/esm/src/rendering/orchestrator/module-loader/index.js +72 -77
  29. package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
  30. package/esm/src/transforms/esm/http-cache.js +6 -29
  31. package/esm/src/transforms/esm/transform-cache.d.ts +25 -0
  32. package/esm/src/transforms/esm/transform-cache.d.ts.map +1 -1
  33. package/esm/src/transforms/esm/transform-cache.js +45 -0
  34. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.d.ts.map +1 -1
  35. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.js +2 -36
  36. package/esm/src/utils/constants/cache.d.ts +4 -0
  37. package/esm/src/utils/constants/cache.d.ts.map +1 -1
  38. package/esm/src/utils/constants/cache.js +14 -1
  39. package/package.json +1 -1
  40. package/src/deno.js +1 -1
  41. package/src/src/cache/backend.ts +62 -0
  42. package/src/src/cache/hash.ts +205 -0
  43. package/src/src/cache/index.ts +3 -0
  44. package/src/src/cache/module-cache.ts +252 -0
  45. package/src/src/cache/multi-tier.ts +503 -0
  46. package/src/src/cli/templates/integration-loader.ts +2 -8
  47. package/src/src/modules/react-loader/ssr-module-loader/loader.ts +137 -18
  48. package/src/src/observability/tracing/span-names.ts +2 -0
  49. package/src/src/rendering/orchestrator/module-loader/cache.ts +14 -8
  50. package/src/src/rendering/orchestrator/module-loader/index.ts +94 -89
  51. package/src/src/transforms/esm/http-cache.ts +12 -32
  52. package/src/src/transforms/esm/transform-cache.ts +53 -0
  53. package/src/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts +2 -40
  54. package/src/src/utils/constants/cache.ts +21 -1
@@ -6,69 +6,41 @@
6
6
  * @module rendering/orchestrator/module-loader
7
7
  */
8
8
  import { parallelMap, rendererLogger as logger } from "../../../utils/index.js";
9
- import { Singleflight } from "../../../utils/singleflight.js";
10
9
  import { getLocalAdapter } from "../../../platform/adapters/registry.js";
11
10
  import { generateHash } from "./cache.js";
12
11
  import { findLocalLibFile, findSourceFile } from "../file-resolver/index.js";
13
12
  import { transformToESM } from "../../../transforms/esm-transform.js";
14
13
  import { getProjectTmpDir } from "../../../modules/react-loader/index.js";
15
- import { TRANSFORM_CACHE_VERSION } from "../../../transforms/esm/package-registry.js";
14
+ import { generateCacheKey as generateTransformCacheKey, getOrComputeTransform, initializeTransformCache, setCachedTransformAsync, } from "../../../transforms/esm/transform-cache.js";
15
+ import { hashString } from "../../../cache/hash.js";
16
+ import { TRANSFORM_DISTRIBUTED_TTL_SEC } from "../../../utils/constants/cache.js";
17
+ import { ensureHttpBundlesExist } from "../../../transforms/esm/http-cache.js";
18
+ import { getHttpBundleCacheDir } from "../../../utils/cache-dir.js";
16
19
  // Re-export utilities
17
20
  export { createEsmCache, createModuleCache, generateHash } from "./cache.js";
18
21
  export { fetchEsmModule, rewriteEsmPaths } from "./esm-rewriter.js";
19
- /** Cache for created directories to avoid repeated mkdir calls */
20
- const createdDirs = new Set();
21
- /**
22
- * Distributed transform cache for cross-pod sharing.
23
- * Caches transformed module code in Redis/API so other pods don't need to re-transform.
24
- */
25
- let distributedTransformCache;
26
- const distributedCacheInit = new Singleflight();
27
- /** TTL for cached transforms (24 hours) */
28
- const TRANSFORM_CACHE_TTL_SECONDS = 86400;
29
- function getDistributedTransformCache() {
30
- if (distributedTransformCache !== undefined) {
31
- return Promise.resolve(distributedTransformCache);
32
- }
33
- return distributedCacheInit.do("init", async () => {
34
- try {
35
- const { CacheBackends } = await import("../../../cache/backend.js");
36
- const backend = await CacheBackends.transform();
37
- // Only use distributed cache if API or Redis (not memory - that's per-process)
38
- if (backend.type === "memory") {
39
- distributedTransformCache = null;
40
- logger.debug("[ModuleLoader] No distributed transform cache (memory only)");
41
- return null;
42
- }
43
- distributedTransformCache = backend;
44
- logger.debug("[ModuleLoader] Distributed transform cache initialized", {
45
- type: backend.type,
46
- });
47
- return backend;
22
+ /** Pattern to match HTTP bundle file:// paths in transformed code */
23
+ const HTTP_BUNDLE_PATTERN = /file:\/\/([^"'\s]+veryfront-http-bundle\/http-([a-f0-9]+)\.mjs)/gi;
24
+ /** Extract HTTP bundle paths from transformed code for proactive recovery */
25
+ function extractHttpBundlePaths(code) {
26
+ const bundles = [];
27
+ const seen = new Set();
28
+ let match;
29
+ while ((match = HTTP_BUNDLE_PATTERN.exec(code)) !== null) {
30
+ const path = match[1];
31
+ const hash = match[2];
32
+ if (!seen.has(hash)) {
33
+ seen.add(hash);
34
+ bundles.push({ path, hash });
48
35
  }
49
- catch (error) {
50
- logger.debug("[ModuleLoader] Failed to init distributed transform cache", { error });
51
- distributedTransformCache = null;
52
- return null;
53
- }
54
- });
55
- }
56
- /**
57
- * Build cache key for transformed module.
58
- * Includes content hash so cache invalidates when source changes.
59
- */
60
- function getTransformCacheKey(projectId, filePath, contentHash) {
61
- return `v${TRANSFORM_CACHE_VERSION}:${projectId}:${filePath}:${contentHash}`;
62
- }
63
- /** Simple string hash for cache keys */
64
- function hashString(str) {
65
- let hash = 0;
66
- for (let i = 0; i < str.length; i++) {
67
- hash = ((hash << 5) - hash) + str.charCodeAt(i);
68
- hash &= hash;
69
36
  }
70
- return Math.abs(hash).toString(36);
37
+ HTTP_BUNDLE_PATTERN.lastIndex = 0;
38
+ return bundles;
71
39
  }
40
+ /** Cache for created directories to avoid repeated mkdir calls */
41
+ const createdDirs = new Set();
42
+ /** TTL for cached transforms (uses centralized config) */
43
+ const TRANSFORM_CACHE_TTL_SECONDS = TRANSFORM_DISTRIBUTED_TTL_SEC;
72
44
  function getModuleCacheKey(filePath, projectId, projectDir) {
73
45
  return `${projectId ?? projectDir ?? "default"}:${filePath}`;
74
46
  }
@@ -154,35 +126,41 @@ export async function transformModuleWithDeps(filePath, tmpDir, localAdapter, co
154
126
  }
155
127
  const contentHash = hashString(fileContent);
156
128
  const effectiveProjectId = projectId ?? projectDir;
157
- const transformCacheKey = getTransformCacheKey(effectiveProjectId, filePath, contentHash);
158
- let transformedCode = null;
159
- const distributedCache = await getDistributedTransformCache();
160
- if (distributedCache) {
161
- try {
162
- const cached = await distributedCache.get(transformCacheKey);
163
- if (cached) {
164
- transformedCode = cached;
165
- logger.debug("[ModuleLoader] Distributed transform cache HIT", {
166
- filePath,
167
- cacheKey: transformCacheKey,
168
- });
169
- }
170
- }
171
- catch (error) {
172
- logger.debug("[ModuleLoader] Distributed cache get failed", { filePath, error });
173
- }
174
- }
175
- if (!transformedCode) {
176
- transformedCode = await transformToESM(fileContent, filePath, projectDir, adapter, {
129
+ const scopedPath = `${effectiveProjectId}:${filePath}`;
130
+ const transformCacheKey = generateTransformCacheKey(scopedPath, contentHash, true); // ssr=true
131
+ // Initialize transform cache (lazy, only once per pod)
132
+ await initializeTransformCache();
133
+ // Use consolidated transform cache with getOrCompute pattern
134
+ let transformedCode = await getOrComputeTransform(transformCacheKey, () => {
135
+ logger.debug("[ModuleLoader] Transform cache miss, transforming", { filePath });
136
+ return transformToESM(fileContent, filePath, projectDir, adapter, {
177
137
  projectId: effectiveProjectId,
178
138
  dev: mode === "development",
179
139
  ssr: true,
180
140
  });
181
- if (distributedCache) {
182
- distributedCache
183
- .set(transformCacheKey, transformedCode, TRANSFORM_CACHE_TTL_SECONDS)
184
- .catch((error) => {
185
- logger.debug("[ModuleLoader] Distributed cache set failed", { filePath, error });
141
+ }, TRANSFORM_CACHE_TTL_SECONDS);
142
+ // Proactively ensure HTTP bundles exist before writing the module.
143
+ // Cached transforms from a different pod may reference file:// paths
144
+ // to HTTP bundles that don't exist locally.
145
+ const bundlePaths = extractHttpBundlePaths(transformedCode);
146
+ if (bundlePaths.length > 0) {
147
+ const cacheDir = getHttpBundleCacheDir();
148
+ const failed = await ensureHttpBundlesExist(bundlePaths, cacheDir);
149
+ if (failed.length > 0) {
150
+ logger.warn("[ModuleLoader] HTTP bundle recovery failed, re-transforming", {
151
+ filePath,
152
+ failed,
153
+ });
154
+ transformedCode = await transformToESM(fileContent, filePath, projectDir, adapter, {
155
+ projectId: effectiveProjectId,
156
+ dev: mode === "development",
157
+ ssr: true,
158
+ });
159
+ setCachedTransformAsync(transformCacheKey, transformedCode, contentHash, TRANSFORM_CACHE_TTL_SECONDS).catch((error) => {
160
+ logger.debug("[ModuleLoader] Failed to update transform cache after re-transform", {
161
+ filePath,
162
+ error,
163
+ });
186
164
  });
187
165
  }
188
166
  }
@@ -219,6 +197,23 @@ export async function loadModule(filePath, config) {
219
197
  return await import(moduleUrl);
220
198
  }
221
199
  catch (error) {
200
+ // If import fails due to missing HTTP bundle, try to recover and retry once
201
+ const errorMsg = error instanceof Error ? error.message : String(error);
202
+ const bundleMatch = errorMsg.match(/veryfront-http-bundle\/http-([a-f0-9]+)\.mjs/);
203
+ if (bundleMatch) {
204
+ const hash = bundleMatch[1];
205
+ logger.warn("[ModuleLoader] Import failed due to missing HTTP bundle, attempting recovery", {
206
+ filePath,
207
+ hash,
208
+ });
209
+ const { recoverHttpBundleByHash } = await import("../../../transforms/esm/http-cache.js");
210
+ const cacheDir = getHttpBundleCacheDir();
211
+ const recovered = await recoverHttpBundleByHash(hash, cacheDir);
212
+ if (recovered) {
213
+ logger.info("[ModuleLoader] HTTP bundle recovered, retrying import", { hash });
214
+ return await import(`file://${tempFilePath}?t=${Date.now()}&retry=1`);
215
+ }
216
+ }
222
217
  logger.error("[ModuleLoader] Failed to import module:", {
223
218
  filePath,
224
219
  tempFilePath,
@@ -1 +1 @@
1
- {"version":3,"file":"http-cache.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/http-cache.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAsCzE,KAAK,YAAY,GAAG;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,eAAe,CAAC;IAC3B,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAoUF;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAOvF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAwC9F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,EAAE,CAAC,CAiFnB"}
1
+ {"version":3,"file":"http-cache.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/http-cache.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAmBzE,KAAK,YAAY,GAAG;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,eAAe,CAAC;IAC3B,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAoUF;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAOvF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAwC9F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,EAAE,CAAC,CAiFnB"}
@@ -12,7 +12,6 @@ import { isAbsolute, join } from "../../platform/compat/path/index.js";
12
12
  import { cwd } from "../../platform/compat/process.js";
13
13
  import { rendererLogger as logger } from "../../utils/index.js";
14
14
  import { simpleHash } from "../../utils/hash-utils.js";
15
- import { Singleflight } from "../../utils/singleflight.js";
16
15
  import { LRUCache } from "../../utils/lru-wrapper.js";
17
16
  import { withSpan } from "../../observability/tracing/otlp-setup.js";
18
17
  import { SpanNames } from "../../observability/tracing/span-names.js";
@@ -20,35 +19,13 @@ import { resolveImport } from "../../modules/import-map/resolver.js";
20
19
  import { isDeno } from "../../platform/compat/runtime.js";
21
20
  import { getDenoNpmReactMap, getReactImportMap, REACT_VERSION } from "./package-registry.js";
22
21
  import { parseImports, replaceSpecifiers } from "./lexer.js";
22
+ import { CacheBackends, createDistributedCacheAccessor } from "../../cache/backend.js";
23
+ import { HTTP_MODULE_CACHE_MAX_ENTRIES, HTTP_MODULE_DISTRIBUTED_TTL_SEC, } from "../../utils/constants/cache.js";
23
24
  /** Lazy-loaded distributed cache backend for cross-pod sharing */
24
- let distributedCache;
25
- const distributedCacheInit = new Singleflight();
26
- function getDistributedCache() {
27
- if (distributedCache !== undefined)
28
- return Promise.resolve(distributedCache);
29
- return distributedCacheInit.do("init", async () => {
30
- try {
31
- const { CacheBackends } = await import("../../cache/backend.js");
32
- const backend = await CacheBackends.httpModule();
33
- if (backend.type === "memory") {
34
- distributedCache = null;
35
- logger.debug("[HTTP-CACHE] No distributed cache available (memory only)");
36
- return null;
37
- }
38
- distributedCache = backend;
39
- logger.debug("[HTTP-CACHE] Distributed cache initialized", { type: backend.type });
40
- return backend;
41
- }
42
- catch (error) {
43
- logger.debug("[HTTP-CACHE] Failed to initialize distributed cache", { error });
44
- distributedCache = null;
45
- return null;
46
- }
47
- });
48
- }
49
- /** TTL for cached modules in distributed cache (24 hours) */
50
- const DISTRIBUTED_CACHE_TTL_SECONDS = 86400;
51
- const cachedPaths = new LRUCache({ maxEntries: 2000 });
25
+ const getDistributedCache = createDistributedCacheAccessor(() => CacheBackends.httpModule(), "HTTP-CACHE");
26
+ /** TTL for cached modules in distributed cache (uses centralized config) */
27
+ const DISTRIBUTED_CACHE_TTL_SECONDS = HTTP_MODULE_DISTRIBUTED_TTL_SEC;
28
+ const cachedPaths = new LRUCache({ maxEntries: HTTP_MODULE_CACHE_MAX_ENTRIES });
52
29
  const processingStack = new Set();
53
30
  function ensureAbsoluteDir(path) {
54
31
  return isAbsolute(path) ? path : join(cwd(), path);
@@ -1,3 +1,4 @@
1
+ import { type CacheBackend } from "../../cache/backend.js";
1
2
  export interface TransformCacheEntry {
2
3
  code: string;
3
4
  hash: string;
@@ -15,6 +16,30 @@ export declare function getCachedTransform(key: string): TransformCacheEntry | u
15
16
  export declare function setCachedTransformAsync(key: string, code: string, hash: string, ttlSeconds?: number): Promise<void>;
16
17
  export declare function setCachedTransform(key: string, code: string, hash: string, ttlSeconds?: number): void;
17
18
  export declare function destroyTransformCache(): void;
19
+ /**
20
+ * Get the underlying distributed cache backend.
21
+ *
22
+ * This is exposed for callers that need direct access to the distributed
23
+ * cache (e.g., MDX module-fetcher that stores raw code strings instead of
24
+ * TransformCacheEntry JSON). Ensures initialization happens only once.
25
+ *
26
+ * Returns null if distributed cache is not available (memory-only mode).
27
+ */
28
+ export declare function getDistributedTransformBackend(): Promise<CacheBackend | null>;
29
+ /**
30
+ * Get a cached transform or compute it if not found.
31
+ *
32
+ * This is the preferred way to use the transform cache - it handles:
33
+ * - Cache lookup (distributed first, then local fallback)
34
+ * - Compute on miss
35
+ * - Cache storage on compute
36
+ *
37
+ * @param key - Cache key (use generateCacheKey to build it)
38
+ * @param computeFn - Function to compute the transform if not cached
39
+ * @param ttlSeconds - TTL for the cached entry
40
+ * @returns The cached or computed code
41
+ */
42
+ export declare function getOrComputeTransform(key: string, computeFn: () => Promise<string>, ttlSeconds?: number): Promise<string>;
18
43
  export declare function getTransformCacheStats(): {
19
44
  fallbackEntries: number;
20
45
  maxFallbackEntries: number;
@@ -1 +1 @@
1
- {"version":3,"file":"transform-cache.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/transform-cache.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAeD,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,OAAO,CAAC,CAuBjE;AAED,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,iCAA2B,CAAC;AAE7D,wDAAwD;AACxD,eAAO,MAAM,mBAAmB,kCAA4B,CAAC;AAE7D,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,GAAG,GAAE,OAAe,EACpB,WAAW,GAAE,OAAe,GAC3B,MAAM,CAER;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAW1C;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAG/E;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAA4B,GACvC,OAAO,CAAC,IAAI,CAAC,CAaf;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAA4B,GACvC,IAAI,CAeN;AAuBD,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,wBAAgB,sBAAsB,IAAI;IACxC,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB,CAMA"}
1
+ {"version":3,"file":"transform-cache.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/transform-cache.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,YAAY,EAAqC,MAAM,wBAAwB,CAAC;AAK9F,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAeD,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,OAAO,CAAC,CAuBjE;AAED,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,iCAA2B,CAAC;AAE7D,wDAAwD;AACxD,eAAO,MAAM,mBAAmB,kCAA4B,CAAC;AAE7D,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,GAAG,GAAE,OAAe,EACpB,WAAW,GAAE,OAAe,GAC3B,MAAM,CAER;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAW1C;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAG/E;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAA4B,GACvC,OAAO,CAAC,IAAI,CAAC,CAaf;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAA4B,GACvC,IAAI,CAeN;AAuBD,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED;;;;;;;;GAQG;AACH,wBAAsB,8BAA8B,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAInF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EAChC,UAAU,GAAE,MAA4B,GACvC,OAAO,CAAC,MAAM,CAAC,CAmBjB;AAED,wBAAgB,sBAAsB,IAAI;IACxC,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB,CAMA"}
@@ -110,6 +110,51 @@ function pruneLocalFallback() {
110
110
  export function destroyTransformCache() {
111
111
  localFallback.clear();
112
112
  }
113
+ /**
114
+ * Get the underlying distributed cache backend.
115
+ *
116
+ * This is exposed for callers that need direct access to the distributed
117
+ * cache (e.g., MDX module-fetcher that stores raw code strings instead of
118
+ * TransformCacheEntry JSON). Ensures initialization happens only once.
119
+ *
120
+ * Returns null if distributed cache is not available (memory-only mode).
121
+ */
122
+ export async function getDistributedTransformBackend() {
123
+ await initializeTransformCache();
124
+ if (!cacheBackend || cacheBackend.type === "memory")
125
+ return null;
126
+ return cacheBackend;
127
+ }
128
+ /**
129
+ * Get a cached transform or compute it if not found.
130
+ *
131
+ * This is the preferred way to use the transform cache - it handles:
132
+ * - Cache lookup (distributed first, then local fallback)
133
+ * - Compute on miss
134
+ * - Cache storage on compute
135
+ *
136
+ * @param key - Cache key (use generateCacheKey to build it)
137
+ * @param computeFn - Function to compute the transform if not cached
138
+ * @param ttlSeconds - TTL for the cached entry
139
+ * @returns The cached or computed code
140
+ */
141
+ export async function getOrComputeTransform(key, computeFn, ttlSeconds = DEFAULT_TTL_SECONDS) {
142
+ // Try to get from cache first
143
+ const cached = await getCachedTransformAsync(key);
144
+ if (cached) {
145
+ logger.debug("[TransformCache] Cache hit", { key });
146
+ return cached.code;
147
+ }
148
+ // Compute on miss
149
+ logger.debug("[TransformCache] Cache miss, computing", { key });
150
+ const code = await computeFn();
151
+ // Store in cache (fire-and-forget for performance)
152
+ const hash = String(Date.now()); // Simple hash for now
153
+ setCachedTransformAsync(key, code, hash, ttlSeconds).catch((error) => {
154
+ logger.debug("[TransformCache] Failed to cache computed transform", { key, error });
155
+ });
156
+ return code;
157
+ }
113
158
  export function getTransformCacheStats() {
114
159
  return {
115
160
  fallbackEntries: localFallback.size,
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAY5E,OAAO,KAAK,EAAE,oBAAoB,EAAsB,MAAM,aAAa,CAAC;AAgI5E;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,MAAM,EACpB,KAAK,CAAC,EAAE,MAAM,GACb,IAAI,CAON;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAiCxD;AAsND;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,oBAAoB,EAC7B,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6CxB;AA4OD;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACvD,oBAAoB,CAUtB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAW5E,OAAO,KAAK,EAAE,oBAAoB,EAAsB,MAAM,aAAa,CAAC;AA4F5E;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,MAAM,EACpB,KAAK,CAAC,EAAE,MAAM,GACb,IAAI,CAON;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAiCxD;AAsND;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,oBAAoB,EAC7B,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6CxB;AA4OD;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACvD,oBAAoB,CAUtB"}
@@ -14,7 +14,6 @@
14
14
  import * as dntShim from "../../../../../_dnt.shims.js";
15
15
  import { join, posix } from "../../../../../deps/deno.land/std@0.220.0/path/mod.js";
16
16
  import { rendererLogger as logger } from "../../../../utils/index.js";
17
- import { Singleflight } from "../../../../utils/singleflight.js";
18
17
  import { withSpan } from "../../../../observability/tracing/otlp-setup.js";
19
18
  import { SpanNames } from "../../../../observability/tracing/span-names.js";
20
19
  import { transformToESM } from "../../../esm-transform.js";
@@ -27,43 +26,10 @@ import { hashString } from "../utils/hash.js";
27
26
  import { createStubModule } from "../utils/stub-module.js";
28
27
  import { resolveModuleFile } from "../resolution/file-finder.js";
29
28
  import { recordSSRModules } from "../../../../modules/manifest/route-module-manifest.js";
29
+ import { getDistributedTransformBackend } from "../../../esm/transform-cache.js";
30
30
  import { TRANSFORM_DISTRIBUTED_TTL_SEC } from "../../../../utils/constants/cache.js";
31
- /**
32
- * Distributed transform cache for cross-pod sharing.
33
- * Caches transformed module code in Redis/API so other pods don't need to re-transform.
34
- */
35
- let distributedTransformCache;
36
- const distributedCacheInit = new Singleflight();
37
31
  /** TTL for cached transforms (uses centralized config) */
38
32
  const TRANSFORM_CACHE_TTL_SECONDS = TRANSFORM_DISTRIBUTED_TTL_SEC;
39
- function getDistributedTransformCache() {
40
- if (distributedTransformCache !== undefined)
41
- return Promise.resolve(distributedTransformCache);
42
- return distributedCacheInit.do("init", async () => {
43
- try {
44
- const { CacheBackends } = await import("../../../../cache/backend.js");
45
- const backend = await CacheBackends.transform();
46
- // Only use distributed cache if API or Redis (not memory - that's per-process)
47
- if (backend.type === "memory") {
48
- distributedTransformCache = null;
49
- logger.debug(`${LOG_PREFIX_MDX_LOADER} No distributed transform cache (memory only)`);
50
- return null;
51
- }
52
- distributedTransformCache = backend;
53
- logger.debug(`${LOG_PREFIX_MDX_LOADER} Distributed transform cache initialized`, {
54
- type: backend.type,
55
- });
56
- return backend;
57
- }
58
- catch (error) {
59
- logger.debug(`${LOG_PREFIX_MDX_LOADER} Failed to init distributed transform cache`, {
60
- error,
61
- });
62
- distributedTransformCache = null;
63
- return null;
64
- }
65
- });
66
- }
67
33
  /**
68
34
  * Build cache key for transformed module.
69
35
  * Includes content hash so cache invalidates when source changes.
@@ -415,7 +381,7 @@ async function doFetchAndCacheModule(normalizedPath, context, fetchAndCacheModul
415
381
  const contentHash = hashString(sourceCode);
416
382
  const transformCacheKey = getTransformCacheKey(projectId, normalizedPath, contentHash);
417
383
  let moduleCode = null;
418
- const distributedCache = await getDistributedTransformCache();
384
+ const distributedCache = await getDistributedTransformBackend();
419
385
  if (distributedCache) {
420
386
  try {
421
387
  const cached = await distributedCache.get(transformCacheKey);
@@ -51,4 +51,8 @@ export declare const REVALIDATION_TIMEOUT_MS: number;
51
51
  export declare const HTTP_MODULE_CACHE_MAX_ENTRIES: number;
52
52
  export declare const HTTP_MODULE_DISTRIBUTED_TTL_SEC: number;
53
53
  export declare const TRANSFORM_DISTRIBUTED_TTL_SEC: number;
54
+ export declare const MODULE_CACHE_MAX_ENTRIES: number;
55
+ export declare const MODULE_CACHE_TTL_MS: number;
56
+ export declare const ESM_CACHE_MAX_ENTRIES: number;
57
+ export declare const ESM_CACHE_TTL_MS: number;
54
58
  //# sourceMappingURL=cache.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../src/src/utils/constants/cache.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,aAAa,OAAO,CAAC;AAElC,eAAO,MAAM,aAAa,QAAqC,CAAC;AAChE,eAAO,MAAM,WAAW,QAAmC,CAAC;AAC5D,eAAO,MAAM,UAAU,QAA8B,CAAC;AAyBtD,eAAO,MAAM,uBAAuB,QAA+C,CAAC;AAEpF,eAAO,MAAM,4BAA4B,QAAoD,CAAC;AAC9F,eAAO,MAAM,uBAAuB,QAAqB,CAAC;AAE1D,eAAO,MAAM,wBAAwB,QAAgD,CAAC;AACtF,eAAO,MAAM,mBAAmB,QAAqB,CAAC;AAEtD,eAAO,MAAM,yBAAyB,QAAiD,CAAC;AACxF,eAAO,MAAM,oBAAoB,QAAoB,CAAC;AAEtD,eAAO,MAAM,sBAAsB,QAA8C,CAAC;AAClF,eAAO,MAAM,iBAAiB,QAAqB,CAAC;AAEpD,eAAO,MAAM,yBAAyB,QAAiD,CAAC;AACxF,eAAO,MAAM,oBAAoB,QAAqB,CAAC;AAEvD,eAAO,MAAM,2BAA2B,QAAa,CAAC;AACtD,eAAO,MAAM,4BAA4B,QAAoB,CAAC;AAE9D,eAAO,MAAM,8BAA8B,QAAa,CAAC;AACzD,eAAO,MAAM,+BAA+B,QAAoB,CAAC;AAEjE,eAAO,MAAM,2BAA2B,QAAiB,CAAC;AAC1D,eAAO,MAAM,0BAA0B,QAAc,CAAC;AAEtD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,eAAO,MAAM,6BAA6B,QAAwC,CAAC;AAMnF,eAAO,MAAM,yCAAyC,QAGrD,CAAC;AACF,eAAO,MAAM,sCAAsC,QAGlD,CAAC;AAEF,eAAO,MAAM,wCAAwC,QAGpD,CAAC;AACF,eAAO,MAAM,qCAAqC,QAGjD,CAAC;AAEF,eAAO,MAAM,mCAAmC,QAG/C,CAAC;AACF,eAAO,MAAM,gCAAgC,QAG5C,CAAC;AAEF,eAAO,MAAM,kCAAkC,QAG9C,CAAC;AACF,eAAO,MAAM,+BAA+B,QAG3C,CAAC;AAEF,6DAA6D;AAC7D,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,YAAY,GAAG,WAAW,GAAG,MAAM,GAAG,KAAK,EACtD,YAAY,GAAE,OAA4B,GACzC,MAAM,CAeR;AAGD,eAAO,MAAM,6BAA6B,QAAS,CAAC;AACpD,eAAO,MAAM,0BAA0B,QAAwC,CAAC;AAChF,eAAO,MAAM,0BAA0B,QAAqD,CAAC;AAC7F,eAAO,MAAM,wBAAwB,QAAiD,CAAC;AACvF,eAAO,MAAM,sBAAsB,QAA+C,CAAC;AACnF,eAAO,MAAM,sBAAsB,QAA8C,CAAC;AAGlF,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAC/C,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAClD,eAAO,MAAM,2BAA2B,WAAW,CAAC;AAGpD,eAAO,MAAM,yBAAyB,QAAQ,CAAC;AAC/C,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAG7C,eAAO,MAAM,4BAA4B,QAAmD,CAAC;AAC7F,eAAO,MAAM,2BAA2B,QAAkD,CAAC;AAC3F,eAAO,MAAM,uBAAuB,QAAiD,CAAC;AAItF,eAAO,MAAM,6BAA6B,QAAsD,CAAC;AACjG,eAAO,MAAM,+BAA+B,QAG3C,CAAC;AAIF,eAAO,MAAM,6BAA6B,QAGzC,CAAC"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../src/src/utils/constants/cache.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,aAAa,OAAO,CAAC;AAElC,eAAO,MAAM,aAAa,QAAqC,CAAC;AAChE,eAAO,MAAM,WAAW,QAAmC,CAAC;AAC5D,eAAO,MAAM,UAAU,QAA8B,CAAC;AA8BtD,eAAO,MAAM,uBAAuB,QAA+C,CAAC;AAEpF,eAAO,MAAM,4BAA4B,QAAoD,CAAC;AAC9F,eAAO,MAAM,uBAAuB,QAAqB,CAAC;AAE1D,eAAO,MAAM,wBAAwB,QAAgD,CAAC;AACtF,eAAO,MAAM,mBAAmB,QAAqB,CAAC;AAEtD,eAAO,MAAM,yBAAyB,QAAiD,CAAC;AACxF,eAAO,MAAM,oBAAoB,QAAoB,CAAC;AAEtD,eAAO,MAAM,sBAAsB,QAA8C,CAAC;AAClF,eAAO,MAAM,iBAAiB,QAAqB,CAAC;AAEpD,eAAO,MAAM,yBAAyB,QAAiD,CAAC;AACxF,eAAO,MAAM,oBAAoB,QAAqB,CAAC;AAEvD,eAAO,MAAM,2BAA2B,QAAa,CAAC;AACtD,eAAO,MAAM,4BAA4B,QAAoB,CAAC;AAE9D,eAAO,MAAM,8BAA8B,QAAa,CAAC;AACzD,eAAO,MAAM,+BAA+B,QAAoB,CAAC;AAEjE,eAAO,MAAM,2BAA2B,QAAiB,CAAC;AAC1D,eAAO,MAAM,0BAA0B,QAAc,CAAC;AAEtD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,eAAO,MAAM,6BAA6B,QAAwC,CAAC;AAMnF,eAAO,MAAM,yCAAyC,QAGrD,CAAC;AACF,eAAO,MAAM,sCAAsC,QAGlD,CAAC;AAEF,eAAO,MAAM,wCAAwC,QAGpD,CAAC;AACF,eAAO,MAAM,qCAAqC,QAGjD,CAAC;AAEF,eAAO,MAAM,mCAAmC,QAG/C,CAAC;AACF,eAAO,MAAM,gCAAgC,QAG5C,CAAC;AAEF,eAAO,MAAM,kCAAkC,QAG9C,CAAC;AACF,eAAO,MAAM,+BAA+B,QAG3C,CAAC;AAEF,6DAA6D;AAC7D,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,YAAY,GAAG,WAAW,GAAG,MAAM,GAAG,KAAK,EACtD,YAAY,GAAE,OAA4B,GACzC,MAAM,CAeR;AAGD,eAAO,MAAM,6BAA6B,QAAS,CAAC;AACpD,eAAO,MAAM,0BAA0B,QAAwC,CAAC;AAChF,eAAO,MAAM,0BAA0B,QAAqD,CAAC;AAC7F,eAAO,MAAM,wBAAwB,QAAiD,CAAC;AACvF,eAAO,MAAM,sBAAsB,QAA+C,CAAC;AACnF,eAAO,MAAM,sBAAsB,QAA8C,CAAC;AAGlF,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAC/C,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAClD,eAAO,MAAM,2BAA2B,WAAW,CAAC;AAGpD,eAAO,MAAM,yBAAyB,QAAQ,CAAC;AAC/C,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAG7C,eAAO,MAAM,4BAA4B,QAAmD,CAAC;AAC7F,eAAO,MAAM,2BAA2B,QAAkD,CAAC;AAC3F,eAAO,MAAM,uBAAuB,QAAiD,CAAC;AAItF,eAAO,MAAM,6BAA6B,QAAsD,CAAC;AACjG,eAAO,MAAM,+BAA+B,QAG3C,CAAC;AAIF,eAAO,MAAM,6BAA6B,QAGzC,CAAC;AAIF,eAAO,MAAM,wBAAwB,QAAkD,CAAC;AACxF,eAAO,MAAM,mBAAmB,QAG/B,CAAC;AAGF,eAAO,MAAM,qBAAqB,QAA8C,CAAC;AACjF,eAAO,MAAM,gBAAgB,QAG5B,CAAC"}
@@ -8,7 +8,13 @@ export const MS_PER_HOUR = MINUTES_PER_HOUR * MS_PER_MINUTE;
8
8
  export const ONE_DAY_MS = HOURS_PER_DAY * MS_PER_HOUR;
9
9
  function getEnvString(key) {
10
10
  const g = dntShim.dntGlobalThis;
11
- return g.Deno?.env?.get?.(key) ?? g.process?.env?.[key];
11
+ try {
12
+ return g.Deno?.env?.get?.(key) ?? g.process?.env?.[key];
13
+ }
14
+ catch {
15
+ // Gracefully handle missing --allow-env permission in Deno
16
+ return undefined;
17
+ }
12
18
  }
13
19
  function getEnvNumber(key, fallback) {
14
20
  const value = getEnvString(key);
@@ -95,3 +101,10 @@ export const HTTP_MODULE_DISTRIBUTED_TTL_SEC = getEnvNumber("HTTP_MODULE_DISTRIB
95
101
  // Transform cache for module compilation
96
102
  // Same TTL as HTTP module cache since transforms are tied to content hashes
97
103
  export const TRANSFORM_DISTRIBUTED_TTL_SEC = getEnvNumber("TRANSFORM_DISTRIBUTED_TTL_SEC", HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE);
104
+ // Pod-level module cache (shared across all RenderPipeline instances)
105
+ // These caches map module paths to transformed temp file paths
106
+ export const MODULE_CACHE_MAX_ENTRIES = getEnvNumber("MODULE_CACHE_MAX_ENTRIES", 10000);
107
+ export const MODULE_CACHE_TTL_MS = getEnvNumber("MODULE_CACHE_TTL_MS", 5 * MS_PER_MINUTE);
108
+ // ESM cache for external module mappings
109
+ export const ESM_CACHE_MAX_ENTRIES = getEnvNumber("ESM_CACHE_MAX_ENTRIES", 5000);
110
+ export const ESM_CACHE_TTL_MS = getEnvNumber("ESM_CACHE_TTL_MS", 10 * MS_PER_MINUTE);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.0.80",
3
+ "version": "0.0.82",
4
4
  "description": "Zero-config React meta-framework for building agentic AI applications",
5
5
  "keywords": [
6
6
  "react",
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.0.80",
3
+ "version": "0.0.82",
4
4
  "nodeModulesDir": "auto",
5
5
  "exclude": [
6
6
  "npm/",
@@ -587,6 +587,68 @@ export function createCacheBackend(config: CacheBackendConfig = {}): Promise<Cac
587
587
  );
588
588
  }
589
589
 
590
+ /**
591
+ * Check if a cache backend supports distributed (cross-pod) caching.
592
+ *
593
+ * Use this instead of checking `backend.type === "memory"` directly,
594
+ * which is a leaky abstraction that exposes implementation details.
595
+ */
596
+ export function isDistributedBackend(backend: CacheBackend): boolean {
597
+ return backend.type !== "memory";
598
+ }
599
+
600
+ /**
601
+ * Create a lazy-initialized distributed cache accessor.
602
+ *
603
+ * This encapsulates the common pattern of:
604
+ * 1. Lazy-init a cache backend via Singleflight
605
+ * 2. Skip if memory-only (not useful for cross-pod sharing)
606
+ * 3. Return null if init fails
607
+ *
608
+ * @param factory - Function that creates the cache backend
609
+ * @param name - Log prefix for debug messages
610
+ * @returns A function that returns the distributed cache backend or null
611
+ */
612
+ export function createDistributedCacheAccessor(
613
+ factory: () => Promise<CacheBackend>,
614
+ name: string,
615
+ ): () => Promise<CacheBackend | null> {
616
+ let backend: CacheBackend | null | undefined;
617
+ const singleflight = new (class {
618
+ private promise: Promise<CacheBackend | null> | null = null;
619
+ do(fn: () => Promise<CacheBackend | null>): Promise<CacheBackend | null> {
620
+ if (!this.promise) {
621
+ this.promise = fn().finally(() => {
622
+ this.promise = null;
623
+ });
624
+ }
625
+ return this.promise;
626
+ }
627
+ })();
628
+
629
+ return () => {
630
+ if (backend !== undefined) return Promise.resolve(backend);
631
+
632
+ return singleflight.do(async () => {
633
+ try {
634
+ const b = await factory();
635
+ if (!isDistributedBackend(b)) {
636
+ backend = null;
637
+ logger.debug(`[${name}] No distributed cache available (memory only)`);
638
+ return null;
639
+ }
640
+ backend = b;
641
+ logger.debug(`[${name}] Distributed cache initialized`, { type: b.type });
642
+ return b;
643
+ } catch (error) {
644
+ logger.debug(`[${name}] Failed to initialize distributed cache`, { error });
645
+ backend = null;
646
+ return null;
647
+ }
648
+ });
649
+ };
650
+ }
651
+
590
652
  /** Convenience wrappers for common cache patterns. */
591
653
  export const CacheBackends = {
592
654
  /** Transform cache for compiled code. */