@molecule/api-code-sandbox-flyio 1.0.12 → 1.0.14
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 +26 -1
- package/dist/api.d.ts +2 -7
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +39 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +23 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-08-
|
|
6
|
+
Generated: 2026-08-08T23:10:20.743Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-code-sandbox-flyio
|
|
@@ -609,6 +609,13 @@ interface FlyRequestOptions {
|
|
|
609
609
|
* Used for idempotent deletes and existence checks (`[404]`).
|
|
610
610
|
*/
|
|
611
611
|
nullOn?: number[]
|
|
612
|
+
/**
|
|
613
|
+
* Extra HTTP statuses to treat as transient (retry) beyond the default
|
|
614
|
+
* `429`/`5xx`. The exec endpoint passes `[404]`: a machine-not-found for a
|
|
615
|
+
* Machine we just created and are actively driving is Fly API inconsistency
|
|
616
|
+
* under exec-burst load, not a real absence, so it is worth repeating.
|
|
617
|
+
*/
|
|
618
|
+
retryStatuses?: number[]
|
|
612
619
|
}
|
|
613
620
|
```
|
|
614
621
|
|
|
@@ -1125,6 +1132,10 @@ function encodePrivateRoutes(routes: Record<string, string>): string
|
|
|
1125
1132
|
Executes a command on a Machine, choosing the direct or detached strategy
|
|
1126
1133
|
based on the caller's time budget.
|
|
1127
1134
|
|
|
1135
|
+
A command too large to pass inline (over {@link FLY_EXEC_MAX_ARG_BYTES}) is
|
|
1136
|
+
spilled to a file via chunked writes and run from there, so there is no size
|
|
1137
|
+
ceiling the caller must respect.
|
|
1138
|
+
|
|
1128
1139
|
```typescript
|
|
1129
1140
|
function execCommand(
|
|
1130
1141
|
rawExec: RawExec,
|
|
@@ -1751,6 +1762,20 @@ Exit status the capture script uses when `tar` failed outright (as opposed to wa
|
|
|
1751
1762
|
const EXIT_TAR_FAILED: 92
|
|
1752
1763
|
```
|
|
1753
1764
|
|
|
1765
|
+
#### `FLY_EXEC_MAX_ARG_BYTES`
|
|
1766
|
+
|
|
1767
|
+
Hard cap on the `sh -c <script>` argument SENT to Fly's exec endpoint. This is
|
|
1768
|
+
NOT the OS `MAX_ARG_STRLEN` (128 KB) — the Fly Machines exec API rejects a
|
|
1769
|
+
command well below that, and does so INVISIBLY: it answers `200` with
|
|
1770
|
+
`exit_code: 0` and an `Unhandled rejection: Rejection([PayloadTooLarge…])`
|
|
1771
|
+
body, so the command never runs yet reads as success. Measured against the
|
|
1772
|
+
live API: ~15.8 KB scripts run, ~23.7 KB are rejected. 15 KB is the safe
|
|
1773
|
+
ceiling; callers that move bulk data (writeFile, importFiles) chunk beneath it.
|
|
1774
|
+
|
|
1775
|
+
```typescript
|
|
1776
|
+
const FLY_EXEC_MAX_ARG_BYTES: 15000
|
|
1777
|
+
```
|
|
1778
|
+
|
|
1754
1779
|
#### `FLY_EXEC_MAX_TIMEOUT_SECONDS`
|
|
1755
1780
|
|
|
1756
1781
|
Fly's hard ceiling on the exec endpoint's `timeout` field, in seconds.
|
package/dist/api.d.ts
CHANGED
|
@@ -113,13 +113,8 @@ export interface FlyApiClientOptions {
|
|
|
113
113
|
/** Injectable sleep, for tests. Defaults to `setTimeout`. */
|
|
114
114
|
sleep?: (ms: number) => Promise<void>;
|
|
115
115
|
}
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
*
|
|
119
|
-
* Deliberately not a generated SDK: this provider touches ~12 endpoints, and a
|
|
120
|
-
* generated client would drag in the whole Machines + Postgres + tokens surface
|
|
121
|
-
* for no benefit.
|
|
122
|
-
*/
|
|
116
|
+
/** Test-only: reset the shared pacer so timing tests start from a clean slate. */
|
|
117
|
+
export declare function resetFlyPacerForTests(): void;
|
|
123
118
|
export declare class FlyApiClient {
|
|
124
119
|
private readonly token;
|
|
125
120
|
private readonly baseUrl;
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAgBH;;;;GAIG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;;;;;OAOG;gBAED,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAgBhC;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0DAA0D;IAC1D,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAQhF;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAoBjE;AAED,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,gFAAgF;IAChF,KAAK,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAC/B,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAgBH;;;;GAIG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;;;;;OAOG;gBAED,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAgBhC;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0DAA0D;IAC1D,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAQhF;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAoBjE;AAED,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,gFAAgF;IAChF,KAAK,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAC/B,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC;AAyCD,kFAAkF;AAClF,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0B;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IAErD;;;OAGG;gBACS,OAAO,EAAE,mBAAmB;IASxC;;;;;;;;;;OAUG;IACG,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;CAiFnF"}
|
package/dist/api.js
CHANGED
|
@@ -124,6 +124,42 @@ export function normalizeApiUrl(value) {
|
|
|
124
124
|
* generated client would drag in the whole Machines + Postgres + tokens surface
|
|
125
125
|
* for no benefit.
|
|
126
126
|
*/
|
|
127
|
+
/**
|
|
128
|
+
* Minimum gap between Fly Machines API requests, PROCESS-WIDE. Fly rate-limits
|
|
129
|
+
* per account (most tightly the exec endpoint, ~a few req/s), and one molecule
|
|
130
|
+
* process makes every call with one token — so without pacing, a sandbox boot's
|
|
131
|
+
* burst of exec calls trips `429`, and retrying under the cap sustains a storm
|
|
132
|
+
* that LIVELOCKS the boot (observed live 2026-08-09: a workspace stuck
|
|
133
|
+
* "creating" with continuous per-second 429s that never drained). ~220 ms ≈
|
|
134
|
+
* 4.5 req/s keeps us under the cap. Tunable via `FLY_API_MIN_REQUEST_GAP_MS`.
|
|
135
|
+
*/
|
|
136
|
+
function minRequestGapMs() {
|
|
137
|
+
const v = Number(process.env.FLY_API_MIN_REQUEST_GAP_MS);
|
|
138
|
+
return Number.isFinite(v) && v >= 0 ? v : 220;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Serializes every request through a shared chain so no two fire closer than
|
|
142
|
+
* {@link MIN_REQUEST_GAP_MS}. Module-level (not per-client) so concurrent
|
|
143
|
+
* sandbox boots share ONE pacer and cannot collectively exceed the account cap.
|
|
144
|
+
* Errors in the chain are swallowed so one failed wait never wedges the queue.
|
|
145
|
+
*/
|
|
146
|
+
let flyPacerTail = Promise.resolve();
|
|
147
|
+
let flyLastRequestAt = 0;
|
|
148
|
+
function paceFlyRequest(sleep) {
|
|
149
|
+
const mine = flyPacerTail.then(async () => {
|
|
150
|
+
const wait = Math.max(0, flyLastRequestAt + minRequestGapMs() - Date.now());
|
|
151
|
+
if (wait > 0)
|
|
152
|
+
await sleep(wait);
|
|
153
|
+
flyLastRequestAt = Date.now();
|
|
154
|
+
});
|
|
155
|
+
flyPacerTail = mine.catch(() => { });
|
|
156
|
+
return mine;
|
|
157
|
+
}
|
|
158
|
+
/** Test-only: reset the shared pacer so timing tests start from a clean slate. */
|
|
159
|
+
export function resetFlyPacerForTests() {
|
|
160
|
+
flyPacerTail = Promise.resolve();
|
|
161
|
+
flyLastRequestAt = 0;
|
|
162
|
+
}
|
|
127
163
|
export class FlyApiClient {
|
|
128
164
|
token;
|
|
129
165
|
baseUrl;
|
|
@@ -164,6 +200,9 @@ export class FlyApiClient {
|
|
|
164
200
|
}
|
|
165
201
|
let lastError;
|
|
166
202
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
203
|
+
// Pace EVERY network call (including retries) through the process-wide
|
|
204
|
+
// limiter so a boot's burst of exec calls never trips Fly's account 429.
|
|
205
|
+
await paceFlyRequest(this.sleep);
|
|
167
206
|
const controller = new AbortController();
|
|
168
207
|
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? this.timeoutMs);
|
|
169
208
|
// Initialized to 0 ("no answer received"), which is the value that
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAUV,eAAe,EAEhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,YAAY,EAAgC,MAAM,UAAU,CAAA;AAsBrE,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAEV,WAAW,EAUZ,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAUV,eAAe,EAEhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,YAAY,EAAgC,MAAM,UAAU,CAAA;AAsBrE,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAEV,WAAW,EAUZ,MAAM,YAAY,CAAA;AA83DnB;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,CAAC,EAAE,YAAY,EACrB,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,GACzB,eAAe,CAEjB"}
|
package/dist/provider.js
CHANGED
|
@@ -82,6 +82,23 @@ const DEFAULT_PROBE_TIMEOUT_MS = 3000;
|
|
|
82
82
|
* like `npm install`, not for un-tarring.
|
|
83
83
|
*/
|
|
84
84
|
const IMPORT_EXTRACT_TIMEOUT_MS = 55_000;
|
|
85
|
+
/**
|
|
86
|
+
* Attempts for the create's `POST /machines`, retrying the app-propagation 404
|
|
87
|
+
* (see {@link MACHINE_CREATE_RETRY_STATUSES}). Fly propagates a new app to its
|
|
88
|
+
* sub-APIs asynchronously, so `POST /apps` can return before
|
|
89
|
+
* `POST /apps/{app}/machines` sees the app — which 404s "app not found" for a
|
|
90
|
+
* few seconds. Observed live during boot on 2026-08-08 (surfaced to the user as
|
|
91
|
+
* "Failed to start sandbox"). Six attempts span the client's 1+2+4+8+16 s
|
|
92
|
+
* backoff ≈ 31 s, comfortably past the observed window.
|
|
93
|
+
*/
|
|
94
|
+
const MACHINE_CREATE_ATTEMPTS = 6;
|
|
95
|
+
/**
|
|
96
|
+
* Retrying a `POST /machines` on 404 is SAFE precisely because "app not found"
|
|
97
|
+
* means no Machine was created — unlike a 5xx/timeout, where one might have
|
|
98
|
+
* been. So only 404 is added here, on top of the client's default retryable
|
|
99
|
+
* set; the retry cannot double-create.
|
|
100
|
+
*/
|
|
101
|
+
const MACHINE_CREATE_RETRY_STATUSES = [404];
|
|
85
102
|
/**
|
|
86
103
|
* Base64 chunk size for {@link Sandbox.importFiles}'s temp-file write. Bounded by
|
|
87
104
|
* the same Fly exec payload limit as `WRITE_CHUNK_BASE64` (~15 KB, silently
|
|
@@ -397,7 +414,7 @@ class FlyioSandboxProvider {
|
|
|
397
414
|
*/
|
|
398
415
|
async destroyStaleMachines(app) {
|
|
399
416
|
const managedKey = `${this.metadataPrefix}.managed`;
|
|
400
|
-
let machines
|
|
417
|
+
let machines;
|
|
401
418
|
try {
|
|
402
419
|
machines = await this.client.request(`/apps/${app}/machines`, {
|
|
403
420
|
nullOn: [404],
|
|
@@ -797,6 +814,11 @@ class FlyioSandboxProvider {
|
|
|
797
814
|
const machine = await this.client.request(`/apps/${app}/machines`, {
|
|
798
815
|
method: 'POST',
|
|
799
816
|
body: { region: this.region(), config: machineConfig },
|
|
817
|
+
// A just-created app propagates to the Machines API asynchronously, so this
|
|
818
|
+
// POST can 404 "app not found" for a few seconds. Retry the 404 (safe: no
|
|
819
|
+
// Machine is created when the app is not found) across the client's backoff.
|
|
820
|
+
attempts: MACHINE_CREATE_ATTEMPTS,
|
|
821
|
+
retryStatuses: MACHINE_CREATE_RETRY_STATUSES,
|
|
800
822
|
});
|
|
801
823
|
if (!machine?.id) {
|
|
802
824
|
throw new Error(t('codeSandbox.flyio.error.createFailed', { app }, { defaultValue: `Fly Machine creation in app "${app}" returned no Machine id.` }));
|