fs-memo.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  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 { Typeson, set, map, typedArrays, undef, infinity } from 'typeson-registry';
  7. import picocolors from 'picocolors';
  8. import { identity } from './misc';
  9. const typeson = new Typeson().register([
  10. typedArrays,
  11. set,
  12. map,
  13. undef,
  14. infinity
  15. ]);
  16. const fsMemoCache = new Cache({ cachePath: path.resolve(__dirname, '../../.cache'), tableName: 'fs_memo_cache' });
  17. const TTL = isCI
  18. // We run CI daily, so 1.5 days TTL is enough to persist the cache across runs
  19. ? 1.5 * 86400 * 1000
  20. // We run locally less frequently, so we need to persist the cache for longer, 7 days
  21. : 7 * 86400 * 1000;
  22. type TypesonValue =
  23. | string
  24. | number
  25. | boolean
  26. | null
  27. | undefined
  28. | Set<any>
  29. | Map<any, any>
  30. | TypesonObject
  31. | TypesonArray;
  32. interface TypesonObject {
  33. [key: string]: TypesonValue
  34. }
  35. interface TypesonArray extends Array<TypesonValue> { }
  36. export type FsMemoCacheOptions<T> = CacheApplyOption<T, string> & {
  37. ttl?: undefined | never
  38. };
  39. export function cache<Args extends TypesonValue[], T>(
  40. fn: (...args: Args) => Promise<T>,
  41. opt: FsMemoCacheOptions<T>
  42. ): (...args: Args) => Promise<T> {
  43. const fixedKey = fn.toString();
  44. return async function cachedCb(...args: Args) {
  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(typeson.stringifySync(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 TypesonValue[], 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. // Construct the complete cache key for this function invocation
  76. // typeson.stringify is still limited. For now we uses typescript to guard the args.
  77. const cacheKey = (await Promise.all([
  78. xxhash64(fixedKey),
  79. xxhash64(typeson.stringifySync(args))
  80. ])).join('|');
  81. const cacheName = fn.name || fixedKey;
  82. if (opt.temporaryBypass) {
  83. return fn(...args);
  84. }
  85. const cached = fsMemoCache.get(cacheKey);
  86. try {
  87. const value = await fn(...args);
  88. const serializer = 'serializer' in opt ? opt.serializer : identity as any;
  89. fsMemoCache.set(cacheKey, serializer(value), TTL);
  90. return value;
  91. } catch (e) {
  92. if (cached == null) {
  93. console.log(picocolors.red('[fail] and no cache, throwing'), picocolors.gray(cacheName || cacheKey));
  94. throw e;
  95. }
  96. fsMemoCache.updateTtl(cacheKey, TTL);
  97. console.log(picocolors.yellow('[fail] try cache'), picocolors.gray(cacheName || cacheKey));
  98. const deserializer = 'deserializer' in opt ? opt.deserializer : identity as any;
  99. return deserializer(cached);
  100. }
  101. };
  102. }