@gentleduck/upload 0.2.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 (68) hide show
  1. package/dist/index.d.ts +1417 -0
  2. package/dist/index.js +6 -0
  3. package/package.json +43 -0
  4. package/src/core/client/config.types.ts +77 -0
  5. package/src/core/client/index.ts +4 -0
  6. package/src/core/client/plugins.types.ts +46 -0
  7. package/src/core/client/retry.types.ts +6 -0
  8. package/src/core/client/uploadClient.ts +19 -0
  9. package/src/core/contracts/backendApi.ts +62 -0
  10. package/src/core/contracts/cursor.types.ts +23 -0
  11. package/src/core/contracts/errors.types.ts +67 -0
  12. package/src/core/contracts/fingerprint.types.ts +18 -0
  13. package/src/core/contracts/index.ts +8 -0
  14. package/src/core/contracts/intent.types.ts +39 -0
  15. package/src/core/contracts/result.types.ts +11 -0
  16. package/src/core/contracts/strategy.types.ts +45 -0
  17. package/src/core/contracts/transport/index.ts +2 -0
  18. package/src/core/contracts/transport/transport.libs.ts +100 -0
  19. package/src/core/contracts/transport/transport.ts +166 -0
  20. package/src/core/contracts/transport/transport.types.ts +44 -0
  21. package/src/core/engine/commands.types.ts +17 -0
  22. package/src/core/engine/event-map.types.ts +45 -0
  23. package/src/core/engine/index.ts +8 -0
  24. package/src/core/engine/internal-events.types.ts +222 -0
  25. package/src/core/engine/outcome.types.ts +9 -0
  26. package/src/core/engine/progress.types.ts +15 -0
  27. package/src/core/engine/reducer.ts +435 -0
  28. package/src/core/engine/store/handlers/add-file.ts +103 -0
  29. package/src/core/engine/store/handlers/cancel.ts +56 -0
  30. package/src/core/engine/store/handlers/clean-up.ts +66 -0
  31. package/src/core/engine/store/handlers/create-intent.ts +76 -0
  32. package/src/core/engine/store/handlers/finalize.ts +59 -0
  33. package/src/core/engine/store/handlers/index.ts +7 -0
  34. package/src/core/engine/store/handlers/pause.ts +16 -0
  35. package/src/core/engine/store/handlers/run-upload.ts +192 -0
  36. package/src/core/engine/store/index.ts +123 -0
  37. package/src/core/engine/store/store.dispatch.ts +78 -0
  38. package/src/core/engine/store/store.libs.ts +169 -0
  39. package/src/core/engine/store/store.runtime.ts +336 -0
  40. package/src/core/engine/store/store.schedule.ts +116 -0
  41. package/src/core/engine/store/store.types.ts +250 -0
  42. package/src/core/engine/validation/file.ts +77 -0
  43. package/src/core/engine/validation/index.ts +3 -0
  44. package/src/core/engine/validation/intent.ts +55 -0
  45. package/src/core/engine/validation/list.ts +50 -0
  46. package/src/core/index.ts +5 -0
  47. package/src/core/persistence/adapters.indexeddb.ts +92 -0
  48. package/src/core/persistence/adapters.local.ts +22 -0
  49. package/src/core/persistence/adapters.memory.ts +15 -0
  50. package/src/core/persistence/index.ts +5 -0
  51. package/src/core/persistence/persistence.ts +194 -0
  52. package/src/core/persistence/persistence.types.ts +66 -0
  53. package/src/core/utils/async.ts +3 -0
  54. package/src/core/utils/constants.ts +32 -0
  55. package/src/core/utils/emitter/emitter.types.ts +23 -0
  56. package/src/core/utils/emitter/index.ts +68 -0
  57. package/src/core/utils/fingerprint.ts +20 -0
  58. package/src/core/utils/guards.ts +3 -0
  59. package/src/core/utils/id.ts +3 -0
  60. package/src/core/utils/index.ts +6 -0
  61. package/src/index.ts +3 -0
  62. package/src/react/index.ts +2 -0
  63. package/src/react/upload-provider.tsx +96 -0
  64. package/src/react/use-uploader.tsx +115 -0
  65. package/src/strategies/index.ts +15 -0
  66. package/src/strategies/multipart/index.ts +243 -0
  67. package/src/strategies/post/index.ts +93 -0
  68. package/src/strategies/registry/index.ts +30 -0
@@ -0,0 +1,76 @@
1
+ import type { CursorMap, IntentMap, UploadError, UploadResultBase } from '../../../contracts'
2
+ import { validateIntent } from '../../validation'
3
+ import { normalizeError, retryDecision, sleep } from '../store.libs'
4
+ import type { StoreRuntime } from '../store.types'
5
+
6
+ export async function createIntent<
7
+ M extends IntentMap,
8
+ C extends CursorMap<M>,
9
+ P extends string,
10
+ R extends UploadResultBase,
11
+ >(rt: StoreRuntime<M, C, P, R>, localId: string) {
12
+ const item = rt.state.items.get(localId)
13
+ if (!item || item.phase !== 'creating_intent') return
14
+ if (rt.inflightIntents.has(localId)) return
15
+
16
+ const controller = new AbortController()
17
+ rt.inflightIntents.set(localId, controller)
18
+
19
+ try {
20
+ const intent = await rt.opts.api.createIntent(
21
+ {
22
+ purpose: item.purpose,
23
+ contentType: item.file.type || 'application/octet-stream',
24
+ size: item.file.size,
25
+ filename: item.file.name,
26
+ checksum: item.fingerprint.checksum,
27
+ },
28
+ { signal: controller.signal },
29
+ )
30
+
31
+ rt.inflightIntents.delete(localId)
32
+
33
+ // Item might have been canceled while intent was creating
34
+ const current = rt.state.items.get(localId)
35
+ if (!current || current.phase !== 'creating_intent') return
36
+
37
+ // Validate intent from backend
38
+ const intentError = validateIntent(intent, intent.strategy)
39
+ if (intentError) {
40
+ const error: UploadError = {
41
+ code: 'validation_failed',
42
+ message: `Invalid intent from backend: ${intentError.message}`,
43
+ cause: intentError,
44
+ retryable: false,
45
+ }
46
+ rt.applyInternal({ type: 'intent.failed', localId, error, retryable: false })
47
+ return
48
+ }
49
+
50
+ rt.applyInternal({ type: 'intent.ok', localId, intent })
51
+ } catch (err: unknown) {
52
+ rt.inflightIntents.delete(localId)
53
+
54
+ if (controller.signal.aborted) {
55
+ // canceled
56
+ return
57
+ }
58
+
59
+ const error = normalizeError(err, rt.opts.errorNormalizer)
60
+ const errorWithContext: UploadError = {
61
+ ...error,
62
+ message: `${error.message} (file: ${item.fingerprint.name}, size: ${item.file.size} bytes, purpose: ${item.purpose})`,
63
+ }
64
+
65
+ const decision = retryDecision(rt.opts.config, { phase: 'intent', attempt: item.attempt, error: errorWithContext })
66
+
67
+ rt.applyInternal({ type: 'intent.failed', localId, error: errorWithContext, retryable: decision.retryable })
68
+
69
+ if (decision.retryable && decision.delayMs !== undefined) {
70
+ rt.enqueueEffect(async () => {
71
+ await sleep(decision.delayMs)
72
+ rt.dispatch({ type: 'retry', localId })
73
+ })
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,59 @@
1
+ import type { CursorMap, IntentMap, UploadError, UploadResultBase } from '../../../contracts'
2
+ import { hasIntent, normalizeError, retryDecision, sleep } from '../store.libs'
3
+ import type { StoreRuntime } from '../store.types'
4
+
5
+ export async function finalizeUpload<
6
+ M extends IntentMap,
7
+ C extends CursorMap<M>,
8
+ P extends string,
9
+ R extends UploadResultBase,
10
+ >(rt: StoreRuntime<M, C, P, R>, localId: string) {
11
+ const item = rt.state.items.get(localId)
12
+ if (!item || item.phase !== 'completing') return
13
+ if (rt.inflightCompletes.has(localId)) return
14
+
15
+ const controller = new AbortController()
16
+ rt.inflightCompletes.set(localId, controller)
17
+
18
+ try {
19
+ // Final commit step (backend should verify upload, then mark/commit it).
20
+ // Strategy-specific API calls (multipart/tus) must happen inside the strategy itself.
21
+ const result = await rt.opts.api.complete({ fileId: item.intent.fileId }, { signal: controller.signal })
22
+ rt.inflightCompletes.delete(localId)
23
+
24
+ // Item might have been canceled
25
+ const current = rt.state.items.get(localId)
26
+ if (!current || current.phase !== 'completing') return
27
+
28
+ rt.applyInternal({ type: 'complete.ok', localId, result })
29
+ } catch (err: unknown) {
30
+ rt.inflightCompletes.delete(localId)
31
+
32
+ if (controller.signal.aborted) return
33
+
34
+ const error = normalizeError(err, rt.opts.errorNormalizer)
35
+
36
+ const itemForContext = rt.state.items.get(localId)
37
+ const errorWithContext: UploadError = itemForContext
38
+ ? {
39
+ ...error,
40
+ message: `${error.message} (file: ${itemForContext.fingerprint.name}, fileId: ${
41
+ (hasIntent(itemForContext) && itemForContext.intent?.fileId) ?? 'unknown'
42
+ })`,
43
+ }
44
+ : error
45
+
46
+ // Use actual attempt number from item state, defaulting to 1
47
+ const attempt = itemForContext && 'attempt' in itemForContext ? (itemForContext.attempt ?? 1) : 1
48
+ const decision = retryDecision(rt.opts.config, { phase: 'complete', attempt, error: errorWithContext })
49
+
50
+ rt.applyInternal({ type: 'complete.failed', localId, error: errorWithContext, retryable: decision.retryable })
51
+
52
+ if (decision.retryable && decision.delayMs !== undefined) {
53
+ rt.enqueueEffect(async () => {
54
+ await sleep(decision.delayMs)
55
+ rt.dispatch({ type: 'retry', localId })
56
+ })
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,7 @@
1
+ export * from './add-file'
2
+ export * from './cancel'
3
+ export * from './clean-up'
4
+ export * from './create-intent'
5
+ export * from './finalize'
6
+ export * from './pause'
7
+ export * from './run-upload'
@@ -0,0 +1,16 @@
1
+ import type { CursorMap, IntentMap, UploadResultBase } from '../../../contracts'
2
+ import type { StoreRuntime } from '../store.types'
3
+
4
+ export function handlePause<M extends IntentMap, C extends CursorMap<M>, P extends string, R extends UploadResultBase>(
5
+ rt: StoreRuntime<M, C, P, R>,
6
+ localId: string,
7
+ ) {
8
+ const inflight = rt.inflightUploads.get(localId)
9
+ if (inflight) {
10
+ inflight.mode = 'pause'
11
+ inflight.controller.abort({ reason: 'pause' })
12
+ return
13
+ }
14
+
15
+ // If not inflight but queued, reducer already reverts it to ready.
16
+ }
@@ -0,0 +1,192 @@
1
+ import type { AnyCursor, CursorMap, IntentMap, UploadError, UploadResultBase } from '../../../contracts'
2
+ import { hasCursor, hasFile, hasIntent, normalizeError, retryDecision, sleep } from '../store.libs'
3
+ import type { StoreRuntime } from '../store.types'
4
+
5
+ export async function runUpload<
6
+ M extends IntentMap,
7
+ C extends CursorMap<M>,
8
+ P extends string,
9
+ R extends UploadResultBase,
10
+ >(rt: StoreRuntime<M, C, P, R>, localId: string) {
11
+ const item = rt.state.items.get(localId)
12
+
13
+ // If the state moved on (or we never had a valid upload), drop any inflight entry.
14
+ if (!item || item.phase !== 'uploading' || !hasIntent(item)) {
15
+ rt.inflightUploads.delete(localId)
16
+ return
17
+ }
18
+
19
+ let inflight = rt.inflightUploads.get(localId)
20
+ if (!inflight) {
21
+ inflight = { controller: new AbortController(), mode: 'normal', started: false }
22
+ rt.inflightUploads.set(localId, inflight)
23
+ }
24
+
25
+ if (inflight.started) return
26
+ inflight.started = true
27
+
28
+ const controller = inflight.controller
29
+
30
+ const applyCanceled = () => {
31
+ const current = rt.state.items.get(localId)
32
+ if (!current || current.phase === 'canceled') return
33
+ rt.applyInternal({ type: 'canceled', localId, canceledAt: Date.now() })
34
+ }
35
+
36
+ const applyPaused = (cursor: AnyCursor<C>) => {
37
+ const current = rt.state.items.get(localId)
38
+ if (!current || current.phase === 'paused') return
39
+ rt.applyInternal({ type: 'paused', localId, cursor, pausedAt: Date.now() })
40
+ }
41
+
42
+ // If paused/canceled before the request begins, complete it immediately.
43
+ if (controller.signal.aborted) {
44
+ rt.inflightUploads.delete(localId)
45
+
46
+ if (inflight.mode === 'pause') {
47
+ const cur = rt.state.items.get(localId)
48
+ let strategy: (keyof M & string) | undefined
49
+ if (item && hasIntent(item)) {
50
+ strategy = item.intent.strategy
51
+ }
52
+
53
+ const cursor =
54
+ (cur && hasCursor(cur) ? cur.cursor : undefined) || (strategy ? { strategy, value: undefined } : undefined)
55
+
56
+ if (cursor) applyPaused(cursor)
57
+ return
58
+ }
59
+
60
+ if (inflight.mode === 'cancel') {
61
+ applyCanceled()
62
+ return
63
+ }
64
+
65
+ return
66
+ }
67
+
68
+ const strategyId = item.intent.strategy
69
+ if (!rt.opts.strategies.has(strategyId)) {
70
+ rt.inflightUploads.delete(localId)
71
+ const error: UploadError = {
72
+ code: 'strategy_missing',
73
+ message: `Strategy not found: ${strategyId}`,
74
+ strategy: String(strategyId),
75
+ retryable: false,
76
+ }
77
+ rt.applyInternal({ type: 'upload.failed', localId, error, retryable: false })
78
+ return
79
+ }
80
+
81
+ const strategy = rt.opts.strategies.get(strategyId)
82
+
83
+ if (!strategy) {
84
+ rt.inflightUploads.delete(localId)
85
+ const error: UploadError = {
86
+ code: 'strategy_missing',
87
+ message: `Strategy not found: ${item.intent.strategy}`,
88
+ strategy: String(item.intent.strategy),
89
+ retryable: false,
90
+ }
91
+ rt.applyInternal({ type: 'upload.failed', localId, error, retryable: false })
92
+ return
93
+ }
94
+
95
+ // Throttle progress events
96
+ let lastEmit = 0
97
+ const progressThrottle = rt.opts.config.progressThrottleMs
98
+
99
+ try {
100
+ await strategy.start({
101
+ file: item.file,
102
+ intent: item.intent,
103
+ signal: controller.signal,
104
+ transport: rt.opts.transport,
105
+ api: rt.opts.api,
106
+
107
+ readCursor: () => {
108
+ const cur = rt.state.items.get(localId)
109
+ if (!cur || !hasCursor(cur) || !cur.cursor) return undefined
110
+ if (cur.cursor.strategy !== strategyId) return undefined
111
+ return cur.cursor.value
112
+ },
113
+
114
+ persistCursor: (cursorValue) => {
115
+ const tagged: AnyCursor<C> = { strategy: strategyId, value: cursorValue }
116
+ rt.applyInternal({ type: 'cursor.updated', localId, cursor: tagged })
117
+ },
118
+
119
+ reportProgress: (p) => {
120
+ const now = Date.now()
121
+ if (now - lastEmit < progressThrottle) return
122
+
123
+ rt.applyInternal({
124
+ type: 'upload.progress',
125
+ localId,
126
+ uploadedBytes: p.uploadedBytes,
127
+ totalBytes: p.totalBytes,
128
+ })
129
+
130
+ lastEmit = now
131
+ },
132
+ })
133
+
134
+ rt.inflightUploads.delete(localId)
135
+
136
+ if (inflight?.mode === 'cancel') {
137
+ applyCanceled()
138
+ return
139
+ }
140
+ if (controller.signal.aborted && inflight?.mode === 'pause') {
141
+ // Paused at the end; reducer expects cursor - if not present, keep last-known or synthesize.
142
+ const cur = rt.state.items.get(localId)
143
+ const cursor = (cur && hasCursor(cur) ? cur.cursor : undefined) || { strategy: strategyId, value: undefined }
144
+ applyPaused(cursor)
145
+ return
146
+ }
147
+
148
+ // Normal completion -> completing
149
+ rt.applyInternal({ type: 'upload.ok', localId })
150
+ // finalizeUpload scheduled by scheduleWork() via completing phase
151
+ } catch (err: unknown) {
152
+ rt.inflightUploads.delete(localId)
153
+
154
+ if (controller.signal.aborted) {
155
+ if (inflight?.mode === 'pause') {
156
+ const cur = rt.state.items.get(localId)
157
+ const cursor = (cur && hasCursor(cur) ? cur.cursor : undefined) || { strategy: strategyId, value: undefined }
158
+ applyPaused(cursor)
159
+ return
160
+ }
161
+
162
+ if (inflight?.mode === 'cancel') {
163
+ applyCanceled()
164
+ return
165
+ }
166
+
167
+ // Unknown abort -> treat as cancel
168
+ applyCanceled()
169
+ return
170
+ }
171
+
172
+ const error = normalizeError(err, rt.opts.errorNormalizer)
173
+ const itemForContext = rt.state.items.get(localId)
174
+ const errorWithContext: UploadError = itemForContext
175
+ ? {
176
+ ...error,
177
+ message: `${error.message} (file: ${itemForContext.fingerprint.name}, size: ${
178
+ (hasFile(itemForContext) && itemForContext.file?.size) ?? 0
179
+ } bytes, phase: ${itemForContext.phase})`,
180
+ }
181
+ : error
182
+ const decision = retryDecision(rt.opts.config, { phase: 'upload', attempt: 1, error: errorWithContext })
183
+ rt.applyInternal({ type: 'upload.failed', localId, error: errorWithContext, retryable: decision.retryable })
184
+
185
+ if (decision.retryable && decision.delayMs !== undefined) {
186
+ rt.enqueueEffect(async () => {
187
+ await sleep(decision.delayMs)
188
+ rt.dispatch({ type: 'retry', localId })
189
+ })
190
+ }
191
+ }
192
+ }
@@ -0,0 +1,123 @@
1
+ import type { CursorMap, IntentMap, UploadResultBase } from '../../contracts'
2
+ import type { UploadOutcome } from '../outcome.types'
3
+ import { dispatch as internalDispatch } from './store.dispatch'
4
+ import { createStoreRuntime } from './store.runtime'
5
+ import type { StoreOptions, StoreRuntime, UploadStore } from './store.types'
6
+
7
+ // TODO: add option for debug mode
8
+
9
+ /**
10
+ * Creates an upload store instance.
11
+ *
12
+ * This is the main runtime object you interact with from the outside:
13
+ * - dispatch user commands (add files, start, pause, cancel, resume)
14
+ * - subscribe to state changes (for UI rendering)
15
+ * - subscribe to typed events (for progress, errors, and lifecycle)
16
+ *
17
+ * Plugins are initialized once during construction and receive a minimal store
18
+ * proxy (events, dispatch, snapshot access) to avoid tight coupling.
19
+ *
20
+ * @template M - Intent map type (keyed by strategy id)
21
+ * @template C - Cursor map type (keyed by strategy id)
22
+ * @template P - Purpose string union type
23
+ *
24
+ * @param opts - Store options (config, backend API, strategies, transport, hooks)
25
+ * @returns A configured {@link UploadStore} instance.
26
+ */
27
+ export function createUploadStore<
28
+ M extends IntentMap,
29
+ C extends CursorMap<M>,
30
+ P extends string,
31
+ R extends UploadResultBase = UploadResultBase,
32
+ >(opts: StoreOptions<M, C, P, R>): UploadStore<M, C, P, R> {
33
+ const rt = createStoreRuntime(opts)
34
+
35
+ // Wire dispatch for handlers that need retries without import cycles.
36
+ rt.dispatch = (cmd) => internalDispatch(rt, cmd)
37
+
38
+ const storeProxy: Pick<UploadStore<M, C, P, R>, 'on' | 'off' | 'dispatch' | 'getSnapshot'> = {
39
+ on: rt.emitter.on.bind(rt.emitter),
40
+ off: rt.emitter.off.bind(rt.emitter),
41
+ dispatch: rt.dispatch,
42
+ getSnapshot: () => rt.state,
43
+ }
44
+
45
+ // Plugin setup
46
+ for (const plugin of opts.plugins || []) {
47
+ try {
48
+ plugin.setup(storeProxy)
49
+ } catch (err) {
50
+ if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') {
51
+ console.error(`[UploadEngine] Plugin "${plugin.name}" failed to setup:`, err)
52
+ }
53
+ }
54
+ }
55
+
56
+ return {
57
+ ...storeProxy,
58
+ subscribe: (listener) => {
59
+ rt.listeners.add(listener)
60
+ return () => rt.listeners.delete(listener)
61
+ },
62
+ waitFor(localIds) {
63
+ return waitForOutcomes(rt, localIds)
64
+ },
65
+ }
66
+ }
67
+
68
+ export * from './store.types'
69
+
70
+ function waitForOutcomes<M extends IntentMap, C extends CursorMap<M>, P extends string, R extends UploadResultBase>(
71
+ rt: StoreRuntime<M, C, P, R>,
72
+ localIds: string[],
73
+ ): Promise<Array<UploadOutcome<R>>> {
74
+ if (localIds.length === 0) return Promise.resolve([])
75
+
76
+ const pending = new Set(localIds)
77
+ const outcomes = new Map<string, UploadOutcome<R>>()
78
+
79
+ const update = () => {
80
+ for (const id of pending) {
81
+ const item = rt.state.items.get(id)
82
+ if (!item) {
83
+ outcomes.set(id, { localId: id, status: 'missing' })
84
+ pending.delete(id)
85
+ continue
86
+ }
87
+
88
+ if (item.phase === 'completed') {
89
+ outcomes.set(id, { localId: id, status: 'completed', completedBy: item.completedBy, result: item.result })
90
+ pending.delete(id)
91
+ continue
92
+ }
93
+
94
+ if (item.phase === 'error') {
95
+ outcomes.set(id, { localId: id, status: 'error', error: item.error })
96
+ pending.delete(id)
97
+ continue
98
+ }
99
+
100
+ if (item.phase === 'canceled') {
101
+ outcomes.set(id, { localId: id, status: 'canceled' })
102
+ pending.delete(id)
103
+ }
104
+ }
105
+ }
106
+
107
+ update()
108
+
109
+ if (pending.size === 0) {
110
+ return Promise.resolve(localIds.map((id) => outcomes.get(id) ?? { localId: id, status: 'missing' }))
111
+ }
112
+
113
+ return new Promise((resolve) => {
114
+ const listener = () => {
115
+ update()
116
+ if (pending.size === 0) {
117
+ rt.listeners.delete(listener)
118
+ resolve(localIds.map((id) => outcomes.get(id) ?? { localId: id, status: 'missing' }))
119
+ }
120
+ }
121
+ rt.listeners.add(listener)
122
+ })
123
+ }
@@ -0,0 +1,78 @@
1
+ import type { CursorMap, IntentMap, UploadResultBase } from '../../contracts'
2
+ import type { UploadCommand } from '../commands.types'
3
+ import { handleAddFiles } from './handlers/add-file'
4
+ import { handleCancel } from './handlers/cancel'
5
+ import { handlePause } from './handlers/pause'
6
+ import type { StoreRuntime } from './store.types'
7
+
8
+ /**
9
+ * Dispatches a command against the runtime.
10
+ *
11
+ * Some commands are expanded into multiple operations (e.g. startAll/pauseAll/cancelAll).
12
+ * Some commands need imperative side effects (pause/cancel/addFiles) and therefore call
13
+ * handler functions before/after applying reducer transitions.
14
+ *
15
+ * @template M - Intent map type
16
+ * @template C - Cursor map type
17
+ * @template P - Purpose string union type
18
+ */
19
+ export function dispatch<M extends IntentMap, C extends CursorMap<M>, P extends string, R extends UploadResultBase>(
20
+ rt: StoreRuntime<M, C, P, R>,
21
+ cmd: UploadCommand<P>,
22
+ ) {
23
+ // Bulk operations
24
+ if (cmd.type === 'startAll') {
25
+ const purpose = cmd.purpose
26
+ Array.from(rt.state.items.values())
27
+ .filter((item) => item.phase === 'ready' && (!purpose || item.purpose === purpose))
28
+ .forEach((item) => rt.dispatch({ type: 'start', localId: item.localId }))
29
+ return
30
+ }
31
+
32
+ if (cmd.type === 'pauseAll') {
33
+ const purpose = cmd.purpose
34
+ Array.from(rt.state.items.values())
35
+ .filter((item) => item.phase === 'uploading' && (!purpose || item.purpose === purpose))
36
+ .forEach((item) => rt.dispatch({ type: 'pause', localId: item.localId }))
37
+ return
38
+ }
39
+
40
+ if (cmd.type === 'cancelAll') {
41
+ const purpose = cmd.purpose
42
+ Array.from(rt.state.items.values())
43
+ .filter(
44
+ (item) => item.phase !== 'completed' && item.phase !== 'canceled' && (!purpose || item.purpose === purpose),
45
+ )
46
+ .forEach((item) => rt.dispatch({ type: 'cancel', localId: item.localId }))
47
+ return
48
+ }
49
+
50
+ if (cmd.type === 'addFiles') {
51
+ handleAddFiles(rt, cmd.files, cmd.purpose)
52
+ return
53
+ }
54
+
55
+ if (cmd.type === 'pause') {
56
+ handlePause(rt, cmd.localId)
57
+ rt.applyCommand(cmd)
58
+ return
59
+ }
60
+
61
+ if (cmd.type === 'cancel') {
62
+ handleCancel(rt, cmd.localId)
63
+ rt.applyCommand(cmd)
64
+ return
65
+ }
66
+
67
+ if (cmd.type === 'start') {
68
+ rt.applyCommand(cmd)
69
+ return
70
+ }
71
+
72
+ if (cmd.type === 'resume') {
73
+ rt.applyCommand(cmd)
74
+ return
75
+ }
76
+
77
+ rt.applyCommand(cmd)
78
+ }