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.
- package/README.md +18 -17
- package/esm/deno.js +1 -1
- package/esm/proxy/cache/index.d.ts +41 -0
- package/esm/proxy/cache/index.d.ts.map +1 -0
- package/esm/proxy/cache/index.js +75 -0
- package/esm/proxy/cache/memory-cache.d.ts +18 -0
- package/esm/proxy/cache/memory-cache.d.ts.map +1 -0
- package/esm/proxy/cache/memory-cache.js +100 -0
- package/esm/proxy/cache/redis-cache.d.ts +27 -0
- package/esm/proxy/cache/redis-cache.d.ts.map +1 -0
- package/esm/proxy/cache/redis-cache.js +183 -0
- package/esm/proxy/cache/resilient-cache.d.ts +44 -0
- package/esm/proxy/cache/resilient-cache.d.ts.map +1 -0
- package/esm/proxy/cache/resilient-cache.js +178 -0
- package/esm/proxy/cache/types.d.ts +65 -0
- package/esm/proxy/cache/types.d.ts.map +1 -0
- package/esm/proxy/cache/types.js +7 -0
- package/esm/proxy/handler.d.ts +81 -0
- package/esm/proxy/handler.d.ts.map +1 -0
- package/esm/proxy/handler.js +417 -0
- package/esm/proxy/logger.d.ts +29 -0
- package/esm/proxy/logger.d.ts.map +1 -0
- package/esm/proxy/logger.js +258 -0
- package/esm/proxy/oauth-client.d.ts +15 -0
- package/esm/proxy/oauth-client.d.ts.map +1 -0
- package/esm/proxy/oauth-client.js +52 -0
- package/esm/proxy/token-manager.d.ts +59 -0
- package/esm/proxy/token-manager.d.ts.map +1 -0
- package/esm/proxy/token-manager.js +125 -0
- package/esm/proxy/tracing.d.ts +39 -0
- package/esm/proxy/tracing.d.ts.map +1 -0
- package/esm/proxy/tracing.js +194 -0
- package/esm/src/cache/backend.d.ts +2 -0
- package/esm/src/cache/backend.d.ts.map +1 -1
- package/esm/src/cache/backend.js +2 -0
- package/esm/src/cache/cache-key-builder.d.ts +0 -4
- package/esm/src/cache/cache-key-builder.d.ts.map +1 -1
- package/esm/src/cache/cache-key-builder.js +0 -6
- package/esm/src/cache/multi-tier.d.ts +0 -29
- package/esm/src/cache/multi-tier.d.ts.map +1 -1
- package/esm/src/cache/multi-tier.js +0 -26
- package/esm/src/cli/app/actions.d.ts +26 -0
- package/esm/src/cli/app/actions.d.ts.map +1 -0
- package/esm/src/cli/app/actions.js +152 -0
- package/esm/src/cli/app/components/inline-input.d.ts +35 -0
- package/esm/src/cli/app/components/inline-input.d.ts.map +1 -0
- package/esm/src/cli/app/components/inline-input.js +220 -0
- package/esm/src/cli/app/components/list-select.d.ts +69 -0
- package/esm/src/cli/app/components/list-select.d.ts.map +1 -0
- package/esm/src/cli/app/components/list-select.js +137 -0
- package/esm/src/cli/app/index.d.ts +45 -0
- package/esm/src/cli/app/index.d.ts.map +1 -0
- package/esm/src/cli/app/index.js +1252 -0
- package/esm/src/cli/app/state.d.ts +122 -0
- package/esm/src/cli/app/state.d.ts.map +1 -0
- package/esm/src/cli/app/state.js +232 -0
- package/esm/src/cli/app/views/dashboard.d.ts +19 -0
- package/esm/src/cli/app/views/dashboard.d.ts.map +1 -0
- package/esm/src/cli/app/views/dashboard.js +178 -0
- package/esm/src/cli/commands/dev.js +2 -2
- package/esm/src/cli/commands/new.js +1 -1
- package/esm/src/cli/index/command-router.d.ts.map +1 -1
- package/esm/src/cli/index/command-router.js +9 -39
- package/esm/src/cli/index/start-handler.d.ts +3 -0
- package/esm/src/cli/index/start-handler.d.ts.map +1 -0
- package/esm/src/cli/index/start-handler.js +145 -0
- package/esm/src/cli/mcp/index.d.ts +11 -0
- package/esm/src/cli/mcp/index.d.ts.map +1 -0
- package/esm/src/cli/mcp/index.js +10 -0
- package/esm/src/cli/ui/tui.js +1 -1
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts +2 -0
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts.map +1 -1
- package/esm/src/middleware/builtin/security/redis-rate-limit.js +23 -9
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts +10 -0
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.js +30 -42
- package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/loader.js +34 -13
- package/esm/src/platform/adapters/fs/cache/file-cache.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/cache/file-cache.js +9 -3
- package/esm/src/server/context/cache-invalidation.d.ts.map +1 -1
- package/esm/src/server/context/cache-invalidation.js +4 -0
- package/esm/src/server/handlers/dev/dashboard/api.js +4 -0
- package/esm/src/server/handlers/dev/projects/ui-handler.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/projects/ui-handler.js +6 -0
- package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
- package/esm/src/transforms/esm/http-cache.js +139 -64
- package/esm/src/utils/index.d.ts +1 -1
- package/esm/src/utils/index.d.ts.map +1 -1
- package/esm/src/utils/index.js +1 -1
- package/package.json +2 -1
- package/src/deno.js +1 -1
- package/src/proxy/cache/index.ts +93 -0
- package/src/proxy/cache/memory-cache.ts +120 -0
- package/src/proxy/cache/redis-cache.ts +203 -0
- package/src/proxy/cache/resilient-cache.ts +205 -0
- package/src/proxy/cache/types.ts +72 -0
- package/src/proxy/handler.ts +593 -0
- package/src/proxy/logger.ts +329 -0
- package/src/proxy/oauth-client.ts +91 -0
- package/src/proxy/token-manager.ts +174 -0
- package/src/proxy/tracing.ts +237 -0
- package/src/src/cache/backend.ts +3 -0
- package/src/src/cache/cache-key-builder.ts +0 -9
- package/src/src/cache/multi-tier.ts +0 -41
- package/src/src/cli/app/actions.ts +190 -0
- package/src/src/cli/app/components/inline-input.ts +255 -0
- package/src/src/cli/app/components/list-select.ts +215 -0
- package/src/src/cli/app/index.ts +1471 -0
- package/src/src/cli/app/state.ts +385 -0
- package/src/src/cli/app/views/dashboard.ts +212 -0
- package/src/src/cli/commands/dev.ts +2 -2
- package/src/src/cli/commands/new.ts +1 -1
- package/src/src/cli/index/command-router.ts +9 -40
- package/src/src/cli/index/start-handler.ts +195 -0
- package/src/src/cli/mcp/index.ts +11 -0
- package/src/src/cli/ui/tui.ts +1 -1
- package/src/src/middleware/builtin/security/redis-rate-limit.ts +24 -11
- package/src/src/modules/react-loader/ssr-module-loader/cache/redis.ts +36 -50
- package/src/src/modules/react-loader/ssr-module-loader/loader.ts +38 -14
- package/src/src/platform/adapters/fs/cache/file-cache.ts +9 -3
- package/src/src/server/context/cache-invalidation.ts +4 -0
- package/src/src/server/handlers/dev/dashboard/api.ts +2 -0
- package/src/src/server/handlers/dev/projects/ui-handler.ts +6 -0
- package/src/src/transforms/esm/http-cache.ts +148 -73
- 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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
384
|
-
//
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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]
|
|
438
|
-
|
|
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
|
-
|
|
442
|
-
|
|
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
|
-
|
|
445
|
-
logger.
|
|
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
|
}
|
package/esm/src/utils/index.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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"}
|
package/esm/src/utils/index.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
@@ -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
|
+
}
|