fs-memo.ts 3.6 KB

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