fs-memo.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  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 { fastStringArrayJoin, 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. if (opt.temporaryBypass) {
  44. return fn;
  45. }
  46. const serializer = 'serializer' in opt ? opt.serializer : identity<T, string>;
  47. const deserializer = 'deserializer' in opt ? opt.deserializer : identity<string, T>;
  48. const fixedKey = fn.toString();
  49. const fixedKeyHashPromise = xxhash64(fixedKey);
  50. const devalueModulePromise = import('devalue');
  51. return async function cachedCb(...args: Args) {
  52. const devalueStringify = (await devalueModulePromise).stringify;
  53. // Construct the complete cache key for this function invocation
  54. // typeson.stringify is still limited. For now we uses typescript to guard the args.
  55. const cacheKey = fastStringArrayJoin(
  56. await Promise.all([
  57. fixedKeyHashPromise,
  58. xxhash64(devalueStringify(args))
  59. ]),
  60. '|'
  61. );
  62. const cacheName = picocolors.gray(fn.name || fixedKey || cacheKey);
  63. const cached = fsMemoCache.get(cacheKey);
  64. if (onlyUseCachedIfFail) {
  65. try {
  66. const value = await fn(...args);
  67. console.log(picocolors.gray('[cache] update'), cacheName);
  68. fsMemoCache.set(cacheKey, serializer(value), TTL);
  69. return value;
  70. } catch (e) {
  71. if (cached == null) {
  72. console.log(picocolors.red('[fail] and no cache, throwing'), cacheName);
  73. throw e;
  74. }
  75. fsMemoCache.updateTtl(cacheKey, TTL);
  76. console.log(picocolors.yellow('[fail] try cache'), cacheName);
  77. return deserializer(cached);
  78. }
  79. } else {
  80. if (cached == null) {
  81. console.log(picocolors.yellow('[cache] miss'), cacheName);
  82. const value = await fn(...args);
  83. fsMemoCache.set(cacheKey, serializer(value), TTL);
  84. return value;
  85. }
  86. console.log(picocolors.green('[cache] hit'), cacheName);
  87. fsMemoCache.updateTtl(cacheKey, TTL);
  88. return deserializer(cached);
  89. }
  90. };
  91. };
  92. }
  93. export const cache = createCache(false);
  94. export const cachedOnlyFail = createCache(true);