@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 +30 -5
- package/dist/index.d.mts +34 -5
- package/dist/index.mjs +125 -29
- package/package.json +1 -1
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.
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
116
|
-
|
|
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
|
-
|
|
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
|
|
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(() =>
|
|
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
|
|
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((
|
|
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
|
|
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
|
|
486
|
+
signal
|
|
465
487
|
}) || null;
|
|
466
|
-
|
|
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
|
|
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
|
-
|
|
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 };
|