@uploadista/client-core 0.0.13 → 0.0.14
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 +972 -33
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -4
- package/src/index.ts +2 -0
- package/src/managers/__tests__/event-subscription-manager.test.ts +566 -0
- package/src/managers/__tests__/upload-manager.test.ts +588 -0
- package/src/managers/event-subscription-manager.ts +280 -0
- package/src/managers/flow-manager.ts +614 -0
- package/src/managers/index.ts +28 -0
- package/src/managers/upload-manager.ts +353 -0
- package/src/services/service-container.ts +213 -1
- package/src/testing/index.ts +16 -0
- package/src/testing/mock-service-container.ts +629 -0
- package/src/types/flow-upload-options.ts +29 -4
- package/src/types/index.ts +1 -0
- package/src/types/upload-metrics.ts +130 -0
- package/src/types/upload-options.ts +17 -1
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import type { FlowEvent, TypedOutput } from "@uploadista/core/flow";
|
|
2
|
+
import { EventType } from "@uploadista/core/flow";
|
|
3
|
+
import type { UploadFile } from "@uploadista/core/types";
|
|
4
|
+
import type { FlowUploadOptions } from "../types/flow-upload-options";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Flow upload status representing the current state of a flow upload lifecycle.
|
|
8
|
+
* Flow uploads progress through: idle → uploading → processing → success/error/aborted
|
|
9
|
+
*/
|
|
10
|
+
export type FlowUploadStatus =
|
|
11
|
+
| "idle"
|
|
12
|
+
| "uploading"
|
|
13
|
+
| "processing"
|
|
14
|
+
| "success"
|
|
15
|
+
| "error"
|
|
16
|
+
| "aborted";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Complete state information for a flow upload operation.
|
|
20
|
+
* Tracks both the upload phase (file transfer) and processing phase (flow execution).
|
|
21
|
+
*
|
|
22
|
+
* @template TOutput - Type of the final output from the flow (defaults to UploadFile)
|
|
23
|
+
*/
|
|
24
|
+
export interface FlowUploadState<TOutput = UploadFile> {
|
|
25
|
+
/** Current upload status */
|
|
26
|
+
status: FlowUploadStatus;
|
|
27
|
+
/** Upload progress percentage (0-100) */
|
|
28
|
+
progress: number;
|
|
29
|
+
/** Number of bytes uploaded */
|
|
30
|
+
bytesUploaded: number;
|
|
31
|
+
/** Total bytes to upload, null if unknown */
|
|
32
|
+
totalBytes: number | null;
|
|
33
|
+
/** Error if upload or processing failed */
|
|
34
|
+
error: Error | null;
|
|
35
|
+
/** Final output from the flow (available when status is "success") */
|
|
36
|
+
result: TOutput | null;
|
|
37
|
+
/** Unique identifier for the flow execution job */
|
|
38
|
+
jobId: string | null;
|
|
39
|
+
/** Whether the flow processing has started */
|
|
40
|
+
flowStarted: boolean;
|
|
41
|
+
/** Name of the currently executing flow node */
|
|
42
|
+
currentNodeName: string | null;
|
|
43
|
+
/** Type of the currently executing flow node */
|
|
44
|
+
currentNodeType: string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Complete typed outputs from all output nodes in the flow.
|
|
47
|
+
* Each output includes nodeId, optional nodeType, data, and timestamp.
|
|
48
|
+
* Available when status is "success".
|
|
49
|
+
*/
|
|
50
|
+
flowOutputs: TypedOutput[] | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Callbacks that FlowManager invokes during the flow upload lifecycle
|
|
55
|
+
*/
|
|
56
|
+
export interface FlowManagerCallbacks<TOutput = UploadFile> {
|
|
57
|
+
/**
|
|
58
|
+
* Called when the flow upload state changes
|
|
59
|
+
*/
|
|
60
|
+
onStateChange: (state: FlowUploadState<TOutput>) => void;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Called when upload progress updates
|
|
64
|
+
* @param progress - Progress percentage (0-100)
|
|
65
|
+
* @param bytesUploaded - Number of bytes uploaded
|
|
66
|
+
* @param totalBytes - Total bytes to upload, null if unknown
|
|
67
|
+
*/
|
|
68
|
+
onProgress?: (
|
|
69
|
+
uploadId: string,
|
|
70
|
+
bytesUploaded: number,
|
|
71
|
+
totalBytes: number | null,
|
|
72
|
+
) => void;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Called when a chunk completes
|
|
76
|
+
* @param chunkSize - Size of the completed chunk
|
|
77
|
+
* @param bytesAccepted - Total bytes accepted so far
|
|
78
|
+
* @param bytesTotal - Total bytes to upload, null if unknown
|
|
79
|
+
*/
|
|
80
|
+
onChunkComplete?: (
|
|
81
|
+
chunkSize: number,
|
|
82
|
+
bytesAccepted: number,
|
|
83
|
+
bytesTotal: number | null,
|
|
84
|
+
) => void;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Called when the flow completes successfully (receives full flow outputs)
|
|
88
|
+
* Each output includes nodeId, optional nodeType (e.g., "storage-output-v1"), data, and timestamp.
|
|
89
|
+
*
|
|
90
|
+
* @param outputs - Array of typed outputs from all output nodes
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* onFlowComplete: (outputs) => {
|
|
95
|
+
* for (const output of outputs) {
|
|
96
|
+
* console.log(`${output.nodeId} (${output.nodeType}):`, output.data);
|
|
97
|
+
* }
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
onFlowComplete?: (outputs: TypedOutput[]) => void;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Called when upload succeeds (receives single extracted output)
|
|
105
|
+
* For single-output flows, receives the value from the specified outputNodeId
|
|
106
|
+
* or the first output node if outputNodeId is not specified
|
|
107
|
+
*/
|
|
108
|
+
onSuccess?: (result: TOutput) => void;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Called when upload or flow processing fails with an error
|
|
112
|
+
* @param error - The error that occurred
|
|
113
|
+
*/
|
|
114
|
+
onError?: (error: Error) => void;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Called when upload or flow is aborted
|
|
118
|
+
*/
|
|
119
|
+
onAbort?: () => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Generic flow upload input type - can be any value that the upload client accepts
|
|
124
|
+
*/
|
|
125
|
+
export type FlowUploadInput = unknown;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Flow configuration for upload
|
|
129
|
+
*/
|
|
130
|
+
export interface FlowConfig {
|
|
131
|
+
flowId: string;
|
|
132
|
+
storageId: string;
|
|
133
|
+
outputNodeId?: string;
|
|
134
|
+
metadata?: Record<string, string>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Abort and pause controller interface for canceling/pausing flow uploads
|
|
139
|
+
*/
|
|
140
|
+
export interface FlowUploadAbortController {
|
|
141
|
+
abort: () => void | Promise<void>;
|
|
142
|
+
pause: () => void | Promise<void>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Internal upload options used by the flow upload function.
|
|
147
|
+
* The upload phase always returns UploadFile, regardless of the final TOutput type.
|
|
148
|
+
*/
|
|
149
|
+
export interface InternalFlowUploadOptions {
|
|
150
|
+
onJobStart?: (jobId: string) => void;
|
|
151
|
+
onProgress?: (
|
|
152
|
+
uploadId: string,
|
|
153
|
+
bytesUploaded: number,
|
|
154
|
+
totalBytes: number | null,
|
|
155
|
+
) => void;
|
|
156
|
+
onChunkComplete?: (
|
|
157
|
+
chunkSize: number,
|
|
158
|
+
bytesAccepted: number,
|
|
159
|
+
bytesTotal: number | null,
|
|
160
|
+
) => void;
|
|
161
|
+
onSuccess?: (result: UploadFile) => void;
|
|
162
|
+
onError?: (error: Error) => void;
|
|
163
|
+
onAbort?: () => void;
|
|
164
|
+
onShouldRetry?: (error: Error, retryAttempt: number) => boolean;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Flow upload function that performs the actual upload with flow processing.
|
|
169
|
+
* Returns a promise that resolves to an abort controller with pause capability.
|
|
170
|
+
*
|
|
171
|
+
* Note: The upload phase onSuccess always receives UploadFile. The final TOutput
|
|
172
|
+
* result comes from the flow execution and is handled via FlowEnd events.
|
|
173
|
+
*/
|
|
174
|
+
export type FlowUploadFunction<TInput = FlowUploadInput> = (
|
|
175
|
+
input: TInput,
|
|
176
|
+
flowConfig: FlowConfig,
|
|
177
|
+
options: InternalFlowUploadOptions,
|
|
178
|
+
) => Promise<FlowUploadAbortController>;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Initial state for a new flow upload
|
|
182
|
+
*/
|
|
183
|
+
const initialState: FlowUploadState = {
|
|
184
|
+
status: "idle",
|
|
185
|
+
progress: 0,
|
|
186
|
+
bytesUploaded: 0,
|
|
187
|
+
totalBytes: null,
|
|
188
|
+
error: null,
|
|
189
|
+
result: null,
|
|
190
|
+
jobId: null,
|
|
191
|
+
flowStarted: false,
|
|
192
|
+
currentNodeName: null,
|
|
193
|
+
currentNodeType: null,
|
|
194
|
+
flowOutputs: null,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Platform-agnostic flow upload manager that handles flow upload state machine,
|
|
199
|
+
* progress tracking, flow event handling, error handling, abort, pause, reset, and retry logic.
|
|
200
|
+
*
|
|
201
|
+
* Framework packages (React, Vue, React Native) should wrap this manager
|
|
202
|
+
* with framework-specific hooks/composables.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const flowUploadFn = (input, options) => client.uploadWithFlow(input, options.flowConfig, options);
|
|
207
|
+
* const manager = new FlowManager(flowUploadFn, {
|
|
208
|
+
* onStateChange: (state) => setState(state),
|
|
209
|
+
* onProgress: (progress, bytes, total) => console.log(`${progress}%`),
|
|
210
|
+
* onSuccess: (result) => console.log('Flow complete:', result),
|
|
211
|
+
* onError: (error) => console.error('Flow failed:', error),
|
|
212
|
+
* }, {
|
|
213
|
+
* flowConfig: { flowId: 'my-flow', storageId: 'storage1' }
|
|
214
|
+
* });
|
|
215
|
+
*
|
|
216
|
+
* // Subscribe to events and forward them to the manager
|
|
217
|
+
* const unsubscribe = client.subscribeToEvents((event) => {
|
|
218
|
+
* if (isFlowEvent(event)) {
|
|
219
|
+
* manager.handleFlowEvent(event);
|
|
220
|
+
* } else if (isUploadProgress(event)) {
|
|
221
|
+
* manager.handleUploadProgress(event);
|
|
222
|
+
* }
|
|
223
|
+
* });
|
|
224
|
+
*
|
|
225
|
+
* await manager.upload(file);
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
export class FlowManager<TInput = FlowUploadInput, TOutput = UploadFile> {
|
|
229
|
+
private state: FlowUploadState<TOutput>;
|
|
230
|
+
private abortController: FlowUploadAbortController | null = null;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Create a new FlowManager
|
|
234
|
+
*
|
|
235
|
+
* @param flowUploadFn - Flow upload function to use for uploads
|
|
236
|
+
* @param callbacks - Callbacks to invoke during flow upload lifecycle
|
|
237
|
+
* @param options - Flow upload configuration options
|
|
238
|
+
*/
|
|
239
|
+
constructor(
|
|
240
|
+
private readonly flowUploadFn: FlowUploadFunction<TInput>,
|
|
241
|
+
private readonly callbacks: FlowManagerCallbacks<TOutput>,
|
|
242
|
+
private readonly options: FlowUploadOptions<TOutput>,
|
|
243
|
+
) {
|
|
244
|
+
this.state = { ...initialState } as FlowUploadState<TOutput>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get the current flow upload state
|
|
249
|
+
*/
|
|
250
|
+
getState(): FlowUploadState<TOutput> {
|
|
251
|
+
return { ...this.state };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if an upload or flow is currently active
|
|
256
|
+
*/
|
|
257
|
+
isUploading(): boolean {
|
|
258
|
+
return (
|
|
259
|
+
this.state.status === "uploading" || this.state.status === "processing"
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check if file upload is in progress
|
|
265
|
+
*/
|
|
266
|
+
isUploadingFile(): boolean {
|
|
267
|
+
return this.state.status === "uploading";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if flow processing is in progress
|
|
272
|
+
*/
|
|
273
|
+
isProcessing(): boolean {
|
|
274
|
+
return this.state.status === "processing";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get the current job ID
|
|
279
|
+
*/
|
|
280
|
+
getJobId(): string | null {
|
|
281
|
+
return this.state.jobId;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Update the internal state and notify callbacks
|
|
286
|
+
*/
|
|
287
|
+
private updateState(update: Partial<FlowUploadState<TOutput>>): void {
|
|
288
|
+
this.state = { ...this.state, ...update };
|
|
289
|
+
this.callbacks.onStateChange(this.state);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Handle flow events from the event subscription
|
|
294
|
+
* This method should be called by the framework wrapper when it receives flow events
|
|
295
|
+
*
|
|
296
|
+
* @param event - Flow event to process
|
|
297
|
+
*/
|
|
298
|
+
handleFlowEvent(event: FlowEvent): void {
|
|
299
|
+
// For FlowStart, accept if we don't have a jobId yet (first event)
|
|
300
|
+
// This handles the race condition where flow events arrive before onJobStart callback
|
|
301
|
+
if (event.eventType === EventType.FlowStart && !this.state.jobId) {
|
|
302
|
+
this.updateState({
|
|
303
|
+
jobId: event.jobId,
|
|
304
|
+
flowStarted: true,
|
|
305
|
+
status: "processing",
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Only handle events for the current job
|
|
311
|
+
if (!this.state.jobId || event.jobId !== this.state.jobId) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
switch (event.eventType) {
|
|
316
|
+
case EventType.FlowStart:
|
|
317
|
+
this.updateState({
|
|
318
|
+
flowStarted: true,
|
|
319
|
+
status: "processing",
|
|
320
|
+
});
|
|
321
|
+
break;
|
|
322
|
+
|
|
323
|
+
case EventType.NodeStart:
|
|
324
|
+
this.updateState({
|
|
325
|
+
status: "processing",
|
|
326
|
+
currentNodeName: event.nodeName,
|
|
327
|
+
currentNodeType: event.nodeType,
|
|
328
|
+
});
|
|
329
|
+
break;
|
|
330
|
+
|
|
331
|
+
case EventType.NodePause:
|
|
332
|
+
// When input node pauses, it's waiting for upload - switch to uploading state
|
|
333
|
+
this.updateState({
|
|
334
|
+
status: "uploading",
|
|
335
|
+
currentNodeName: event.nodeName,
|
|
336
|
+
// NodePause doesn't have nodeType, keep previous value
|
|
337
|
+
});
|
|
338
|
+
break;
|
|
339
|
+
|
|
340
|
+
case EventType.NodeResume:
|
|
341
|
+
// When node resumes, upload is complete - switch to processing state
|
|
342
|
+
this.updateState({
|
|
343
|
+
status: "processing",
|
|
344
|
+
currentNodeName: event.nodeName,
|
|
345
|
+
currentNodeType: event.nodeType,
|
|
346
|
+
});
|
|
347
|
+
break;
|
|
348
|
+
|
|
349
|
+
case EventType.NodeEnd:
|
|
350
|
+
this.updateState({
|
|
351
|
+
status:
|
|
352
|
+
this.state.status === "uploading"
|
|
353
|
+
? "processing"
|
|
354
|
+
: this.state.status,
|
|
355
|
+
currentNodeName: null,
|
|
356
|
+
currentNodeType: null,
|
|
357
|
+
});
|
|
358
|
+
break;
|
|
359
|
+
|
|
360
|
+
case EventType.FlowEnd: {
|
|
361
|
+
// Get typed outputs from the event
|
|
362
|
+
const flowOutputs = event.outputs || null;
|
|
363
|
+
|
|
364
|
+
// Call onFlowComplete with full typed outputs
|
|
365
|
+
if (flowOutputs && this.callbacks.onFlowComplete) {
|
|
366
|
+
this.callbacks.onFlowComplete(flowOutputs);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Extract single output for onSuccess callback
|
|
370
|
+
let extractedOutput: TOutput | null = null;
|
|
371
|
+
if (flowOutputs && flowOutputs.length > 0) {
|
|
372
|
+
if (this.options.flowConfig.outputNodeId) {
|
|
373
|
+
// Find output by specified nodeId
|
|
374
|
+
const targetOutput = flowOutputs.find(
|
|
375
|
+
(output) => output.nodeId === this.options.flowConfig.outputNodeId,
|
|
376
|
+
);
|
|
377
|
+
if (targetOutput) {
|
|
378
|
+
extractedOutput = targetOutput.data as TOutput;
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
// Use first output
|
|
382
|
+
const firstOutput = flowOutputs[0];
|
|
383
|
+
if (firstOutput) {
|
|
384
|
+
extractedOutput = firstOutput.data as TOutput;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Call onSuccess with extracted output
|
|
390
|
+
if (extractedOutput && this.callbacks.onSuccess) {
|
|
391
|
+
this.callbacks.onSuccess(extractedOutput);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
this.updateState({
|
|
395
|
+
status: "success",
|
|
396
|
+
currentNodeName: null,
|
|
397
|
+
currentNodeType: null,
|
|
398
|
+
result: extractedOutput,
|
|
399
|
+
flowOutputs,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
this.abortController = null;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
case EventType.FlowError: {
|
|
407
|
+
const error = new Error(event.error);
|
|
408
|
+
this.updateState({
|
|
409
|
+
status: "error",
|
|
410
|
+
error,
|
|
411
|
+
});
|
|
412
|
+
this.callbacks.onError?.(error);
|
|
413
|
+
this.abortController = null;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
case EventType.NodeError: {
|
|
418
|
+
const error = new Error(event.error);
|
|
419
|
+
this.updateState({
|
|
420
|
+
status: "error",
|
|
421
|
+
error,
|
|
422
|
+
});
|
|
423
|
+
this.callbacks.onError?.(error);
|
|
424
|
+
this.abortController = null;
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
case EventType.FlowCancel:
|
|
429
|
+
this.updateState({
|
|
430
|
+
status: "aborted",
|
|
431
|
+
});
|
|
432
|
+
this.callbacks.onAbort?.();
|
|
433
|
+
this.abortController = null;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Handle upload progress events from the event subscription
|
|
440
|
+
* This method should be called by the framework wrapper when it receives upload progress events
|
|
441
|
+
*
|
|
442
|
+
* @param uploadId - The unique identifier for this upload
|
|
443
|
+
* @param bytesUploaded - Number of bytes uploaded
|
|
444
|
+
* @param totalBytes - Total bytes to upload, null if unknown
|
|
445
|
+
*/
|
|
446
|
+
handleUploadProgress(
|
|
447
|
+
uploadId: string,
|
|
448
|
+
bytesUploaded: number,
|
|
449
|
+
totalBytes: number | null,
|
|
450
|
+
): void {
|
|
451
|
+
// Calculate progress percentage
|
|
452
|
+
const progress =
|
|
453
|
+
totalBytes && totalBytes > 0
|
|
454
|
+
? Math.round((bytesUploaded / totalBytes) * 100)
|
|
455
|
+
: 0;
|
|
456
|
+
|
|
457
|
+
this.updateState({
|
|
458
|
+
bytesUploaded,
|
|
459
|
+
totalBytes,
|
|
460
|
+
progress,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
this.callbacks.onProgress?.(uploadId, bytesUploaded, totalBytes);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Start uploading a file through the flow
|
|
468
|
+
*
|
|
469
|
+
* @param input - File or input to upload (type depends on platform)
|
|
470
|
+
*/
|
|
471
|
+
async upload(input: TInput): Promise<void> {
|
|
472
|
+
// Determine totalBytes from input if possible (File/Blob on browser platforms)
|
|
473
|
+
let totalBytes: number | null = null;
|
|
474
|
+
if (input && typeof input === "object") {
|
|
475
|
+
if ("size" in input && typeof input.size === "number") {
|
|
476
|
+
totalBytes = input.size;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Reset state but keep reference for potential retries
|
|
481
|
+
this.updateState({
|
|
482
|
+
status: "uploading",
|
|
483
|
+
progress: 0,
|
|
484
|
+
bytesUploaded: 0,
|
|
485
|
+
totalBytes,
|
|
486
|
+
error: null,
|
|
487
|
+
result: null,
|
|
488
|
+
jobId: null,
|
|
489
|
+
flowStarted: false,
|
|
490
|
+
currentNodeName: null,
|
|
491
|
+
currentNodeType: null,
|
|
492
|
+
flowOutputs: null,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
// Build internal upload options with our callbacks
|
|
497
|
+
const internalOptions: InternalFlowUploadOptions = {
|
|
498
|
+
onJobStart: (jobId: string) => {
|
|
499
|
+
this.updateState({
|
|
500
|
+
jobId,
|
|
501
|
+
});
|
|
502
|
+
this.options?.onJobStart?.(jobId);
|
|
503
|
+
},
|
|
504
|
+
onProgress: (
|
|
505
|
+
uploadId: string,
|
|
506
|
+
bytesUploaded: number,
|
|
507
|
+
totalBytes: number | null,
|
|
508
|
+
) => {
|
|
509
|
+
this.handleUploadProgress(uploadId, bytesUploaded, totalBytes);
|
|
510
|
+
this.options?.onProgress?.(uploadId, bytesUploaded, totalBytes);
|
|
511
|
+
},
|
|
512
|
+
onChunkComplete: (
|
|
513
|
+
chunkSize: number,
|
|
514
|
+
bytesAccepted: number,
|
|
515
|
+
bytesTotal: number | null,
|
|
516
|
+
) => {
|
|
517
|
+
this.callbacks.onChunkComplete?.(
|
|
518
|
+
chunkSize,
|
|
519
|
+
bytesAccepted,
|
|
520
|
+
bytesTotal,
|
|
521
|
+
);
|
|
522
|
+
this.options?.onChunkComplete?.(chunkSize, bytesAccepted, bytesTotal);
|
|
523
|
+
},
|
|
524
|
+
onSuccess: (_result: UploadFile) => {
|
|
525
|
+
// Note: This gets called when upload phase completes, not flow completion
|
|
526
|
+
// Flow completion is handled by FlowEnd event
|
|
527
|
+
this.updateState({
|
|
528
|
+
progress: 100,
|
|
529
|
+
});
|
|
530
|
+
// Don't call callbacks.onSuccess here - wait for FlowEnd event with TOutput
|
|
531
|
+
},
|
|
532
|
+
onError: (error: Error) => {
|
|
533
|
+
this.updateState({
|
|
534
|
+
status: "error",
|
|
535
|
+
error,
|
|
536
|
+
});
|
|
537
|
+
this.callbacks.onError?.(error);
|
|
538
|
+
this.options?.onError?.(error);
|
|
539
|
+
this.abortController = null;
|
|
540
|
+
},
|
|
541
|
+
onAbort: () => {
|
|
542
|
+
this.updateState({
|
|
543
|
+
status: "aborted",
|
|
544
|
+
});
|
|
545
|
+
this.callbacks.onAbort?.();
|
|
546
|
+
this.options?.onAbort?.();
|
|
547
|
+
this.abortController = null;
|
|
548
|
+
},
|
|
549
|
+
onShouldRetry: this.options?.onShouldRetry,
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// Start the flow upload
|
|
553
|
+
this.abortController = await this.flowUploadFn(
|
|
554
|
+
input,
|
|
555
|
+
this.options.flowConfig,
|
|
556
|
+
internalOptions,
|
|
557
|
+
);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
// Handle errors from upload initiation
|
|
560
|
+
const uploadError =
|
|
561
|
+
error instanceof Error ? error : new Error(String(error));
|
|
562
|
+
this.updateState({
|
|
563
|
+
status: "error",
|
|
564
|
+
error: uploadError,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
this.callbacks.onError?.(uploadError);
|
|
568
|
+
this.options?.onError?.(uploadError);
|
|
569
|
+
this.abortController = null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Abort the current flow upload
|
|
575
|
+
*/
|
|
576
|
+
abort(): void {
|
|
577
|
+
if (this.abortController) {
|
|
578
|
+
this.abortController.abort();
|
|
579
|
+
// Note: State update happens in onAbort callback or FlowCancel event
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Pause the current flow upload
|
|
585
|
+
*/
|
|
586
|
+
pause(): void {
|
|
587
|
+
if (this.abortController) {
|
|
588
|
+
this.abortController.pause();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Reset the flow upload state to idle
|
|
594
|
+
*/
|
|
595
|
+
reset(): void {
|
|
596
|
+
if (this.abortController) {
|
|
597
|
+
this.abortController.abort();
|
|
598
|
+
this.abortController = null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
this.state = { ...initialState } as FlowUploadState<TOutput>;
|
|
602
|
+
this.callbacks.onStateChange(this.state);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Clean up resources (call when disposing the manager)
|
|
607
|
+
*/
|
|
608
|
+
cleanup(): void {
|
|
609
|
+
if (this.abortController) {
|
|
610
|
+
this.abortController.abort();
|
|
611
|
+
this.abortController = null;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type EventFilterOptions,
|
|
3
|
+
type EventSource,
|
|
4
|
+
EventSubscriptionManager,
|
|
5
|
+
type GenericEvent,
|
|
6
|
+
type SubscriptionEventHandler,
|
|
7
|
+
type UnsubscribeFunction,
|
|
8
|
+
} from "./event-subscription-manager";
|
|
9
|
+
export {
|
|
10
|
+
type FlowConfig,
|
|
11
|
+
FlowManager,
|
|
12
|
+
type FlowManagerCallbacks,
|
|
13
|
+
type FlowUploadAbortController,
|
|
14
|
+
type FlowUploadFunction,
|
|
15
|
+
type FlowUploadInput,
|
|
16
|
+
type FlowUploadState,
|
|
17
|
+
type FlowUploadStatus,
|
|
18
|
+
type InternalFlowUploadOptions,
|
|
19
|
+
} from "./flow-manager";
|
|
20
|
+
export {
|
|
21
|
+
type UploadAbortController,
|
|
22
|
+
type UploadFunction,
|
|
23
|
+
type UploadInput,
|
|
24
|
+
UploadManager,
|
|
25
|
+
type UploadManagerCallbacks,
|
|
26
|
+
type UploadState,
|
|
27
|
+
type UploadStatus,
|
|
28
|
+
} from "./upload-manager";
|