veryfront 0.0.82 → 0.0.84

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 (126) hide show
  1. package/README.md +18 -17
  2. package/esm/deno.js +1 -1
  3. package/esm/proxy/cache/index.d.ts +41 -0
  4. package/esm/proxy/cache/index.d.ts.map +1 -0
  5. package/esm/proxy/cache/index.js +75 -0
  6. package/esm/proxy/cache/memory-cache.d.ts +18 -0
  7. package/esm/proxy/cache/memory-cache.d.ts.map +1 -0
  8. package/esm/proxy/cache/memory-cache.js +100 -0
  9. package/esm/proxy/cache/redis-cache.d.ts +27 -0
  10. package/esm/proxy/cache/redis-cache.d.ts.map +1 -0
  11. package/esm/proxy/cache/redis-cache.js +183 -0
  12. package/esm/proxy/cache/resilient-cache.d.ts +44 -0
  13. package/esm/proxy/cache/resilient-cache.d.ts.map +1 -0
  14. package/esm/proxy/cache/resilient-cache.js +178 -0
  15. package/esm/proxy/cache/types.d.ts +65 -0
  16. package/esm/proxy/cache/types.d.ts.map +1 -0
  17. package/esm/proxy/cache/types.js +7 -0
  18. package/esm/proxy/handler.d.ts +81 -0
  19. package/esm/proxy/handler.d.ts.map +1 -0
  20. package/esm/proxy/handler.js +417 -0
  21. package/esm/proxy/logger.d.ts +29 -0
  22. package/esm/proxy/logger.d.ts.map +1 -0
  23. package/esm/proxy/logger.js +258 -0
  24. package/esm/proxy/oauth-client.d.ts +15 -0
  25. package/esm/proxy/oauth-client.d.ts.map +1 -0
  26. package/esm/proxy/oauth-client.js +52 -0
  27. package/esm/proxy/token-manager.d.ts +59 -0
  28. package/esm/proxy/token-manager.d.ts.map +1 -0
  29. package/esm/proxy/token-manager.js +125 -0
  30. package/esm/proxy/tracing.d.ts +39 -0
  31. package/esm/proxy/tracing.d.ts.map +1 -0
  32. package/esm/proxy/tracing.js +194 -0
  33. package/esm/src/cache/backend.d.ts +2 -0
  34. package/esm/src/cache/backend.d.ts.map +1 -1
  35. package/esm/src/cache/backend.js +2 -0
  36. package/esm/src/cache/cache-key-builder.d.ts +0 -4
  37. package/esm/src/cache/cache-key-builder.d.ts.map +1 -1
  38. package/esm/src/cache/cache-key-builder.js +0 -6
  39. package/esm/src/cache/multi-tier.d.ts +0 -29
  40. package/esm/src/cache/multi-tier.d.ts.map +1 -1
  41. package/esm/src/cache/multi-tier.js +0 -26
  42. package/esm/src/cli/app/actions.d.ts +26 -0
  43. package/esm/src/cli/app/actions.d.ts.map +1 -0
  44. package/esm/src/cli/app/actions.js +152 -0
  45. package/esm/src/cli/app/components/inline-input.d.ts +35 -0
  46. package/esm/src/cli/app/components/inline-input.d.ts.map +1 -0
  47. package/esm/src/cli/app/components/inline-input.js +220 -0
  48. package/esm/src/cli/app/components/list-select.d.ts +69 -0
  49. package/esm/src/cli/app/components/list-select.d.ts.map +1 -0
  50. package/esm/src/cli/app/components/list-select.js +137 -0
  51. package/esm/src/cli/app/index.d.ts +45 -0
  52. package/esm/src/cli/app/index.d.ts.map +1 -0
  53. package/esm/src/cli/app/index.js +1252 -0
  54. package/esm/src/cli/app/state.d.ts +122 -0
  55. package/esm/src/cli/app/state.d.ts.map +1 -0
  56. package/esm/src/cli/app/state.js +232 -0
  57. package/esm/src/cli/app/views/dashboard.d.ts +19 -0
  58. package/esm/src/cli/app/views/dashboard.d.ts.map +1 -0
  59. package/esm/src/cli/app/views/dashboard.js +178 -0
  60. package/esm/src/cli/commands/dev.js +2 -2
  61. package/esm/src/cli/commands/new.js +1 -1
  62. package/esm/src/cli/index/command-router.d.ts.map +1 -1
  63. package/esm/src/cli/index/command-router.js +9 -39
  64. package/esm/src/cli/index/start-handler.d.ts +3 -0
  65. package/esm/src/cli/index/start-handler.d.ts.map +1 -0
  66. package/esm/src/cli/index/start-handler.js +145 -0
  67. package/esm/src/cli/mcp/index.d.ts +11 -0
  68. package/esm/src/cli/mcp/index.d.ts.map +1 -0
  69. package/esm/src/cli/mcp/index.js +10 -0
  70. package/esm/src/cli/ui/tui.js +1 -1
  71. package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts +2 -0
  72. package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts.map +1 -1
  73. package/esm/src/middleware/builtin/security/redis-rate-limit.js +23 -9
  74. package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts +10 -0
  75. package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts.map +1 -1
  76. package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.js +30 -42
  77. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
  78. package/esm/src/modules/react-loader/ssr-module-loader/loader.js +34 -13
  79. package/esm/src/platform/adapters/fs/cache/file-cache.d.ts.map +1 -1
  80. package/esm/src/platform/adapters/fs/cache/file-cache.js +9 -3
  81. package/esm/src/server/context/cache-invalidation.d.ts.map +1 -1
  82. package/esm/src/server/context/cache-invalidation.js +4 -0
  83. package/esm/src/server/handlers/dev/dashboard/api.js +4 -0
  84. package/esm/src/server/handlers/dev/projects/ui-handler.d.ts.map +1 -1
  85. package/esm/src/server/handlers/dev/projects/ui-handler.js +6 -0
  86. package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
  87. package/esm/src/transforms/esm/http-cache.js +139 -64
  88. package/esm/src/utils/index.d.ts +1 -1
  89. package/esm/src/utils/index.d.ts.map +1 -1
  90. package/esm/src/utils/index.js +1 -1
  91. package/package.json +2 -1
  92. package/src/deno.js +1 -1
  93. package/src/proxy/cache/index.ts +93 -0
  94. package/src/proxy/cache/memory-cache.ts +120 -0
  95. package/src/proxy/cache/redis-cache.ts +203 -0
  96. package/src/proxy/cache/resilient-cache.ts +205 -0
  97. package/src/proxy/cache/types.ts +72 -0
  98. package/src/proxy/handler.ts +593 -0
  99. package/src/proxy/logger.ts +329 -0
  100. package/src/proxy/oauth-client.ts +91 -0
  101. package/src/proxy/token-manager.ts +174 -0
  102. package/src/proxy/tracing.ts +237 -0
  103. package/src/src/cache/backend.ts +3 -0
  104. package/src/src/cache/cache-key-builder.ts +0 -9
  105. package/src/src/cache/multi-tier.ts +0 -41
  106. package/src/src/cli/app/actions.ts +190 -0
  107. package/src/src/cli/app/components/inline-input.ts +255 -0
  108. package/src/src/cli/app/components/list-select.ts +215 -0
  109. package/src/src/cli/app/index.ts +1471 -0
  110. package/src/src/cli/app/state.ts +385 -0
  111. package/src/src/cli/app/views/dashboard.ts +212 -0
  112. package/src/src/cli/commands/dev.ts +2 -2
  113. package/src/src/cli/commands/new.ts +1 -1
  114. package/src/src/cli/index/command-router.ts +9 -40
  115. package/src/src/cli/index/start-handler.ts +195 -0
  116. package/src/src/cli/mcp/index.ts +11 -0
  117. package/src/src/cli/ui/tui.ts +1 -1
  118. package/src/src/middleware/builtin/security/redis-rate-limit.ts +24 -11
  119. package/src/src/modules/react-loader/ssr-module-loader/cache/redis.ts +36 -50
  120. package/src/src/modules/react-loader/ssr-module-loader/loader.ts +38 -14
  121. package/src/src/platform/adapters/fs/cache/file-cache.ts +9 -3
  122. package/src/src/server/context/cache-invalidation.ts +4 -0
  123. package/src/src/server/handlers/dev/dashboard/api.ts +2 -0
  124. package/src/src/server/handlers/dev/projects/ui-handler.ts +6 -0
  125. package/src/src/transforms/esm/http-cache.ts +148 -73
  126. package/src/src/utils/index.ts +0 -1
@@ -224,16 +224,22 @@ async function cacheHttpModule(url, options) {
224
224
  await fs.mkdir(cacheDir, { recursive: true });
225
225
  await fs.writeTextFile(cachePath, code);
226
226
  if (distributed) {
227
- // Store code by URL, by hash (for direct recovery), and URL mapping (for debugging)
228
- // Storing code by hash enables recovery without needing URL lookup
227
+ // Store code by URL, by hash (for direct recovery), and URL mapping (for debugging).
228
+ // Storing code by hash enables recovery without needing URL lookup.
229
+ // IMPORTANT: await the writes so other pods can recover this bundle immediately.
230
+ // Without await, a transform referencing this bundle could reach Redis before
231
+ // the bundle code does, causing ensureHttpBundlesExist on another pod to miss.
229
232
  const hash = simpleHash(normalizedUrl);
230
- Promise.all([
231
- distributed.set(normalizedUrl, code, DISTRIBUTED_CACHE_TTL_SECONDS),
232
- distributed.set(`code:${hash}`, code, DISTRIBUTED_CACHE_TTL_SECONDS),
233
- distributed.set(`hash:${hash}`, normalizedUrl, DISTRIBUTED_CACHE_TTL_SECONDS),
234
- ]).catch((error) => {
233
+ try {
234
+ await Promise.all([
235
+ distributed.set(normalizedUrl, code, DISTRIBUTED_CACHE_TTL_SECONDS),
236
+ distributed.set(`code:${hash}`, code, DISTRIBUTED_CACHE_TTL_SECONDS),
237
+ distributed.set(`hash:${hash}`, normalizedUrl, DISTRIBUTED_CACHE_TTL_SECONDS),
238
+ ]);
239
+ }
240
+ catch (error) {
235
241
  logger.debug("[HTTP-CACHE] Distributed cache set failed", { url: normalizedUrl, error });
236
- });
242
+ }
237
243
  }
238
244
  cachedPaths.set(cacheKey, cachePath);
239
245
  return cachePath;
@@ -342,6 +348,26 @@ export async function recoverHttpBundleByHash(hash, cacheDir) {
342
348
  await fs.mkdir(absoluteCacheDir, { recursive: true });
343
349
  await fs.writeTextFile(cachePath, cachedCode);
344
350
  logger.info("[HTTP-CACHE] Bundle recovery successful (direct)", { hash, path: cachePath });
351
+ // Proactively recover transitive deps so the import retry doesn't
352
+ // fail again with a different missing bundle.
353
+ const BUNDLE_RE = /file:\/\/([^"'\s]+veryfront-http-bundle\/http-([a-f0-9]+)\.mjs)/gi;
354
+ const transitiveDeps = [];
355
+ let m;
356
+ while ((m = BUNDLE_RE.exec(cachedCode)) !== null) {
357
+ const tHash = m[2];
358
+ if (tHash !== hash) {
359
+ transitiveDeps.push({
360
+ path: join(absoluteCacheDir, `http-${tHash}.mjs`),
361
+ hash: tHash,
362
+ });
363
+ }
364
+ }
365
+ if (transitiveDeps.length > 0) {
366
+ logger.info("[HTTP-CACHE] Recovering transitive deps from last-resort recovery", {
367
+ count: transitiveDeps.length,
368
+ });
369
+ await ensureHttpBundlesExist(transitiveDeps, cacheDir);
370
+ }
345
371
  return true;
346
372
  }
347
373
  // Strategy 2: URL lookup then re-fetch (fallback for bundles cached before code:{hash} was added)
@@ -380,69 +406,118 @@ export async function ensureHttpBundlesExist(bundlePaths, cacheDir) {
380
406
  if (bundlePaths.length === 0)
381
407
  return [];
382
408
  const fs = createFileSystem();
383
- const _absoluteCacheDir = ensureAbsoluteDir(cacheDir);
384
- // Check which bundles exist locally
385
- const existenceChecks = await Promise.all(bundlePaths.map(async ({ path, hash }) => ({
386
- path,
387
- hash,
388
- exists: await exists(path),
389
- })));
390
- const missing = existenceChecks.filter((b) => !b.exists);
391
- if (missing.length === 0) {
392
- logger.debug("[HTTP-CACHE] All bundles exist locally", { count: bundlePaths.length });
393
- return [];
394
- }
395
- logger.info("[HTTP-CACHE] Fetching missing bundles from distributed cache", {
396
- missing: missing.length,
397
- total: bundlePaths.length,
398
- });
399
- const distributed = await getDistributedCache();
400
- if (!distributed) {
401
- logger.error("[HTTP-CACHE] No distributed cache available for bundle recovery");
402
- return missing.map((m) => m.hash);
403
- }
404
- // Batch fetch from distributed cache
405
- const codeKeys = missing.map((m) => `code:${m.hash}`);
406
- let codes;
407
- try {
408
- if (distributed.getBatch) {
409
- codes = await distributed.getBatch(codeKeys);
409
+ const absoluteCacheDir = ensureAbsoluteDir(cacheDir);
410
+ // Use [a-f0-9]+ to match both hex and decimal hashes consistently
411
+ const BUNDLE_RE = /file:\/\/([^"'\s]+veryfront-http-bundle\/http-([a-f0-9]+)\.mjs)/gi;
412
+ const extractBundleRefs = (code) => {
413
+ const refs = [];
414
+ const dedup = new Set();
415
+ let match;
416
+ while ((match = BUNDLE_RE.exec(code)) !== null) {
417
+ const hash = match[2];
418
+ if (!dedup.has(hash)) {
419
+ dedup.add(hash);
420
+ refs.push({ hash });
421
+ }
410
422
  }
411
- else {
412
- // Fallback to individual gets
413
- const results = await Promise.all(codeKeys.map(async (key) => [key, await distributed.get(key)]));
414
- codes = new Map(results);
423
+ BUNDLE_RE.lastIndex = 0;
424
+ return refs;
425
+ };
426
+ const pending = bundlePaths.map((b) => ({ hash: b.hash }));
427
+ const seen = new Set();
428
+ const failed = new Set();
429
+ while (pending.length > 0) {
430
+ const batch = pending.splice(0, pending.length).filter((b) => !seen.has(b.hash));
431
+ if (batch.length === 0)
432
+ break;
433
+ for (const item of batch) {
434
+ seen.add(item.hash);
415
435
  }
416
- }
417
- catch (error) {
418
- logger.error("[HTTP-CACHE] Batch fetch from distributed cache failed", { error });
419
- return missing.map((m) => m.hash);
420
- }
421
- // Write fetched bundles to disk
422
- const failed = [];
423
- await Promise.all(missing.map(async ({ path, hash }) => {
424
- const code = codes.get(`code:${hash}`);
425
- if (!code) {
426
- logger.warn("[HTTP-CACHE] Bundle not found in distributed cache", { hash });
427
- failed.push(hash);
428
- return;
436
+ // Check which bundles exist locally using canonical paths derived from
437
+ // cacheDir + hash. Don't trust caller-provided paths — they may reference
438
+ // a different pod's absolute directory.
439
+ const existenceChecks = await Promise.all(batch.map(async ({ hash }) => ({
440
+ hash,
441
+ canonicalPath: join(absoluteCacheDir, `http-${hash}.mjs`),
442
+ exists: await exists(join(absoluteCacheDir, `http-${hash}.mjs`)),
443
+ })));
444
+ const missing = existenceChecks.filter((b) => !b.exists);
445
+ if (missing.length === 0)
446
+ continue;
447
+ logger.info("[HTTP-CACHE] Fetching missing bundles from distributed cache", {
448
+ missing: missing.length,
449
+ total: batch.length,
450
+ });
451
+ const distributed = await getDistributedCache();
452
+ if (!distributed) {
453
+ logger.error("[HTTP-CACHE] No distributed cache available for bundle recovery");
454
+ for (const m of missing)
455
+ failed.add(m.hash);
456
+ continue;
429
457
  }
458
+ // Batch fetch from distributed cache
459
+ const codeKeys = missing.map((m) => `code:${m.hash}`);
460
+ let codes;
430
461
  try {
431
- const dir = path.substring(0, path.lastIndexOf("/"));
432
- await fs.mkdir(dir, { recursive: true });
433
- await fs.writeTextFile(path, code);
434
- logger.debug("[HTTP-CACHE] Wrote bundle to disk", { hash, path });
462
+ if (distributed.getBatch) {
463
+ codes = await distributed.getBatch(codeKeys);
464
+ }
465
+ else {
466
+ const results = await Promise.all(codeKeys.map(async (key) => [key, await distributed.get(key)]));
467
+ codes = new Map(results);
468
+ }
435
469
  }
436
470
  catch (error) {
437
- logger.error("[HTTP-CACHE] Failed to write bundle to disk", { hash, error });
438
- failed.push(hash);
471
+ logger.error("[HTTP-CACHE] Batch fetch from distributed cache failed", { error });
472
+ for (const m of missing)
473
+ failed.add(m.hash);
474
+ continue;
439
475
  }
440
- }));
441
- if (failed.length > 0) {
442
- logger.warn("[HTTP-CACHE] Some bundles could not be recovered", { failed });
476
+ // Write fetched bundles to disk using canonical paths and scan for transitive deps
477
+ await Promise.all(missing.map(async ({ hash, canonicalPath }) => {
478
+ const code = codes.get(`code:${hash}`);
479
+ if (!code) {
480
+ // Try single-bundle recovery as last resort
481
+ const recovered = await recoverHttpBundleByHash(hash, absoluteCacheDir);
482
+ if (!recovered) {
483
+ failed.add(hash);
484
+ }
485
+ else {
486
+ // Read the recovered bundle to scan for transitive deps
487
+ try {
488
+ const recoveredCode = await fs.readTextFile(canonicalPath);
489
+ for (const ref of extractBundleRefs(recoveredCode)) {
490
+ if (!seen.has(ref.hash))
491
+ pending.push(ref);
492
+ }
493
+ }
494
+ catch { /* ignore read errors for dep scanning */ }
495
+ }
496
+ return;
497
+ }
498
+ try {
499
+ await fs.mkdir(absoluteCacheDir, { recursive: true });
500
+ await fs.writeTextFile(canonicalPath, code);
501
+ logger.debug("[HTTP-CACHE] Wrote bundle to disk", { hash, path: canonicalPath });
502
+ // Scan recovered code for transitive HTTP bundle dependencies.
503
+ // HTTP bundles import other bundles (e.g., esm.sh packages depending
504
+ // on other packages). Without this, Pod B recovers only the top-level
505
+ // bundle and gets "Module not found" for transitive deps at import time.
506
+ for (const ref of extractBundleRefs(code)) {
507
+ if (!seen.has(ref.hash))
508
+ pending.push(ref);
509
+ }
510
+ }
511
+ catch (error) {
512
+ logger.error("[HTTP-CACHE] Failed to write bundle to disk", { hash, error });
513
+ failed.add(hash);
514
+ }
515
+ }));
443
516
  }
444
- else {
445
- logger.info("[HTTP-CACHE] All missing bundles recovered", { count: missing.length });
517
+ if (failed.size > 0) {
518
+ logger.warn("[HTTP-CACHE] Some bundles could not be recovered", {
519
+ failed: Array.from(failed),
520
+ });
446
521
  }
447
- return failed;
522
+ return Array.from(failed);
448
523
  }
@@ -3,7 +3,7 @@ export * from "./logger/index.js";
3
3
  export * from "./constants/index.js";
4
4
  export { VERSION } from "./version.js";
5
5
  export * from "./paths.js";
6
- export { type BundleCode as HashBundleCode, computeCodeHash, computeContentHash, computeHash, getContentHash, shortHash, simpleHash, simpleHash as numericHash, } from "./hash-utils.js";
6
+ export { type BundleCode as HashBundleCode, computeCodeHash, computeContentHash, computeHash, getContentHash, shortHash, simpleHash, } from "./hash-utils.js";
7
7
  export { MemoCache, memoize, memoizeAsync, simpleHash as memoizeHash } from "./memoize.js";
8
8
  export * from "./path-utils.js";
9
9
  export * from "./format-utils.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,cAAc,YAAY,CAAC;AAE3B,OAAO,EACL,KAAK,UAAU,IAAI,cAAc,EACjC,eAAe,EACf,kBAAkB,EAClB,WAAW,EACX,cAAc,EACd,SAAS,EACT,UAAU,EACV,UAAU,IAAI,WAAW,GAC1B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,IAAI,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3F,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,cAAc,YAAY,CAAC;AAE3B,OAAO,EACL,KAAK,UAAU,IAAI,cAAc,EACjC,eAAe,EACf,kBAAkB,EAClB,WAAW,EACX,cAAc,EACd,SAAS,EACT,UAAU,GACX,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,IAAI,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3F,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC"}
@@ -3,7 +3,7 @@ export * from "./logger/index.js";
3
3
  export * from "./constants/index.js";
4
4
  export { VERSION } from "./version.js";
5
5
  export * from "./paths.js";
6
- export { computeCodeHash, computeContentHash, computeHash, getContentHash, shortHash, simpleHash, simpleHash as numericHash, } from "./hash-utils.js";
6
+ export { computeCodeHash, computeContentHash, computeHash, getContentHash, shortHash, simpleHash, } from "./hash-utils.js";
7
7
  export { MemoCache, memoize, memoizeAsync, simpleHash as memoizeHash } from "./memoize.js";
8
8
  export * from "./path-utils.js";
9
9
  export * from "./format-utils.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.0.82",
3
+ "version": "0.0.84",
4
4
  "description": "Zero-config React meta-framework for building agentic AI applications",
5
5
  "keywords": [
6
6
  "react",
@@ -195,6 +195,7 @@
195
195
  "picocolors": "^1.1.0",
196
196
  "react": "19.1.1",
197
197
  "react-dom": "19.1.1",
198
+ "redis": "*",
198
199
  "rehype-highlight": "7.0.2",
199
200
  "rehype-slug": "6.0.0",
200
201
  "rehype-starry-night": "2.2.0",
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.0.82",
3
+ "version": "0.0.84",
4
4
  "nodeModulesDir": "auto",
5
5
  "exclude": [
6
6
  "npm/",
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Token Cache Module
3
+ *
4
+ * Provides configurable caching for OAuth tokens.
5
+ * Supports in-memory (single instance) and Redis (distributed) backends.
6
+ * Redis cache includes automatic fallback to memory when Redis is unavailable.
7
+ */
8
+
9
+ export type {
10
+ CacheOptions,
11
+ CacheStats,
12
+ MemoryCacheOptions,
13
+ RedisCacheOptions,
14
+ TokenCache,
15
+ TokenCacheEntry,
16
+ } from "./types.js";
17
+ export { MemoryCache } from "./memory-cache.js";
18
+ export { RedisCache } from "./redis-cache.js";
19
+ export { ResilientCache } from "./resilient-cache.js";
20
+
21
+ import type { CacheOptions, TokenCache } from "./types.js";
22
+ import { MemoryCache } from "./memory-cache.js";
23
+ import { RedisCache } from "./redis-cache.js";
24
+ import { ResilientCache } from "./resilient-cache.js";
25
+ import { getEnv } from "../../src/platform/compat/process.js";
26
+ import { proxyLogger } from "../logger.js";
27
+ import { withSpan } from "../tracing.js";
28
+
29
+ const logger = proxyLogger.child({ module: "cache" });
30
+
31
+ /**
32
+ * Create a token cache based on configuration.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * // In-memory cache (default)
37
+ * const cache = createCache({ type: "memory" });
38
+ *
39
+ * // Redis cache for distributed deployments
40
+ * const cache = createCache({
41
+ * type: "redis",
42
+ * options: { url: "redis://localhost:6379" }
43
+ * });
44
+ * ```
45
+ */
46
+ export async function createCache(options: CacheOptions): Promise<TokenCache> {
47
+ return withSpan("cache.create", async () => {
48
+ switch (options.type) {
49
+ case "redis":
50
+ return new RedisCache(options.options);
51
+ case "memory":
52
+ default:
53
+ return new MemoryCache(options.options);
54
+ }
55
+ }, { "cache.type": options.type });
56
+ }
57
+
58
+ /**
59
+ * Create cache from environment variables.
60
+ *
61
+ * Environment variables:
62
+ * - CACHE_TYPE: "memory" or "redis" (default: "memory")
63
+ * - REDIS_URL: Redis connection URL (required if CACHE_TYPE=redis)
64
+ * - REDIS_PREFIX: Key prefix (default: "vf:token:")
65
+ *
66
+ * When CACHE_TYPE=redis, automatically wraps with ResilientCache for
67
+ * graceful fallback to memory when Redis is unavailable.
68
+ */
69
+ export async function createCacheFromEnv(): Promise<TokenCache> {
70
+ return withSpan("cache.createFromEnv", async () => {
71
+ const cacheType = getEnv("CACHE_TYPE") || "memory";
72
+
73
+ if (cacheType === "redis") {
74
+ const url = getEnv("REDIS_URL");
75
+ if (!url) {
76
+ logger.warn("[Cache] CACHE_TYPE=redis but REDIS_URL not set, falling back to memory");
77
+ return new MemoryCache();
78
+ }
79
+
80
+ const redisCache = new RedisCache({
81
+ url,
82
+ prefix: getEnv("REDIS_PREFIX") || "vf:token:",
83
+ });
84
+
85
+ // Wrap Redis with resilient fallback to memory cache
86
+ // This ensures the proxy continues to function when Redis is unavailable
87
+ logger.info("[Cache] Using Redis with memory fallback (ResilientCache)");
88
+ return new ResilientCache(redisCache, new MemoryCache());
89
+ }
90
+
91
+ return new MemoryCache();
92
+ }, { "cache.type": getEnv("CACHE_TYPE") || "memory" });
93
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * In-Memory Token Cache - single-instance deployments.
3
+ */
4
+ import * as dntShim from "../../_dnt.shims.js";
5
+
6
+
7
+ import type { CacheStats, MemoryCacheOptions, TokenCache, TokenCacheEntry } from "./types.js";
8
+ import { withSpan } from "../tracing.js";
9
+
10
+ const DEFAULT_MAX_SIZE = 1000;
11
+ const DEFAULT_CLEANUP_INTERVAL = 60_000;
12
+
13
+ export class MemoryCache implements TokenCache {
14
+ private cache = new Map<string, TokenCacheEntry>();
15
+ private hits = 0;
16
+ private misses = 0;
17
+ private maxSize: number;
18
+ private cleanupTimer: ReturnType<typeof dntShim.setInterval> | null = null;
19
+
20
+ constructor(options: MemoryCacheOptions = {}) {
21
+ this.maxSize = options.maxSize ?? DEFAULT_MAX_SIZE;
22
+ const interval = options.cleanupInterval ?? DEFAULT_CLEANUP_INTERVAL;
23
+ this.cleanupTimer = dntShim.setInterval(() => this.cleanup(), interval);
24
+ }
25
+
26
+ get(key: string): Promise<TokenCacheEntry | null> {
27
+ return withSpan("cache.memory.get", async () => {
28
+ const entry = this.cache.get(key);
29
+
30
+ if (!entry) {
31
+ this.misses++;
32
+ return null;
33
+ }
34
+
35
+ if (Date.now() >= entry.expiresAt) {
36
+ this.cache.delete(key);
37
+ this.misses++;
38
+ return null;
39
+ }
40
+
41
+ this.hits++;
42
+ return entry;
43
+ }, { "cache.key": key });
44
+ }
45
+
46
+ set(key: string, entry: TokenCacheEntry): Promise<void> {
47
+ return withSpan("cache.memory.set", async () => {
48
+ if (this.cache.size >= this.maxSize) {
49
+ const firstKey = this.cache.keys().next().value;
50
+ if (firstKey) {
51
+ this.cache.delete(firstKey);
52
+ }
53
+ }
54
+ this.cache.set(key, entry);
55
+ }, { "cache.key": key });
56
+ }
57
+
58
+ delete(key: string): Promise<void> {
59
+ return withSpan("cache.memory.delete", async () => {
60
+ this.cache.delete(key);
61
+ }, { "cache.key": key });
62
+ }
63
+
64
+ clear(): Promise<void> {
65
+ return withSpan("cache.memory.clear", async () => {
66
+ this.cache.clear();
67
+ this.hits = 0;
68
+ this.misses = 0;
69
+ });
70
+ }
71
+
72
+ has(key: string): Promise<boolean> {
73
+ return withSpan("cache.memory.has", async () => {
74
+ const entry = this.cache.get(key);
75
+ if (!entry) return false;
76
+
77
+ if (Date.now() >= entry.expiresAt) {
78
+ this.cache.delete(key);
79
+ return false;
80
+ }
81
+
82
+ return true;
83
+ }, { "cache.key": key });
84
+ }
85
+
86
+ stats(): Promise<CacheStats> {
87
+ return withSpan("cache.memory.stats", async () => ({
88
+ hits: this.hits,
89
+ misses: this.misses,
90
+ size: this.cache.size,
91
+ type: "memory" as const,
92
+ }));
93
+ }
94
+
95
+ close(): Promise<void> {
96
+ return withSpan("cache.memory.close", async () => {
97
+ if (this.cleanupTimer) {
98
+ clearInterval(this.cleanupTimer);
99
+ this.cleanupTimer = null;
100
+ }
101
+ this.cache.clear();
102
+ });
103
+ }
104
+
105
+ private cleanup(): void {
106
+ const now = Date.now();
107
+ let cleaned = 0;
108
+
109
+ for (const [key, entry] of this.cache.entries()) {
110
+ if (now >= entry.expiresAt) {
111
+ this.cache.delete(key);
112
+ cleaned++;
113
+ }
114
+ }
115
+
116
+ if (cleaned > 0) {
117
+ console.log(`[MemoryCache] Cleaned ${cleaned} expired entries`);
118
+ }
119
+ }
120
+ }