@stim-cli/cache 1.0.0-rc.5
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/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/index.d.mts +252 -0
- package/dist/index.mjs +889 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Janic Duplessis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# @stim-cli/cache
|
|
2
|
+
|
|
3
|
+
The cache provider contract Stim uses for Metro transforms and native build
|
|
4
|
+
artifacts, plus the tier coordination, timeout, and warning policy that sit in
|
|
5
|
+
front of it.
|
|
6
|
+
|
|
7
|
+
The local filesystem is always the first tier. A project can add one optional
|
|
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.
|
|
11
|
+
|
|
12
|
+
If Stim is not installed globally, replace `stim` with `npx stim-cli`.
|
|
13
|
+
|
|
14
|
+
## Selecting a provider
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"cache": {
|
|
19
|
+
"provider": "./tools/cache-provider.cjs",
|
|
20
|
+
"options": { "bucket": "mobile-cache" }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The reference is a package name or a path relative to the settings file that
|
|
26
|
+
declares it. Machine settings override committed `.stim.json` settings, and the
|
|
27
|
+
existing nested merge rules apply to `cache.options`. Keep secrets out of
|
|
28
|
+
committed settings; read them from the environment or from machine settings.
|
|
29
|
+
|
|
30
|
+
## Writing a provider
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
export const apiVersion = 1;
|
|
34
|
+
|
|
35
|
+
export async function createCacheProvider({ projectRoot, options }) {
|
|
36
|
+
return {
|
|
37
|
+
metro: {
|
|
38
|
+
async get({ key, cacheName, signal }) {
|
|
39
|
+
return null;
|
|
40
|
+
},
|
|
41
|
+
async set({ key, value, cacheName, signal }) {},
|
|
42
|
+
},
|
|
43
|
+
builds: {
|
|
44
|
+
async resolve({ platform, key, destinationDir, signal }) {
|
|
45
|
+
return null;
|
|
46
|
+
},
|
|
47
|
+
async store({ platform, key, sourcePath, overwrite, signal }) {},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A provider implements one or both capabilities. It owns transport,
|
|
54
|
+
serialization, archive format, authentication, and remote retention. Stim owns
|
|
55
|
+
fingerprints, cache keys, and local artifact paths.
|
|
56
|
+
|
|
57
|
+
`metro.get` returns the stored value or `null`.
|
|
58
|
+
|
|
59
|
+
`builds.resolve` returns an existing path to the artifact, or `null` for a
|
|
60
|
+
miss. `destinationDir` is a scratch directory Stim creates and owns: a provider
|
|
61
|
+
that fetches the artifact must materialize it there and return a path inside
|
|
62
|
+
it, and must leave the directory empty on a miss. The built-in filesystem tier
|
|
63
|
+
already holds the artifact, so it returns its own cache path instead.
|
|
64
|
+
|
|
65
|
+
`builds.store` receives the `.app` directory or `.apk` file that Stim just
|
|
66
|
+
built. `overwrite: false` must keep an entry that already exists for the key,
|
|
67
|
+
and `overwrite: true` must replace it.
|
|
68
|
+
|
|
69
|
+
Every call receives an `AbortSignal`. A provider must honor it: Stim abandons
|
|
70
|
+
the call at the deadline and keeps building or bundling with the local tier.
|
|
71
|
+
|
|
72
|
+
`stim gc` never deletes provider data, and the contract has no delete
|
|
73
|
+
operation, so shared team or CI data is never removed by a local command.
|
|
74
|
+
|
|
75
|
+
## Failure rules
|
|
76
|
+
|
|
77
|
+
Provider failures are cache misses. A timeout, module error, authentication
|
|
78
|
+
error, or network error produces one warning per failure class per command or
|
|
79
|
+
supervisor run and never fails a bundle, an install, a launch, or a successful
|
|
80
|
+
build.
|
|
81
|
+
|
|
82
|
+
## Contract tests
|
|
83
|
+
|
|
84
|
+
Run the shipped checks against your own module:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
import { runCacheProviderContract } from '@stim-cli/cache';
|
|
88
|
+
|
|
89
|
+
const results = await runCacheProviderContract({
|
|
90
|
+
provider: await createCacheProvider({ projectRoot, options }),
|
|
91
|
+
projectRoot,
|
|
92
|
+
workDir,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
for (const result of results) {
|
|
96
|
+
if (!result.passed) throw new Error(`${result.name}: ${result.error}`);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Pass `providerModule` instead of `provider` to load the module the way Stim
|
|
101
|
+
does, which also checks `apiVersion` and the factory:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
const results = await runCacheProviderContract({
|
|
105
|
+
providerModule: './tools/cache-provider.cjs',
|
|
106
|
+
projectRoot,
|
|
107
|
+
workDir,
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`cacheProviderContractChecks()` returns the same checks as individual cases for
|
|
112
|
+
a test runner that reports each one separately. Both helpers only check the
|
|
113
|
+
capabilities a provider advertises, bound every check with a deadline, and
|
|
114
|
+
verify that a call settles once its `AbortSignal` aborts.
|
|
115
|
+
|
|
116
|
+
## Budgets
|
|
117
|
+
|
|
118
|
+
Every provider call is bounded. The defaults are 2s for a Metro read, 10s for a
|
|
119
|
+
Metro write, 30s for a build lookup, 60s for a build upload, and 10s to load the
|
|
120
|
+
module. Override any of them per run with an environment variable:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
STIM_CACHE_METRO_READ_TIMEOUT_MS=5000 stim start
|
|
124
|
+
STIM_CACHE_BUILD_RESOLVE_TIMEOUT_MS=60000 stim ios
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The variables are `STIM_CACHE_METRO_READ_TIMEOUT_MS`,
|
|
128
|
+
`STIM_CACHE_METRO_WRITE_TIMEOUT_MS`, `STIM_CACHE_BUILD_RESOLVE_TIMEOUT_MS`,
|
|
129
|
+
`STIM_CACHE_BUILD_UPLOAD_TIMEOUT_MS`, and `STIM_CACHE_LOAD_TIMEOUT_MS`. Each
|
|
130
|
+
takes whole milliseconds; any other value keeps the default.
|
|
131
|
+
|
|
132
|
+
The tiered Metro store also exposes `flush()`, which resolves once queued
|
|
133
|
+
provider writes have drained. Metro never calls it; every in-flight write holds
|
|
134
|
+
a referenced deadline, so a Metro process drains on its own within the write
|
|
135
|
+
budget. It exists for tests and embedders that own the process.
|
|
136
|
+
|
|
137
|
+
Metro reads are also capped: at most six can be in flight, and the tier turns
|
|
138
|
+
itself off for the rest of the run after five consecutive failures, so a broken
|
|
139
|
+
provider costs one round of warnings rather than a timeout per transform.
|
|
140
|
+
|
|
141
|
+
The npm scope remains `@stim-cli` until the `@stim` scope is available.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
//#region provider.d.ts
|
|
2
|
+
declare const CACHE_PROVIDER_API_VERSION: 1;
|
|
3
|
+
declare const CACHE_PROVIDER_ENV = "STIM_CACHE_PROVIDER_CONFIG";
|
|
4
|
+
declare const CACHE_PROVIDER_ENV_NONE = "none";
|
|
5
|
+
declare const PROVIDER_LOAD_TIMEOUT_MS = 1e4;
|
|
6
|
+
declare const PROVIDER_LOAD_TIMEOUT_ENV = "STIM_CACHE_LOAD_TIMEOUT_MS";
|
|
7
|
+
/**
|
|
8
|
+
* Reads a positive integer millisecond override from the environment. Any other
|
|
9
|
+
* value falls back to the shipped default.
|
|
10
|
+
*/
|
|
11
|
+
declare function timeoutFromEnv(name: string, fallback: number, env?: NodeJS.ProcessEnv): number;
|
|
12
|
+
/**
|
|
13
|
+
* One resolved provider selection: the module reference, the options passed to
|
|
14
|
+
* its factory, and the directory the reference resolves from.
|
|
15
|
+
*/
|
|
16
|
+
interface CacheProviderConfig {
|
|
17
|
+
provider: string;
|
|
18
|
+
options: Record<string, unknown>;
|
|
19
|
+
baseDir: string;
|
|
20
|
+
}
|
|
21
|
+
interface MetroCacheContext {
|
|
22
|
+
projectRoot: string;
|
|
23
|
+
cacheName: string;
|
|
24
|
+
signal: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
interface MetroCacheGetInput extends MetroCacheContext {
|
|
27
|
+
key: Buffer;
|
|
28
|
+
}
|
|
29
|
+
interface MetroCacheSetInput extends MetroCacheContext {
|
|
30
|
+
key: Buffer;
|
|
31
|
+
value: unknown;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Metro transform cache. `get` returns `null` or `undefined` for a miss. `set`
|
|
35
|
+
* stores the value under the key. Both receive an `AbortSignal` the provider
|
|
36
|
+
* must honor.
|
|
37
|
+
*/
|
|
38
|
+
interface MetroCacheCapability {
|
|
39
|
+
get(input: MetroCacheGetInput): unknown;
|
|
40
|
+
set(input: MetroCacheSetInput): void | Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
interface BuildCacheTarget {
|
|
43
|
+
projectRoot: string;
|
|
44
|
+
platform: "ios" | "android";
|
|
45
|
+
key: string;
|
|
46
|
+
}
|
|
47
|
+
interface BuildCacheContext extends BuildCacheTarget {
|
|
48
|
+
signal: AbortSignal;
|
|
49
|
+
}
|
|
50
|
+
interface BuildResolveInput extends BuildCacheContext {
|
|
51
|
+
destinationDir: string;
|
|
52
|
+
}
|
|
53
|
+
interface BuildStoreInput extends BuildCacheContext {
|
|
54
|
+
sourcePath: string;
|
|
55
|
+
overwrite: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Native build artifacts.
|
|
59
|
+
*
|
|
60
|
+
* `resolve` returns an existing path to the artifact for the key, or `null` for
|
|
61
|
+
* a miss. `destinationDir` is a scratch directory Stim creates and owns: a
|
|
62
|
+
* capability that fetches the artifact must materialize it there and return a
|
|
63
|
+
* path inside it, and a capability that already holds a local copy (the
|
|
64
|
+
* built-in filesystem tier) returns that copy instead. A miss must leave
|
|
65
|
+
* `destinationDir` empty.
|
|
66
|
+
*
|
|
67
|
+
* `store` publishes the `.app` directory or `.apk` file at `sourcePath`.
|
|
68
|
+
* `overwrite: false` must keep an entry that already exists for the key;
|
|
69
|
+
* `overwrite: true` must replace it. A capability that owns a local path
|
|
70
|
+
* returns it, and any other capability returns nothing.
|
|
71
|
+
*
|
|
72
|
+
* Stim owns fingerprints and cache keys; the capability owns transport,
|
|
73
|
+
* archive format, authentication, and retention.
|
|
74
|
+
*/
|
|
75
|
+
interface BuildCacheCapability {
|
|
76
|
+
resolve(input: BuildResolveInput): string | null | Promise<string | null>;
|
|
77
|
+
store(input: BuildStoreInput): string | null | void | Promise<string | null | void>;
|
|
78
|
+
}
|
|
79
|
+
interface CacheProvider {
|
|
80
|
+
metro?: MetroCacheCapability;
|
|
81
|
+
builds?: BuildCacheCapability;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The module shape a provider author exports. The loader rejects any other
|
|
85
|
+
* `apiVersion`.
|
|
86
|
+
*/
|
|
87
|
+
interface CacheProviderModule {
|
|
88
|
+
apiVersion: typeof CACHE_PROVIDER_API_VERSION;
|
|
89
|
+
createCacheProvider(input: {
|
|
90
|
+
projectRoot: string;
|
|
91
|
+
options: Record<string, unknown>;
|
|
92
|
+
}): CacheProvider | Promise<CacheProvider>;
|
|
93
|
+
}
|
|
94
|
+
interface LoadCacheProviderResult {
|
|
95
|
+
provider?: CacheProvider;
|
|
96
|
+
name?: string;
|
|
97
|
+
none?: true;
|
|
98
|
+
unavailable?: string;
|
|
99
|
+
}
|
|
100
|
+
interface ProviderCallResult<T> {
|
|
101
|
+
value?: T;
|
|
102
|
+
failed?: string;
|
|
103
|
+
timedOut?: true;
|
|
104
|
+
}
|
|
105
|
+
type WarnOnce = (code: string, message: string) => void;
|
|
106
|
+
declare function loadCacheProvider({ projectRoot, config, timeoutMs }: {
|
|
107
|
+
projectRoot: string;
|
|
108
|
+
config?: CacheProviderConfig | null;
|
|
109
|
+
timeoutMs?: number;
|
|
110
|
+
}): Promise<LoadCacheProviderResult>;
|
|
111
|
+
declare function callWithTimeout<T>(call: (signal: AbortSignal) => T | Promise<T>, timeoutMs: number): Promise<ProviderCallResult<T>>;
|
|
112
|
+
declare function createWarnOnce(emit: (message: string) => void): WarnOnce;
|
|
113
|
+
declare function cacheProviderEnv(config: CacheProviderConfig | null): string;
|
|
114
|
+
/**
|
|
115
|
+
* True when a parent process decided the provider selection, including the
|
|
116
|
+
* `none` sentinel. A child must not run its own search once this is set.
|
|
117
|
+
*/
|
|
118
|
+
declare function cacheProviderEnvIsSet(env?: NodeJS.ProcessEnv): boolean;
|
|
119
|
+
declare function cacheProviderConfigFromEnv(env?: NodeJS.ProcessEnv): CacheProviderConfig | null;
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region metro.d.ts
|
|
122
|
+
declare const METRO_READ_TIMEOUT_MS = 2e3;
|
|
123
|
+
declare const METRO_WRITE_TIMEOUT_MS = 1e4;
|
|
124
|
+
declare const METRO_READ_CONCURRENCY = 6;
|
|
125
|
+
declare const METRO_READ_FAILURE_LIMIT = 5;
|
|
126
|
+
declare const METRO_UPLOAD_CONCURRENCY = 4;
|
|
127
|
+
declare const METRO_UPLOAD_MAX_ITEMS = 128;
|
|
128
|
+
declare const METRO_UPLOAD_MAX_BYTES: number;
|
|
129
|
+
declare const METRO_READ_TIMEOUT_ENV = "STIM_CACHE_METRO_READ_TIMEOUT_MS";
|
|
130
|
+
declare const METRO_WRITE_TIMEOUT_ENV = "STIM_CACHE_METRO_WRITE_TIMEOUT_MS";
|
|
131
|
+
/**
|
|
132
|
+
* Metro's structural cache-store contract: `get` returns the value or `null`,
|
|
133
|
+
* `set` stores it, and `clear` empties the store.
|
|
134
|
+
*/
|
|
135
|
+
interface MetroCacheStore {
|
|
136
|
+
get(key: Buffer): unknown;
|
|
137
|
+
set(key: Buffer, value: unknown): unknown;
|
|
138
|
+
clear(): unknown;
|
|
139
|
+
}
|
|
140
|
+
interface TieredMetroStore extends MetroCacheStore {
|
|
141
|
+
get(key: Buffer): Promise<unknown>;
|
|
142
|
+
set(key: Buffer, value: unknown): Promise<void>;
|
|
143
|
+
clear(): unknown;
|
|
144
|
+
/**
|
|
145
|
+
* Resolves once queued provider writes have drained. Metro never calls it:
|
|
146
|
+
* every in-flight write already holds a referenced deadline, so the process
|
|
147
|
+
* drains on its own. Tests and embedders that own the process use it to wait
|
|
148
|
+
* without polling.
|
|
149
|
+
*/
|
|
150
|
+
flush(): Promise<void>;
|
|
151
|
+
}
|
|
152
|
+
interface MetroTierLimits {
|
|
153
|
+
concurrency?: number;
|
|
154
|
+
maxItems?: number;
|
|
155
|
+
maxBytes?: number;
|
|
156
|
+
readConcurrency?: number;
|
|
157
|
+
failureLimit?: number;
|
|
158
|
+
}
|
|
159
|
+
interface MetroTierTimeouts {
|
|
160
|
+
readMs?: number;
|
|
161
|
+
writeMs?: number;
|
|
162
|
+
}
|
|
163
|
+
interface TieredMetroStoreOptions {
|
|
164
|
+
local: MetroCacheStore;
|
|
165
|
+
projectRoot: string;
|
|
166
|
+
cacheName: string;
|
|
167
|
+
loadProvider: () => Promise<LoadCacheProviderResult>;
|
|
168
|
+
warn?: WarnOnce;
|
|
169
|
+
limits?: MetroTierLimits;
|
|
170
|
+
timeouts?: MetroTierTimeouts;
|
|
171
|
+
}
|
|
172
|
+
declare function metroCapabilityFromStore(store: MetroCacheStore): MetroCacheCapability;
|
|
173
|
+
declare function createTieredMetroStore({ local, projectRoot, cacheName, loadProvider, warn: emit, limits, timeouts }: TieredMetroStoreOptions): TieredMetroStore;
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region builds.d.ts
|
|
176
|
+
declare const BUILD_RESOLVE_TIMEOUT_MS = 3e4;
|
|
177
|
+
declare const BUILD_UPLOAD_TIMEOUT_MS = 6e4;
|
|
178
|
+
declare const BUILD_RESOLVE_TIMEOUT_ENV = "STIM_CACHE_BUILD_RESOLVE_TIMEOUT_MS";
|
|
179
|
+
declare const BUILD_UPLOAD_TIMEOUT_ENV = "STIM_CACHE_BUILD_UPLOAD_TIMEOUT_MS";
|
|
180
|
+
declare function buildResolveTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
181
|
+
declare function buildUploadTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
182
|
+
interface TieredBuildResolution {
|
|
183
|
+
path: string;
|
|
184
|
+
tier: "local" | "provider";
|
|
185
|
+
providerName?: string;
|
|
186
|
+
storedLocally?: boolean;
|
|
187
|
+
}
|
|
188
|
+
type LoadBuildProvider = () => Promise<LoadCacheProviderResult> | LoadCacheProviderResult;
|
|
189
|
+
interface ResolveTieredBuildOptions {
|
|
190
|
+
local: BuildCacheCapability;
|
|
191
|
+
loadProvider?: LoadBuildProvider | null;
|
|
192
|
+
target: BuildCacheTarget;
|
|
193
|
+
destinationDir: string;
|
|
194
|
+
/** Called right before a provider is asked, so a local hit creates nothing on disk. */
|
|
195
|
+
ensureDestination?: (destinationDir: string) => void;
|
|
196
|
+
skipRead?: boolean;
|
|
197
|
+
warn?: WarnOnce;
|
|
198
|
+
timeoutMs?: number;
|
|
199
|
+
}
|
|
200
|
+
interface StoreTieredBuildOptions {
|
|
201
|
+
local: BuildCacheCapability;
|
|
202
|
+
loadProvider?: LoadBuildProvider | null;
|
|
203
|
+
target: BuildCacheTarget;
|
|
204
|
+
sourcePath: string;
|
|
205
|
+
overwrite: boolean;
|
|
206
|
+
warn?: WarnOnce;
|
|
207
|
+
timeoutMs?: number;
|
|
208
|
+
}
|
|
209
|
+
interface TieredBuildStoreResult {
|
|
210
|
+
localPath: string | null;
|
|
211
|
+
providerUpload: Promise<ProviderCallResult<void>> | null;
|
|
212
|
+
providerName: string | null;
|
|
213
|
+
}
|
|
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>;
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region contract.d.ts
|
|
218
|
+
interface CacheContractCheck {
|
|
219
|
+
name: string;
|
|
220
|
+
capability: "metro" | "builds";
|
|
221
|
+
run(): Promise<void>;
|
|
222
|
+
}
|
|
223
|
+
interface CacheContractResult {
|
|
224
|
+
name: string;
|
|
225
|
+
capability: "metro" | "builds" | "module";
|
|
226
|
+
passed: boolean;
|
|
227
|
+
error?: string;
|
|
228
|
+
}
|
|
229
|
+
interface CacheContractOptions {
|
|
230
|
+
provider: CacheProvider;
|
|
231
|
+
projectRoot: string;
|
|
232
|
+
workDir: string;
|
|
233
|
+
cacheName?: string;
|
|
234
|
+
platform?: "ios" | "android";
|
|
235
|
+
checkTimeoutMs?: number;
|
|
236
|
+
abortSettleMs?: number;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Loads the provider the way Stim does, from a module reference, so a provider
|
|
240
|
+
* author exercises `apiVersion` and factory validation as well as the
|
|
241
|
+
* capability checks.
|
|
242
|
+
*/
|
|
243
|
+
interface CacheContractModuleOptions extends Omit<CacheContractOptions, "provider"> {
|
|
244
|
+
providerModule: string;
|
|
245
|
+
options?: Record<string, unknown>;
|
|
246
|
+
baseDir?: string;
|
|
247
|
+
}
|
|
248
|
+
declare const CACHE_CONTRACT_CHECK_TIMEOUT_MS = 3e4;
|
|
249
|
+
declare function cacheProviderContractChecks(options: CacheContractOptions): CacheContractCheck[];
|
|
250
|
+
declare function runCacheProviderContract(options: CacheContractOptions | CacheContractModuleOptions): Promise<CacheContractResult[]>;
|
|
251
|
+
//#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 };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
6
|
+
//#region provider.ts
|
|
7
|
+
const CACHE_PROVIDER_API_VERSION = 1;
|
|
8
|
+
const CACHE_PROVIDER_ENV = "STIM_CACHE_PROVIDER_CONFIG";
|
|
9
|
+
const CACHE_PROVIDER_ENV_NONE = "none";
|
|
10
|
+
const PROVIDER_LOAD_TIMEOUT_MS = 1e4;
|
|
11
|
+
const PROVIDER_LOAD_TIMEOUT_ENV = "STIM_CACHE_LOAD_TIMEOUT_MS";
|
|
12
|
+
/**
|
|
13
|
+
* Reads a positive integer millisecond override from the environment. Any other
|
|
14
|
+
* value falls back to the shipped default.
|
|
15
|
+
*/
|
|
16
|
+
function timeoutFromEnv(name, fallback, env = process.env) {
|
|
17
|
+
const raw = env[name];
|
|
18
|
+
if (typeof raw !== "string" || !/^[1-9]\d*$/.test(raw.trim())) return fallback;
|
|
19
|
+
const parsed = Number(raw.trim());
|
|
20
|
+
return Number.isSafeInteger(parsed) ? parsed : fallback;
|
|
21
|
+
}
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function reason(error) {
|
|
26
|
+
return String(error?.message || error || "unknown error").split("\n")[0].trim() || "unknown error";
|
|
27
|
+
}
|
|
28
|
+
function moduleCandidate(namespace) {
|
|
29
|
+
if (!isRecord(namespace)) return namespace;
|
|
30
|
+
if ("apiVersion" in namespace) return namespace;
|
|
31
|
+
return "default" in namespace ? namespace.default : namespace;
|
|
32
|
+
}
|
|
33
|
+
function capabilityError(provider) {
|
|
34
|
+
const metro = provider.metro;
|
|
35
|
+
if (metro !== void 0 && (typeof metro?.get !== "function" || typeof metro?.set !== "function")) return "the metro capability must implement get() and set()";
|
|
36
|
+
const builds = provider.builds;
|
|
37
|
+
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";
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
async function loadCacheProvider({ projectRoot, config, timeoutMs = timeoutFromEnv(PROVIDER_LOAD_TIMEOUT_ENV, PROVIDER_LOAD_TIMEOUT_MS) }) {
|
|
42
|
+
const reference = typeof config?.provider === "string" ? config.provider.trim() : "";
|
|
43
|
+
if (!config || reference === "") return { none: true };
|
|
44
|
+
const outcome = await callWithTimeout(() => importCacheProvider({
|
|
45
|
+
projectRoot,
|
|
46
|
+
config,
|
|
47
|
+
reference
|
|
48
|
+
}), timeoutMs);
|
|
49
|
+
if (outcome.timedOut) return {
|
|
50
|
+
name: reference,
|
|
51
|
+
unavailable: `the module did not load within ${timeoutMs}ms`
|
|
52
|
+
};
|
|
53
|
+
if (outcome.failed) return {
|
|
54
|
+
name: reference,
|
|
55
|
+
unavailable: outcome.failed
|
|
56
|
+
};
|
|
57
|
+
return outcome.value ?? {
|
|
58
|
+
name: reference,
|
|
59
|
+
unavailable: "the module produced no provider"
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function importCacheProvider({ projectRoot, config, reference }) {
|
|
63
|
+
const options = isRecord(config.options) ? config.options : {};
|
|
64
|
+
const baseDir = typeof config.baseDir === "string" && config.baseDir !== "" ? config.baseDir : projectRoot;
|
|
65
|
+
let namespace;
|
|
66
|
+
try {
|
|
67
|
+
const resolved = createRequire(join(baseDir, "package.json")).resolve(reference);
|
|
68
|
+
namespace = await import(pathToFileURL(resolved).href);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
name: reference,
|
|
72
|
+
unavailable: reason(error)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const candidate = moduleCandidate(namespace);
|
|
76
|
+
if (!isRecord(candidate) || candidate.apiVersion !== 1) return {
|
|
77
|
+
name: reference,
|
|
78
|
+
unavailable: `expected apiVersion 1, found ${JSON.stringify(isRecord(candidate) ? candidate.apiVersion : candidate)}`
|
|
79
|
+
};
|
|
80
|
+
if (typeof candidate.createCacheProvider !== "function") return {
|
|
81
|
+
name: reference,
|
|
82
|
+
unavailable: "the module does not export createCacheProvider()"
|
|
83
|
+
};
|
|
84
|
+
let provider;
|
|
85
|
+
try {
|
|
86
|
+
provider = await candidate.createCacheProvider({
|
|
87
|
+
projectRoot,
|
|
88
|
+
options
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return {
|
|
92
|
+
name: reference,
|
|
93
|
+
unavailable: `createCacheProvider() failed: ${reason(error)}`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (!isRecord(provider)) return {
|
|
97
|
+
name: reference,
|
|
98
|
+
unavailable: "createCacheProvider() did not return a provider object"
|
|
99
|
+
};
|
|
100
|
+
const invalid = capabilityError(provider);
|
|
101
|
+
if (invalid) return {
|
|
102
|
+
name: reference,
|
|
103
|
+
unavailable: invalid
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
name: reference,
|
|
107
|
+
provider
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function callWithTimeout(call, timeoutMs) {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
let timer = null;
|
|
113
|
+
const expired = new Promise((resolve) => {
|
|
114
|
+
timer = setTimeout(() => {
|
|
115
|
+
controller.abort();
|
|
116
|
+
resolve("timeout");
|
|
117
|
+
}, timeoutMs);
|
|
118
|
+
});
|
|
119
|
+
try {
|
|
120
|
+
const outcome = await Promise.race([Promise.resolve().then(() => call(controller.signal)).then((value) => ({ value }), (error) => ({ failed: reason(error) })), expired]);
|
|
121
|
+
return outcome === "timeout" ? { timedOut: true } : outcome;
|
|
122
|
+
} finally {
|
|
123
|
+
if (timer) clearTimeout(timer);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function createWarnOnce(emit) {
|
|
127
|
+
const seen = /* @__PURE__ */ new Set();
|
|
128
|
+
return (code, message) => {
|
|
129
|
+
if (seen.has(code)) return;
|
|
130
|
+
seen.add(code);
|
|
131
|
+
emit(message);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function cacheProviderEnv(config) {
|
|
135
|
+
if (!config) return CACHE_PROVIDER_ENV_NONE;
|
|
136
|
+
return JSON.stringify({
|
|
137
|
+
provider: config.provider,
|
|
138
|
+
options: config.options ?? {},
|
|
139
|
+
baseDir: config.baseDir
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* True when a parent process decided the provider selection, including the
|
|
144
|
+
* `none` sentinel. A child must not run its own search once this is set.
|
|
145
|
+
*/
|
|
146
|
+
function cacheProviderEnvIsSet(env = process.env) {
|
|
147
|
+
const raw = env[CACHE_PROVIDER_ENV];
|
|
148
|
+
return typeof raw === "string" && raw.trim() !== "";
|
|
149
|
+
}
|
|
150
|
+
function cacheProviderConfigFromEnv(env = process.env) {
|
|
151
|
+
const raw = env[CACHE_PROVIDER_ENV];
|
|
152
|
+
if (typeof raw !== "string" || raw.trim() === "" || raw.trim() === "none") return null;
|
|
153
|
+
let parsed;
|
|
154
|
+
try {
|
|
155
|
+
parsed = JSON.parse(raw);
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (!isRecord(parsed)) return null;
|
|
160
|
+
const provider = parsed.provider;
|
|
161
|
+
const baseDir = parsed.baseDir;
|
|
162
|
+
if (typeof provider !== "string" || provider.trim() === "" || typeof baseDir !== "string" || baseDir === "") return null;
|
|
163
|
+
return {
|
|
164
|
+
provider,
|
|
165
|
+
options: isRecord(parsed.options) ? parsed.options : {},
|
|
166
|
+
baseDir
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region metro.ts
|
|
171
|
+
const METRO_READ_TIMEOUT_MS = 2e3;
|
|
172
|
+
const METRO_WRITE_TIMEOUT_MS = 1e4;
|
|
173
|
+
const METRO_READ_CONCURRENCY = 6;
|
|
174
|
+
const METRO_READ_FAILURE_LIMIT = 5;
|
|
175
|
+
const METRO_UPLOAD_CONCURRENCY = 4;
|
|
176
|
+
const METRO_UPLOAD_MAX_ITEMS = 128;
|
|
177
|
+
const METRO_UPLOAD_MAX_BYTES = 33554432;
|
|
178
|
+
const METRO_READ_TIMEOUT_ENV = "STIM_CACHE_METRO_READ_TIMEOUT_MS";
|
|
179
|
+
const METRO_WRITE_TIMEOUT_ENV = "STIM_CACHE_METRO_WRITE_TIMEOUT_MS";
|
|
180
|
+
function metroCapabilityFromStore(store) {
|
|
181
|
+
return {
|
|
182
|
+
get: ({ key }) => store.get(key),
|
|
183
|
+
set: async ({ key, value }) => {
|
|
184
|
+
await store.set(key, value);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const defaultWarn = (_code, message) => {
|
|
189
|
+
process.stderr.write(`${message}\n`);
|
|
190
|
+
};
|
|
191
|
+
function onceByCode(warn) {
|
|
192
|
+
const seen = /* @__PURE__ */ new Set();
|
|
193
|
+
return (code, message) => {
|
|
194
|
+
if (seen.has(code)) return;
|
|
195
|
+
seen.add(code);
|
|
196
|
+
warn(code, message);
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function valueBytes(value) {
|
|
200
|
+
if (Buffer.isBuffer(value)) return value.length;
|
|
201
|
+
try {
|
|
202
|
+
const json = JSON.stringify(value);
|
|
203
|
+
return json === void 0 ? null : Buffer.byteLength(json);
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function createUploadQueue({ concurrency, maxItems, maxBytes }) {
|
|
209
|
+
const pending = [];
|
|
210
|
+
const waiters = [];
|
|
211
|
+
let active = 0;
|
|
212
|
+
let items = 0;
|
|
213
|
+
let bytes = 0;
|
|
214
|
+
function settle() {
|
|
215
|
+
if (active > 0 || pending.length > 0) return;
|
|
216
|
+
while (waiters.length) waiters.shift()?.();
|
|
217
|
+
}
|
|
218
|
+
function pump() {
|
|
219
|
+
while (active < concurrency && pending.length > 0) {
|
|
220
|
+
const task = pending.shift();
|
|
221
|
+
active += 1;
|
|
222
|
+
task.run().catch(() => {}).finally(() => {
|
|
223
|
+
active -= 1;
|
|
224
|
+
items -= 1;
|
|
225
|
+
bytes -= task.bytes;
|
|
226
|
+
pump();
|
|
227
|
+
settle();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
add(size, run) {
|
|
233
|
+
if (items + 1 > maxItems || bytes + size > maxBytes) return false;
|
|
234
|
+
items += 1;
|
|
235
|
+
bytes += size;
|
|
236
|
+
pending.push({
|
|
237
|
+
bytes: size,
|
|
238
|
+
run
|
|
239
|
+
});
|
|
240
|
+
pump();
|
|
241
|
+
return true;
|
|
242
|
+
},
|
|
243
|
+
idle() {
|
|
244
|
+
if (active === 0 && pending.length === 0) return Promise.resolve();
|
|
245
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function createTieredMetroStore({ local, projectRoot, cacheName, loadProvider, warn: emit = defaultWarn, limits = {}, timeouts = {} }) {
|
|
250
|
+
const warn = onceByCode(emit);
|
|
251
|
+
const localCapability = metroCapabilityFromStore(local);
|
|
252
|
+
const readMs = timeouts.readMs ?? timeoutFromEnv("STIM_CACHE_METRO_READ_TIMEOUT_MS", 2e3);
|
|
253
|
+
const writeMs = timeouts.writeMs ?? timeoutFromEnv("STIM_CACHE_METRO_WRITE_TIMEOUT_MS", 1e4);
|
|
254
|
+
const readConcurrency = limits.readConcurrency ?? 6;
|
|
255
|
+
const failureLimit = limits.failureLimit ?? 5;
|
|
256
|
+
const queue = createUploadQueue({
|
|
257
|
+
concurrency: limits.concurrency ?? 4,
|
|
258
|
+
maxItems: limits.maxItems ?? 128,
|
|
259
|
+
maxBytes: limits.maxBytes ?? 33554432
|
|
260
|
+
});
|
|
261
|
+
let loading = null;
|
|
262
|
+
let disabled = false;
|
|
263
|
+
let consecutiveFailures = 0;
|
|
264
|
+
let activeReads = 0;
|
|
265
|
+
function recordFailure() {
|
|
266
|
+
consecutiveFailures += 1;
|
|
267
|
+
if (consecutiveFailures < failureLimit) return;
|
|
268
|
+
disabled = true;
|
|
269
|
+
warn("provider-disabled", `the cache provider failed ${consecutiveFailures} times in a row; this run keeps its transforms local`);
|
|
270
|
+
}
|
|
271
|
+
function providerCapability() {
|
|
272
|
+
if (disabled) return Promise.resolve(null);
|
|
273
|
+
loading ??= loadProvider().then((loaded) => {
|
|
274
|
+
if (loaded?.unavailable) {
|
|
275
|
+
disabled = true;
|
|
276
|
+
warn("provider-load", `cache provider ${loaded.name} is not usable: ${loaded.unavailable}; using local transforms`);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
return loaded?.provider?.metro ?? null;
|
|
280
|
+
});
|
|
281
|
+
return loading;
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
async get(key) {
|
|
285
|
+
const hit = await localCapability.get({
|
|
286
|
+
key,
|
|
287
|
+
projectRoot,
|
|
288
|
+
cacheName,
|
|
289
|
+
signal: neverAborted$2()
|
|
290
|
+
});
|
|
291
|
+
if (hit !== null && hit !== void 0) return hit;
|
|
292
|
+
if (activeReads >= readConcurrency) {
|
|
293
|
+
warn("provider-read-busy", `the cache provider already has ${activeReads} reads in flight; further transforms read locally until it catches up`);
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
activeReads += 1;
|
|
297
|
+
let outcome;
|
|
298
|
+
try {
|
|
299
|
+
const capability = await providerCapability();
|
|
300
|
+
if (!capability) return null;
|
|
301
|
+
outcome = await callWithTimeout((signal) => capability.get({
|
|
302
|
+
key,
|
|
303
|
+
projectRoot,
|
|
304
|
+
cacheName,
|
|
305
|
+
signal
|
|
306
|
+
}), readMs);
|
|
307
|
+
} finally {
|
|
308
|
+
activeReads -= 1;
|
|
309
|
+
}
|
|
310
|
+
if (outcome.timedOut) {
|
|
311
|
+
recordFailure();
|
|
312
|
+
warn("provider-read", `the cache provider did not answer a transform read within ${readMs}ms; using local transforms`);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
if (outcome.failed) {
|
|
316
|
+
recordFailure();
|
|
317
|
+
warn("provider-read", `the cache provider could not read a transform: ${outcome.failed}; using local transforms`);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
consecutiveFailures = 0;
|
|
321
|
+
const value = outcome.value;
|
|
322
|
+
if (value === null || value === void 0) return null;
|
|
323
|
+
try {
|
|
324
|
+
await localCapability.set({
|
|
325
|
+
key,
|
|
326
|
+
value,
|
|
327
|
+
projectRoot,
|
|
328
|
+
cacheName,
|
|
329
|
+
signal: neverAborted$2()
|
|
330
|
+
});
|
|
331
|
+
} catch (error) {
|
|
332
|
+
warn("provider-backfill", `a provider transform could not be written locally: ${String(error?.message || error)}`);
|
|
333
|
+
}
|
|
334
|
+
return value;
|
|
335
|
+
},
|
|
336
|
+
async set(key, value) {
|
|
337
|
+
await localCapability.set({
|
|
338
|
+
key,
|
|
339
|
+
value,
|
|
340
|
+
projectRoot,
|
|
341
|
+
cacheName,
|
|
342
|
+
signal: neverAborted$2()
|
|
343
|
+
});
|
|
344
|
+
const capability = await providerCapability();
|
|
345
|
+
if (!capability) return;
|
|
346
|
+
const bytes = valueBytes(value);
|
|
347
|
+
if (bytes === null) {
|
|
348
|
+
warn("provider-write", "a transform could not be measured for the cache provider; it stays local");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (!queue.add(bytes, async () => {
|
|
352
|
+
if (disabled) return;
|
|
353
|
+
const outcome = await callWithTimeout((signal) => capability.set({
|
|
354
|
+
key,
|
|
355
|
+
value,
|
|
356
|
+
projectRoot,
|
|
357
|
+
cacheName,
|
|
358
|
+
signal
|
|
359
|
+
}), writeMs);
|
|
360
|
+
if (outcome.timedOut || outcome.failed) recordFailure();
|
|
361
|
+
else consecutiveFailures = 0;
|
|
362
|
+
if (outcome.timedOut) warn("provider-write", `the cache provider did not accept a transform within ${writeMs}ms; later transforms stay local until it answers`);
|
|
363
|
+
else if (outcome.failed) warn("provider-write", `the cache provider could not store a transform: ${outcome.failed}`);
|
|
364
|
+
})) warn("provider-queue", "the cache provider upload queue is full; those transforms stay local");
|
|
365
|
+
},
|
|
366
|
+
clear() {
|
|
367
|
+
return local.clear();
|
|
368
|
+
},
|
|
369
|
+
flush() {
|
|
370
|
+
return queue.idle();
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function neverAborted$2() {
|
|
375
|
+
return new AbortController().signal;
|
|
376
|
+
}
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region builds.ts
|
|
379
|
+
const BUILD_RESOLVE_TIMEOUT_MS = 3e4;
|
|
380
|
+
const BUILD_UPLOAD_TIMEOUT_MS = 6e4;
|
|
381
|
+
const BUILD_RESOLVE_TIMEOUT_ENV = "STIM_CACHE_BUILD_RESOLVE_TIMEOUT_MS";
|
|
382
|
+
const BUILD_UPLOAD_TIMEOUT_ENV = "STIM_CACHE_BUILD_UPLOAD_TIMEOUT_MS";
|
|
383
|
+
function buildResolveTimeoutMs(env) {
|
|
384
|
+
return timeoutFromEnv(BUILD_RESOLVE_TIMEOUT_ENV, BUILD_RESOLVE_TIMEOUT_MS, env);
|
|
385
|
+
}
|
|
386
|
+
function buildUploadTimeoutMs(env) {
|
|
387
|
+
return timeoutFromEnv(BUILD_UPLOAD_TIMEOUT_ENV, BUILD_UPLOAD_TIMEOUT_MS, env);
|
|
388
|
+
}
|
|
389
|
+
async function providerTier(loadProvider, warn) {
|
|
390
|
+
if (!loadProvider) return null;
|
|
391
|
+
const loaded = await loadProvider();
|
|
392
|
+
if (loaded?.unavailable) {
|
|
393
|
+
warn("provider-load", `provider not usable (${loaded.name ?? "the cache provider"}): ${loaded.unavailable}; using local cache`);
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
const capability = loaded?.provider?.builds;
|
|
397
|
+
if (!capability) return null;
|
|
398
|
+
return {
|
|
399
|
+
capability,
|
|
400
|
+
name: loaded.name ?? "the cache provider"
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function neverAborted$1() {
|
|
404
|
+
return new AbortController().signal;
|
|
405
|
+
}
|
|
406
|
+
function ignore() {}
|
|
407
|
+
async function resolveTieredBuild({ local, loadProvider = null, target, destinationDir, ensureDestination, skipRead = false, warn = ignore, timeoutMs = buildResolveTimeoutMs() }) {
|
|
408
|
+
if (skipRead) return null;
|
|
409
|
+
const hit = await local.resolve({
|
|
410
|
+
...target,
|
|
411
|
+
destinationDir,
|
|
412
|
+
signal: neverAborted$1()
|
|
413
|
+
});
|
|
414
|
+
if (hit) return {
|
|
415
|
+
path: hit,
|
|
416
|
+
tier: "local"
|
|
417
|
+
};
|
|
418
|
+
const tier = await providerTier(loadProvider, warn);
|
|
419
|
+
if (!tier) return null;
|
|
420
|
+
const { capability: provider, name: label } = tier;
|
|
421
|
+
ensureDestination?.(destinationDir);
|
|
422
|
+
const outcome = await callWithTimeout((signal) => provider.resolve({
|
|
423
|
+
...target,
|
|
424
|
+
destinationDir,
|
|
425
|
+
signal
|
|
426
|
+
}), timeoutMs);
|
|
427
|
+
if (outcome.timedOut) {
|
|
428
|
+
warn("provider-resolve", `${label} did not answer within ${timeoutMs}ms; building instead`);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
if (outcome.failed) {
|
|
432
|
+
warn("provider-resolve", `${label} could not be used: ${outcome.failed}; building instead`);
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const path = typeof outcome.value === "string" ? outcome.value.trim() : "";
|
|
436
|
+
if (path === "") return null;
|
|
437
|
+
if (!existsSync(path)) {
|
|
438
|
+
warn("provider-resolve", `${label} returned ${path}, which does not exist; building instead`);
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
let stored = null;
|
|
442
|
+
try {
|
|
443
|
+
stored = await local.store({
|
|
444
|
+
...target,
|
|
445
|
+
sourcePath: path,
|
|
446
|
+
overwrite: false,
|
|
447
|
+
signal: neverAborted$1()
|
|
448
|
+
}) || null;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
warn("provider-backfill", `a ${label} hit could not be stored locally: ${String(error?.message || error)}`);
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
path: stored || path,
|
|
454
|
+
tier: "provider",
|
|
455
|
+
providerName: label,
|
|
456
|
+
storedLocally: Boolean(stored)
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
async function storeTieredBuild({ local, loadProvider = null, target, sourcePath, overwrite, warn = ignore, timeoutMs = buildUploadTimeoutMs() }) {
|
|
460
|
+
const localPath = await local.store({
|
|
461
|
+
...target,
|
|
462
|
+
sourcePath,
|
|
463
|
+
overwrite,
|
|
464
|
+
signal: neverAborted$1()
|
|
465
|
+
}) || null;
|
|
466
|
+
const tier = await providerTier(loadProvider, warn);
|
|
467
|
+
if (!tier) return {
|
|
468
|
+
localPath,
|
|
469
|
+
providerUpload: null,
|
|
470
|
+
providerName: null
|
|
471
|
+
};
|
|
472
|
+
return {
|
|
473
|
+
localPath,
|
|
474
|
+
providerUpload: callWithTimeout(async (signal) => {
|
|
475
|
+
await tier.capability.store({
|
|
476
|
+
...target,
|
|
477
|
+
sourcePath,
|
|
478
|
+
overwrite,
|
|
479
|
+
signal
|
|
480
|
+
});
|
|
481
|
+
}, timeoutMs),
|
|
482
|
+
providerName: tier.name
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region contract.ts
|
|
487
|
+
const CACHE_CONTRACT_CHECK_TIMEOUT_MS = 3e4;
|
|
488
|
+
const ABORT_SETTLE_MS = 1e3;
|
|
489
|
+
async function settlesAfterAbort(start, settleMs) {
|
|
490
|
+
const controller = new AbortController();
|
|
491
|
+
const call = Promise.resolve().then(() => start(controller.signal)).then(() => "settled", () => "settled");
|
|
492
|
+
controller.abort();
|
|
493
|
+
let timer;
|
|
494
|
+
const outcome = await Promise.race([call, new Promise((resolve) => {
|
|
495
|
+
timer = setTimeout(() => resolve("ignored"), settleMs);
|
|
496
|
+
})]);
|
|
497
|
+
clearTimeout(timer);
|
|
498
|
+
assert(outcome === "settled", `the call did not settle within ${settleMs}ms of its signal aborting`);
|
|
499
|
+
}
|
|
500
|
+
function isInside(parent, child) {
|
|
501
|
+
const rel = relative(parent, child);
|
|
502
|
+
return rel !== "" && !rel.startsWith(`..${sep}`) && rel !== "..";
|
|
503
|
+
}
|
|
504
|
+
function neverAborted() {
|
|
505
|
+
return new AbortController().signal;
|
|
506
|
+
}
|
|
507
|
+
function assert(condition, message) {
|
|
508
|
+
if (!condition) throw new Error(message);
|
|
509
|
+
}
|
|
510
|
+
function metroChecks({ provider, projectRoot, cacheName = "app", abortSettleMs = ABORT_SETTLE_MS }) {
|
|
511
|
+
const metro = provider.metro;
|
|
512
|
+
if (!metro) return [];
|
|
513
|
+
const context = {
|
|
514
|
+
projectRoot,
|
|
515
|
+
cacheName,
|
|
516
|
+
signal: neverAborted()
|
|
517
|
+
};
|
|
518
|
+
return [
|
|
519
|
+
{
|
|
520
|
+
name: "metro get returns null or undefined for an unknown key",
|
|
521
|
+
capability: "metro",
|
|
522
|
+
async run() {
|
|
523
|
+
const value = await metro.get({
|
|
524
|
+
...context,
|
|
525
|
+
key: randomBytes(32)
|
|
526
|
+
});
|
|
527
|
+
assert(value === null || value === void 0, `expected a miss, received ${JSON.stringify(value)}`);
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: "metro set then get returns the stored buffer",
|
|
532
|
+
capability: "metro",
|
|
533
|
+
async run() {
|
|
534
|
+
const key = randomBytes(32);
|
|
535
|
+
const value = randomBytes(64);
|
|
536
|
+
await metro.set({
|
|
537
|
+
...context,
|
|
538
|
+
key,
|
|
539
|
+
value
|
|
540
|
+
});
|
|
541
|
+
const stored = await metro.get({
|
|
542
|
+
...context,
|
|
543
|
+
key
|
|
544
|
+
});
|
|
545
|
+
assert(Buffer.isBuffer(stored), `expected a Buffer, received ${typeof stored}`);
|
|
546
|
+
assert(Buffer.compare(stored, value) === 0, "the stored buffer does not match the written buffer");
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "metro set then get returns the stored object",
|
|
551
|
+
capability: "metro",
|
|
552
|
+
async run() {
|
|
553
|
+
const key = randomBytes(32);
|
|
554
|
+
const value = {
|
|
555
|
+
code: `contract-${randomUUID()}`,
|
|
556
|
+
map: [
|
|
557
|
+
1,
|
|
558
|
+
2,
|
|
559
|
+
3
|
|
560
|
+
]
|
|
561
|
+
};
|
|
562
|
+
await metro.set({
|
|
563
|
+
...context,
|
|
564
|
+
key,
|
|
565
|
+
value
|
|
566
|
+
});
|
|
567
|
+
const stored = await metro.get({
|
|
568
|
+
...context,
|
|
569
|
+
key
|
|
570
|
+
});
|
|
571
|
+
assert(JSON.stringify(stored) === JSON.stringify(value), `expected ${JSON.stringify(value)}, received ${JSON.stringify(stored)}`);
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: "metro get settles when its signal aborts",
|
|
576
|
+
capability: "metro",
|
|
577
|
+
async run() {
|
|
578
|
+
await settlesAfterAbort((signal) => metro.get({
|
|
579
|
+
projectRoot,
|
|
580
|
+
cacheName,
|
|
581
|
+
key: randomBytes(32),
|
|
582
|
+
signal
|
|
583
|
+
}), abortSettleMs);
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: "metro keys do not collide",
|
|
588
|
+
capability: "metro",
|
|
589
|
+
async run() {
|
|
590
|
+
const first = randomBytes(32);
|
|
591
|
+
const second = randomBytes(32);
|
|
592
|
+
await metro.set({
|
|
593
|
+
...context,
|
|
594
|
+
key: first,
|
|
595
|
+
value: Buffer.from("first")
|
|
596
|
+
});
|
|
597
|
+
await metro.set({
|
|
598
|
+
...context,
|
|
599
|
+
key: second,
|
|
600
|
+
value: Buffer.from("second")
|
|
601
|
+
});
|
|
602
|
+
const stored = await metro.get({
|
|
603
|
+
...context,
|
|
604
|
+
key: first
|
|
605
|
+
});
|
|
606
|
+
assert(Buffer.isBuffer(stored) && stored.toString() === "first", "the second write overwrote the value of an unrelated key");
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
];
|
|
610
|
+
}
|
|
611
|
+
function buildChecks({ provider, projectRoot, workDir, platform = "android", abortSettleMs = ABORT_SETTLE_MS }) {
|
|
612
|
+
const builds = provider.builds;
|
|
613
|
+
if (!builds) return [];
|
|
614
|
+
const signal = neverAborted();
|
|
615
|
+
function artifact() {
|
|
616
|
+
const dir = join(workDir, `source-${randomUUID()}`);
|
|
617
|
+
mkdirSync(dir, { recursive: true });
|
|
618
|
+
const sourcePath = join(dir, platform === "ios" ? "Contract.app" : "contract.apk");
|
|
619
|
+
const contents = randomBytes(128);
|
|
620
|
+
if (platform === "ios") {
|
|
621
|
+
mkdirSync(sourcePath, { recursive: true });
|
|
622
|
+
writeFileSync(join(sourcePath, "contract.bin"), contents);
|
|
623
|
+
} else writeFileSync(sourcePath, contents);
|
|
624
|
+
return {
|
|
625
|
+
sourcePath,
|
|
626
|
+
contents
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function destination() {
|
|
630
|
+
const dir = join(workDir, `destination-${randomUUID()}`);
|
|
631
|
+
mkdirSync(dir, { recursive: true });
|
|
632
|
+
return dir;
|
|
633
|
+
}
|
|
634
|
+
function storedContents(path) {
|
|
635
|
+
return readFileSync(platform === "ios" ? join(path, "contract.bin") : path);
|
|
636
|
+
}
|
|
637
|
+
return [
|
|
638
|
+
{
|
|
639
|
+
name: "builds resolve returns null for an unknown key",
|
|
640
|
+
capability: "builds",
|
|
641
|
+
async run() {
|
|
642
|
+
const found = await builds.resolve({
|
|
643
|
+
projectRoot,
|
|
644
|
+
platform,
|
|
645
|
+
key: `contract-miss-${randomUUID()}`,
|
|
646
|
+
destinationDir: destination(),
|
|
647
|
+
signal
|
|
648
|
+
});
|
|
649
|
+
assert(found === null || found === void 0, `expected a miss, received ${String(found)}`);
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
name: "builds store then resolve returns the same artifact",
|
|
654
|
+
capability: "builds",
|
|
655
|
+
async run() {
|
|
656
|
+
const key = `contract-${randomUUID()}`;
|
|
657
|
+
const { sourcePath, contents } = artifact();
|
|
658
|
+
await builds.store({
|
|
659
|
+
projectRoot,
|
|
660
|
+
platform,
|
|
661
|
+
key,
|
|
662
|
+
sourcePath,
|
|
663
|
+
overwrite: false,
|
|
664
|
+
signal
|
|
665
|
+
});
|
|
666
|
+
const found = await builds.resolve({
|
|
667
|
+
projectRoot,
|
|
668
|
+
platform,
|
|
669
|
+
key,
|
|
670
|
+
destinationDir: destination(),
|
|
671
|
+
signal
|
|
672
|
+
});
|
|
673
|
+
assert(typeof found === "string" && found !== "", "the stored key did not resolve");
|
|
674
|
+
assert(Buffer.compare(storedContents(found), contents) === 0, "the resolved artifact does not match the stored artifact");
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
name: "builds resolve settles when its signal aborts",
|
|
679
|
+
capability: "builds",
|
|
680
|
+
async run() {
|
|
681
|
+
await settlesAfterAbort((aborting) => builds.resolve({
|
|
682
|
+
projectRoot,
|
|
683
|
+
platform,
|
|
684
|
+
key: `contract-abort-${randomUUID()}`,
|
|
685
|
+
destinationDir: destination(),
|
|
686
|
+
signal: aborting
|
|
687
|
+
}), abortSettleMs);
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
name: "builds resolve leaves the destination directory empty on a miss",
|
|
692
|
+
capability: "builds",
|
|
693
|
+
async run() {
|
|
694
|
+
const dir = destination();
|
|
695
|
+
await builds.resolve({
|
|
696
|
+
projectRoot,
|
|
697
|
+
platform,
|
|
698
|
+
key: `contract-miss-${randomUUID()}`,
|
|
699
|
+
destinationDir: dir,
|
|
700
|
+
signal
|
|
701
|
+
});
|
|
702
|
+
assert(readdirSync(dir).length === 0, `a miss left ${readdirSync(dir).join(", ")} in the destination`);
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
name: "builds resolve returns a path it owns or one inside the destination",
|
|
707
|
+
capability: "builds",
|
|
708
|
+
async run() {
|
|
709
|
+
const key = `contract-${randomUUID()}`;
|
|
710
|
+
const { sourcePath, contents } = artifact();
|
|
711
|
+
await builds.store({
|
|
712
|
+
projectRoot,
|
|
713
|
+
platform,
|
|
714
|
+
key,
|
|
715
|
+
sourcePath,
|
|
716
|
+
overwrite: false,
|
|
717
|
+
signal
|
|
718
|
+
});
|
|
719
|
+
const dir = destination();
|
|
720
|
+
const found = await builds.resolve({
|
|
721
|
+
projectRoot,
|
|
722
|
+
platform,
|
|
723
|
+
key,
|
|
724
|
+
destinationDir: dir,
|
|
725
|
+
signal
|
|
726
|
+
});
|
|
727
|
+
assert(typeof found === "string" && found !== "", "the stored key did not resolve");
|
|
728
|
+
const path = found;
|
|
729
|
+
assert(isInside(dir, path) || Buffer.compare(storedContents(path), contents) === 0, `${path} is neither inside the destination nor a copy the capability owns`);
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
name: "builds store honors overwrite",
|
|
734
|
+
capability: "builds",
|
|
735
|
+
async run() {
|
|
736
|
+
const key = `contract-${randomUUID()}`;
|
|
737
|
+
const first = artifact();
|
|
738
|
+
const second = artifact();
|
|
739
|
+
await builds.store({
|
|
740
|
+
projectRoot,
|
|
741
|
+
platform,
|
|
742
|
+
key,
|
|
743
|
+
sourcePath: first.sourcePath,
|
|
744
|
+
overwrite: false,
|
|
745
|
+
signal
|
|
746
|
+
});
|
|
747
|
+
await builds.store({
|
|
748
|
+
projectRoot,
|
|
749
|
+
platform,
|
|
750
|
+
key,
|
|
751
|
+
sourcePath: second.sourcePath,
|
|
752
|
+
overwrite: false,
|
|
753
|
+
signal
|
|
754
|
+
});
|
|
755
|
+
const kept = await builds.resolve({
|
|
756
|
+
projectRoot,
|
|
757
|
+
platform,
|
|
758
|
+
key,
|
|
759
|
+
destinationDir: destination(),
|
|
760
|
+
signal
|
|
761
|
+
});
|
|
762
|
+
assert(typeof kept === "string", "the stored key did not resolve");
|
|
763
|
+
assert(Buffer.compare(storedContents(kept), first.contents) === 0, "overwrite: false replaced an entry that already existed");
|
|
764
|
+
await builds.store({
|
|
765
|
+
projectRoot,
|
|
766
|
+
platform,
|
|
767
|
+
key,
|
|
768
|
+
sourcePath: second.sourcePath,
|
|
769
|
+
overwrite: true,
|
|
770
|
+
signal
|
|
771
|
+
});
|
|
772
|
+
const replaced = await builds.resolve({
|
|
773
|
+
projectRoot,
|
|
774
|
+
platform,
|
|
775
|
+
key,
|
|
776
|
+
destinationDir: destination(),
|
|
777
|
+
signal
|
|
778
|
+
});
|
|
779
|
+
assert(typeof replaced === "string", "the overwritten key did not resolve");
|
|
780
|
+
assert(Buffer.compare(storedContents(replaced), second.contents) === 0, "overwrite: true kept the previous entry");
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: "builds store keeps unrelated keys separate",
|
|
785
|
+
capability: "builds",
|
|
786
|
+
async run() {
|
|
787
|
+
const first = `contract-${randomUUID()}`;
|
|
788
|
+
const second = `contract-${randomUUID()}`;
|
|
789
|
+
const one = artifact();
|
|
790
|
+
const two = artifact();
|
|
791
|
+
await builds.store({
|
|
792
|
+
projectRoot,
|
|
793
|
+
platform,
|
|
794
|
+
key: first,
|
|
795
|
+
sourcePath: one.sourcePath,
|
|
796
|
+
overwrite: false,
|
|
797
|
+
signal
|
|
798
|
+
});
|
|
799
|
+
await builds.store({
|
|
800
|
+
projectRoot,
|
|
801
|
+
platform,
|
|
802
|
+
key: second,
|
|
803
|
+
sourcePath: two.sourcePath,
|
|
804
|
+
overwrite: false,
|
|
805
|
+
signal
|
|
806
|
+
});
|
|
807
|
+
const found = await builds.resolve({
|
|
808
|
+
projectRoot,
|
|
809
|
+
platform,
|
|
810
|
+
key: first,
|
|
811
|
+
destinationDir: destination(),
|
|
812
|
+
signal
|
|
813
|
+
});
|
|
814
|
+
assert(typeof found === "string" && found !== "", "the first key stopped resolving after the second store");
|
|
815
|
+
assert(Buffer.compare(storedContents(found), one.contents) === 0, "the second store replaced the artifact of an unrelated key");
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
];
|
|
819
|
+
}
|
|
820
|
+
const MODULE_CHECK = "the module loads through loadCacheProvider()";
|
|
821
|
+
async function loadContractProvider(options) {
|
|
822
|
+
const loaded = await loadCacheProvider({
|
|
823
|
+
projectRoot: options.projectRoot,
|
|
824
|
+
config: {
|
|
825
|
+
provider: options.providerModule,
|
|
826
|
+
options: options.options ?? {},
|
|
827
|
+
baseDir: options.baseDir ?? options.projectRoot
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
if (!loaded.provider) return { failure: {
|
|
831
|
+
name: MODULE_CHECK,
|
|
832
|
+
capability: "module",
|
|
833
|
+
passed: false,
|
|
834
|
+
error: loaded.unavailable ?? "the reference selected no provider"
|
|
835
|
+
} };
|
|
836
|
+
return {
|
|
837
|
+
...options,
|
|
838
|
+
provider: loaded.provider
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function cacheProviderContractChecks(options) {
|
|
842
|
+
return [...metroChecks({
|
|
843
|
+
provider: options.provider,
|
|
844
|
+
projectRoot: options.projectRoot,
|
|
845
|
+
...options.cacheName ? { cacheName: options.cacheName } : {},
|
|
846
|
+
...options.abortSettleMs ? { abortSettleMs: options.abortSettleMs } : {}
|
|
847
|
+
}), ...buildChecks({
|
|
848
|
+
provider: options.provider,
|
|
849
|
+
projectRoot: options.projectRoot,
|
|
850
|
+
workDir: options.workDir,
|
|
851
|
+
...options.platform ? { platform: options.platform } : {},
|
|
852
|
+
...options.abortSettleMs ? { abortSettleMs: options.abortSettleMs } : {}
|
|
853
|
+
})];
|
|
854
|
+
}
|
|
855
|
+
async function runCacheProviderContract(options) {
|
|
856
|
+
const resolved = "providerModule" in options ? await loadContractProvider(options) : options;
|
|
857
|
+
if ("failure" in resolved) return [resolved.failure];
|
|
858
|
+
const checkTimeoutMs = resolved.checkTimeoutMs ?? 3e4;
|
|
859
|
+
const results = [];
|
|
860
|
+
for (const check of cacheProviderContractChecks(resolved)) try {
|
|
861
|
+
await withDeadline(check.run(), checkTimeoutMs);
|
|
862
|
+
results.push({
|
|
863
|
+
name: check.name,
|
|
864
|
+
capability: check.capability,
|
|
865
|
+
passed: true
|
|
866
|
+
});
|
|
867
|
+
} catch (error) {
|
|
868
|
+
results.push({
|
|
869
|
+
name: check.name,
|
|
870
|
+
capability: check.capability,
|
|
871
|
+
passed: false,
|
|
872
|
+
error: String(error?.message || error)
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
return results;
|
|
876
|
+
}
|
|
877
|
+
async function withDeadline(work, timeoutMs) {
|
|
878
|
+
let timer;
|
|
879
|
+
const expired = new Promise((_resolve, reject) => {
|
|
880
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`the check did not finish within ${timeoutMs}ms`)), timeoutMs);
|
|
881
|
+
});
|
|
882
|
+
try {
|
|
883
|
+
await Promise.race([work, expired]);
|
|
884
|
+
} finally {
|
|
885
|
+
clearTimeout(timer);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
//#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 };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stim-cli/cache",
|
|
3
|
+
"version": "1.0.0-rc.5",
|
|
4
|
+
"description": "Cache provider contract and tier coordination for Stim.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/appandflow/stim.git",
|
|
9
|
+
"directory": "packages/cache"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "dist/index.mjs",
|
|
18
|
+
"types": "dist/index.d.mts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.mts",
|
|
22
|
+
"module-sync": "./dist/index.mjs",
|
|
23
|
+
"default": "./dist/index.mjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": "^20.19.4 || >=22.12.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsdown"
|
|
31
|
+
}
|
|
32
|
+
}
|