fs-memo.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import path from 'node:path';
  2. import { Cache } from './cache-filesystem';
  3. import type { CacheApplyOption } from './cache-filesystem';
  4. import { isCI } from 'ci-info';
  5. import { xxhash64 } from 'hash-wasm';
  6. import picocolors from 'picocolors';
  7. import { identity } from './misc';
  8. const fsMemoCache = new Cache({ cachePath: path.resolve(__dirname, '../../.cache'), tableName: 'fs_memo_cache' });
  9. const TTL = isCI
  10. // We run CI daily, so 1.5 days TTL is enough to persist the cache across runs
  11. ? 1.5 * 86400 * 1000
  12. // We run locally less frequently, so we need to persist the cache for longer, 7 days
  13. : 7 * 86400 * 1000;
  14. type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;
  15. // https://github.com/Rich-Harris/devalue/blob/f3fd2aa93d79f21746555671f955a897335edb1b/src/stringify.js#L77
  16. type Devalue =
  17. | number
  18. | string
  19. | boolean
  20. | bigint
  21. | Date
  22. | RegExp
  23. | Set<Devalue>
  24. | Devalue[]
  25. | null
  26. | undefined
  27. | Map<Devalue, Devalue>
  28. | DevalueObject
  29. | TypedArray
  30. | ArrayBuffer;
  31. // Has to use an interface to avoid circular reference
  32. interface DevalueObject {
  33. [key: string]: Devalue
  34. }
  35. export type FsMemoCacheOptions<T> = CacheApplyOption<T, string> & {
  36. ttl?: undefined | never
  37. };
  38. function createCache(onlyUseCachedIfFail: boolean) {
  39. return function cache<Args extends Devalue[], T>(
  40. fn: (...args: Args) => Promise<T>,
  41. opt: FsMemoCacheOptions<T>
  42. ): (...args: Args) => Promise<T> {
  43. const fixedKey = fn.toString();
  44. if (opt.temporaryBypass) {
  45. return fn;
  46. }
  47. return async function cachedCb(...args: Args) {
  48. const { stringify: devalueStringify } = await import('devalue');
  49. // Construct the complete cache key for this function invocation
  50. // typeson.stringify is still limited. For now we uses typescript to guard the args.
  51. const cacheKey = (await Promise.all([
  52. xxhash64(fixedKey),
  53. xxhash64(devalueStringify(args))
  54. ])).join('|');
  55. const cacheName = picocolors.gray(fn.name || fixedKey || cacheKey);
  56. const cached = fsMemoCache.get(cacheKey);
  57. const serializer = 'serializer' in opt ? opt.serializer : identity as any;
  58. const deserializer = 'deserializer' in opt ? opt.deserializer : identity as any;
  59. if (onlyUseCachedIfFail) {
  60. try {
  61. const value = await fn(...args);
  62. console.log(picocolors.gray('[cache] update'), cacheName);
  63. fsMemoCache.set(cacheKey, serializer(value), TTL);
  64. return value;
  65. } catch (e) {
  66. if (cached == null) {
  67. console.log(picocolors.red('[fail] and no cache, throwing'), cacheName);
  68. throw e;
  69. }
  70. fsMemoCache.updateTtl(cacheKey, TTL);
  71. console.log(picocolors.yellow('[fail] try cache'), cacheName);
  72. return deserializer(cached);
  73. }
  74. } else {
  75. if (cached == null) {
  76. console.log(picocolors.yellow('[cache] miss'), cacheName);
  77. const value = await fn(...args);
  78. fsMemoCache.set(cacheKey, serializer(value), TTL);
  79. return value;
  80. }
  81. console.log(picocolors.green('[cache] hit'), cacheName);
  82. fsMemoCache.updateTtl(cacheKey, TTL);
  83. return deserializer(cached);
  84. }
  85. };
  86. };
  87. }
  88. export const cache = createCache(false);
  89. export const cachedOnlyFail = createCache(true);