@stim-cli/cache 1.20.2 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,8 +6,8 @@ front of it.
6
6
 
7
7
  The local filesystem is always the first tier. A project can add one optional
8
8
  second tier by pointing `cache.provider` at a module that implements this
9
- contract. Stim ships no network provider; writing one is an addition, not a
10
- change to Stim.
9
+ contract. `@stim-cli/cache-s3` provides an S3-compatible second tier, including
10
+ Cloudflare R2.
11
11
 
12
12
  If Stim is not installed globally, replace `stim` with `npx stim`.
13
13
 
@@ -30,6 +30,12 @@ provider, and lower-precedence layers that name the same provider resolved from
30
30
  the same directory; options written for another provider are ignored. Keep secrets out of
31
31
  committed settings; read them from the environment or from machine settings.
32
32
 
33
+ ## S3-compatible storage
34
+
35
+ Install the separate [`@stim-cli/cache-s3`](../cache-s3/README.md) provider and
36
+ select it through `cache.provider`. The AWS SDK and archive dependencies belong
37
+ to that opt-in package; `@stim-cli/cache` has no runtime dependencies.
38
+
33
39
  ## Writing a provider
34
40
 
35
41
  ```js
@@ -53,10 +59,25 @@ export async function createCacheProvider({ projectRoot, options }) {
53
59
  }
54
60
  ```
55
61
 
56
- A provider implements one or both capabilities. It owns transport,
62
+ The factory may also receive `env`, a captured caller environment. Use it for
63
+ authentication instead of reading or changing `process.env` when it is present.
64
+
65
+ A provider implements one or more capabilities. It owns transport,
57
66
  serialization, archive format, authentication, and remote retention. Stim owns
58
67
  fingerprints, cache keys, and local artifact paths.
59
68
 
69
+ The optional `files` capability stores opaque complete files with
70
+ `store({ key, sourcePath, overwrite, signal })` and downloads them with
71
+ `resolve({ key, destinationDir, signal })`. A hit returns a path inside
72
+ `destinationDir`; a miss returns `null` and leaves it empty. The CLI consumes
73
+ the `metro` and `builds` adapters. Native Gradle APK caching consumes `files`
74
+ directly to share the APK together with its verification receipt; a files-only
75
+ provider can support this flow. An optional
76
+ `stat({ key, signal })` returns `{ size }` or `null` without downloading;
77
+ consumers still verify downloaded bytes. `createFilesystemFileCache(directory)`
78
+ implements the same contract in an owned local directory without dependencies.
79
+ It publishes atomically and materializes downloads in the requested scratch directory.
80
+
60
81
  `metro.get` returns the stored value or `null`.
61
82
 
62
83
  `builds.resolve` returns an existing path to the artifact, or `null` for a
@@ -71,6 +92,10 @@ and `overwrite: true` must replace it.
71
92
 
72
93
  Every call receives an `AbortSignal`. A provider must honor it: Stim abandons
73
94
  the call at the deadline and keeps building or bundling with the local tier.
95
+ Factories and callbacks run as trusted host code in programmatic clients and the
96
+ server. They must keep the event loop responsive and settle when cancelled.
97
+ Deadlines are cooperative; they do not sandbox a module that blocks JavaScript
98
+ or terminates its host.
74
99
 
75
100
  `stim gc` never deletes provider data, and the contract has no delete
76
101
  operation, so shared team or CI data is never removed by a local command.
@@ -112,8 +137,8 @@ const results = await runCacheProviderContract({
112
137
  ```
113
138
 
114
139
  `cacheProviderContractChecks()` returns the same checks as individual cases for
115
- a test runner that reports each one separately. Both helpers only check the
116
- capabilities a provider advertises, bound every check with a deadline, and
140
+ a test runner that reports each one separately. Both helpers check advertised
141
+ Metro and build capabilities, bound every check with a deadline, and
117
142
  verify that a call settles once its `AbortSignal` aborts.
118
143
 
119
144
  ## Budgets
package/dist/index.d.mts CHANGED
@@ -76,7 +76,28 @@ interface BuildCacheCapability {
76
76
  resolve(input: BuildResolveInput): string | null | Promise<string | null>;
77
77
  store(input: BuildStoreInput): string | null | void | Promise<string | null | void>;
78
78
  }
79
+ interface FileCacheResolveInput {
80
+ key: string;
81
+ destinationDir: string;
82
+ signal: AbortSignal;
83
+ }
84
+ interface FileCacheStoreInput {
85
+ key: string;
86
+ sourcePath: string;
87
+ overwrite: boolean;
88
+ signal: AbortSignal;
89
+ }
90
+ /** Opaque, complete files. Downloads are materialized inside destinationDir; a miss leaves it empty. */
91
+ interface FileCacheCapability {
92
+ resolve(input: FileCacheResolveInput): Promise<string | null>;
93
+ store(input: FileCacheStoreInput): Promise<void>;
94
+ /** Checks presence without downloading; the bytes still need verification when fetched. */
95
+ stat?(input: Pick<FileCacheResolveInput, "key" | "signal">): Promise<{
96
+ size: number;
97
+ } | null>;
98
+ }
79
99
  interface CacheProvider {
100
+ files?: FileCacheCapability;
80
101
  metro?: MetroCacheCapability;
81
102
  builds?: BuildCacheCapability;
82
103
  }
@@ -89,6 +110,7 @@ interface CacheProviderModule {
89
110
  createCacheProvider(input: {
90
111
  projectRoot: string;
91
112
  options: Record<string, unknown>;
113
+ env?: NodeJS.ProcessEnv;
92
114
  }): CacheProvider | Promise<CacheProvider>;
93
115
  }
94
116
  interface LoadCacheProviderResult {
@@ -103,12 +125,13 @@ interface ProviderCallResult<T> {
103
125
  timedOut?: true;
104
126
  }
105
127
  type WarnOnce = (code: string, message: string) => void;
106
- declare function loadCacheProvider({ projectRoot, config, timeoutMs }: {
128
+ declare function loadCacheProvider({ projectRoot, config, env, timeoutMs }: {
107
129
  projectRoot: string;
108
130
  config?: CacheProviderConfig | null;
131
+ env?: NodeJS.ProcessEnv;
109
132
  timeoutMs?: number;
110
133
  }): Promise<LoadCacheProviderResult>;
111
- declare function callWithTimeout<T>(call: (signal: AbortSignal) => T | Promise<T>, timeoutMs: number): Promise<ProviderCallResult<T>>;
134
+ declare function callWithTimeout<T>(call: (signal: AbortSignal) => T | Promise<T>, timeoutMs: number, signal?: AbortSignal): Promise<ProviderCallResult<T>>;
112
135
  declare function createWarnOnce(emit: (message: string) => void): WarnOnce;
113
136
  declare function cacheProviderEnv(config: CacheProviderConfig | null): string;
114
137
  /**
@@ -196,6 +219,7 @@ interface ResolveTieredBuildOptions {
196
219
  skipRead?: boolean;
197
220
  warn?: WarnOnce;
198
221
  timeoutMs?: number;
222
+ signal?: AbortSignal;
199
223
  }
200
224
  interface StoreTieredBuildOptions {
201
225
  local: BuildCacheCapability;
@@ -205,14 +229,15 @@ interface StoreTieredBuildOptions {
205
229
  overwrite: boolean;
206
230
  warn?: WarnOnce;
207
231
  timeoutMs?: number;
232
+ signal?: AbortSignal;
208
233
  }
209
234
  interface TieredBuildStoreResult {
210
235
  localPath: string | null;
211
236
  providerUpload: Promise<ProviderCallResult<void>> | null;
212
237
  providerName: string | null;
213
238
  }
214
- declare function resolveTieredBuild({ local, loadProvider, target, destinationDir, ensureDestination, skipRead, warn, timeoutMs }: ResolveTieredBuildOptions): Promise<TieredBuildResolution | null>;
215
- declare function storeTieredBuild({ local, loadProvider, target, sourcePath, overwrite, warn, timeoutMs }: StoreTieredBuildOptions): Promise<TieredBuildStoreResult>;
239
+ declare function resolveTieredBuild({ local, loadProvider, target, destinationDir, ensureDestination, skipRead, warn, timeoutMs, signal }: ResolveTieredBuildOptions): Promise<TieredBuildResolution | null>;
240
+ declare function storeTieredBuild({ local, loadProvider, target, sourcePath, overwrite, warn, timeoutMs, signal }: StoreTieredBuildOptions): Promise<TieredBuildStoreResult>;
216
241
  //#endregion
217
242
  //#region contract.d.ts
218
243
  interface CacheContractCheck {
@@ -249,4 +274,8 @@ declare const CACHE_CONTRACT_CHECK_TIMEOUT_MS = 3e4;
249
274
  declare function cacheProviderContractChecks(options: CacheContractOptions): CacheContractCheck[];
250
275
  declare function runCacheProviderContract(options: CacheContractOptions | CacheContractModuleOptions): Promise<CacheContractResult[]>;
251
276
  //#endregion
252
- export { BUILD_RESOLVE_TIMEOUT_ENV, BUILD_RESOLVE_TIMEOUT_MS, BUILD_UPLOAD_TIMEOUT_ENV, BUILD_UPLOAD_TIMEOUT_MS, BuildCacheCapability, BuildCacheContext, BuildCacheTarget, BuildResolveInput, BuildStoreInput, CACHE_CONTRACT_CHECK_TIMEOUT_MS, CACHE_PROVIDER_API_VERSION, CACHE_PROVIDER_ENV, CACHE_PROVIDER_ENV_NONE, CacheContractCheck, CacheContractModuleOptions, CacheContractOptions, CacheContractResult, CacheProvider, CacheProviderConfig, CacheProviderModule, LoadBuildProvider, LoadCacheProviderResult, METRO_READ_CONCURRENCY, METRO_READ_FAILURE_LIMIT, METRO_READ_TIMEOUT_ENV, METRO_READ_TIMEOUT_MS, METRO_UPLOAD_CONCURRENCY, METRO_UPLOAD_MAX_BYTES, METRO_UPLOAD_MAX_ITEMS, METRO_WRITE_TIMEOUT_ENV, METRO_WRITE_TIMEOUT_MS, MetroCacheCapability, MetroCacheContext, MetroCacheGetInput, MetroCacheSetInput, MetroCacheStore, MetroTierLimits, MetroTierTimeouts, PROVIDER_LOAD_TIMEOUT_ENV, PROVIDER_LOAD_TIMEOUT_MS, ProviderCallResult, ResolveTieredBuildOptions, StoreTieredBuildOptions, TieredBuildResolution, TieredBuildStoreResult, TieredMetroStore, TieredMetroStoreOptions, WarnOnce, buildResolveTimeoutMs, buildUploadTimeoutMs, cacheProviderConfigFromEnv, cacheProviderContractChecks, cacheProviderEnv, cacheProviderEnvIsSet, callWithTimeout, createTieredMetroStore, createWarnOnce, loadCacheProvider, metroCapabilityFromStore, resolveTieredBuild, runCacheProviderContract, storeTieredBuild, timeoutFromEnv };
277
+ //#region files.d.ts
278
+ /** Opaque immutable files in a caller-owned directory, with atomic publication and cancellable downloads. */
279
+ declare function createFilesystemFileCache(directory: string): FileCacheCapability & Required<Pick<FileCacheCapability, "stat">>;
280
+ //#endregion
281
+ export { BUILD_RESOLVE_TIMEOUT_ENV, BUILD_RESOLVE_TIMEOUT_MS, BUILD_UPLOAD_TIMEOUT_ENV, BUILD_UPLOAD_TIMEOUT_MS, BuildCacheCapability, BuildCacheContext, BuildCacheTarget, BuildResolveInput, BuildStoreInput, CACHE_CONTRACT_CHECK_TIMEOUT_MS, CACHE_PROVIDER_API_VERSION, CACHE_PROVIDER_ENV, CACHE_PROVIDER_ENV_NONE, CacheContractCheck, CacheContractModuleOptions, CacheContractOptions, CacheContractResult, CacheProvider, CacheProviderConfig, CacheProviderModule, FileCacheCapability, FileCacheResolveInput, FileCacheStoreInput, LoadBuildProvider, LoadCacheProviderResult, METRO_READ_CONCURRENCY, METRO_READ_FAILURE_LIMIT, METRO_READ_TIMEOUT_ENV, METRO_READ_TIMEOUT_MS, METRO_UPLOAD_CONCURRENCY, METRO_UPLOAD_MAX_BYTES, METRO_UPLOAD_MAX_ITEMS, METRO_WRITE_TIMEOUT_ENV, METRO_WRITE_TIMEOUT_MS, MetroCacheCapability, MetroCacheContext, MetroCacheGetInput, MetroCacheSetInput, MetroCacheStore, MetroTierLimits, MetroTierTimeouts, PROVIDER_LOAD_TIMEOUT_ENV, PROVIDER_LOAD_TIMEOUT_MS, ProviderCallResult, ResolveTieredBuildOptions, StoreTieredBuildOptions, TieredBuildResolution, TieredBuildStoreResult, TieredMetroStore, TieredMetroStoreOptions, WarnOnce, buildResolveTimeoutMs, buildUploadTimeoutMs, cacheProviderConfigFromEnv, cacheProviderContractChecks, cacheProviderEnv, cacheProviderEnvIsSet, callWithTimeout, createFilesystemFileCache, createTieredMetroStore, createWarnOnce, loadCacheProvider, metroCapabilityFromStore, resolveTieredBuild, runCacheProviderContract, storeTieredBuild, timeoutFromEnv };
package/dist/index.mjs CHANGED
@@ -1,8 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { join, relative, sep } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
- import { randomBytes, randomUUID } from "node:crypto";
4
+ import { createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
6
+ import { link, lstat, mkdir, rename, rm } from "node:fs/promises";
7
+ import { pipeline } from "node:stream/promises";
6
8
  //#region provider.ts
7
9
  const CACHE_PROVIDER_API_VERSION = 1;
8
10
  const CACHE_PROVIDER_ENV = "STIM_CACHE_PROVIDER_CONFIG";
@@ -31,20 +33,24 @@ function moduleCandidate(namespace) {
31
33
  return "default" in namespace ? namespace.default : namespace;
32
34
  }
33
35
  function capabilityError(provider) {
36
+ const files = provider.files;
37
+ if (files !== void 0 && (typeof files?.resolve !== "function" || typeof files?.store !== "function")) return "the files capability must implement resolve() and store()";
34
38
  const metro = provider.metro;
35
39
  if (metro !== void 0 && (typeof metro?.get !== "function" || typeof metro?.set !== "function")) return "the metro capability must implement get() and set()";
36
40
  const builds = provider.builds;
37
41
  if (builds !== void 0 && (typeof builds?.resolve !== "function" || typeof builds?.store !== "function")) return "the builds capability must implement resolve() and store()";
38
- if (metro === void 0 && builds === void 0) return "the provider advertises neither a metro nor a builds capability";
42
+ if (metro === void 0 && builds === void 0 && files === void 0) return "the provider advertises no files, metro or builds capability";
39
43
  return null;
40
44
  }
41
- async function loadCacheProvider({ projectRoot, config, timeoutMs = timeoutFromEnv(PROVIDER_LOAD_TIMEOUT_ENV, PROVIDER_LOAD_TIMEOUT_MS) }) {
45
+ async function loadCacheProvider({ projectRoot, config, env, timeoutMs = timeoutFromEnv(PROVIDER_LOAD_TIMEOUT_ENV, PROVIDER_LOAD_TIMEOUT_MS) }) {
42
46
  const reference = typeof config?.provider === "string" ? config.provider.trim() : "";
43
47
  if (!config || reference === "") return { none: true };
48
+ const environment = env === void 0 ? void 0 : { ...env };
44
49
  const outcome = await callWithTimeout(() => importCacheProvider({
45
50
  projectRoot,
46
51
  config,
47
- reference
52
+ reference,
53
+ env: environment
48
54
  }), timeoutMs);
49
55
  if (outcome.timedOut) return {
50
56
  name: reference,
@@ -59,7 +65,7 @@ async function loadCacheProvider({ projectRoot, config, timeoutMs = timeoutFromE
59
65
  unavailable: "the module produced no provider"
60
66
  };
61
67
  }
62
- async function importCacheProvider({ projectRoot, config, reference }) {
68
+ async function importCacheProvider({ projectRoot, config, reference, env }) {
63
69
  const options = isRecord(config.options) ? config.options : {};
64
70
  const baseDir = typeof config.baseDir === "string" && config.baseDir !== "" ? config.baseDir : projectRoot;
65
71
  let namespace;
@@ -85,7 +91,8 @@ async function importCacheProvider({ projectRoot, config, reference }) {
85
91
  try {
86
92
  provider = await candidate.createCacheProvider({
87
93
  projectRoot,
88
- options
94
+ options,
95
+ ...env === void 0 ? {} : { env: { ...env } }
89
96
  });
90
97
  } catch (error) {
91
98
  return {
@@ -107,8 +114,11 @@ async function importCacheProvider({ projectRoot, config, reference }) {
107
114
  provider
108
115
  };
109
116
  }
110
- async function callWithTimeout(call, timeoutMs) {
117
+ async function callWithTimeout(call, timeoutMs, signal) {
118
+ signal?.throwIfAborted();
111
119
  const controller = new AbortController();
120
+ const abort = () => controller.abort(signal?.reason);
121
+ signal?.addEventListener("abort", abort, { once: true });
112
122
  let timer = null;
113
123
  const expired = new Promise((resolve) => {
114
124
  timer = setTimeout(() => {
@@ -117,9 +127,14 @@ async function callWithTimeout(call, timeoutMs) {
117
127
  }, timeoutMs);
118
128
  });
119
129
  try {
120
- const outcome = await Promise.race([Promise.resolve().then(() => call(controller.signal)).then((value) => ({ value }), (error) => ({ failed: reason(error) })), expired]);
130
+ const outcome = await Promise.race([Promise.resolve().then(() => {
131
+ signal?.throwIfAborted();
132
+ return call(controller.signal);
133
+ }).then((value) => ({ value }), (error) => ({ failed: reason(error) })), expired]);
134
+ signal?.throwIfAborted();
121
135
  return outcome === "timeout" ? { timedOut: true } : outcome;
122
136
  } finally {
137
+ signal?.removeEventListener("abort", abort);
123
138
  if (timer) clearTimeout(timer);
124
139
  }
125
140
  }
@@ -386,9 +401,10 @@ function buildResolveTimeoutMs(env) {
386
401
  function buildUploadTimeoutMs(env) {
387
402
  return timeoutFromEnv(BUILD_UPLOAD_TIMEOUT_ENV, BUILD_UPLOAD_TIMEOUT_MS, env);
388
403
  }
389
- async function providerTier(loadProvider, warn) {
404
+ async function providerTier(loadProvider, warn, signal) {
390
405
  if (!loadProvider) return null;
391
406
  const loaded = await loadProvider();
407
+ signal?.throwIfAborted();
392
408
  if (loaded?.unavailable) {
393
409
  warn("provider-load", `provider not usable (${loaded.name ?? "the cache provider"}): ${loaded.unavailable}; using local cache`);
394
410
  return null;
@@ -404,26 +420,29 @@ function neverAborted$1() {
404
420
  return new AbortController().signal;
405
421
  }
406
422
  function ignore() {}
407
- async function resolveTieredBuild({ local, loadProvider = null, target, destinationDir, ensureDestination, skipRead = false, warn = ignore, timeoutMs = buildResolveTimeoutMs() }) {
423
+ async function resolveTieredBuild({ local, loadProvider = null, target, destinationDir, ensureDestination, skipRead = false, warn = ignore, timeoutMs = buildResolveTimeoutMs(), signal = neverAborted$1() }) {
424
+ signal.throwIfAborted();
408
425
  if (skipRead) return null;
409
426
  const hit = await local.resolve({
410
427
  ...target,
411
428
  destinationDir,
412
- signal: neverAborted$1()
429
+ signal
413
430
  });
431
+ signal.throwIfAborted();
414
432
  if (hit) return {
415
433
  path: hit,
416
434
  tier: "local"
417
435
  };
418
- const tier = await providerTier(loadProvider, warn);
436
+ const tier = await providerTier(loadProvider, warn, signal);
437
+ signal.throwIfAborted();
419
438
  if (!tier) return null;
420
439
  const { capability: provider, name: label } = tier;
421
440
  ensureDestination?.(destinationDir);
422
- const outcome = await callWithTimeout((signal) => provider.resolve({
441
+ const outcome = await callWithTimeout((childSignal) => provider.resolve({
423
442
  ...target,
424
443
  destinationDir,
425
- signal
426
- }), timeoutMs);
444
+ signal: childSignal
445
+ }), timeoutMs, signal);
427
446
  if (outcome.timedOut) {
428
447
  warn("provider-resolve", `${label} did not answer within ${timeoutMs}ms; building instead`);
429
448
  return null;
@@ -444,11 +463,13 @@ async function resolveTieredBuild({ local, loadProvider = null, target, destinat
444
463
  ...target,
445
464
  sourcePath: path,
446
465
  overwrite: false,
447
- signal: neverAborted$1()
466
+ signal
448
467
  }) || null;
449
468
  } catch (error) {
469
+ signal.throwIfAborted();
450
470
  warn("provider-backfill", `a ${label} hit could not be stored locally: ${String(error?.message || error)}`);
451
471
  }
472
+ signal.throwIfAborted();
452
473
  return {
453
474
  path: stored || path,
454
475
  tier: "provider",
@@ -456,29 +477,34 @@ async function resolveTieredBuild({ local, loadProvider = null, target, destinat
456
477
  storedLocally: Boolean(stored)
457
478
  };
458
479
  }
459
- async function storeTieredBuild({ local, loadProvider = null, target, sourcePath, overwrite, warn = ignore, timeoutMs = buildUploadTimeoutMs() }) {
480
+ async function storeTieredBuild({ local, loadProvider = null, target, sourcePath, overwrite, warn = ignore, timeoutMs = buildUploadTimeoutMs(), signal = neverAborted$1() }) {
481
+ signal.throwIfAborted();
460
482
  const localPath = await local.store({
461
483
  ...target,
462
484
  sourcePath,
463
485
  overwrite,
464
- signal: neverAborted$1()
486
+ signal
465
487
  }) || null;
466
- const tier = await providerTier(loadProvider, warn);
488
+ signal.throwIfAborted();
489
+ const tier = await providerTier(loadProvider, warn, signal);
490
+ signal.throwIfAborted();
467
491
  if (!tier) return {
468
492
  localPath,
469
493
  providerUpload: null,
470
494
  providerName: null
471
495
  };
496
+ const providerUpload = callWithTimeout(async (childSignal) => {
497
+ await tier.capability.store({
498
+ ...target,
499
+ sourcePath,
500
+ overwrite,
501
+ signal: childSignal
502
+ });
503
+ }, timeoutMs, signal);
504
+ providerUpload.catch(() => {});
472
505
  return {
473
506
  localPath,
474
- providerUpload: callWithTimeout(async (signal) => {
475
- await tier.capability.store({
476
- ...target,
477
- sourcePath,
478
- overwrite,
479
- signal
480
- });
481
- }, timeoutMs),
507
+ providerUpload,
482
508
  providerName: tier.name
483
509
  };
484
510
  }
@@ -886,4 +912,74 @@ async function withDeadline(work, timeoutMs) {
886
912
  }
887
913
  }
888
914
  //#endregion
889
- export { BUILD_RESOLVE_TIMEOUT_ENV, BUILD_RESOLVE_TIMEOUT_MS, BUILD_UPLOAD_TIMEOUT_ENV, BUILD_UPLOAD_TIMEOUT_MS, CACHE_CONTRACT_CHECK_TIMEOUT_MS, CACHE_PROVIDER_API_VERSION, CACHE_PROVIDER_ENV, CACHE_PROVIDER_ENV_NONE, METRO_READ_CONCURRENCY, METRO_READ_FAILURE_LIMIT, METRO_READ_TIMEOUT_ENV, METRO_READ_TIMEOUT_MS, METRO_UPLOAD_CONCURRENCY, METRO_UPLOAD_MAX_BYTES, METRO_UPLOAD_MAX_ITEMS, METRO_WRITE_TIMEOUT_ENV, METRO_WRITE_TIMEOUT_MS, PROVIDER_LOAD_TIMEOUT_ENV, PROVIDER_LOAD_TIMEOUT_MS, buildResolveTimeoutMs, buildUploadTimeoutMs, cacheProviderConfigFromEnv, cacheProviderContractChecks, cacheProviderEnv, cacheProviderEnvIsSet, callWithTimeout, createTieredMetroStore, createWarnOnce, loadCacheProvider, metroCapabilityFromStore, resolveTieredBuild, runCacheProviderContract, storeTieredBuild, timeoutFromEnv };
915
+ //#region files.ts
916
+ /** Opaque immutable files in a caller-owned directory, with atomic publication and cancellable downloads. */
917
+ function createFilesystemFileCache(directory) {
918
+ const entry = (key) => join(directory, createHash("sha256").update(key).digest("hex"));
919
+ const present = async ({ key, signal }) => {
920
+ signal.throwIfAborted();
921
+ try {
922
+ const info = await lstat(entry(key));
923
+ signal.throwIfAborted();
924
+ return info.isFile() ? { size: info.size } : null;
925
+ } catch (error) {
926
+ if ((error instanceof Error && "code" in error ? error.code : void 0) === "ENOENT") return null;
927
+ throw error;
928
+ }
929
+ };
930
+ return {
931
+ stat: present,
932
+ async resolve({ key, destinationDir, signal }) {
933
+ if (!await present({
934
+ key,
935
+ signal
936
+ })) return null;
937
+ const temporary = join(destinationDir, `${randomUUID()}.partial`);
938
+ const complete = join(destinationDir, `${randomUUID()}.cache`);
939
+ try {
940
+ await mkdir(destinationDir, {
941
+ recursive: true,
942
+ mode: 448
943
+ });
944
+ await pipeline(createReadStream(entry(key)), createWriteStream(temporary, {
945
+ flags: "wx",
946
+ mode: 384
947
+ }), { signal });
948
+ signal.throwIfAborted();
949
+ await rename(temporary, complete);
950
+ return complete;
951
+ } catch (error) {
952
+ signal.throwIfAborted();
953
+ if ((error instanceof Error && "code" in error ? error.code : void 0) === "ENOENT") return null;
954
+ throw error;
955
+ } finally {
956
+ await rm(temporary, { force: true });
957
+ }
958
+ },
959
+ async store({ key, sourcePath, overwrite, signal }) {
960
+ signal.throwIfAborted();
961
+ await mkdir(directory, {
962
+ recursive: true,
963
+ mode: 448
964
+ });
965
+ const temporary = join(directory, `${randomUUID()}.partial`);
966
+ try {
967
+ await pipeline(createReadStream(sourcePath), createWriteStream(temporary, {
968
+ flags: "wx",
969
+ mode: 384
970
+ }), { signal });
971
+ signal.throwIfAborted();
972
+ if (overwrite) await rename(temporary, entry(key));
973
+ else try {
974
+ await link(temporary, entry(key));
975
+ } catch (error) {
976
+ if ((error instanceof Error && "code" in error ? error.code : void 0) !== "EEXIST") throw error;
977
+ }
978
+ } finally {
979
+ await rm(temporary, { force: true });
980
+ }
981
+ }
982
+ };
983
+ }
984
+ //#endregion
985
+ export { BUILD_RESOLVE_TIMEOUT_ENV, BUILD_RESOLVE_TIMEOUT_MS, BUILD_UPLOAD_TIMEOUT_ENV, BUILD_UPLOAD_TIMEOUT_MS, CACHE_CONTRACT_CHECK_TIMEOUT_MS, CACHE_PROVIDER_API_VERSION, CACHE_PROVIDER_ENV, CACHE_PROVIDER_ENV_NONE, METRO_READ_CONCURRENCY, METRO_READ_FAILURE_LIMIT, METRO_READ_TIMEOUT_ENV, METRO_READ_TIMEOUT_MS, METRO_UPLOAD_CONCURRENCY, METRO_UPLOAD_MAX_BYTES, METRO_UPLOAD_MAX_ITEMS, METRO_WRITE_TIMEOUT_ENV, METRO_WRITE_TIMEOUT_MS, PROVIDER_LOAD_TIMEOUT_ENV, PROVIDER_LOAD_TIMEOUT_MS, buildResolveTimeoutMs, buildUploadTimeoutMs, cacheProviderConfigFromEnv, cacheProviderContractChecks, cacheProviderEnv, cacheProviderEnvIsSet, callWithTimeout, createFilesystemFileCache, createTieredMetroStore, createWarnOnce, loadCacheProvider, metroCapabilityFromStore, resolveTieredBuild, runCacheProviderContract, storeTieredBuild, timeoutFromEnv };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stim-cli/cache",
3
- "version": "1.20.2",
3
+ "version": "1.22.0",
4
4
  "description": "Cache provider contract and tier coordination for Stim.",
5
5
  "license": "MIT",
6
6
  "repository": {