fs-memo.ts 4.0 KB

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