@zero-transfer/s3 0.3.1 → 0.4.2

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/dist/index.mjs CHANGED
@@ -1767,6 +1767,186 @@ function summarizeDiagnosticError(error) {
1767
1767
  return { message: String(error) };
1768
1768
  }
1769
1769
 
1770
+ // src/core/ConnectionPool.ts
1771
+ var DEFAULT_MAX_IDLE_PER_KEY = 4;
1772
+ var DEFAULT_IDLE_TIMEOUT_MS = 6e4;
1773
+ function createPooledTransferClient(inner, options = {}) {
1774
+ const maxIdlePerKey = Math.max(1, options.maxIdlePerKey ?? DEFAULT_MAX_IDLE_PER_KEY);
1775
+ const idleTimeoutMs = Math.max(0, options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS);
1776
+ const keyOf = options.keyOf ?? defaultKeyOf;
1777
+ const state = {
1778
+ drained: false,
1779
+ idle: /* @__PURE__ */ new Map()
1780
+ };
1781
+ const release = (key, session, tainted) => {
1782
+ if (tainted || state.drained) {
1783
+ return safelyDisconnect(session);
1784
+ }
1785
+ let bucket = state.idle.get(key);
1786
+ if (bucket === void 0) {
1787
+ bucket = [];
1788
+ state.idle.set(key, bucket);
1789
+ }
1790
+ const entry = { session };
1791
+ if (idleTimeoutMs > 0) {
1792
+ entry.idleTimer = setTimeout(() => {
1793
+ evictEntry(state, key, entry);
1794
+ }, idleTimeoutMs);
1795
+ const timer = entry.idleTimer;
1796
+ if (timer !== void 0 && typeof timer.unref === "function") {
1797
+ timer.unref();
1798
+ }
1799
+ }
1800
+ bucket.push(entry);
1801
+ while (bucket.length > maxIdlePerKey) {
1802
+ const dropped = bucket.shift();
1803
+ if (dropped !== void 0) {
1804
+ clearEntryTimer(dropped);
1805
+ void safelyDisconnect(dropped.session);
1806
+ }
1807
+ }
1808
+ return Promise.resolve();
1809
+ };
1810
+ const acquire = async (profile) => {
1811
+ const key = keyOf(profile);
1812
+ const bucket = state.idle.get(key);
1813
+ if (bucket !== void 0 && bucket.length > 0) {
1814
+ const entry = bucket.pop();
1815
+ if (entry !== void 0) {
1816
+ clearEntryTimer(entry);
1817
+ if (bucket.length === 0) state.idle.delete(key);
1818
+ return { key, session: entry.session };
1819
+ }
1820
+ }
1821
+ const session = await inner.connect(profile);
1822
+ return { key, session };
1823
+ };
1824
+ return {
1825
+ connect: async (profile) => {
1826
+ const { key, session } = await acquire(profile);
1827
+ return wrapPooledSession(session, key, release);
1828
+ },
1829
+ drainPool: async () => {
1830
+ state.drained = true;
1831
+ const entries = [];
1832
+ for (const bucket of state.idle.values()) {
1833
+ for (const entry of bucket) {
1834
+ clearEntryTimer(entry);
1835
+ entries.push(entry);
1836
+ }
1837
+ }
1838
+ state.idle.clear();
1839
+ await Promise.all(entries.map((entry) => safelyDisconnect(entry.session)));
1840
+ },
1841
+ getCapabilities: ((providerId) => {
1842
+ if (providerId === void 0) return inner.getCapabilities();
1843
+ return inner.getCapabilities(providerId);
1844
+ }),
1845
+ hasProvider: (providerId) => inner.hasProvider(providerId),
1846
+ poolSize: () => {
1847
+ let total = 0;
1848
+ for (const bucket of state.idle.values()) total += bucket.length;
1849
+ return total;
1850
+ }
1851
+ };
1852
+ }
1853
+ function defaultKeyOf(profile) {
1854
+ const provider = profile.provider ?? profile.protocol ?? "unknown";
1855
+ const host = profile.host ?? "";
1856
+ const port = profile.port ?? "";
1857
+ const username = typeof profile.username === "string" ? profile.username : "";
1858
+ return `${provider}|${host}|${String(port)}|${username}`;
1859
+ }
1860
+ function evictEntry(state, key, entry) {
1861
+ const bucket = state.idle.get(key);
1862
+ if (bucket === void 0) return;
1863
+ const index = bucket.indexOf(entry);
1864
+ if (index < 0) return;
1865
+ bucket.splice(index, 1);
1866
+ if (bucket.length === 0) state.idle.delete(key);
1867
+ clearEntryTimer(entry);
1868
+ void safelyDisconnect(entry.session);
1869
+ }
1870
+ function clearEntryTimer(entry) {
1871
+ if (entry.idleTimer !== void 0) {
1872
+ clearTimeout(entry.idleTimer);
1873
+ delete entry.idleTimer;
1874
+ }
1875
+ }
1876
+ async function safelyDisconnect(session) {
1877
+ try {
1878
+ await session.disconnect();
1879
+ } catch {
1880
+ }
1881
+ }
1882
+ function isTaintingError(error) {
1883
+ return error instanceof ConnectionError || error instanceof TimeoutError || error instanceof ProtocolError;
1884
+ }
1885
+ function wrapPooledSession(session, key, release) {
1886
+ let tainted = false;
1887
+ let released = false;
1888
+ const guard = (fn) => {
1889
+ let promise;
1890
+ try {
1891
+ promise = fn();
1892
+ } catch (error) {
1893
+ if (isTaintingError(error)) tainted = true;
1894
+ return Promise.reject(error instanceof Error ? error : new Error(String(error)));
1895
+ }
1896
+ return promise.catch((error) => {
1897
+ if (isTaintingError(error)) tainted = true;
1898
+ throw error;
1899
+ });
1900
+ };
1901
+ const fs = wrapFs(session.fs, guard);
1902
+ const transfers = session.transfers === void 0 ? void 0 : wrapTransfers(session.transfers, guard);
1903
+ const wrapped = {
1904
+ capabilities: session.capabilities,
1905
+ disconnect: async () => {
1906
+ if (released) return;
1907
+ released = true;
1908
+ await release(key, session, tainted);
1909
+ },
1910
+ fs,
1911
+ provider: session.provider,
1912
+ ...transfers !== void 0 ? { transfers } : {}
1913
+ };
1914
+ if (typeof session.raw === "function") {
1915
+ const rawFn = session.raw.bind(session);
1916
+ wrapped.raw = () => rawFn();
1917
+ }
1918
+ return wrapped;
1919
+ }
1920
+ function wrapFs(fs, guard) {
1921
+ const wrapped = {
1922
+ list: (path2, options) => guard(() => options !== void 0 ? fs.list(path2, options) : fs.list(path2)),
1923
+ stat: (path2, options) => guard(() => options !== void 0 ? fs.stat(path2, options) : fs.stat(path2))
1924
+ };
1925
+ if (typeof fs.remove === "function") {
1926
+ const remove = fs.remove.bind(fs);
1927
+ wrapped.remove = (path2, options) => guard(() => options !== void 0 ? remove(path2, options) : remove(path2));
1928
+ }
1929
+ if (typeof fs.rename === "function") {
1930
+ const rename2 = fs.rename.bind(fs);
1931
+ wrapped.rename = (from, to, options) => guard(() => options !== void 0 ? rename2(from, to, options) : rename2(from, to));
1932
+ }
1933
+ if (typeof fs.mkdir === "function") {
1934
+ const mkdir2 = fs.mkdir.bind(fs);
1935
+ wrapped.mkdir = (path2, options) => guard(() => options !== void 0 ? mkdir2(path2, options) : mkdir2(path2));
1936
+ }
1937
+ if (typeof fs.rmdir === "function") {
1938
+ const rmdir = fs.rmdir.bind(fs);
1939
+ wrapped.rmdir = (path2, options) => guard(() => options !== void 0 ? rmdir(path2, options) : rmdir(path2));
1940
+ }
1941
+ return wrapped;
1942
+ }
1943
+ function wrapTransfers(transfers, guard) {
1944
+ return {
1945
+ read: (request) => guard(() => Promise.resolve(transfers.read(request))),
1946
+ write: (request) => guard(() => Promise.resolve(transfers.write(request)))
1947
+ };
1948
+ }
1949
+
1770
1950
  // src/providers/local/LocalProvider.ts
1771
1951
  import { createReadStream } from "fs";
1772
1952
  import {
@@ -4605,6 +4785,17 @@ function isModifiedAtDifferent2(source, destination, toleranceMs) {
4605
4785
  return Math.abs(sourceTime - destinationTime) > toleranceMs;
4606
4786
  }
4607
4787
 
4788
+ // src/providers/web/S3Provider.ts
4789
+ import { createHash as createHash2 } from "crypto";
4790
+ import {
4791
+ mkdir as fsMkdir,
4792
+ readFile as fsReadFile,
4793
+ rename as fsRename,
4794
+ unlink as fsUnlink,
4795
+ writeFile as fsWriteFile
4796
+ } from "fs/promises";
4797
+ import { join as joinPath } from "path";
4798
+
4608
4799
  // src/providers/web/awsSigv4.ts
4609
4800
  import { createHash, createHmac as createHmac2 } from "crypto";
4610
4801
  function signSigV4(input) {
@@ -4775,6 +4966,48 @@ function createMemoryS3MultipartResumeStore() {
4775
4966
  }
4776
4967
  };
4777
4968
  }
4969
+ function createFileSystemS3MultipartResumeStore(options) {
4970
+ const directory = options.directory;
4971
+ if (typeof directory !== "string" || directory.length === 0) {
4972
+ throw new ConfigurationError({
4973
+ message: "createFileSystemS3MultipartResumeStore requires a non-empty directory option",
4974
+ retryable: false
4975
+ });
4976
+ }
4977
+ const fileFor = (key) => {
4978
+ const hash = createHash2("sha256").update(`${key.bucket}\0${key.jobId}\0${key.path}`).digest("hex");
4979
+ return joinPath(directory, `${hash}.json`);
4980
+ };
4981
+ return {
4982
+ async clear(key) {
4983
+ try {
4984
+ await fsUnlink(fileFor(key));
4985
+ } catch (error) {
4986
+ if (error.code !== "ENOENT") throw error;
4987
+ }
4988
+ },
4989
+ async load(key) {
4990
+ try {
4991
+ const text = await fsReadFile(fileFor(key), "utf8");
4992
+ const parsed = JSON.parse(text);
4993
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.uploadId !== "string" || !Array.isArray(parsed.parts)) {
4994
+ return void 0;
4995
+ }
4996
+ return parsed;
4997
+ } catch (error) {
4998
+ if (error.code === "ENOENT") return void 0;
4999
+ throw error;
5000
+ }
5001
+ },
5002
+ async save(key, checkpoint) {
5003
+ await fsMkdir(directory, { recursive: true });
5004
+ const target = fileFor(key);
5005
+ const tmp = `${target}.${String(process.pid)}.${String(Date.now())}.tmp`;
5006
+ await fsWriteFile(tmp, JSON.stringify(checkpoint), { encoding: "utf8", mode: 384 });
5007
+ await fsRename(tmp, target);
5008
+ }
5009
+ };
5010
+ }
4778
5011
  var DEFAULT_MULTIPART_PART_SIZE = 8 * 1024 * 1024;
4779
5012
  var DEFAULT_MULTIPART_THRESHOLD = 8 * 1024 * 1024;
4780
5013
  var S3_CHECKSUM_CAPABILITIES = ["etag"];
@@ -4802,7 +5035,7 @@ function createS3ProviderFactory(options = {}) {
4802
5035
  retryable: false
4803
5036
  });
4804
5037
  }
4805
- const multipartEnabled = options.multipart?.enabled ?? false;
5038
+ const multipartEnabled = options.multipart?.enabled ?? true;
4806
5039
  const multipart = {
4807
5040
  enabled: multipartEnabled,
4808
5041
  partSizeBytes: options.multipart?.partSizeBytes ?? DEFAULT_MULTIPART_PART_SIZE,
@@ -4819,9 +5052,11 @@ function createS3ProviderFactory(options = {}) {
4819
5052
  maxConcurrency: 16,
4820
5053
  metadata: ["modifiedAt", "mimeType", "uniqueId"],
4821
5054
  notes: multipartEnabled ? [
4822
- `S3 multipart upload enabled (partSize=${String(multipart.partSizeBytes)}B, threshold=${String(multipart.thresholdBytes)}B).`
5055
+ `S3 multipart upload enabled by default (partSize=${String(multipart.partSizeBytes)}B, threshold=${String(multipart.thresholdBytes)}B).`,
5056
+ "Payloads at or below the threshold automatically fall back to single-shot PUT.",
5057
+ "Pass `multipart: { enabled: false }` to force the legacy single-shot behaviour."
4823
5058
  ] : [
4824
- "S3 provider performs single-shot PUT uploads; pass multipart.enabled to stream large objects."
5059
+ "S3 provider performs single-shot PUT uploads; entire object is buffered in memory before transmission."
4825
5060
  ],
4826
5061
  provider: id,
4827
5062
  readStream: true,
@@ -5427,10 +5662,12 @@ export {
5427
5662
  copyBetween,
5428
5663
  createAtomicDeployPlan,
5429
5664
  createBandwidthThrottle,
5665
+ createFileSystemS3MultipartResumeStore,
5430
5666
  createLocalProviderFactory,
5431
5667
  createMemoryProviderFactory,
5432
5668
  createMemoryS3MultipartResumeStore,
5433
5669
  createOAuthTokenSecretSource,
5670
+ createPooledTransferClient,
5434
5671
  createProgressEvent,
5435
5672
  createProviderTransferExecutor,
5436
5673
  createRemoteBrowser,