@molecule/api-code-sandbox-flyio 1.2.5 → 1.2.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAWV,eAAe,EAGhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,YAAY,EAAgC,MAAM,UAAU,CAAA;AAwBrE,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAIV,WAAW,EAUZ,MAAM,YAAY,CAAA;AA20EnB;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,CAAC,EAAE,YAAY,EACrB,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,GACzB,eAAe,CAEjB"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAWV,eAAe,EAGhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,YAAY,EAAgC,MAAM,UAAU,CAAA;AAwBrE,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAIV,WAAW,EAUZ,MAAM,YAAY,CAAA;AA+2EnB;;;;;;;;;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
@@ -1713,7 +1713,12 @@ class FlyioSandboxProvider {
1713
1713
  *
1714
1714
  * A Machine that is already `started` may reject a redundant start. Rather
1715
1715
  * than pattern-matching an error string, the state is re-read and the call
1716
- * treated as successful when the Machine is in fact running.
1716
+ * treated as successful when the Machine is in fact running. A Machine still
1717
+ * `starting` (its own start lost to a concurrent one — the proxy's autostart
1718
+ * racing a `wake()`) is NOT that case: only `waitForStarted` may end this
1719
+ * call, or it resolves against a Machine that is still booting and the
1720
+ * caller's first `exec` fails with 412 "machine not running" — the exact
1721
+ * failure this method's wait exists to prevent.
1717
1722
  * @param app - The Fly app name.
1718
1723
  * @param machineId - The Fly Machine id.
1719
1724
  */
@@ -1725,7 +1730,7 @@ class FlyioSandboxProvider {
1725
1730
  const machine = await this.client
1726
1731
  .request(`/apps/${app}/machines/${machineId}`, { nullOn: [404] })
1727
1732
  .catch(() => null);
1728
- if (machine && (machine.state === 'started' || machine.state === 'starting')) {
1733
+ if (machine?.state === 'started') {
1729
1734
  logger.debug('Fly Machine start rejected but the Machine is already running', {
1730
1735
  app,
1731
1736
  machineId,
@@ -1733,7 +1738,9 @@ class FlyioSandboxProvider {
1733
1738
  });
1734
1739
  return;
1735
1740
  }
1736
- throw error;
1741
+ if (machine?.state !== 'starting')
1742
+ throw error;
1743
+ logger.debug('Fly Machine start rejected but the Machine is already starting — waiting for started', { app, machineId });
1737
1744
  }
1738
1745
  await this.waitForStarted(app, machineId);
1739
1746
  }
@@ -1858,10 +1865,12 @@ class FlyioSandboxProvider {
1858
1865
  throw new Error(t('codeSandbox.flyio.error.writeFailed', { path, error: result.stderr }, { defaultValue: `Failed to write ${path}: ${result.stderr}` }));
1859
1866
  }
1860
1867
  };
1861
- // Chunked for the same reason as the Docker provider: the whole command
1862
- // is one `sh -c` argument, capped at MAX_ARG_STRLEN (128 KB). Each chunk
1863
- // is a multiple of 4 base64 characters (= 3 decoded bytes) so it decodes
1864
- // standalone and the bytes concatenate exactly.
1868
+ // Chunked for the same reason as WRITE_CHUNK_BASE64 above: the whole
1869
+ // command is one argument to Fly's exec API, which silently rejects
1870
+ // anything over ~15 KB (FLY_EXEC_MAX_ARG_BYTES) — NOT the OS
1871
+ // MAX_ARG_STRLEN (128 KB) a Docker-based provider is bounded by. Each
1872
+ // chunk is a multiple of 4 base64 characters (= 3 decoded bytes) so it
1873
+ // decodes standalone and the bytes concatenate exactly.
1865
1874
  failIfError(await this.exec(`mkdir -p "$(dirname ${quoted})" && printf %s ${shellQuote(base64.slice(0, WRITE_CHUNK_BASE64))} | base64 -d > ${quoted}`, { timeout: FILE_OP_TIMEOUT_MS }));
1866
1875
  for (let i = WRITE_CHUNK_BASE64; i < base64.length; i += WRITE_CHUNK_BASE64) {
1867
1876
  failIfError(await this.exec(`printf %s ${shellQuote(base64.slice(i, i + WRITE_CHUNK_BASE64))} | base64 -d >> ${quoted}`, { timeout: FILE_OP_TIMEOUT_MS }));
@@ -1958,6 +1967,14 @@ class FlyioSandboxProvider {
1958
1967
  try {
1959
1968
  const putUrl = await store.presignPut(key, IMPORT_PRESIGN_EXPIRY_SECONDS);
1960
1969
  const putRes = await fetch(putUrl, { method: 'PUT', body: gz });
1970
+ // The PUT's body is never read (a presigned store answers an empty
1971
+ // 2xx or a short error document): release it on EVERY path, or the
1972
+ // connection stays stranded in the pool until GC reclaims it — the
1973
+ // same release every other discarded response body here gets.
1974
+ await putRes.body?.cancel().catch((_error) => {
1975
+ // Best-effort release only; the fallback decision below runs on
1976
+ // the already-known status.
1977
+ });
1961
1978
  if (!putRes.ok) {
1962
1979
  throw new Error(`presigned PUT to the object store failed: ${putRes.status}`);
1963
1980
  }
@@ -1967,7 +1984,12 @@ class FlyioSandboxProvider {
1967
1984
  `mkdir -p ${quotedPath}`,
1968
1985
  'archive=$(mktemp /tmp/mol-import-XXXXXX.tar.gz)',
1969
1986
  `curl --fail-with-body -sS --retry 3 --retry-connrefused -o "$archive" ${shellQuote(getUrl)}`,
1970
- `tar -C ${quotedPath} --no-same-owner --no-same-permissions -xzf "$archive"`,
1987
+ // The archive is removed whether the extract SUCCEEDS or fails:
1988
+ // `&& rm` after tar (under `set -e`) left every failed extract's
1989
+ // download behind in the sandbox's /tmp until the Machine died.
1990
+ // The `|| { }` guard keeps `set -e` from aborting before the
1991
+ // cleanup and re-raises the real exit code.
1992
+ `tar -C ${quotedPath} --no-same-owner --no-same-permissions -xzf "$archive" || { rc=$?; rm -f "$archive"; exit $rc; }`,
1971
1993
  'rm -f "$archive"',
1972
1994
  ].join('\n'), { timeout: IMPORT_EXTRACT_TIMEOUT_MS });
1973
1995
  // Best-effort: the temp object expires anyway, but delete it promptly.
@@ -1989,15 +2011,26 @@ class FlyioSandboxProvider {
1989
2011
  // configured. The temp tar lives on the WORKSPACE VOLUME, not /tmp — the
1990
2012
  // chunked write and the extract are separate exec calls and a Machine
1991
2013
  // with autostop can cold-start between them, wiping /tmp (rootfs) while
1992
- // the ext4 volume at /workspace persists.
2014
+ // the ext4 volume at /workspace persists. The name is unique per call
2015
+ // (same shape as the store path's key): a fixed, machineId-only name
2016
+ // made two concurrent imports to one sandbox interleave their `>>`
2017
+ // appends into ONE file, so each extracted a mix of both archives.
1993
2018
  const base64 = gz.toString('base64');
1994
- const tmp = `${WORKSPACE_PATH}/.mol-import-${machineId}.tgz`;
2019
+ const tmp = `${WORKSPACE_PATH}/.mol-import-${machineId}-${Date.now().toString(36)}${Math.floor(Math.random() * 1e9).toString(36)}.tgz`;
1995
2020
  const quotedTmp = shellQuote(tmp);
1996
2021
  failIfError(await this.exec(`printf %s ${shellQuote(base64.slice(0, IMPORT_WRITE_CHUNK_BASE64))} | base64 -d > ${quotedTmp}`, { timeout: FILE_OP_TIMEOUT_MS }), 'write');
1997
2022
  for (let i = IMPORT_WRITE_CHUNK_BASE64; i < base64.length; i += IMPORT_WRITE_CHUNK_BASE64) {
1998
2023
  failIfError(await this.exec(`printf %s ${shellQuote(base64.slice(i, i + IMPORT_WRITE_CHUNK_BASE64))} | base64 -d >> ${quotedTmp}`, { timeout: FILE_OP_TIMEOUT_MS }), 'write');
1999
2024
  }
2000
- failIfError(await this.exec(`mkdir -p ${quotedPath} && tar -xzf ${quotedTmp} -C ${quotedPath} --no-same-owner --no-same-permissions && rm -f ${quotedTmp}`, { timeout: IMPORT_EXTRACT_TIMEOUT_MS }), 'extract');
2025
+ failIfError(await this.exec(
2026
+ // The temp archive is removed whether the extract SUCCEEDS or fails:
2027
+ // `&& rm` after tar left a failed extract's multi-MB archive behind
2028
+ // on the WORKSPACE VOLUME — the durable path this fallback
2029
+ // deliberately writes to — until the next import overwrote it.
2030
+ // `; rc=$?; rm …; exit $rc` cleans up unconditionally and still
2031
+ // reports tar's real exit code (the same shape exec.ts's script
2032
+ // spill uses).
2033
+ `mkdir -p ${quotedPath} && tar -xzf ${quotedTmp} -C ${quotedPath} --no-same-owner --no-same-permissions; rc=$?; rm -f ${quotedTmp}; exit $rc`, { timeout: IMPORT_EXTRACT_TIMEOUT_MS }), 'extract');
2001
2034
  },
2002
2035
  getPreviewUrl(port) {
2003
2036
  return provider.renderPreviewUrl(app, machineId, port);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-code-sandbox-flyio",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "Fly.io Machines (Firecracker microVM) code sandbox provider",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",