@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/dist/index.d.mts +83 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/client/create-uploadista-client.ts +44 -6
- package/src/managers/flow-manager.ts +67 -7
- package/src/services/abort-controller-service.ts +106 -0
- package/src/upload/flow-upload.ts +25 -0
- package/src/upload/single-upload.ts +11 -0
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
771
|
-
|
|
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,
|