@tranquilload/core 0.1.0 → 0.1.1

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.
Files changed (55) hide show
  1. package/.turbo/turbo-build.log +68 -68
  2. package/CHANGELOG.md +67 -0
  3. package/dist/errors.cjs +3 -1
  4. package/dist/errors.d.cts +2 -2
  5. package/dist/errors.d.mts +2 -2
  6. package/dist/errors.mjs +2 -2
  7. package/dist/index-BaeUV_fj.d.cts +139 -0
  8. package/dist/index-BaeUV_fj.d.cts.map +1 -0
  9. package/dist/index-bpWq6tje.d.mts +139 -0
  10. package/dist/index-bpWq6tje.d.mts.map +1 -0
  11. package/dist/multipart.cjs +80 -12
  12. package/dist/multipart.cjs.map +1 -1
  13. package/dist/multipart.d.cts +2 -2
  14. package/dist/multipart.d.mts +2 -2
  15. package/dist/multipart.mjs +80 -12
  16. package/dist/multipart.mjs.map +1 -1
  17. package/dist/{normalize-callback-BNBZZ1jT.cjs → normalize-callback-BdLtk9jb.cjs} +2 -2
  18. package/dist/{normalize-callback-BNBZZ1jT.cjs.map → normalize-callback-BdLtk9jb.cjs.map} +1 -1
  19. package/dist/{normalize-callback-DQ6C4gaV.mjs → normalize-callback-tcZ_nyq5.mjs} +2 -2
  20. package/dist/{normalize-callback-DQ6C4gaV.mjs.map → normalize-callback-tcZ_nyq5.mjs.map} +1 -1
  21. package/dist/oneshot.cjs +2 -2
  22. package/dist/oneshot.d.cts +2 -2
  23. package/dist/oneshot.d.mts +2 -2
  24. package/dist/oneshot.mjs +2 -2
  25. package/dist/progress.d.cts +2 -2
  26. package/dist/progress.d.mts +2 -2
  27. package/dist/{upload-error-BUexBh08.cjs → upload-error-BG1dOOl3.cjs} +26 -1
  28. package/dist/upload-error-BG1dOOl3.cjs.map +1 -0
  29. package/dist/{upload-error-B2ISUc_k.d.cts → upload-error-DTYVNlaJ.d.cts} +19 -3
  30. package/dist/{upload-error-B2ISUc_k.d.cts.map → upload-error-DTYVNlaJ.d.cts.map} +1 -1
  31. package/dist/{upload-error-zDvpxT9X.mjs → upload-error-Dbz_9j81.mjs} +21 -2
  32. package/dist/upload-error-Dbz_9j81.mjs.map +1 -0
  33. package/dist/{upload-error-jol-eoDW.d.mts → upload-error-jBco270d.d.mts} +19 -3
  34. package/dist/{upload-error-jol-eoDW.d.mts.map → upload-error-jBco270d.d.mts.map} +1 -1
  35. package/dist/{upload-event-C9TOVp5l.d.mts → upload-event-BT_nXgM9.d.cts} +7 -1
  36. package/dist/upload-event-BT_nXgM9.d.cts.map +1 -0
  37. package/dist/{upload-event-D77olieX.d.cts → upload-event-DOGbegxa.d.mts} +7 -1
  38. package/dist/upload-event-DOGbegxa.d.mts.map +1 -0
  39. package/package.json +1 -1
  40. package/src/errors/index.ts +2 -0
  41. package/src/errors/upload-error.test.ts +35 -0
  42. package/src/errors/upload-error.ts +27 -0
  43. package/src/multipart/index.test.ts +164 -1
  44. package/src/multipart/index.ts +96 -5
  45. package/src/multipart/upload-stream.test.ts +201 -2
  46. package/src/multipart/upload-stream.ts +160 -16
  47. package/src/progress/upload-event.ts +6 -0
  48. package/dist/index-Ch8xM6Xt.d.cts +0 -60
  49. package/dist/index-Ch8xM6Xt.d.cts.map +0 -1
  50. package/dist/index-DBGtgXEd.d.mts +0 -60
  51. package/dist/index-DBGtgXEd.d.mts.map +0 -1
  52. package/dist/upload-error-BUexBh08.cjs.map +0 -1
  53. package/dist/upload-error-zDvpxT9X.mjs.map +0 -1
  54. package/dist/upload-event-C9TOVp5l.d.mts.map +0 -1
  55. package/dist/upload-event-D77olieX.d.cts.map +0 -1
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_compression_service = require("./compression-service-Bn86iTJe.cjs");
3
3
  const require_logger_service = require("./logger-service-cx8vzkXs.cjs");
4
- const require_upload_error = require("./upload-error-BUexBh08.cjs");
5
- const require_normalize_callback = require("./normalize-callback-BNBZZ1jT.cjs");
4
+ const require_upload_error = require("./upload-error-BG1dOOl3.cjs");
5
+ const require_normalize_callback = require("./normalize-callback-BdLtk9jb.cjs");
6
6
  let effect = require("effect");
7
7
  //#region src/multipart/circuit-breaker.ts
8
8
  const makeCircuitBreaker = (config) => effect.Effect.gen(function* () {
@@ -77,20 +77,49 @@ const chunkStream = (stream, chunkSize) => {
77
77
  const DEFAULT_MAX_CONCURRENCY = 4;
78
78
  const DEFAULT_RETRY_SCHEDULE = effect.Schedule.exponential("100 millis").pipe(effect.Schedule.compose(effect.Schedule.recurs(2)));
79
79
  const uploadMultipartEffect = (options) => {
80
- const { stream, chunkSize, uploadPart, completeUpload, initiate, reconcileCompletedParts, maxConcurrency = DEFAULT_MAX_CONCURRENCY, signal, retrySchedule = DEFAULT_RETRY_SCHEDULE } = options;
80
+ const { stream, chunkSize, uploadPart, completeUpload, initiate, reconcileCompletedParts, resumeFrom, getContentDigest, pipelineIdentity, maxConcurrency = DEFAULT_MAX_CONCURRENCY, signal, retrySchedule = DEFAULT_RETRY_SCHEDULE } = options;
81
+ if (!Number.isFinite(chunkSize) || chunkSize <= 0) throw new TypeError(`uploadMultipart: chunkSize must be a positive finite number, got ${chunkSize}`);
82
+ if (resumeFrom !== void 0) {
83
+ if (typeof resumeFrom.uploadId !== "string" || resumeFrom.uploadId === "") throw new TypeError("uploadMultipart: ResumeState.uploadId must be a non-empty string");
84
+ if (resumeFrom.version !== 1) throw new require_upload_error.ResumeMismatchError("version_mismatch");
85
+ if (resumeFrom.chunkSize !== chunkSize) throw new require_upload_error.ResumeMismatchError("chunksize_mismatch");
86
+ if (resumeFrom.pipelineIdentity !== pipelineIdentity) throw new require_upload_error.ResumeMismatchError("pipeline_mismatch");
87
+ if (resumeFrom.contentDigestCaptured === true && resumeFrom.contentDigest === void 0) throw new require_upload_error.ResumeMismatchError("content_mismatch");
88
+ }
81
89
  return effect.Stream.unwrap(effect.Effect.gen(function* () {
82
90
  const logger = yield* require_logger_service.LoggerService;
83
91
  const semaphore = yield* effect.Effect.makeSemaphore(maxConcurrency);
84
92
  const refParts = yield* effect.Ref.make([]);
85
93
  const refBytesUploaded = yield* effect.Ref.make(0);
86
94
  const refUploadId = yield* effect.Ref.make("");
95
+ const refDigest = yield* effect.Ref.make(effect.Option.none());
87
96
  const breaker = options.circuitBreaker ? yield* makeCircuitBreaker(options.circuitBreaker) : null;
88
97
  const reconciledMap = reconcileCompletedParts ? new Map((yield* require_normalize_callback.normalizeCallback(reconcileCompletedParts).pipe(effect.Effect.mapError((cause) => new require_upload_error.ReconcileError(cause)))).map((p) => [p.partNumber, p.etag])) : /* @__PURE__ */ new Map();
89
- const initiateStream = initiate ? effect.Stream.fromEffect(require_normalize_callback.normalizeCallback(initiate).pipe(effect.Effect.mapError((cause) => new require_upload_error.InitiateUploadError(cause)), effect.Effect.flatMap(({ uploadId }) => effect.Ref.set(refUploadId, uploadId).pipe(effect.Effect.as({
90
- _tag: "UploadInitiated",
91
- uploadId,
92
- timestamp: Date.now()
93
- }))))) : effect.Stream.empty;
98
+ const runFreshInit = effect.Effect.gen(function* () {
99
+ const { uploadId } = yield* require_normalize_callback.normalizeCallback(initiate).pipe(effect.Effect.mapError((cause) => new require_upload_error.InitiateUploadError(cause)));
100
+ yield* effect.Ref.set(refUploadId, uploadId);
101
+ if (getContentDigest !== void 0) {
102
+ const digest = yield* require_normalize_callback.normalizeCallback(getContentDigest).pipe(effect.Effect.mapError((cause) => new require_upload_error.InitiateUploadError(cause)));
103
+ yield* effect.Ref.set(refDigest, effect.Option.some(digest));
104
+ }
105
+ const capturedDigest = yield* effect.Ref.get(refDigest);
106
+ return {
107
+ _tag: "UploadInitiated",
108
+ uploadId,
109
+ contentDigest: effect.Option.getOrUndefined(capturedDigest),
110
+ timestamp: Date.now()
111
+ };
112
+ });
113
+ const runResumeSetup = effect.Effect.gen(function* () {
114
+ const rf = resumeFrom;
115
+ if (rf.contentDigest !== void 0 && getContentDigest !== void 0) {
116
+ const digest = yield* require_normalize_callback.normalizeCallback(getContentDigest).pipe(effect.Effect.mapError((cause) => new require_upload_error.ResumeMismatchError("content_mismatch", cause)));
117
+ if (digest !== rf.contentDigest) return yield* effect.Effect.fail(new require_upload_error.ResumeMismatchError("content_mismatch"));
118
+ yield* effect.Ref.set(refDigest, effect.Option.some(digest));
119
+ }
120
+ yield* effect.Ref.set(refUploadId, rf.uploadId);
121
+ });
122
+ const setupStream = resumeFrom !== void 0 ? effect.Stream.fromEffect(runResumeSetup).pipe(effect.Stream.drain) : initiate !== void 0 ? effect.Stream.fromEffect(runFreshInit) : effect.Stream.empty;
94
123
  const makeUploadOne = (partNumber, chunk) => effect.Effect.gen(function* () {
95
124
  const reconciledEtag = reconciledMap.get(partNumber);
96
125
  if (reconciledEtag !== void 0) {
@@ -184,12 +213,15 @@ const uploadMultipartEffect = (options) => {
184
213
  timestamp: Date.now()
185
214
  };
186
215
  });
187
- return effect.Stream.concat(initiateStream, partsStream.pipe(effect.Stream.concat(effect.Stream.fromEffect(finalEffect))));
216
+ return effect.Stream.concat(setupStream, partsStream.pipe(effect.Stream.concat(effect.Stream.fromEffect(finalEffect))));
188
217
  }));
189
218
  };
190
219
  //#endregion
191
220
  //#region src/multipart/index.ts
221
+ const NO_RESUME_CONTEXT_ERROR_MESSAGE = "uploadMultipart: resumeState is only available when `initiate` or `resumeFrom` is provided";
192
222
  const uploadMultipart = (options) => {
223
+ if (options.initiate !== void 0 && options.reconcileCompletedParts !== void 0 && options.resumeFrom === void 0) console.warn("Tranquilload: detected legacy resume pattern. You're passing `initiate` and `reconcileCompletedParts` without `resumeFrom: ResumeState`. The new API requires the persisted ResumeState to validate chunkSize/pipeline/digest match across sessions. See MIGRATION.md for migration steps.");
224
+ if (options.pipeline !== void 0 && options.pipelineIdentity === void 0) console.warn("Tranquilload: pipeline is set but pipelineIdentity is not. Without an identity, the resume validation cannot detect a pipeline mismatch across sessions. See README → Resume Safety.");
193
225
  const refProgress = effect.Effect.runSync(effect.Ref.make({
194
226
  bytesUploaded: 0,
195
227
  totalBytes: options.totalBytes !== void 0 ? effect.Option.some(options.totalBytes) : effect.Option.none()
@@ -198,6 +230,26 @@ const uploadMultipart = (options) => {
198
230
  const uploadIdPromise = new Promise((resolve) => {
199
231
  resolveUploadId = resolve;
200
232
  });
233
+ let resolveResumeState;
234
+ let rejectResumeState;
235
+ let resumeStateSettled = false;
236
+ const resumeStatePromise = new Promise((resolve, reject) => {
237
+ resolveResumeState = (s) => {
238
+ if (resumeStateSettled) return;
239
+ resumeStateSettled = true;
240
+ resolve(s);
241
+ };
242
+ rejectResumeState = (e) => {
243
+ if (resumeStateSettled) return;
244
+ resumeStateSettled = true;
245
+ reject(e);
246
+ };
247
+ });
248
+ resumeStatePromise.catch(() => {});
249
+ if (options.resumeFrom !== void 0) {
250
+ resolveUploadId(options.resumeFrom.uploadId);
251
+ resolveResumeState(options.resumeFrom);
252
+ }
201
253
  const collected = (async () => {
202
254
  let processedStream = options.stream;
203
255
  if (options.pipeline !== void 0) if (typeof options.pipeline === "function") processedStream = options.pipeline(options.stream);
@@ -206,7 +258,18 @@ const uploadMultipart = (options) => {
206
258
  ...options,
207
259
  stream: processedStream
208
260
  }).pipe(effect.Stream.tap((event) => {
209
- if (event._tag === "UploadInitiated") return effect.Effect.sync(() => resolveUploadId(event.uploadId));
261
+ if (event._tag === "UploadInitiated") return effect.Effect.sync(() => {
262
+ resolveUploadId(event.uploadId);
263
+ const state = {
264
+ version: 1,
265
+ uploadId: event.uploadId,
266
+ chunkSize: options.chunkSize,
267
+ ...options.pipelineIdentity !== void 0 ? { pipelineIdentity: options.pipelineIdentity } : {},
268
+ ...event.contentDigest !== void 0 ? { contentDigest: event.contentDigest } : {},
269
+ contentDigestCaptured: options.getContentDigest !== void 0
270
+ };
271
+ resolveResumeState(state);
272
+ });
210
273
  if (event._tag === "PartCompleted") return effect.Ref.update(refProgress, (p) => ({
211
274
  ...p,
212
275
  bytesUploaded: p.bytesUploaded + event.bytesUploaded
@@ -217,7 +280,11 @@ const uploadMultipart = (options) => {
217
280
  if (effect.Exit.isSuccess(exit)) return exit.value;
218
281
  return Promise.reject(effect.Cause.squash(exit.cause));
219
282
  })();
220
- collected.finally(() => resolveUploadId("")).catch(() => {});
283
+ collected.then(() => {
284
+ if (!resumeStateSettled) rejectResumeState(new Error(NO_RESUME_CONTEXT_ERROR_MESSAGE));
285
+ }).catch((err) => {
286
+ if (!resumeStateSettled) rejectResumeState(err);
287
+ }).finally(() => resolveUploadId(""));
221
288
  return {
222
289
  events: new ReadableStream({ async start(controller) {
223
290
  try {
@@ -234,7 +301,8 @@ const uploadMultipart = (options) => {
234
301
  return last;
235
302
  }),
236
303
  getProgress: Object.assign(() => effect.Effect.runPromise(effect.Ref.get(refProgress)), { effect: effect.Ref.get(refProgress) }),
237
- uploadId: uploadIdPromise
304
+ uploadId: uploadIdPromise,
305
+ resumeState: resumeStatePromise
238
306
  };
239
307
  };
240
308
  uploadMultipart.effect = uploadMultipartEffect;
@@ -1 +1 @@
1
- {"version":3,"file":"multipart.cjs","names":["Effect","Ref","CircuitOpenError","Stream","Schedule","Stream","Effect","LoggerService","Ref","normalizeCallback","ReconcileError","InitiateUploadError","PartUploadError","MaxRetriesExceededError","fromAbortSignal","Exit","CircuitOpenError","Cause","Option","CompleteUploadError","Effect","Ref","Option","CompressionServiceLive","Stream","LoggerServiceLive","Exit","Cause"],"sources":["../src/multipart/circuit-breaker.ts","../src/multipart/chunk-stream.ts","../src/multipart/upload-stream.ts","../src/multipart/index.ts"],"sourcesContent":["import { Effect, Ref } from \"effect\"\nimport { CircuitOpenError } from \"../errors/upload-error.js\"\nimport type { CircuitOpen } from \"../progress/upload-event.js\"\n\nexport interface CircuitBreakerConfig {\n readonly threshold: number\n readonly cooldown: number\n}\n\ntype CircuitState =\n | { readonly _tag: \"Closed\"; readonly consecutiveFailures: number }\n | { readonly _tag: \"Open\"; readonly openedAt: number }\n | { readonly _tag: \"HalfOpen\" }\n\nexport interface CircuitBreaker {\n readonly guard: Effect.Effect<void, CircuitOpenError>\n readonly onSuccess: Effect.Effect<void>\n readonly onFailure: Effect.Effect<CircuitOpen | null>\n}\n\nexport const makeCircuitBreaker = (config: CircuitBreakerConfig): Effect.Effect<CircuitBreaker> =>\n Effect.gen(function* () {\n const refState = yield* Ref.make<CircuitState>({ _tag: \"Closed\", consecutiveFailures: 0 })\n\n const guard: Effect.Effect<void, CircuitOpenError> = Effect.gen(function* () {\n const blocked = yield* Ref.modify(refState, (state): [boolean, CircuitState] => {\n if (state._tag !== \"Open\") return [false, state]\n const elapsed = Date.now() - state.openedAt\n if (elapsed < config.cooldown) return [true, state]\n return [false, { _tag: \"HalfOpen\" as const }]\n })\n if (blocked) {\n return yield* Effect.fail(new CircuitOpenError(config.threshold))\n }\n })\n\n const onSuccess: Effect.Effect<void> = Ref.update(refState, state =>\n state._tag === \"HalfOpen\" || state._tag === \"Closed\"\n ? { _tag: \"Closed\" as const, consecutiveFailures: 0 }\n : state\n )\n\n const onFailure: Effect.Effect<CircuitOpen | null> = Ref.modify(refState, (state): [CircuitOpen | null, CircuitState] => {\n if (state._tag === \"Closed\") {\n const newFailures = state.consecutiveFailures + 1\n if (newFailures >= config.threshold) {\n const event: CircuitOpen = {\n _tag: \"CircuitOpen\",\n failedParts: newFailures,\n timestamp: Date.now(),\n }\n return [event, { _tag: \"Open\" as const, openedAt: Date.now() }]\n }\n return [null, { _tag: \"Closed\" as const, consecutiveFailures: newFailures }]\n }\n if (state._tag === \"HalfOpen\") {\n const event: CircuitOpen = {\n _tag: \"CircuitOpen\",\n failedParts: config.threshold,\n timestamp: Date.now(),\n }\n return [event, { _tag: \"Open\" as const, openedAt: Date.now() }]\n }\n return [null, state]\n })\n\n return { guard, onSuccess, onFailure }\n })\n","import { Stream } from \"effect\"\n\nexport const chunkStream = (\n stream: ReadableStream<Uint8Array>,\n chunkSize: number\n): Stream.Stream<Uint8Array, unknown> => {\n let buffer = new Uint8Array(0)\n\n const transform = new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n // Concatenate incoming chunk into the buffer\n const merged = new Uint8Array(buffer.length + chunk.length)\n merged.set(buffer)\n merged.set(chunk, buffer.length)\n buffer = merged\n\n // Emit every full-size chunk\n while (buffer.length >= chunkSize) {\n controller.enqueue(buffer.slice(0, chunkSize))\n buffer = buffer.slice(chunkSize)\n }\n },\n flush(controller) {\n // Emit remaining bytes (last partial chunk)\n if (buffer.length > 0) {\n controller.enqueue(buffer)\n }\n },\n })\n\n const chunked = stream.pipeThrough(transform)\n\n return Stream.fromReadableStream(\n () => chunked,\n (e) => e\n )\n}\n","import { Cause, Effect, Exit, Option, Ref, Schedule, Stream } from \"effect\"\nimport type { UploadError } from \"../errors/upload-error.js\"\nimport { CircuitOpenError, CompleteUploadError, InitiateUploadError, MaxRetriesExceededError, PartUploadError, ReconcileError } from \"../errors/upload-error.js\"\nimport type { CircuitOpen, PartCompleted, ProgressTick, UploadCompleted, UploadEvent, UploadInitiated } from \"../progress/upload-event.js\"\nimport { LoggerService } from \"../services/logger-service.js\"\nimport { fromAbortSignal } from \"../utils/abort-interop.js\"\nimport { normalizeCallback } from \"../utils/normalize-callback.js\"\nimport { makeCircuitBreaker, type CircuitBreakerConfig } from \"./circuit-breaker.js\"\nimport { chunkStream } from \"./chunk-stream.js\"\n\nexport interface CompletedPart {\n readonly partNumber: number\n readonly etag: string\n}\n\nexport interface UploadMultipartOptions {\n readonly stream: ReadableStream<Uint8Array>\n readonly chunkSize: number\n readonly uploadPart: (\n partNumber: number,\n chunk: Uint8Array\n ) => string | Promise<string> | Effect.Effect<string, UploadError>\n readonly completeUpload: (\n uploadId: string,\n parts: ReadonlyArray<CompletedPart>\n ) => void | Promise<void> | Effect.Effect<void, UploadError>\n readonly initiate?: () =>\n | { uploadId: string }\n | Promise<{ uploadId: string }>\n | Effect.Effect<{ uploadId: string }, UploadError>\n readonly reconcileCompletedParts?: () =>\n | ReadonlyArray<CompletedPart>\n | Promise<ReadonlyArray<CompletedPart>>\n | Effect.Effect<ReadonlyArray<CompletedPart>, UploadError>\n readonly maxConcurrency?: number\n readonly signal?: AbortSignal\n readonly retrySchedule?: Schedule.Schedule<unknown, PartUploadError>\n readonly circuitBreaker?: CircuitBreakerConfig\n}\n\nconst DEFAULT_MAX_CONCURRENCY = 4\n\n// 3 total attempts: 1 initial + 2 retries, with exponential backoff\nconst DEFAULT_RETRY_SCHEDULE = Schedule.exponential(\"100 millis\").pipe(\n Schedule.compose(Schedule.recurs(2))\n)\n\nexport const uploadMultipartEffect = (\n options: UploadMultipartOptions\n): Stream.Stream<UploadEvent, UploadError, LoggerService> => {\n const {\n stream,\n chunkSize,\n uploadPart,\n completeUpload,\n initiate,\n reconcileCompletedParts,\n maxConcurrency = DEFAULT_MAX_CONCURRENCY,\n signal,\n retrySchedule = DEFAULT_RETRY_SCHEDULE,\n } = options\n\n return Stream.unwrap(\n Effect.gen(function* () {\n const logger = yield* LoggerService\n const semaphore = yield* Effect.makeSemaphore(maxConcurrency)\n const refParts = yield* Ref.make<CompletedPart[]>([])\n const refBytesUploaded = yield* Ref.make(0)\n const refUploadId = yield* Ref.make(\"\")\n const breaker = options.circuitBreaker\n ? yield* makeCircuitBreaker(options.circuitBreaker)\n : null\n\n const reconciledMap: Map<number, string> = reconcileCompletedParts\n ? new Map(\n (yield* normalizeCallback(reconcileCompletedParts).pipe(\n Effect.mapError((cause): UploadError => new ReconcileError(cause))\n )).map(p => [p.partNumber, p.etag])\n )\n : new Map()\n\n const initiateStream: Stream.Stream<UploadEvent, UploadError, never> = initiate\n ? Stream.fromEffect(\n normalizeCallback(initiate).pipe(\n Effect.mapError((cause): UploadError => new InitiateUploadError(cause)),\n Effect.flatMap(({ uploadId }) =>\n Ref.set(refUploadId, uploadId).pipe(\n Effect.as({\n _tag: \"UploadInitiated\" as const,\n uploadId,\n timestamp: Date.now(),\n } satisfies UploadInitiated)\n )\n )\n )\n )\n : Stream.empty\n\n const makeUploadOne = (\n partNumber: number,\n chunk: Uint8Array\n ): Effect.Effect<PartCompleted, UploadError> =>\n Effect.gen(function* () {\n const reconciledEtag = reconciledMap.get(partNumber)\n if (reconciledEtag !== undefined) {\n const event: PartCompleted = {\n _tag: \"PartCompleted\" as const,\n partNumber,\n etag: reconciledEtag,\n bytesUploaded: chunk.length,\n timestamp: Date.now(),\n }\n yield* Ref.update(refParts, parts => [...parts, { partNumber, etag: reconciledEtag }])\n yield* Effect.sync(() => logger.log(\"info\", `Part ${partNumber} skipped (reconciled)`))\n return event\n }\n\n const refAttempts = yield* Ref.make(0)\n\n const single: Effect.Effect<string, PartUploadError> = Effect.gen(function* () {\n yield* Ref.update(refAttempts, n => n + 1)\n const attempt = yield* Ref.get(refAttempts)\n return yield* normalizeCallback(() => uploadPart(partNumber, chunk)).pipe(\n Effect.mapError(\n (cause): PartUploadError => new PartUploadError(partNumber, attempt, cause)\n )\n )\n })\n\n const etag = yield* Effect.retry(single, retrySchedule).pipe(\n Effect.catchAll(err =>\n Effect.gen(function* () {\n const totalAttempts = yield* Ref.get(refAttempts)\n if (totalAttempts <= 1) {\n return yield* Effect.fail(err)\n }\n return yield* Effect.fail(\n new MaxRetriesExceededError(partNumber, totalAttempts, err.cause)\n )\n })\n )\n )\n\n const event: PartCompleted = {\n _tag: \"PartCompleted\" as const,\n partNumber,\n etag,\n bytesUploaded: chunk.length,\n timestamp: Date.now(),\n }\n\n yield* Ref.update(refParts, parts => [...parts, { partNumber, etag }])\n yield* Effect.sync(() => logger.log(\"info\", `Part ${partNumber} completed`))\n return event\n })\n\n const partsStream: Stream.Stream<UploadEvent, UploadError, never> = chunkStream(\n stream,\n chunkSize\n ).pipe(\n Stream.mapError((cause): UploadError => new PartUploadError(0, 0, cause)),\n Stream.zipWithIndex,\n Stream.mapEffect(\n ([chunk, idx]) => {\n const partNumber = Number(idx) + 1\n\n if (!breaker) {\n const partEffect = semaphore.withPermits(1)(\n makeUploadOne(partNumber, chunk)\n )\n return signal ? Effect.raceFirst(partEffect, fromAbortSignal(signal)) : partEffect\n }\n\n const partEffect = Effect.gen(function* () {\n yield* breaker.guard\n return yield* semaphore.withPermits(1)(\n Effect.gen(function* () {\n const exit = yield* Effect.exit(makeUploadOne(partNumber, chunk))\n if (Exit.isSuccess(exit)) {\n yield* breaker.onSuccess\n return exit.value\n }\n const circuitEvent = yield* breaker.onFailure\n if (circuitEvent !== null) {\n return yield* Effect.fail(new CircuitOpenError(circuitEvent.failedParts))\n }\n return yield* Effect.fail(Cause.squash(exit.cause) as UploadError)\n })\n )\n })\n\n return signal ? Effect.raceFirst(partEffect, fromAbortSignal(signal)) : partEffect\n },\n { concurrency: \"unbounded\" }\n ),\n Stream.flatMap(\n (event): Stream.Stream<UploadEvent, UploadError, never> => {\n const tickEffect = Ref.updateAndGet(refBytesUploaded, (n) => n + event.bytesUploaded).pipe(\n Effect.map(\n (total): ProgressTick => ({\n _tag: \"ProgressTick\" as const,\n bytesUploaded: total,\n totalBytes: Option.none(),\n timestamp: Date.now(),\n })\n )\n )\n return Stream.concat(Stream.make(event), Stream.fromEffect(tickEffect))\n }\n ),\n Stream.catchAll((err: UploadError): Stream.Stream<UploadEvent, UploadError, never> => {\n if (breaker && err._tag === \"CircuitOpenError\") {\n const event: UploadEvent = {\n _tag: \"CircuitOpen\",\n failedParts: err.failedParts,\n timestamp: Date.now(),\n }\n return Stream.concat(Stream.succeed(event), Stream.fail(err))\n }\n return Stream.fail(err)\n })\n )\n\n const finalEffect: Effect.Effect<UploadEvent, UploadError, never> = Effect.gen(\n function* () {\n const uploadId = yield* Ref.get(refUploadId)\n const parts = yield* Ref.get(refParts)\n yield* normalizeCallback(() => completeUpload(uploadId, parts)).pipe(\n Effect.mapError(\n (cause): UploadError => new CompleteUploadError(cause)\n )\n )\n yield* Effect.sync(() => logger.log(\"info\", \"Multipart upload completed\"))\n return {\n _tag: \"UploadCompleted\" as const,\n uploadId,\n totalParts: parts.length,\n timestamp: Date.now(),\n } satisfies UploadCompleted\n }\n )\n\n return Stream.concat(initiateStream, partsStream.pipe(Stream.concat(Stream.fromEffect(finalEffect))))\n })\n )\n}\n","import { Cause, Effect, Exit, Option, Ref, Stream } from \"effect\"\nimport type { UploadCompleted, UploadEvent } from \"../progress/upload-event.js\"\nimport type { Transform } from \"../pipeline/middleware.js\"\nimport { CompressionServiceLive } from \"../services/compression-service.js\"\nimport { LoggerServiceLive } from \"../services/logger-service.js\"\nimport { uploadMultipartEffect, type CompletedPart, type UploadMultipartOptions } from \"./upload-stream.js\"\n\nexport type UploadResult = UploadCompleted\nexport type { CompletedPart, UploadMultipartOptions }\n\nexport interface Progress {\n readonly bytesUploaded: number\n readonly totalBytes: Option.Option<number>\n}\n\nexport interface MultipartPublicOptions extends UploadMultipartOptions {\n readonly totalBytes?: number\n readonly pipeline?: Transform | Effect.Effect<Transform, unknown, unknown>\n}\n\nexport const uploadMultipart = (\n options: MultipartPublicOptions\n): {\n events: ReadableStream<UploadEvent>\n result: Promise<UploadResult>\n getProgress: (() => Promise<Progress>) & { effect: Effect.Effect<Progress> }\n uploadId: Promise<string>\n} => {\n const refProgress = Effect.runSync(\n Ref.make<Progress>({\n bytesUploaded: 0,\n totalBytes: options.totalBytes !== undefined ? Option.some(options.totalBytes) : Option.none(),\n })\n )\n\n let resolveUploadId!: (id: string) => void\n const uploadIdPromise: Promise<string> = new Promise<string>((resolve) => {\n resolveUploadId = resolve\n })\n\n const collected: Promise<ReadonlyArray<UploadEvent>> = (async () => {\n // Step 1: resolve pipeline to get the processed stream\n let processedStream = options.stream\n if (options.pipeline !== undefined) {\n if (typeof options.pipeline === \"function\") {\n processedStream = options.pipeline(options.stream)\n } else {\n // Effect pipeline — resolve with CompressionServiceLive\n const transform = await Effect.runPromise(\n Effect.provide(\n options.pipeline as Effect.Effect<Transform, unknown, never>,\n CompressionServiceLive\n )\n )\n processedStream = transform(options.stream)\n }\n }\n\n // Step 2: run upload with processedStream\n const program = uploadMultipartEffect({ ...options, stream: processedStream }).pipe(\n Stream.tap((event) => {\n if (event._tag === \"UploadInitiated\") {\n return Effect.sync(() => resolveUploadId(event.uploadId))\n }\n if (event._tag === \"PartCompleted\") {\n return Ref.update(refProgress, (p) => ({\n ...p,\n bytesUploaded: p.bytesUploaded + event.bytesUploaded,\n }))\n }\n return Effect.void\n }),\n Stream.provideLayer(LoggerServiceLive)\n )\n\n const exit = await Stream.runCollect(program).pipe(\n Effect.map((chunk) => Array.from(chunk)),\n Effect.runPromiseExit\n )\n if (Exit.isSuccess(exit)) return exit.value\n return Promise.reject(Cause.squash(exit.cause))\n })()\n\n // Rejection is surfaced via `result`; suppress the propagated rejection from .finally()\n collected.finally(() => resolveUploadId(\"\")).catch(() => {})\n\n // events: ReadableStream built from collected array; closes cleanly on error\n const events = new ReadableStream<UploadEvent>({\n async start(controller) {\n try {\n const evts = await collected\n for (const event of evts) controller.enqueue(event)\n controller.close()\n } catch (_) {\n // Close cleanly — upload errors surface via `result` only\n controller.close()\n }\n },\n })\n\n // result: resolves with UploadCompleted, rejects with UploadError on failure\n const result: Promise<UploadResult> = collected.then((evts) => {\n const last = evts[evts.length - 1]\n if (last === undefined) {\n return Promise.reject(new Error(\"uploadMultipart: stream ended without emitting an event\"))\n }\n return last as UploadResult\n })\n\n const getProgress = Object.assign(\n (): Promise<Progress> => Effect.runPromise(Ref.get(refProgress)),\n { effect: Ref.get(refProgress) }\n )\n\n return { events, result, getProgress, uploadId: uploadIdPromise }\n}\n\n// Effect escape hatch — LoggerService layer left open for user composition\nuploadMultipart.effect = uploadMultipartEffect\n"],"mappings":"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"}
1
+ {"version":3,"file":"multipart.cjs","names":["Effect","Ref","CircuitOpenError","Stream","Schedule","ResumeMismatchError","Stream","Effect","LoggerService","Ref","Option","normalizeCallback","ReconcileError","InitiateUploadError","PartUploadError","MaxRetriesExceededError","fromAbortSignal","Exit","CircuitOpenError","Cause","CompleteUploadError","Effect","Ref","Option","CompressionServiceLive","Stream","LoggerServiceLive","Exit","Cause"],"sources":["../src/multipart/circuit-breaker.ts","../src/multipart/chunk-stream.ts","../src/multipart/upload-stream.ts","../src/multipart/index.ts"],"sourcesContent":["import { Effect, Ref } from \"effect\"\nimport { CircuitOpenError } from \"../errors/upload-error.js\"\nimport type { CircuitOpen } from \"../progress/upload-event.js\"\n\nexport interface CircuitBreakerConfig {\n readonly threshold: number\n readonly cooldown: number\n}\n\ntype CircuitState =\n | { readonly _tag: \"Closed\"; readonly consecutiveFailures: number }\n | { readonly _tag: \"Open\"; readonly openedAt: number }\n | { readonly _tag: \"HalfOpen\" }\n\nexport interface CircuitBreaker {\n readonly guard: Effect.Effect<void, CircuitOpenError>\n readonly onSuccess: Effect.Effect<void>\n readonly onFailure: Effect.Effect<CircuitOpen | null>\n}\n\nexport const makeCircuitBreaker = (config: CircuitBreakerConfig): Effect.Effect<CircuitBreaker> =>\n Effect.gen(function* () {\n const refState = yield* Ref.make<CircuitState>({ _tag: \"Closed\", consecutiveFailures: 0 })\n\n const guard: Effect.Effect<void, CircuitOpenError> = Effect.gen(function* () {\n const blocked = yield* Ref.modify(refState, (state): [boolean, CircuitState] => {\n if (state._tag !== \"Open\") return [false, state]\n const elapsed = Date.now() - state.openedAt\n if (elapsed < config.cooldown) return [true, state]\n return [false, { _tag: \"HalfOpen\" as const }]\n })\n if (blocked) {\n return yield* Effect.fail(new CircuitOpenError(config.threshold))\n }\n })\n\n const onSuccess: Effect.Effect<void> = Ref.update(refState, state =>\n state._tag === \"HalfOpen\" || state._tag === \"Closed\"\n ? { _tag: \"Closed\" as const, consecutiveFailures: 0 }\n : state\n )\n\n const onFailure: Effect.Effect<CircuitOpen | null> = Ref.modify(refState, (state): [CircuitOpen | null, CircuitState] => {\n if (state._tag === \"Closed\") {\n const newFailures = state.consecutiveFailures + 1\n if (newFailures >= config.threshold) {\n const event: CircuitOpen = {\n _tag: \"CircuitOpen\",\n failedParts: newFailures,\n timestamp: Date.now(),\n }\n return [event, { _tag: \"Open\" as const, openedAt: Date.now() }]\n }\n return [null, { _tag: \"Closed\" as const, consecutiveFailures: newFailures }]\n }\n if (state._tag === \"HalfOpen\") {\n const event: CircuitOpen = {\n _tag: \"CircuitOpen\",\n failedParts: config.threshold,\n timestamp: Date.now(),\n }\n return [event, { _tag: \"Open\" as const, openedAt: Date.now() }]\n }\n return [null, state]\n })\n\n return { guard, onSuccess, onFailure }\n })\n","import { Stream } from \"effect\"\n\nexport const chunkStream = (\n stream: ReadableStream<Uint8Array>,\n chunkSize: number\n): Stream.Stream<Uint8Array, unknown> => {\n let buffer = new Uint8Array(0)\n\n const transform = new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n // Concatenate incoming chunk into the buffer\n const merged = new Uint8Array(buffer.length + chunk.length)\n merged.set(buffer)\n merged.set(chunk, buffer.length)\n buffer = merged\n\n // Emit every full-size chunk\n while (buffer.length >= chunkSize) {\n controller.enqueue(buffer.slice(0, chunkSize))\n buffer = buffer.slice(chunkSize)\n }\n },\n flush(controller) {\n // Emit remaining bytes (last partial chunk)\n if (buffer.length > 0) {\n controller.enqueue(buffer)\n }\n },\n })\n\n const chunked = stream.pipeThrough(transform)\n\n return Stream.fromReadableStream(\n () => chunked,\n (e) => e\n )\n}\n","import { Cause, Effect, Exit, Option, Ref, Schedule, Stream } from \"effect\"\nimport type { UploadError } from \"../errors/upload-error.js\"\nimport { CircuitOpenError, CompleteUploadError, InitiateUploadError, MaxRetriesExceededError, PartUploadError, ReconcileError, ResumeMismatchError } from \"../errors/upload-error.js\"\nimport type { CircuitOpen, PartCompleted, ProgressTick, UploadCompleted, UploadEvent, UploadInitiated } from \"../progress/upload-event.js\"\nimport { LoggerService } from \"../services/logger-service.js\"\nimport { fromAbortSignal } from \"../utils/abort-interop.js\"\nimport { normalizeCallback } from \"../utils/normalize-callback.js\"\nimport { makeCircuitBreaker, type CircuitBreakerConfig } from \"./circuit-breaker.js\"\nimport { chunkStream } from \"./chunk-stream.js\"\n\nexport interface CompletedPart {\n readonly partNumber: number\n readonly etag: string\n}\n\n/**\n * Opaque resume metadata returned by `uploadMultipart` and persisted by the\n * caller (typically `JSON.stringify` → localStorage). Pass it back as\n * `resumeFrom` on the next session.\n *\n * The lib validates `version`, `chunkSize`, `pipelineIdentity`, and the content\n * digest before any byte is uploaded. A mismatch fails the upload with a typed\n * `ResumeMismatchError` — preventing the silent-corruption classes documented\n * in the v0.2.x release notes.\n *\n * **Schema versioning.** The `version: 1` literal is a tripwire for schema\n * evolution: future v2 schemas widen this union, and a persisted v1 state\n * passed to a future v2 lib will fail with `ResumeMismatchError(\"version_mismatch\")`\n * rather than silently misinterpreting old fields.\n */\nexport interface ResumeState {\n readonly version: 1\n readonly uploadId: string\n readonly chunkSize: number\n readonly pipelineIdentity?: string\n readonly contentDigest?: string\n /**\n * True if the original session captured a digest. Detects persistence layers\n * that drop the `contentDigest` field (which would otherwise silently bypass\n * content-mismatch validation).\n */\n readonly contentDigestCaptured: boolean\n}\n\nexport interface UploadMultipartOptions {\n readonly stream: ReadableStream<Uint8Array>\n readonly chunkSize: number\n readonly uploadPart: (\n partNumber: number,\n chunk: Uint8Array\n ) => string | Promise<string> | Effect.Effect<string, UploadError>\n readonly completeUpload: (\n uploadId: string,\n parts: ReadonlyArray<CompletedPart>\n ) => void | Promise<void> | Effect.Effect<void, UploadError>\n readonly initiate?: () =>\n | { uploadId: string }\n | Promise<{ uploadId: string }>\n | Effect.Effect<{ uploadId: string }, UploadError>\n readonly reconcileCompletedParts?: () =>\n | ReadonlyArray<CompletedPart>\n | Promise<ReadonlyArray<CompletedPart>>\n | Effect.Effect<ReadonlyArray<CompletedPart>, UploadError>\n /**\n * Resume metadata persisted from a previous session. When set, the lib skips\n * the `initiate` callback (the `uploadId` is read from `resumeFrom`) and\n * validates `version`, `chunkSize`, `pipelineIdentity`, and `contentDigest`\n * before any byte is uploaded. A mismatch fails the upload with\n * `ResumeMismatchError`.\n *\n * Synchronous (pre-flight) validation happens at `uploadMultipart()` call\n * time — `TypeError` for an empty `uploadId`, `ResumeMismatchError` for the\n * rest. The asynchronous content-digest *value* match is verified inside\n * the Effect once the upload stream is consumed.\n */\n readonly resumeFrom?: ResumeState\n /**\n * Called once on fresh initiate to capture a digest of the source content.\n * On a subsequent resume session, called again and compared to\n * `resumeFrom.contentDigest`; a mismatch fails the upload with\n * `ResumeMismatchError(\"content_mismatch\")` before any byte is uploaded.\n *\n * **MUST be lightweight and stable across sessions.** Suggested patterns:\n * - Browser `File`: `` `${name}|${size}|${lastModified}` ``\n * - Node `Readable` from a file: `` `${path}|${stat.size}|${stat.mtimeMs}` ``\n * - Synchronous strings; avoid full-file crypto hashes on the synchronous path.\n *\n * **MUST NOT consume bytes from the source stream** (passed in\n * `options.stream`). The lib calls `getContentDigest` before any chunk is\n * pulled from the source; consuming from the source here will produce a\n * zero-byte upload because no bytes remain for `chunkStream`.\n */\n readonly getContentDigest?: () =>\n | string\n | Promise<string>\n | Effect.Effect<string, UploadError>\n /**\n * An opaque, stable identifier for the upstream pipeline composition.\n * Captured in `ResumeState` and validated strict-equality on resume.\n * **You own keeping this stable** — if you configure `compress(\"deflate-raw\")`\n * in session A, you must pass the same `pipelineIdentity` on resume.\n *\n * **Strict equality limitation:** a pipeline that is logically identical but\n * produces different identifier strings (e.g. tag bumps, version-stamped\n * strings) triggers `ResumeMismatchError(\"pipeline_mismatch\")`. Pick a stable\n * string (e.g. `\"deflate-raw-v1\"`) and only change it when the pipeline's\n * *byte-level output* changes.\n *\n * **Compression non-determinism caveat:** even with identical\n * `pipelineIdentity`, a non-deterministic pipeline (e.g. gzip with `mtime`\n * headers, encryption with random salt) produces different bytes per run.\n * Resume against the same uploaded parts only works if the pipeline is\n * byte-deterministic. Verify your pipeline's determinism before relying on\n * this.\n */\n readonly pipelineIdentity?: string\n readonly maxConcurrency?: number\n readonly signal?: AbortSignal\n readonly retrySchedule?: Schedule.Schedule<unknown, PartUploadError>\n readonly circuitBreaker?: CircuitBreakerConfig\n}\n\nconst DEFAULT_MAX_CONCURRENCY = 4\n\n// 3 total attempts: 1 initial + 2 retries, with exponential backoff\nconst DEFAULT_RETRY_SCHEDULE = Schedule.exponential(\"100 millis\").pipe(\n Schedule.compose(Schedule.recurs(2))\n)\n\nexport const uploadMultipartEffect = (\n options: UploadMultipartOptions\n): Stream.Stream<UploadEvent, UploadError, LoggerService> => {\n const {\n stream,\n chunkSize,\n uploadPart,\n completeUpload,\n initiate,\n reconcileCompletedParts,\n resumeFrom,\n getContentDigest,\n pipelineIdentity,\n maxConcurrency = DEFAULT_MAX_CONCURRENCY,\n signal,\n retrySchedule = DEFAULT_RETRY_SCHEDULE,\n } = options\n\n if (!Number.isFinite(chunkSize) || chunkSize <= 0) {\n throw new TypeError(\n `uploadMultipart: chunkSize must be a positive finite number, got ${chunkSize}`\n )\n }\n\n if (resumeFrom !== undefined) {\n if (typeof resumeFrom.uploadId !== \"string\" || resumeFrom.uploadId === \"\") {\n throw new TypeError(\n \"uploadMultipart: ResumeState.uploadId must be a non-empty string\"\n )\n }\n if (resumeFrom.version !== 1) {\n throw new ResumeMismatchError(\"version_mismatch\")\n }\n if (resumeFrom.chunkSize !== chunkSize) {\n throw new ResumeMismatchError(\"chunksize_mismatch\")\n }\n if (resumeFrom.pipelineIdentity !== pipelineIdentity) {\n throw new ResumeMismatchError(\"pipeline_mismatch\")\n }\n if (\n resumeFrom.contentDigestCaptured === true &&\n resumeFrom.contentDigest === undefined\n ) {\n throw new ResumeMismatchError(\"content_mismatch\")\n }\n }\n\n return Stream.unwrap(\n Effect.gen(function* () {\n const logger = yield* LoggerService\n const semaphore = yield* Effect.makeSemaphore(maxConcurrency)\n const refParts = yield* Ref.make<CompletedPart[]>([])\n const refBytesUploaded = yield* Ref.make(0)\n const refUploadId = yield* Ref.make(\"\")\n const refDigest = yield* Ref.make<Option.Option<string>>(Option.none())\n const breaker = options.circuitBreaker\n ? yield* makeCircuitBreaker(options.circuitBreaker)\n : null\n\n const reconciledMap: Map<number, string> = reconcileCompletedParts\n ? new Map(\n (yield* normalizeCallback(reconcileCompletedParts).pipe(\n Effect.mapError((cause): UploadError => new ReconcileError(cause))\n )).map(p => [p.partNumber, p.etag])\n )\n : new Map()\n\n const runFreshInit: Effect.Effect<UploadInitiated, UploadError> = Effect.gen(\n function* () {\n const { uploadId } = yield* normalizeCallback(initiate!).pipe(\n Effect.mapError((cause): UploadError => new InitiateUploadError(cause))\n )\n yield* Ref.set(refUploadId, uploadId)\n if (getContentDigest !== undefined) {\n const digest = yield* normalizeCallback(getContentDigest).pipe(\n Effect.mapError((cause): UploadError => new InitiateUploadError(cause))\n )\n yield* Ref.set(refDigest, Option.some(digest))\n }\n const capturedDigest = yield* Ref.get(refDigest)\n return {\n _tag: \"UploadInitiated\" as const,\n uploadId,\n contentDigest: Option.getOrUndefined(capturedDigest),\n timestamp: Date.now(),\n } satisfies UploadInitiated\n }\n )\n\n const runResumeSetup: Effect.Effect<void, UploadError> = Effect.gen(function* () {\n // `resumeFrom` is non-undefined here (checked by setupStream selector below).\n const rf = resumeFrom!\n if (rf.contentDigest !== undefined && getContentDigest !== undefined) {\n const digest = yield* normalizeCallback(getContentDigest).pipe(\n Effect.mapError(\n (cause): UploadError => new ResumeMismatchError(\"content_mismatch\", cause)\n )\n )\n if (digest !== rf.contentDigest) {\n return yield* Effect.fail(new ResumeMismatchError(\"content_mismatch\"))\n }\n yield* Ref.set(refDigest, Option.some(digest))\n }\n yield* Ref.set(refUploadId, rf.uploadId)\n })\n\n const setupStream: Stream.Stream<UploadEvent, UploadError, never> =\n resumeFrom !== undefined\n ? Stream.fromEffect(runResumeSetup).pipe(Stream.drain)\n : initiate !== undefined\n ? Stream.fromEffect(runFreshInit)\n : Stream.empty\n\n const makeUploadOne = (\n partNumber: number,\n chunk: Uint8Array\n ): Effect.Effect<PartCompleted, UploadError> =>\n Effect.gen(function* () {\n const reconciledEtag = reconciledMap.get(partNumber)\n if (reconciledEtag !== undefined) {\n const event: PartCompleted = {\n _tag: \"PartCompleted\" as const,\n partNumber,\n etag: reconciledEtag,\n bytesUploaded: chunk.length,\n timestamp: Date.now(),\n }\n yield* Ref.update(refParts, parts => [...parts, { partNumber, etag: reconciledEtag }])\n yield* Effect.sync(() => logger.log(\"info\", `Part ${partNumber} skipped (reconciled)`))\n return event\n }\n\n const refAttempts = yield* Ref.make(0)\n\n const single: Effect.Effect<string, PartUploadError> = Effect.gen(function* () {\n yield* Ref.update(refAttempts, n => n + 1)\n const attempt = yield* Ref.get(refAttempts)\n return yield* normalizeCallback(() => uploadPart(partNumber, chunk)).pipe(\n Effect.mapError(\n (cause): PartUploadError => new PartUploadError(partNumber, attempt, cause)\n )\n )\n })\n\n const etag = yield* Effect.retry(single, retrySchedule).pipe(\n Effect.catchAll(err =>\n Effect.gen(function* () {\n const totalAttempts = yield* Ref.get(refAttempts)\n if (totalAttempts <= 1) {\n return yield* Effect.fail(err)\n }\n return yield* Effect.fail(\n new MaxRetriesExceededError(partNumber, totalAttempts, err.cause)\n )\n })\n )\n )\n\n const event: PartCompleted = {\n _tag: \"PartCompleted\" as const,\n partNumber,\n etag,\n bytesUploaded: chunk.length,\n timestamp: Date.now(),\n }\n\n yield* Ref.update(refParts, parts => [...parts, { partNumber, etag }])\n yield* Effect.sync(() => logger.log(\"info\", `Part ${partNumber} completed`))\n return event\n })\n\n const partsStream: Stream.Stream<UploadEvent, UploadError, never> = chunkStream(\n stream,\n chunkSize\n ).pipe(\n Stream.mapError((cause): UploadError => new PartUploadError(0, 0, cause)),\n Stream.zipWithIndex,\n Stream.mapEffect(\n ([chunk, idx]) => {\n const partNumber = Number(idx) + 1\n\n if (!breaker) {\n const partEffect = semaphore.withPermits(1)(\n makeUploadOne(partNumber, chunk)\n )\n return signal ? Effect.raceFirst(partEffect, fromAbortSignal(signal)) : partEffect\n }\n\n const partEffect = Effect.gen(function* () {\n yield* breaker.guard\n return yield* semaphore.withPermits(1)(\n Effect.gen(function* () {\n const exit = yield* Effect.exit(makeUploadOne(partNumber, chunk))\n if (Exit.isSuccess(exit)) {\n yield* breaker.onSuccess\n return exit.value\n }\n const circuitEvent = yield* breaker.onFailure\n if (circuitEvent !== null) {\n return yield* Effect.fail(new CircuitOpenError(circuitEvent.failedParts))\n }\n return yield* Effect.fail(Cause.squash(exit.cause) as UploadError)\n })\n )\n })\n\n return signal ? Effect.raceFirst(partEffect, fromAbortSignal(signal)) : partEffect\n },\n { concurrency: \"unbounded\" }\n ),\n Stream.flatMap(\n (event): Stream.Stream<UploadEvent, UploadError, never> => {\n const tickEffect = Ref.updateAndGet(refBytesUploaded, (n) => n + event.bytesUploaded).pipe(\n Effect.map(\n (total): ProgressTick => ({\n _tag: \"ProgressTick\" as const,\n bytesUploaded: total,\n totalBytes: Option.none(),\n timestamp: Date.now(),\n })\n )\n )\n return Stream.concat(Stream.make(event), Stream.fromEffect(tickEffect))\n }\n ),\n Stream.catchAll((err: UploadError): Stream.Stream<UploadEvent, UploadError, never> => {\n if (breaker && err._tag === \"CircuitOpenError\") {\n const event: UploadEvent = {\n _tag: \"CircuitOpen\",\n failedParts: err.failedParts,\n timestamp: Date.now(),\n }\n return Stream.concat(Stream.succeed(event), Stream.fail(err))\n }\n return Stream.fail(err)\n })\n )\n\n const finalEffect: Effect.Effect<UploadEvent, UploadError, never> = Effect.gen(\n function* () {\n const uploadId = yield* Ref.get(refUploadId)\n const parts = yield* Ref.get(refParts)\n yield* normalizeCallback(() => completeUpload(uploadId, parts)).pipe(\n Effect.mapError(\n (cause): UploadError => new CompleteUploadError(cause)\n )\n )\n yield* Effect.sync(() => logger.log(\"info\", \"Multipart upload completed\"))\n return {\n _tag: \"UploadCompleted\" as const,\n uploadId,\n totalParts: parts.length,\n timestamp: Date.now(),\n } satisfies UploadCompleted\n }\n )\n\n return Stream.concat(setupStream, partsStream.pipe(Stream.concat(Stream.fromEffect(finalEffect))))\n })\n )\n}\n","import { Cause, Effect, Exit, Option, Ref, Stream } from \"effect\"\nimport type { UploadCompleted, UploadEvent } from \"../progress/upload-event.js\"\nimport type { Transform } from \"../pipeline/middleware.js\"\nimport { CompressionServiceLive } from \"../services/compression-service.js\"\nimport { LoggerServiceLive } from \"../services/logger-service.js\"\nimport { uploadMultipartEffect, type CompletedPart, type ResumeState, type UploadMultipartOptions } from \"./upload-stream.js\"\n\nexport type UploadResult = UploadCompleted\nexport type { CompletedPart, ResumeState, UploadMultipartOptions }\n\nexport interface Progress {\n readonly bytesUploaded: number\n readonly totalBytes: Option.Option<number>\n}\n\nexport interface MultipartPublicOptions extends UploadMultipartOptions {\n readonly totalBytes?: number\n readonly pipeline?: Transform | Effect.Effect<Transform, unknown, unknown>\n}\n\nconst NO_RESUME_CONTEXT_ERROR_MESSAGE =\n \"uploadMultipart: resumeState is only available when `initiate` or `resumeFrom` is provided\"\n\nexport const uploadMultipart = (\n options: MultipartPublicOptions\n): {\n events: ReadableStream<UploadEvent>\n result: Promise<UploadResult>\n getProgress: (() => Promise<Progress>) & { effect: Effect.Effect<Progress> }\n uploadId: Promise<string>\n resumeState: Promise<ResumeState>\n} => {\n // Legacy-pattern detection: warns unconditionally so first-time-after-upgrade\n // users also see the migration message (per G3 — empty reconcile would have\n // hidden the warning under the old \"warn when reconcile returned >= 1\" rule).\n if (\n options.initiate !== undefined &&\n options.reconcileCompletedParts !== undefined &&\n options.resumeFrom === undefined\n ) {\n console.warn(\n \"Tranquilload: detected legacy resume pattern. You're passing `initiate` \" +\n \"and `reconcileCompletedParts` without `resumeFrom: ResumeState`. The new \" +\n \"API requires the persisted ResumeState to validate chunkSize/pipeline/\" +\n \"digest match across sessions. See MIGRATION.md for migration steps.\"\n )\n }\n // Pipeline-without-identity: the resume validation cannot detect a pipeline\n // mismatch across sessions when the user runs a pipeline but provides no\n // identity. We warn so the user is aware their resume safety is reduced.\n if (options.pipeline !== undefined && options.pipelineIdentity === undefined) {\n console.warn(\n \"Tranquilload: pipeline is set but pipelineIdentity is not. Without an \" +\n \"identity, the resume validation cannot detect a pipeline mismatch \" +\n \"across sessions. See README → Resume Safety.\"\n )\n }\n\n const refProgress = Effect.runSync(\n Ref.make<Progress>({\n bytesUploaded: 0,\n totalBytes: options.totalBytes !== undefined ? Option.some(options.totalBytes) : Option.none(),\n })\n )\n\n let resolveUploadId!: (id: string) => void\n const uploadIdPromise: Promise<string> = new Promise<string>((resolve) => {\n resolveUploadId = resolve\n })\n\n let resolveResumeState!: (s: ResumeState) => void\n let rejectResumeState!: (e: unknown) => void\n let resumeStateSettled = false\n const resumeStatePromise: Promise<ResumeState> = new Promise<ResumeState>((resolve, reject) => {\n resolveResumeState = (s) => {\n if (resumeStateSettled) return\n resumeStateSettled = true\n resolve(s)\n }\n rejectResumeState = (e) => {\n if (resumeStateSettled) return\n resumeStateSettled = true\n reject(e)\n }\n })\n // Avoid unhandled-rejection warnings: callers may not await resumeState.\n resumeStatePromise.catch(() => {})\n\n // Resume branch: resolve uploadId AND resumeState synchronously before the\n // stream runs. uploadId per AC22; resumeState per Task 4.1 (the lib has no\n // new state to add — the user already has the value they passed in).\n if (options.resumeFrom !== undefined) {\n resolveUploadId(options.resumeFrom.uploadId)\n resolveResumeState(options.resumeFrom)\n }\n\n const collected: Promise<ReadonlyArray<UploadEvent>> = (async () => {\n // Step 1: resolve pipeline to get the processed stream\n let processedStream = options.stream\n if (options.pipeline !== undefined) {\n if (typeof options.pipeline === \"function\") {\n processedStream = options.pipeline(options.stream)\n } else {\n // Effect pipeline — resolve with CompressionServiceLive\n const transform = await Effect.runPromise(\n Effect.provide(\n options.pipeline as Effect.Effect<Transform, unknown, never>,\n CompressionServiceLive\n )\n )\n processedStream = transform(options.stream)\n }\n }\n\n // Step 2: run upload with processedStream\n const program = uploadMultipartEffect({ ...options, stream: processedStream }).pipe(\n Stream.tap((event) => {\n if (event._tag === \"UploadInitiated\") {\n // Fresh-init branch: resolve uploadId on the event, and build the\n // ResumeState from the event payload + caller-supplied fields.\n return Effect.sync(() => {\n resolveUploadId(event.uploadId)\n const state: ResumeState = {\n version: 1,\n uploadId: event.uploadId,\n chunkSize: options.chunkSize,\n ...(options.pipelineIdentity !== undefined\n ? { pipelineIdentity: options.pipelineIdentity }\n : {}),\n ...(event.contentDigest !== undefined\n ? { contentDigest: event.contentDigest }\n : {}),\n contentDigestCaptured: options.getContentDigest !== undefined,\n }\n resolveResumeState(state)\n })\n }\n if (event._tag === \"PartCompleted\") {\n return Ref.update(refProgress, (p) => ({\n ...p,\n bytesUploaded: p.bytesUploaded + event.bytesUploaded,\n }))\n }\n return Effect.void\n }),\n Stream.provideLayer(LoggerServiceLive)\n )\n\n const exit = await Stream.runCollect(program).pipe(\n Effect.map((chunk) => Array.from(chunk)),\n Effect.runPromiseExit\n )\n if (Exit.isSuccess(exit)) return exit.value\n return Promise.reject(Cause.squash(exit.cause))\n })()\n\n // Rejection is surfaced via `result`; suppress the propagated rejection from .finally()\n collected\n .then(() => {\n // Success-but-no-context: neither initiate nor resumeFrom produced state.\n if (!resumeStateSettled) {\n rejectResumeState(new Error(NO_RESUME_CONTEXT_ERROR_MESSAGE))\n }\n })\n .catch((err) => {\n if (!resumeStateSettled) {\n rejectResumeState(err)\n }\n })\n .finally(() => resolveUploadId(\"\"))\n\n // events: ReadableStream built from collected array; closes cleanly on error\n const events = new ReadableStream<UploadEvent>({\n async start(controller) {\n try {\n const evts = await collected\n for (const event of evts) controller.enqueue(event)\n controller.close()\n } catch (_) {\n // Close cleanly — upload errors surface via `result` only\n controller.close()\n }\n },\n })\n\n // result: resolves with UploadCompleted, rejects with UploadError on failure\n const result: Promise<UploadResult> = collected.then((evts) => {\n const last = evts[evts.length - 1]\n if (last === undefined) {\n return Promise.reject(new Error(\"uploadMultipart: stream ended without emitting an event\"))\n }\n return last as UploadResult\n })\n\n const getProgress = Object.assign(\n (): Promise<Progress> => Effect.runPromise(Ref.get(refProgress)),\n { effect: Ref.get(refProgress) }\n )\n\n return {\n events,\n result,\n getProgress,\n uploadId: uploadIdPromise,\n resumeState: resumeStatePromise,\n }\n}\n\n// Effect escape hatch — LoggerService layer left open for user composition\nuploadMultipart.effect = uploadMultipartEffect\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { a as CompletedPart, i as uploadMultipart, n as Progress, o as UploadMultipartOptions, r as UploadResult, t as MultipartPublicOptions } from "./index-Ch8xM6Xt.cjs";
2
- export { CompletedPart, MultipartPublicOptions, Progress, UploadMultipartOptions, UploadResult, uploadMultipart };
1
+ import { a as CompletedPart, i as uploadMultipart, n as Progress, o as ResumeState, r as UploadResult, s as UploadMultipartOptions, t as MultipartPublicOptions } from "./index-BaeUV_fj.cjs";
2
+ export { CompletedPart, MultipartPublicOptions, Progress, ResumeState, UploadMultipartOptions, UploadResult, uploadMultipart };
@@ -1,2 +1,2 @@
1
- import { a as CompletedPart, i as uploadMultipart, n as Progress, o as UploadMultipartOptions, r as UploadResult, t as MultipartPublicOptions } from "./index-DBGtgXEd.mjs";
2
- export { CompletedPart, MultipartPublicOptions, Progress, UploadMultipartOptions, UploadResult, uploadMultipart };
1
+ import { a as CompletedPart, i as uploadMultipart, n as Progress, o as ResumeState, r as UploadResult, s as UploadMultipartOptions, t as MultipartPublicOptions } from "./index-bpWq6tje.mjs";
2
+ export { CompletedPart, MultipartPublicOptions, Progress, ResumeState, UploadMultipartOptions, UploadResult, uploadMultipart };
@@ -1,7 +1,7 @@
1
1
  import { n as CompressionServiceLive } from "./compression-service-Bm1VBnhT.mjs";
2
2
  import { n as LoggerServiceLive, t as LoggerService } from "./logger-service-1J5r_akj.mjs";
3
- import { a as MaxRetriesExceededError, c as ReconcileError, i as InitiateUploadError, n as CircuitOpenError, o as PartUploadError, r as CompleteUploadError } from "./upload-error-zDvpxT9X.mjs";
4
- import { n as fromAbortSignal, t as normalizeCallback } from "./normalize-callback-DQ6C4gaV.mjs";
3
+ import { a as MaxRetriesExceededError, c as ReconcileError, i as InitiateUploadError, l as ResumeMismatchError, n as CircuitOpenError, o as PartUploadError, r as CompleteUploadError } from "./upload-error-Dbz_9j81.mjs";
4
+ import { n as fromAbortSignal, t as normalizeCallback } from "./normalize-callback-tcZ_nyq5.mjs";
5
5
  import { Cause, Effect, Exit, Option, Ref, Schedule, Stream } from "effect";
6
6
  //#region src/multipart/circuit-breaker.ts
7
7
  const makeCircuitBreaker = (config) => Effect.gen(function* () {
@@ -76,20 +76,49 @@ const chunkStream = (stream, chunkSize) => {
76
76
  const DEFAULT_MAX_CONCURRENCY = 4;
77
77
  const DEFAULT_RETRY_SCHEDULE = Schedule.exponential("100 millis").pipe(Schedule.compose(Schedule.recurs(2)));
78
78
  const uploadMultipartEffect = (options) => {
79
- const { stream, chunkSize, uploadPart, completeUpload, initiate, reconcileCompletedParts, maxConcurrency = DEFAULT_MAX_CONCURRENCY, signal, retrySchedule = DEFAULT_RETRY_SCHEDULE } = options;
79
+ const { stream, chunkSize, uploadPart, completeUpload, initiate, reconcileCompletedParts, resumeFrom, getContentDigest, pipelineIdentity, maxConcurrency = DEFAULT_MAX_CONCURRENCY, signal, retrySchedule = DEFAULT_RETRY_SCHEDULE } = options;
80
+ if (!Number.isFinite(chunkSize) || chunkSize <= 0) throw new TypeError(`uploadMultipart: chunkSize must be a positive finite number, got ${chunkSize}`);
81
+ if (resumeFrom !== void 0) {
82
+ if (typeof resumeFrom.uploadId !== "string" || resumeFrom.uploadId === "") throw new TypeError("uploadMultipart: ResumeState.uploadId must be a non-empty string");
83
+ if (resumeFrom.version !== 1) throw new ResumeMismatchError("version_mismatch");
84
+ if (resumeFrom.chunkSize !== chunkSize) throw new ResumeMismatchError("chunksize_mismatch");
85
+ if (resumeFrom.pipelineIdentity !== pipelineIdentity) throw new ResumeMismatchError("pipeline_mismatch");
86
+ if (resumeFrom.contentDigestCaptured === true && resumeFrom.contentDigest === void 0) throw new ResumeMismatchError("content_mismatch");
87
+ }
80
88
  return Stream.unwrap(Effect.gen(function* () {
81
89
  const logger = yield* LoggerService;
82
90
  const semaphore = yield* Effect.makeSemaphore(maxConcurrency);
83
91
  const refParts = yield* Ref.make([]);
84
92
  const refBytesUploaded = yield* Ref.make(0);
85
93
  const refUploadId = yield* Ref.make("");
94
+ const refDigest = yield* Ref.make(Option.none());
86
95
  const breaker = options.circuitBreaker ? yield* makeCircuitBreaker(options.circuitBreaker) : null;
87
96
  const reconciledMap = reconcileCompletedParts ? new Map((yield* normalizeCallback(reconcileCompletedParts).pipe(Effect.mapError((cause) => new ReconcileError(cause)))).map((p) => [p.partNumber, p.etag])) : /* @__PURE__ */ new Map();
88
- const initiateStream = initiate ? Stream.fromEffect(normalizeCallback(initiate).pipe(Effect.mapError((cause) => new InitiateUploadError(cause)), Effect.flatMap(({ uploadId }) => Ref.set(refUploadId, uploadId).pipe(Effect.as({
89
- _tag: "UploadInitiated",
90
- uploadId,
91
- timestamp: Date.now()
92
- }))))) : Stream.empty;
97
+ const runFreshInit = Effect.gen(function* () {
98
+ const { uploadId } = yield* normalizeCallback(initiate).pipe(Effect.mapError((cause) => new InitiateUploadError(cause)));
99
+ yield* Ref.set(refUploadId, uploadId);
100
+ if (getContentDigest !== void 0) {
101
+ const digest = yield* normalizeCallback(getContentDigest).pipe(Effect.mapError((cause) => new InitiateUploadError(cause)));
102
+ yield* Ref.set(refDigest, Option.some(digest));
103
+ }
104
+ const capturedDigest = yield* Ref.get(refDigest);
105
+ return {
106
+ _tag: "UploadInitiated",
107
+ uploadId,
108
+ contentDigest: Option.getOrUndefined(capturedDigest),
109
+ timestamp: Date.now()
110
+ };
111
+ });
112
+ const runResumeSetup = Effect.gen(function* () {
113
+ const rf = resumeFrom;
114
+ if (rf.contentDigest !== void 0 && getContentDigest !== void 0) {
115
+ const digest = yield* normalizeCallback(getContentDigest).pipe(Effect.mapError((cause) => new ResumeMismatchError("content_mismatch", cause)));
116
+ if (digest !== rf.contentDigest) return yield* Effect.fail(new ResumeMismatchError("content_mismatch"));
117
+ yield* Ref.set(refDigest, Option.some(digest));
118
+ }
119
+ yield* Ref.set(refUploadId, rf.uploadId);
120
+ });
121
+ const setupStream = resumeFrom !== void 0 ? Stream.fromEffect(runResumeSetup).pipe(Stream.drain) : initiate !== void 0 ? Stream.fromEffect(runFreshInit) : Stream.empty;
93
122
  const makeUploadOne = (partNumber, chunk) => Effect.gen(function* () {
94
123
  const reconciledEtag = reconciledMap.get(partNumber);
95
124
  if (reconciledEtag !== void 0) {
@@ -183,12 +212,15 @@ const uploadMultipartEffect = (options) => {
183
212
  timestamp: Date.now()
184
213
  };
185
214
  });
186
- return Stream.concat(initiateStream, partsStream.pipe(Stream.concat(Stream.fromEffect(finalEffect))));
215
+ return Stream.concat(setupStream, partsStream.pipe(Stream.concat(Stream.fromEffect(finalEffect))));
187
216
  }));
188
217
  };
189
218
  //#endregion
190
219
  //#region src/multipart/index.ts
220
+ const NO_RESUME_CONTEXT_ERROR_MESSAGE = "uploadMultipart: resumeState is only available when `initiate` or `resumeFrom` is provided";
191
221
  const uploadMultipart = (options) => {
222
+ if (options.initiate !== void 0 && options.reconcileCompletedParts !== void 0 && options.resumeFrom === void 0) console.warn("Tranquilload: detected legacy resume pattern. You're passing `initiate` and `reconcileCompletedParts` without `resumeFrom: ResumeState`. The new API requires the persisted ResumeState to validate chunkSize/pipeline/digest match across sessions. See MIGRATION.md for migration steps.");
223
+ if (options.pipeline !== void 0 && options.pipelineIdentity === void 0) console.warn("Tranquilload: pipeline is set but pipelineIdentity is not. Without an identity, the resume validation cannot detect a pipeline mismatch across sessions. See README → Resume Safety.");
192
224
  const refProgress = Effect.runSync(Ref.make({
193
225
  bytesUploaded: 0,
194
226
  totalBytes: options.totalBytes !== void 0 ? Option.some(options.totalBytes) : Option.none()
@@ -197,6 +229,26 @@ const uploadMultipart = (options) => {
197
229
  const uploadIdPromise = new Promise((resolve) => {
198
230
  resolveUploadId = resolve;
199
231
  });
232
+ let resolveResumeState;
233
+ let rejectResumeState;
234
+ let resumeStateSettled = false;
235
+ const resumeStatePromise = new Promise((resolve, reject) => {
236
+ resolveResumeState = (s) => {
237
+ if (resumeStateSettled) return;
238
+ resumeStateSettled = true;
239
+ resolve(s);
240
+ };
241
+ rejectResumeState = (e) => {
242
+ if (resumeStateSettled) return;
243
+ resumeStateSettled = true;
244
+ reject(e);
245
+ };
246
+ });
247
+ resumeStatePromise.catch(() => {});
248
+ if (options.resumeFrom !== void 0) {
249
+ resolveUploadId(options.resumeFrom.uploadId);
250
+ resolveResumeState(options.resumeFrom);
251
+ }
200
252
  const collected = (async () => {
201
253
  let processedStream = options.stream;
202
254
  if (options.pipeline !== void 0) if (typeof options.pipeline === "function") processedStream = options.pipeline(options.stream);
@@ -205,7 +257,18 @@ const uploadMultipart = (options) => {
205
257
  ...options,
206
258
  stream: processedStream
207
259
  }).pipe(Stream.tap((event) => {
208
- if (event._tag === "UploadInitiated") return Effect.sync(() => resolveUploadId(event.uploadId));
260
+ if (event._tag === "UploadInitiated") return Effect.sync(() => {
261
+ resolveUploadId(event.uploadId);
262
+ const state = {
263
+ version: 1,
264
+ uploadId: event.uploadId,
265
+ chunkSize: options.chunkSize,
266
+ ...options.pipelineIdentity !== void 0 ? { pipelineIdentity: options.pipelineIdentity } : {},
267
+ ...event.contentDigest !== void 0 ? { contentDigest: event.contentDigest } : {},
268
+ contentDigestCaptured: options.getContentDigest !== void 0
269
+ };
270
+ resolveResumeState(state);
271
+ });
209
272
  if (event._tag === "PartCompleted") return Ref.update(refProgress, (p) => ({
210
273
  ...p,
211
274
  bytesUploaded: p.bytesUploaded + event.bytesUploaded
@@ -216,7 +279,11 @@ const uploadMultipart = (options) => {
216
279
  if (Exit.isSuccess(exit)) return exit.value;
217
280
  return Promise.reject(Cause.squash(exit.cause));
218
281
  })();
219
- collected.finally(() => resolveUploadId("")).catch(() => {});
282
+ collected.then(() => {
283
+ if (!resumeStateSettled) rejectResumeState(new Error(NO_RESUME_CONTEXT_ERROR_MESSAGE));
284
+ }).catch((err) => {
285
+ if (!resumeStateSettled) rejectResumeState(err);
286
+ }).finally(() => resolveUploadId(""));
220
287
  return {
221
288
  events: new ReadableStream({ async start(controller) {
222
289
  try {
@@ -233,7 +300,8 @@ const uploadMultipart = (options) => {
233
300
  return last;
234
301
  }),
235
302
  getProgress: Object.assign(() => Effect.runPromise(Ref.get(refProgress)), { effect: Ref.get(refProgress) }),
236
- uploadId: uploadIdPromise
303
+ uploadId: uploadIdPromise,
304
+ resumeState: resumeStatePromise
237
305
  };
238
306
  };
239
307
  uploadMultipart.effect = uploadMultipartEffect;