@uploadista/client-core 0.1.3-beta.9 → 0.1.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uploadista/client-core",
3
3
  "type": "module",
4
- "version": "0.1.3-beta.9",
4
+ "version": "0.1.3",
5
5
  "description": "Platform-agnostic core upload client logic for Uploadista",
6
6
  "license": "MIT",
7
7
  "author": "Uploadista",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "js-base64": "3.7.8",
36
- "@uploadista/core": "0.1.3-beta.9"
36
+ "@uploadista/core": "0.1.3"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "zod": "^4.0.0"
@@ -42,7 +42,7 @@
42
42
  "tsdown": "0.20.1",
43
43
  "vitest": "4.0.18",
44
44
  "zod": "4.3.6",
45
- "@uploadista/typescript-config": "0.1.3-beta.9"
45
+ "@uploadista/typescript-config": "0.1.3"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc --noEmit && tsdown",
@@ -9,7 +9,10 @@ import type { Logger } from "../logger";
9
9
  import { createLogger } from "../logger";
10
10
  import { defaultClientCapabilities } from "../mock-data-store";
11
11
  import { NetworkMonitor, type NetworkMonitorConfig } from "../network-monitor";
12
- import type { AbortControllerFactory } from "../services/abort-controller-service";
12
+ import {
13
+ type AbortControllerFactory,
14
+ PausableAbortController,
15
+ } from "../services/abort-controller-service";
13
16
  import type { ChecksumService } from "../services/checksum-service";
14
17
  import type { FileReaderService } from "../services/file-reader-service";
15
18
  import type { FingerprintService } from "../services/fingerprint-service";
@@ -671,13 +674,15 @@ export function createUploadistaClient<UploadInput>({
671
674
  onShouldRetry,
672
675
  onJobStart,
673
676
  onError,
677
+ onAbort,
674
678
  }: Omit<
675
679
  UploadistaUploadOptions,
676
680
  "uploadLengthDeferred" | "uploadSize" | "metadata"
677
- > = {},
681
+ > & { onAbort?: () => void } = {},
678
682
  ): Promise<{
679
683
  abort: () => Promise<void>;
680
684
  pause: () => Promise<void>;
685
+ resume: () => Promise<void>;
681
686
  jobId: string;
682
687
  }> => {
683
688
  const source = await fileReader.openFile(file, chunkSize);
@@ -712,12 +717,17 @@ export function createUploadistaClient<UploadInput>({
712
717
  return {
713
718
  abort: async () => {},
714
719
  pause: async () => {},
720
+ resume: async () => {},
715
721
  jobId: "",
716
722
  };
717
723
  }
718
724
 
719
725
  const { jobId, uploadFile, inputNodeId } = result;
720
- const abortController = abortControllerFactory.create();
726
+ // Use PausableAbortController to support pausing chunk uploads
727
+ // Pass a real AbortController from the factory to ensure fetch compatibility
728
+ const abortController = new PausableAbortController(
729
+ abortControllerFactory.create(),
730
+ );
721
731
 
722
732
  // Open upload WebSocket to receive upload progress events
723
733
  await wsManager.openUploadWebSocket(uploadFile.id);
@@ -742,6 +752,7 @@ export function createUploadistaClient<UploadInput>({
742
752
  onChunkComplete,
743
753
  onSuccess,
744
754
  onShouldRetry,
755
+ onAbort,
745
756
  onRetry: (timeout) => {
746
757
  timeoutId = timeout;
747
758
  },
@@ -769,7 +780,13 @@ export function createUploadistaClient<UploadInput>({
769
780
  wsManager.closeUploadWebSocket(uploadFile.id);
770
781
  },
771
782
  pause: async () => {
772
- await uploadistaApi.pauseFlow(jobId);
783
+ // Pause client-side chunk uploads immediately
784
+ // This will cause the upload loop to wait at the start of the next chunk
785
+ abortController.pause();
786
+ },
787
+ resume: async () => {
788
+ // Resume client-side chunk uploads
789
+ abortController.resume();
773
790
  },
774
791
  jobId,
775
792
  };
@@ -812,6 +829,7 @@ export function createUploadistaClient<UploadInput>({
812
829
  ): Promise<{
813
830
  abort: () => Promise<void>;
814
831
  pause: () => Promise<void>;
832
+ resume: () => Promise<void>;
815
833
  jobId: string;
816
834
  }> => {
817
835
  // Start the flow and get job ID
@@ -905,7 +923,11 @@ export function createUploadistaClient<UploadInput>({
905
923
  input.inputType === "file" && input.uploadFile && input.source,
906
924
  )
907
925
  .map(async ({ nodeId, uploadFile, source }) => {
908
- const abortController = abortControllerFactory.create();
926
+ // Use PausableAbortController to support pausing chunk uploads
927
+ // Pass a real AbortController from the factory to ensure fetch compatibility
928
+ const abortController = new PausableAbortController(
929
+ abortControllerFactory.create(),
930
+ );
909
931
  abortControllers.set(nodeId, abortController);
910
932
 
911
933
  const metrics = new UploadMetrics({
@@ -1009,7 +1031,23 @@ export function createUploadistaClient<UploadInput>({
1009
1031
  }
1010
1032
  },
1011
1033
  pause: async () => {
1012
- await uploadistaApi.pauseFlow(jobId);
1034
+ // Pause all client-side chunk uploads
1035
+ for (const controller of abortControllers.values()) {
1036
+ if ("pause" in controller && typeof controller.pause === "function") {
1037
+ controller.pause();
1038
+ }
1039
+ }
1040
+ },
1041
+ resume: async () => {
1042
+ // Resume all client-side chunk uploads
1043
+ for (const controller of abortControllers.values()) {
1044
+ if (
1045
+ "resume" in controller &&
1046
+ typeof controller.resume === "function"
1047
+ ) {
1048
+ controller.resume();
1049
+ }
1050
+ }
1013
1051
  },
1014
1052
  jobId,
1015
1053
  };
@@ -184,6 +184,7 @@ export interface FlowConfig {
184
184
  export interface FlowUploadAbortController {
185
185
  abort: () => void | Promise<void>;
186
186
  pause: () => void | Promise<void>;
187
+ resume: () => void | Promise<void>;
187
188
  }
188
189
 
189
190
  /**
@@ -754,22 +755,81 @@ export class FlowManager<TInput = FlowUploadInput> {
754
755
  }
755
756
 
756
757
  /**
757
- * Abort the current flow upload
758
+ * Abort the current flow upload.
759
+ * This will:
760
+ * 1. Cancel the flow on the server
761
+ * 2. Abort any in-progress chunk uploads
762
+ * 3. Close WebSocket connections
758
763
  */
759
- abort(): void {
764
+ async abort(): Promise<void> {
760
765
  if (this.abortController) {
761
- this.abortController.abort();
766
+ await this.abortController.abort();
762
767
  // Note: State update happens in onAbort callback or FlowCancel event
763
768
  }
764
769
  }
765
770
 
766
771
  /**
767
- * Pause the current flow upload
772
+ * Pause the current flow upload.
773
+ * During upload phase: Pauses chunk uploads client-side.
774
+ * During processing phase: Calls server to pause flow execution.
768
775
  */
769
- pause(): void {
770
- if (this.abortController) {
771
- this.abortController.pause();
776
+ async pause(): Promise<void> {
777
+ // Only pause if we're actively uploading or processing
778
+ if (
779
+ this.state.status !== "uploading" &&
780
+ this.state.status !== "processing"
781
+ ) {
782
+ return;
783
+ }
784
+
785
+ // Call abort controller's pause method if available
786
+ // This pauses chunk uploads during upload phase
787
+ if (this.abortController?.pause) {
788
+ try {
789
+ await this.abortController.pause();
790
+ } catch {
791
+ // If pause fails, continue with state update
792
+ }
772
793
  }
794
+
795
+ // Update client state to paused
796
+ this.updateState({
797
+ status: "paused",
798
+ pausedAtNodeId: this.state.currentNodeName ?? null,
799
+ });
800
+ }
801
+
802
+ /**
803
+ * Resume a paused flow upload.
804
+ * Continues chunk uploads if paused during upload phase.
805
+ */
806
+ async resume(): Promise<void> {
807
+ // Only resume if we're paused
808
+ if (this.state.status !== "paused") {
809
+ return;
810
+ }
811
+
812
+ // Determine what status to resume to
813
+ // If we had started uploading, resume to uploading state
814
+ // Otherwise, this shouldn't happen (can only pause during upload/processing)
815
+ const resumeStatus: "uploading" | "processing" =
816
+ this.state.flowStarted ? "processing" : "uploading";
817
+
818
+ // Call abort controller's resume method if available
819
+ // This allows chunk uploads to continue
820
+ if (this.abortController?.resume) {
821
+ try {
822
+ await this.abortController.resume();
823
+ } catch {
824
+ // If resume fails, continue with state update
825
+ }
826
+ }
827
+
828
+ // Update client state to resume previous status
829
+ this.updateState({
830
+ status: resumeStatus,
831
+ pausedAtNodeId: null,
832
+ });
773
833
  }
774
834
 
775
835
  /**
@@ -5,6 +5,23 @@
5
5
  export interface AbortControllerLike {
6
6
  readonly signal: AbortSignalLike;
7
7
  abort(reason?: unknown): void;
8
+ /**
9
+ * Pause the operation (optional, may not be supported by all implementations)
10
+ */
11
+ pause?(): void;
12
+ /**
13
+ * Resume a paused operation (optional, may not be supported by all implementations)
14
+ */
15
+ resume?(): void;
16
+ /**
17
+ * Whether the operation is currently paused
18
+ */
19
+ readonly isPaused?: boolean;
20
+ /**
21
+ * Wait for resume to be called (returns immediately if not paused)
22
+ * Used by upload loops to pause between chunk uploads
23
+ */
24
+ waitForResume?(): Promise<void>;
8
25
  }
9
26
 
10
27
  export interface AbortSignalLike {
@@ -19,3 +36,92 @@ export interface AbortControllerFactory {
19
36
  */
20
37
  create(): AbortControllerLike;
21
38
  }
39
+
40
+ /**
41
+ * A wrapper that adds pause/resume functionality to an AbortController.
42
+ * Used by FlowManager and upload loops to support pausing chunk uploads.
43
+ *
44
+ * IMPORTANT: This class requires an inner AbortController to be provided.
45
+ * The inner controller's signal is used directly, which ensures compatibility
46
+ * with browser's fetch API (which requires a real AbortSignal).
47
+ */
48
+ export class PausableAbortController implements AbortControllerLike {
49
+ private _isPaused = false;
50
+ private _resumeResolvers: Array<() => void> = [];
51
+ private readonly _innerController: AbortControllerLike;
52
+
53
+ /**
54
+ * Create a PausableAbortController that wraps an inner AbortController.
55
+ *
56
+ * @param innerController - The inner AbortController to wrap. This should be
57
+ * a real AbortController (from the browser or platform) so that its signal
58
+ * is compatible with fetch API.
59
+ */
60
+ constructor(innerController: AbortControllerLike) {
61
+ this._innerController = innerController;
62
+ }
63
+
64
+ /**
65
+ * Returns the inner controller's signal directly.
66
+ * This ensures compatibility with browser's fetch API which requires a real AbortSignal.
67
+ */
68
+ get signal(): AbortSignalLike {
69
+ return this._innerController.signal;
70
+ }
71
+
72
+ get isPaused(): boolean {
73
+ return this._isPaused;
74
+ }
75
+
76
+ abort(reason?: unknown): void {
77
+ // When aborting, also resolve any pending waitForResume promises
78
+ // so the upload loop can exit cleanly
79
+ for (const resolve of this._resumeResolvers) {
80
+ resolve();
81
+ }
82
+ this._resumeResolvers = [];
83
+
84
+ // Delegate to inner controller
85
+ this._innerController.abort(reason);
86
+ }
87
+
88
+ pause(): void {
89
+ this._isPaused = true;
90
+ }
91
+
92
+ resume(): void {
93
+ this._isPaused = false;
94
+ // Resolve all pending waitForResume promises
95
+ for (const resolve of this._resumeResolvers) {
96
+ resolve();
97
+ }
98
+ this._resumeResolvers = [];
99
+ }
100
+
101
+ /**
102
+ * Returns a promise that resolves when resume() is called.
103
+ * If not paused, resolves immediately.
104
+ * If aborted while waiting, resolves immediately (caller should check signal.aborted).
105
+ */
106
+ waitForResume(): Promise<void> {
107
+ if (!this._isPaused || this._innerController.signal.aborted) {
108
+ return Promise.resolve();
109
+ }
110
+
111
+ return new Promise<void>((resolve) => {
112
+ this._resumeResolvers.push(resolve);
113
+ });
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Helper function to wait for resume if the controller supports it.
119
+ * Returns immediately if the controller doesn't support pause or isn't paused.
120
+ */
121
+ export async function waitForResumeIfPaused(
122
+ controller: AbortControllerLike,
123
+ ): Promise<void> {
124
+ if (controller.isPaused && controller.waitForResume) {
125
+ await controller.waitForResume();
126
+ }
127
+ }
@@ -8,6 +8,7 @@ import type { PlatformService, Timeout } from "../services/platform-service";
8
8
  import type { SmartChunker, SmartChunkerConfig } from "../smart-chunker";
9
9
  import type { FlowUploadConfig } from "../types/flow-upload-config";
10
10
 
11
+ import { waitForResumeIfPaused } from "../services/abort-controller-service";
11
12
  import { shouldRetry } from "./chunk-upload";
12
13
  import type { Callbacks } from "./single-upload";
13
14
  import type { UploadMetrics } from "./upload-metrics";
@@ -185,6 +186,16 @@ export async function performFlowUpload({
185
186
  let currentOffset = offset;
186
187
 
187
188
  try {
189
+ // Check if paused before starting chunk upload
190
+ // This allows the upload loop to pause between chunks
191
+ await waitForResumeIfPaused(abortController);
192
+
193
+ // Check if aborted after waiting for resume
194
+ if (abortController.signal.aborted) {
195
+ callbacks.onAbort?.();
196
+ return;
197
+ }
198
+
188
199
  // Get optimal chunk size
189
200
  const remainingBytes = source.size ? source.size - offset : undefined;
190
201
  const chunkSizeDecision = smartChunker.getNextChunkSize(remainingBytes);
@@ -306,6 +317,20 @@ export async function performFlowUpload({
306
317
  ...callbacks,
307
318
  });
308
319
  } catch (err) {
320
+ // Check if this is an abort error (from fetch being cancelled)
321
+ const isAbortError =
322
+ abortController.signal.aborted ||
323
+ (err instanceof Error &&
324
+ (err.name === "AbortError" ||
325
+ (err as { code?: number }).code === 20 || // DOMException.ABORT_ERR
326
+ err.message?.includes("abort")));
327
+
328
+ if (isAbortError) {
329
+ logger.log(`Flow upload aborted for job ${jobId}`);
330
+ callbacks.onAbort?.();
331
+ return;
332
+ }
333
+
309
334
  // Retry logic similar to single-upload
310
335
  if (retryDelays != null) {
311
336
  const shouldResetDelays =
@@ -10,6 +10,7 @@ import type { PlatformService, Timeout } from "../services/platform-service";
10
10
  import type { WebSocketLike } from "../services/websocket-service";
11
11
  import type { SmartChunker, SmartChunkerConfig } from "../smart-chunker";
12
12
  import type { ClientStorage } from "../storage/client-storage";
13
+ import { waitForResumeIfPaused } from "../services/abort-controller-service";
13
14
  import {
14
15
  type OnProgress,
15
16
  type OnShouldRetry,
@@ -32,6 +33,7 @@ export type Callbacks = {
32
33
  ) => void;
33
34
  onSuccess?: (payload: UploadFile) => void;
34
35
  onError?: (error: Error | UploadistaError) => void;
36
+ onAbort?: () => void;
35
37
  onStart?: (file: { uploadId: string; size: number | null }) => void;
36
38
  onJobStart?: (jobId: string) => void;
37
39
  onShouldRetry?: OnShouldRetry;
@@ -84,6 +86,15 @@ export async function performUpload({
84
86
  let currentOffset = offset;
85
87
 
86
88
  try {
89
+ // Check if paused before starting chunk upload
90
+ // This allows the upload loop to pause between chunks
91
+ await waitForResumeIfPaused(abortController);
92
+
93
+ // Check if aborted after waiting for resume
94
+ if (abortController.signal.aborted) {
95
+ return;
96
+ }
97
+
87
98
  const res = await uploadChunk({
88
99
  uploadId,
89
100
  source,