@gentleduck/upload 0.2.6 → 0.3.0
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/README.md +170 -193
- package/dist/core/index.d.ts +2 -3
- package/dist/core/index.js +1 -1
- package/dist/core-BVgloTiP.js +10 -0
- package/dist/index-DCqyBVcx.d.ts +1441 -0
- package/dist/index-DsZHPmJm.d.ts +225 -0
- package/dist/index-KROXtLQh.d.ts +281 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +2 -51
- package/dist/react/index.js +1 -1
- package/dist/react-euPrDv24.js +1 -0
- package/dist/strategies/index.d.ts +2 -56
- package/dist/strategies/index.js +1 -1
- package/dist/strategies-CUF0s68j.js +1 -0
- package/package.json +3 -2
- package/src/{__test__ → __tests__}/emitter.test.ts +4 -4
- package/src/{__test__ → __tests__}/fingerprint.test.ts +1 -1
- package/src/{__test__ → __tests__}/guards.test.ts +15 -5
- package/src/__tests__/id.test.ts +67 -0
- package/src/__tests__/mime-sniff.test.ts +142 -0
- package/src/{__test__ → __tests__}/persistence.test.ts +43 -8
- package/src/__tests__/sanitize-filename.test.ts +109 -0
- package/src/__tests__/sleep.test.ts +34 -0
- package/src/__tests__/static-analysis.test.ts +145 -0
- package/src/{__test__ → __tests__}/store-libs.test.ts +57 -3
- package/src/__tests__/transport-fetch.test.ts +108 -0
- package/src/{__test__ → __tests__}/transport.test.ts +5 -5
- package/src/core/__tests__/errors.test.ts +152 -0
- package/src/core/client.ts +3 -0
- package/src/core/contracts/contracts.types.ts +335 -141
- package/src/core/contracts/index.ts +3 -8
- package/src/core/contracts/transport/index.ts +3 -2
- package/src/core/contracts/transport/transport.fetch.ts +203 -0
- package/src/core/contracts/transport/transport.libs.ts +7 -8
- package/src/core/contracts/transport/transport.ts +91 -83
- package/src/core/contracts/transport/transport.types.ts +71 -35
- package/src/core/engine/__tests__/config-nan-bypass.test.ts +40 -0
- package/src/core/engine/engine.libs.ts +35 -0
- package/src/core/engine/engine.types.ts +367 -241
- package/src/core/engine/index.ts +3 -8
- package/src/core/engine/reducer.ts +89 -43
- package/src/core/engine/store/handlers/add-file.ts +39 -29
- package/src/core/engine/store/handlers/cancel.ts +13 -8
- package/src/core/engine/store/handlers/clean-up.ts +11 -8
- package/src/core/engine/store/handlers/create-intent.ts +82 -27
- package/src/core/engine/store/handlers/finalize.ts +30 -13
- package/src/core/engine/store/handlers/index.ts +7 -7
- package/src/core/engine/store/handlers/pause.ts +8 -6
- package/src/core/engine/store/handlers/run-upload.ts +49 -34
- package/src/core/engine/store/index.ts +27 -21
- package/src/core/engine/store/store.dispatch.ts +10 -8
- package/src/core/engine/store/store.libs.ts +184 -41
- package/src/core/engine/store/store.runtime.ts +45 -36
- package/src/core/engine/store/store.schedule.ts +27 -21
- package/src/core/engine/store/store.types.ts +177 -227
- package/src/core/engine/validation/__tests__/intent.test.ts +70 -0
- package/src/core/engine/validation/file.ts +7 -3
- package/src/core/engine/validation/index.ts +1 -0
- package/src/core/engine/validation/intent.ts +49 -27
- package/src/core/engine/validation/list.ts +14 -15
- package/src/core/engine/validation/mime.ts +39 -0
- package/src/core/errors.ts +271 -0
- package/src/core/index.ts +28 -5
- package/src/core/persistence/__tests__/deserialize-nan-bypass.test.ts +100 -0
- package/src/core/persistence/__tests__/persistence.test.ts +85 -0
- package/src/core/persistence/adapters.indexeddb.ts +9 -3
- package/src/core/persistence/adapters.local.ts +9 -3
- package/src/core/persistence/adapters.memory.ts +35 -13
- package/src/core/persistence/index.ts +5 -5
- package/src/core/persistence/persistence.ts +73 -60
- package/src/core/persistence/persistence.types.ts +140 -60
- package/src/core/utils/__tests__/guards.test.ts +93 -0
- package/src/core/utils/__tests__/url-safety.test.ts +112 -0
- package/src/core/utils/async.ts +7 -2
- package/src/core/utils/constants.ts +3 -9
- package/src/core/utils/emitter/emitter.ts +7 -24
- package/src/core/utils/emitter/emitter.types.ts +33 -16
- package/src/core/utils/emitter/index.ts +2 -74
- package/src/core/utils/fingerprint.ts +16 -5
- package/src/core/utils/guards.ts +91 -1
- package/src/core/utils/hash/__tests__/sha256.test.ts +61 -0
- package/src/core/utils/hash/index.ts +126 -0
- package/src/core/utils/hash/sha256.ts +179 -0
- package/src/core/utils/id.ts +33 -1
- package/src/core/utils/index.ts +15 -6
- package/src/core/utils/mime-sniff.ts +119 -0
- package/src/core/utils/sanitize-filename.ts +85 -0
- package/src/core/utils/url-safety.ts +156 -0
- package/src/react/__tests__/use-dropzone.test.ts +64 -0
- package/src/react/index.ts +5 -2
- package/src/react/upload-provider.tsx +68 -53
- package/src/react/uploader.types.ts +58 -61
- package/src/react/use-dropzone.tsx +226 -0
- package/src/react/use-uploader.tsx +98 -91
- package/src/strategies/_shared/__tests__/retry.test.ts +120 -0
- package/src/strategies/_shared/retry.ts +79 -0
- package/src/strategies/index.ts +9 -15
- package/src/strategies/multipart/__tests__/multipart.test.ts +229 -0
- package/src/strategies/multipart/index.ts +206 -108
- package/src/strategies/post/__tests__/post-url-safety.test.ts +119 -0
- package/src/strategies/post/index.ts +115 -33
- package/src/strategies/put/__tests__/put.test.ts +132 -0
- package/src/strategies/put/index.ts +139 -0
- package/src/strategies/registry/index.ts +12 -9
- package/src/strategies/tus/__tests__/tus.test.ts +159 -0
- package/src/strategies/tus/index.ts +211 -0
- package/dist/async-DxRWu4le.js +0 -1
- package/dist/core-CMkrP9DX.js +0 -2
- package/dist/index-C38Jmizu.d.ts +0 -1005
- package/dist/strategy.types-gg9ZStRe.d.ts +0 -225
- package/src/__test__/effect-pool.test.ts +0 -5
- package/src/__test__/id.test.ts +0 -14
- package/src/__test__/store-integration.test.ts +0 -5
- package/src/core/client/client.ts +0 -50
- package/src/core/client/client.types.ts +0 -184
- package/src/core/client/config.types.ts +0 -77
- package/src/core/client/index.ts +0 -4
- package/src/core/client/plugins.types.ts +0 -46
- package/src/core/client/retry.types.ts +0 -6
- package/src/core/client/uploadClient.ts +0 -19
- package/src/core/contracts/backendApi.ts +0 -62
- package/src/core/contracts/cursor.types.ts +0 -23
- package/src/core/contracts/errors.types.ts +0 -67
- package/src/core/contracts/fingerprint.types.ts +0 -18
- package/src/core/contracts/intent.types.ts +0 -39
- package/src/core/contracts/result.types.ts +0 -11
- package/src/core/contracts/strategy.types.ts +0 -45
- package/src/core/engine/commands.types.ts +0 -17
- package/src/core/engine/event-map.types.ts +0 -45
- package/src/core/engine/internal-events.types.ts +0 -202
- package/src/core/engine/outcome.types.ts +0 -9
- package/src/core/engine/progress.types.ts +0 -15
- package/src/core/engine/store/handlers.ts +0 -819
- package/src/core/engine/validation.ts +0 -216
- /package/src/{__test__ → __tests__}/transport-libs.test.ts +0 -0
|
@@ -1,19 +1,114 @@
|
|
|
1
|
-
import type { Contracts } from '../contracts'
|
|
1
|
+
import type { Contracts } from '../contracts/contracts.types'
|
|
2
|
+
import type { UploadError } from '../errors'
|
|
3
|
+
import type { Store } from './store'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
|
-
* Engine-level type surface
|
|
5
|
-
*
|
|
6
|
+
* Engine-level type surface containing runtime state models, commands, progress,
|
|
7
|
+
* and configuration options.
|
|
6
8
|
* @author wildduck2 <https://github.com/wildduck2>
|
|
7
9
|
*/
|
|
8
10
|
export namespace Engine {
|
|
9
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Return value from a retry policy determining if and when a task should be re-scheduled.
|
|
13
|
+
*/
|
|
14
|
+
export type RetryDecision =
|
|
15
|
+
| {
|
|
16
|
+
/** The task has failed terminal or cannot recover. */
|
|
17
|
+
retryable: false
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
/** The task is recoverable and should retry. */
|
|
21
|
+
retryable: true
|
|
22
|
+
/** Delay duration in milliseconds before invoking the next attempt. */
|
|
23
|
+
delayMs: number
|
|
24
|
+
}
|
|
10
25
|
|
|
11
26
|
/**
|
|
12
|
-
*
|
|
27
|
+
* Global upload engine configuration.
|
|
28
|
+
*
|
|
29
|
+
* Coordinates concurrency limits, retry strategies, constraints, and validation.
|
|
13
30
|
*
|
|
14
|
-
* @
|
|
31
|
+
* @typeParam P - Union of allowed `purpose` strings for your app (e.g. `"avatar" | "attachment"`).
|
|
32
|
+
*/
|
|
33
|
+
export type Config<P extends string> = {
|
|
34
|
+
/**
|
|
35
|
+
* Maximum number of parallel uploads active at any time.
|
|
36
|
+
* Queued items wait until active uploads complete or fail.
|
|
37
|
+
*/
|
|
38
|
+
maxConcurrentUploads: number
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Auto-start uploads for specific purposes.
|
|
42
|
+
* - Array form: only files with these purposes automatically begin uploading on registration.
|
|
43
|
+
* - Function form: dynamic callback determining whether to auto-start.
|
|
44
|
+
*/
|
|
45
|
+
autoStart?: (readonly P[] | ((purpose: P) => boolean)) | undefined
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Throttle delay in milliseconds for progress events.
|
|
49
|
+
* Limits rendering updates and state changes under busy progress flows.
|
|
50
|
+
*/
|
|
51
|
+
progressThrottleMs: number
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validation rules keyed by purpose.
|
|
55
|
+
*/
|
|
56
|
+
validation: Partial<Record<P, Contracts.ValidationRules>>
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Maximum number of retry attempts permitted for upload/intent actions.
|
|
60
|
+
*/
|
|
61
|
+
maxAttempts: number
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Custom retry policy handler.
|
|
65
|
+
* Overrides default exponential backoff calculation.
|
|
66
|
+
*/
|
|
67
|
+
retryPolicy?:
|
|
68
|
+
| ((ctx: {
|
|
69
|
+
/** The active execution phase. */
|
|
70
|
+
phase: 'intent' | 'upload' | 'complete'
|
|
71
|
+
/** Current attempt number (starting at 1). */
|
|
72
|
+
attempt: number
|
|
73
|
+
/** The normalized error structure. */
|
|
74
|
+
error: UploadError
|
|
75
|
+
}) => RetryDecision)
|
|
76
|
+
| undefined
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Maximum number of items to keep in memory/state.
|
|
80
|
+
* When exceeded, oldest completed/canceled items are evicted automatically.
|
|
81
|
+
*
|
|
82
|
+
* Set to `undefined` for no limit (not recommended for long-running single-page apps).
|
|
83
|
+
*/
|
|
84
|
+
maxItems: number
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Duration in milliseconds after which completed items are evicted from the store.
|
|
88
|
+
* Set to `undefined` to preserve completed items indefinitely.
|
|
89
|
+
*/
|
|
90
|
+
completedItemTTL?: number | undefined
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Set to `true` to validate files by sniffing magic bytes/mime signature
|
|
94
|
+
* and rejecting files whose suffix does not match structural magic numbers.
|
|
95
|
+
*/
|
|
96
|
+
strictMimeMatch?: boolean | undefined
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* File-size boundary in bytes between the two checksum paths. At or below
|
|
100
|
+
* it, files hash inline with native Web Crypto (fastest). Above it, files
|
|
101
|
+
* hash incrementally in bounded slices — off the main thread via a Web
|
|
102
|
+
* Worker when available — so large files no longer freeze the UI and are
|
|
103
|
+
* still deduplicated. Set to `null` or `undefined` for the default (64 MiB).
|
|
104
|
+
*/
|
|
105
|
+
checksumMaxSize?: number | null | undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Upload progress snapshot.
|
|
15
110
|
*/
|
|
16
|
-
export
|
|
111
|
+
export type Progress = {
|
|
17
112
|
/** Bytes uploaded so far. */
|
|
18
113
|
uploadedBytes: number
|
|
19
114
|
/** Total bytes to upload. */
|
|
@@ -22,255 +117,269 @@ export namespace Engine {
|
|
|
22
117
|
pct: number
|
|
23
118
|
}
|
|
24
119
|
|
|
25
|
-
// -------------------- Outcome --------------------
|
|
26
|
-
|
|
27
120
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
121
|
+
* Describes how a `completed` upload item resolved.
|
|
122
|
+
* - `'upload'`: standard byte transfer.
|
|
123
|
+
* - `'dedupe'`: server matched existing file checksum, skipping byte transfer.
|
|
31
124
|
*/
|
|
32
125
|
export type CompletionKind = 'upload' | 'dedupe'
|
|
33
126
|
|
|
34
127
|
/**
|
|
35
|
-
* Terminal outcome returned by `
|
|
128
|
+
* Terminal outcome returned by `waitFor`.
|
|
36
129
|
*
|
|
37
130
|
* @template R Result type carried on successful completion.
|
|
38
|
-
* @author wildduck2 <https://github.com/wildduck2>
|
|
39
131
|
*/
|
|
40
|
-
export type Outcome<R extends Contracts.
|
|
41
|
-
| {
|
|
42
|
-
|
|
43
|
-
|
|
132
|
+
export type Outcome<R extends Contracts.Result.Base> =
|
|
133
|
+
| {
|
|
134
|
+
localId: string
|
|
135
|
+
status: 'completed'
|
|
136
|
+
completedBy: CompletionKind
|
|
137
|
+
result: R
|
|
138
|
+
}
|
|
139
|
+
| {
|
|
140
|
+
localId: string
|
|
141
|
+
status: 'error'
|
|
142
|
+
error: UploadError
|
|
143
|
+
}
|
|
144
|
+
| {
|
|
145
|
+
localId: string
|
|
146
|
+
status: 'canceled'
|
|
147
|
+
}
|
|
44
148
|
| {
|
|
45
149
|
localId: string
|
|
46
150
|
status: 'missing'
|
|
47
151
|
/**
|
|
48
|
-
* Why the id is missing.
|
|
49
|
-
*
|
|
50
|
-
* - `'
|
|
51
|
-
* - `'
|
|
52
|
-
* - `'
|
|
53
|
-
* - `'destroyed'`: `store.destroy()` ran while the wait was pending.
|
|
152
|
+
* Why the id is missing.
|
|
153
|
+
* - `'removed'`: explicit user removal.
|
|
154
|
+
* - `'evicted'`: automatically removed (maxItems/TTL rules).
|
|
155
|
+
* - `'never-existed'`: id was never registered.
|
|
156
|
+
* - `'destroyed'`: store was destroyed.
|
|
54
157
|
*/
|
|
55
158
|
reason: 'removed' | 'evicted' | 'never-existed' | 'destroyed'
|
|
56
159
|
}
|
|
57
160
|
|
|
58
|
-
// -------------------- Commands --------------------
|
|
59
|
-
|
|
60
161
|
/**
|
|
61
|
-
* Public commands
|
|
162
|
+
* Public commands dispatched to the upload store.
|
|
62
163
|
*
|
|
63
164
|
* @template P Purpose discriminator string.
|
|
64
|
-
* @author wildduck2 <https://github.com/wildduck2>
|
|
65
165
|
*/
|
|
66
166
|
export type Command<P extends string> =
|
|
67
|
-
| { type: 'addFiles'; files: File[]; purpose: P }
|
|
68
|
-
| { type: 'start'; localId: string }
|
|
69
|
-
| { type: 'startAll'; purpose?: P }
|
|
70
|
-
| { type: 'pause'; localId: string }
|
|
71
|
-
| { type: 'pauseAll'; purpose?: P }
|
|
72
|
-
| { type: 'resume'; localId: string }
|
|
73
|
-
| { type: 'cancel'; localId: string }
|
|
74
|
-
| { type: 'cancelAll'; purpose?: P }
|
|
75
|
-
| { type: 'retry'; localId: string }
|
|
76
|
-
| { type: 'rebind'; localId: string; file: File }
|
|
77
|
-
| { type: 'remove'; localId: string }
|
|
78
|
-
|
|
79
|
-
// -------------------- Phases / Items --------------------
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Every upload phase; the `phase` discriminator of {@link Engine.Item}.
|
|
83
|
-
*
|
|
84
|
-
* @author wildduck2 <https://github.com/wildduck2>
|
|
85
|
-
*/
|
|
86
|
-
export type Phase =
|
|
87
|
-
| 'validating'
|
|
88
|
-
| 'creating_intent'
|
|
89
|
-
| 'ready'
|
|
90
|
-
| 'queued'
|
|
91
|
-
| 'uploading'
|
|
92
|
-
| 'paused'
|
|
93
|
-
| 'completing'
|
|
94
|
-
| 'completed'
|
|
95
|
-
| 'error'
|
|
96
|
-
| 'canceled'
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Enum-style const map of every {@link Phase} value.
|
|
100
|
-
*
|
|
101
|
-
* Reading `Phases.completing` matches the string literal at compile time
|
|
102
|
-
* AND gives consumers a single import point instead of N magic-string
|
|
103
|
-
* checks. Use whichever style fits the call site.
|
|
104
|
-
*
|
|
105
|
-
* @author wildduck2 <https://github.com/wildduck2>
|
|
106
|
-
*/
|
|
107
|
-
export const Phases = {
|
|
108
|
-
validating: 'validating',
|
|
109
|
-
creating_intent: 'creating_intent',
|
|
110
|
-
ready: 'ready',
|
|
111
|
-
queued: 'queued',
|
|
112
|
-
uploading: 'uploading',
|
|
113
|
-
paused: 'paused',
|
|
114
|
-
completing: 'completing',
|
|
115
|
-
completed: 'completed',
|
|
116
|
-
error: 'error',
|
|
117
|
-
canceled: 'canceled',
|
|
118
|
-
} as const satisfies Record<Phase, Phase>
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Discriminated union on `phase` describing a single upload's state.
|
|
122
|
-
*
|
|
123
|
-
* @template M Intent map keyed by strategy id.
|
|
124
|
-
* @template C Cursor map keyed by strategy id.
|
|
125
|
-
* @template P Purpose discriminator string.
|
|
126
|
-
* @template R Backend result payload.
|
|
127
|
-
* @author wildduck2 <https://github.com/wildduck2>
|
|
128
|
-
*/
|
|
129
|
-
export type Item<
|
|
130
|
-
M extends Contracts.IntentMap,
|
|
131
|
-
C extends Contracts.CursorMap<M>,
|
|
132
|
-
P extends string,
|
|
133
|
-
R extends Contracts.IResultBase = Contracts.IResultBase,
|
|
134
|
-
> =
|
|
135
167
|
| {
|
|
136
|
-
|
|
137
|
-
|
|
168
|
+
/** Register files into the upload store for a specific purpose. */
|
|
169
|
+
type: 'addFiles'
|
|
170
|
+
files: File[]
|
|
138
171
|
purpose: P
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
createdAt: number
|
|
172
|
+
/** Optional caller metadata forwarded to every backend API call for this file. */
|
|
173
|
+
meta?: Record<string, unknown> | undefined
|
|
142
174
|
}
|
|
143
175
|
| {
|
|
144
|
-
|
|
176
|
+
/** Start uploading a specific file. */
|
|
177
|
+
type: 'start'
|
|
145
178
|
localId: string
|
|
146
|
-
purpose: P
|
|
147
|
-
fingerprint: Contracts.IFileFingerprint
|
|
148
|
-
file: File
|
|
149
|
-
attempt: number
|
|
150
|
-
createdAt: number
|
|
151
179
|
}
|
|
152
180
|
| {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
purpose
|
|
156
|
-
fingerprint: Contracts.IFileFingerprint
|
|
157
|
-
file: File
|
|
158
|
-
intent: Contracts.AnyIntent<M>
|
|
159
|
-
createdAt: number
|
|
160
|
-
cursor?: Contracts.AnyCursor<C>
|
|
161
|
-
progress?: IUploadProgress
|
|
162
|
-
attempt?: number
|
|
181
|
+
/** Start all queued files, optionally filtering by purpose. */
|
|
182
|
+
type: 'startAll'
|
|
183
|
+
purpose?: P
|
|
163
184
|
}
|
|
164
185
|
| {
|
|
165
|
-
|
|
186
|
+
/** Pause an active upload. */
|
|
187
|
+
type: 'pause'
|
|
166
188
|
localId: string
|
|
167
|
-
purpose: P
|
|
168
|
-
fingerprint: Contracts.IFileFingerprint
|
|
169
|
-
file: File
|
|
170
|
-
intent: Contracts.AnyIntent<M>
|
|
171
|
-
requestedAt: number
|
|
172
|
-
createdAt: number
|
|
173
|
-
cursor?: Contracts.AnyCursor<C>
|
|
174
|
-
progress?: IUploadProgress
|
|
175
|
-
attempt?: number
|
|
176
189
|
}
|
|
177
190
|
| {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
purpose
|
|
181
|
-
fingerprint: Contracts.IFileFingerprint
|
|
182
|
-
file: File
|
|
183
|
-
intent: Contracts.AnyIntent<M>
|
|
184
|
-
startedAt: number
|
|
185
|
-
progress: IUploadProgress
|
|
186
|
-
createdAt: number
|
|
187
|
-
cursor?: Contracts.AnyCursor<C>
|
|
188
|
-
attempt?: number
|
|
191
|
+
/** Pause all active uploads, optionally filtering by purpose. */
|
|
192
|
+
type: 'pauseAll'
|
|
193
|
+
purpose?: P
|
|
189
194
|
}
|
|
190
195
|
| {
|
|
191
|
-
|
|
196
|
+
/** Resume a paused upload. */
|
|
197
|
+
type: 'resume'
|
|
192
198
|
localId: string
|
|
193
|
-
purpose: P
|
|
194
|
-
fingerprint: Contracts.IFileFingerprint
|
|
195
|
-
intent: Contracts.AnyIntent<M>
|
|
196
|
-
cursor: Contracts.AnyCursor<C>
|
|
197
|
-
progress: IUploadProgress
|
|
198
|
-
pausedAt: number
|
|
199
|
-
createdAt: number
|
|
200
|
-
file?: File
|
|
201
|
-
attempt?: number
|
|
202
199
|
}
|
|
203
200
|
| {
|
|
204
|
-
|
|
201
|
+
/** Cancel a pending or active upload. */
|
|
202
|
+
type: 'cancel'
|
|
205
203
|
localId: string
|
|
206
|
-
purpose: P
|
|
207
|
-
fingerprint: Contracts.IFileFingerprint
|
|
208
|
-
file: File
|
|
209
|
-
intent: Contracts.AnyIntent<M>
|
|
210
|
-
progress: IUploadProgress
|
|
211
|
-
completingAt: number
|
|
212
|
-
createdAt: number
|
|
213
|
-
attempt?: number
|
|
214
204
|
}
|
|
215
205
|
| {
|
|
216
|
-
|
|
206
|
+
/** Cancel all pending and active uploads, optionally filtering by purpose. */
|
|
207
|
+
type: 'cancelAll'
|
|
208
|
+
purpose?: P
|
|
209
|
+
}
|
|
210
|
+
| {
|
|
211
|
+
/** Retry a failed upload item. */
|
|
212
|
+
type: 'retry'
|
|
217
213
|
localId: string
|
|
218
|
-
file?: File
|
|
219
|
-
purpose: P
|
|
220
|
-
fingerprint: Contracts.IFileFingerprint
|
|
221
|
-
intent?: Contracts.AnyIntent<M>
|
|
222
|
-
completedBy: CompletionKind
|
|
223
|
-
result: R
|
|
224
|
-
completedAt: number
|
|
225
|
-
createdAt: number
|
|
226
|
-
attempt?: number
|
|
227
214
|
}
|
|
228
215
|
| {
|
|
229
|
-
|
|
216
|
+
/** Re-bind a new browser `File` object to a resumed/paused item missing a file reference. */
|
|
217
|
+
type: 'rebind'
|
|
230
218
|
localId: string
|
|
231
|
-
|
|
232
|
-
fingerprint: Contracts.IFileFingerprint
|
|
233
|
-
error: Contracts.Error
|
|
234
|
-
retryable: boolean
|
|
235
|
-
attempt: number
|
|
236
|
-
failedAt: number
|
|
237
|
-
createdAt: number
|
|
238
|
-
file?: File
|
|
239
|
-
intent?: Contracts.AnyIntent<M>
|
|
240
|
-
cursor?: Contracts.AnyCursor<C>
|
|
241
|
-
progress?: IUploadProgress
|
|
219
|
+
file: File
|
|
242
220
|
}
|
|
243
221
|
| {
|
|
244
|
-
|
|
222
|
+
/** Remove an item from the store state entirely. */
|
|
223
|
+
type: 'remove'
|
|
245
224
|
localId: string
|
|
246
|
-
purpose: P
|
|
247
|
-
fingerprint: Contracts.IFileFingerprint
|
|
248
|
-
canceledAt: number
|
|
249
|
-
createdAt: number
|
|
250
|
-
file?: File
|
|
251
|
-
intent?: Contracts.AnyIntent<M>
|
|
252
|
-
cursor?: Contracts.AnyCursor<C>
|
|
253
|
-
progress?: IUploadProgress
|
|
254
|
-
attempt?: number
|
|
255
225
|
}
|
|
256
226
|
|
|
257
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Const mapping of upload stages.
|
|
229
|
+
*/
|
|
230
|
+
export const Phases = {
|
|
231
|
+
/** Initial validation checks are running. */
|
|
232
|
+
validating: 'validating',
|
|
233
|
+
/** Fetching signed URLs and strategies from backend. */
|
|
234
|
+
creating_intent: 'creating_intent',
|
|
235
|
+
/** Ready to start uploading bytes. */
|
|
236
|
+
ready: 'ready',
|
|
237
|
+
/** Waiting in queue (concurrency cap reached). */
|
|
238
|
+
queued: 'queued',
|
|
239
|
+
/** Actively transferring bytes. */
|
|
240
|
+
uploading: 'uploading',
|
|
241
|
+
/** Upload paused; resumable state preserved. */
|
|
242
|
+
paused: 'paused',
|
|
243
|
+
/** Triggering backend finalization. */
|
|
244
|
+
completing: 'completing',
|
|
245
|
+
/** File is fully completed and registered on backend. */
|
|
246
|
+
completed: 'completed',
|
|
247
|
+
/** Execution hit a terminal error or maximum retry cap. */
|
|
248
|
+
error: 'error',
|
|
249
|
+
/** Upload was canceled by the user. */
|
|
250
|
+
canceled: 'canceled',
|
|
251
|
+
} as const
|
|
252
|
+
|
|
253
|
+
/** Every upload phase; the `phase` discriminator of {@link Item}. */
|
|
254
|
+
export type Phase = keyof typeof Phases
|
|
255
|
+
|
|
256
|
+
interface BaseItem<P extends string> {
|
|
257
|
+
/** Unique client-side identifier for this file item. */
|
|
258
|
+
localId: string
|
|
259
|
+
/** Target upload purpose. */
|
|
260
|
+
purpose: P
|
|
261
|
+
/** Metadata properties identifying the file. */
|
|
262
|
+
fingerprint: Contracts.FingerprintFile
|
|
263
|
+
/** Epoch timestamp in ms when the item was registered. */
|
|
264
|
+
createdAt: number
|
|
265
|
+
attempt?: number | undefined
|
|
266
|
+
file?: File | undefined
|
|
267
|
+
/** Full phase history. Each entry records when a phase was entered/left and any stripped error. */
|
|
268
|
+
steps: Contracts.UploadStep[]
|
|
269
|
+
/** Arbitrary caller-supplied metadata from the addFiles command. Never persisted. */
|
|
270
|
+
meta: Record<string, unknown>
|
|
271
|
+
}
|
|
258
272
|
|
|
259
273
|
/**
|
|
260
|
-
*
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
274
|
+
* Representational union of upload items in their respective lifecycle phases.
|
|
275
|
+
*/
|
|
276
|
+
export type Item<
|
|
277
|
+
M extends Contracts.Intent.Map,
|
|
278
|
+
C extends Contracts.Cursor.Map<M>,
|
|
279
|
+
P extends string,
|
|
280
|
+
R extends Contracts.Result.Base = Contracts.Result.Base,
|
|
281
|
+
> = BaseItem<P> &
|
|
282
|
+
(
|
|
283
|
+
| { phase: 'validating'; file: File }
|
|
284
|
+
| { phase: 'creating_intent'; file: File; attempt: number }
|
|
285
|
+
| {
|
|
286
|
+
phase: 'ready'
|
|
287
|
+
file: File
|
|
288
|
+
intent: Contracts.Intent.Any<M>
|
|
289
|
+
cursor?: Contracts.Cursor.Any<C> | undefined
|
|
290
|
+
progress?: Progress | undefined
|
|
291
|
+
}
|
|
292
|
+
| {
|
|
293
|
+
phase: 'queued'
|
|
294
|
+
file: File
|
|
295
|
+
intent: Contracts.Intent.Any<M>
|
|
296
|
+
requestedAt: number
|
|
297
|
+
cursor?: Contracts.Cursor.Any<C> | undefined
|
|
298
|
+
progress?: Progress | undefined
|
|
299
|
+
}
|
|
300
|
+
| {
|
|
301
|
+
phase: 'uploading'
|
|
302
|
+
file: File
|
|
303
|
+
intent: Contracts.Intent.Any<M>
|
|
304
|
+
startedAt: number
|
|
305
|
+
progress: Progress
|
|
306
|
+
cursor?: Contracts.Cursor.Any<C> | undefined
|
|
307
|
+
}
|
|
308
|
+
| {
|
|
309
|
+
phase: 'paused'
|
|
310
|
+
intent: Contracts.Intent.Any<M>
|
|
311
|
+
cursor: Contracts.Cursor.Any<C>
|
|
312
|
+
progress: Progress
|
|
313
|
+
pausedAt: number
|
|
314
|
+
file?: File | undefined
|
|
315
|
+
}
|
|
316
|
+
| { phase: 'completing'; file: File; intent: Contracts.Intent.Any<M>; progress: Progress; completingAt: number }
|
|
317
|
+
| {
|
|
318
|
+
phase: 'completed'
|
|
319
|
+
completedBy: CompletionKind
|
|
320
|
+
result: R
|
|
321
|
+
completedAt: number
|
|
322
|
+
intent?: Contracts.Intent.Any<M> | undefined
|
|
323
|
+
file?: File | undefined
|
|
324
|
+
}
|
|
325
|
+
| {
|
|
326
|
+
phase: 'error'
|
|
327
|
+
error: UploadError
|
|
328
|
+
retryable: boolean
|
|
329
|
+
attempt: number
|
|
330
|
+
failedAt: number
|
|
331
|
+
file?: File | undefined
|
|
332
|
+
intent?: Contracts.Intent.Any<M> | undefined
|
|
333
|
+
cursor?: Contracts.Cursor.Any<C> | undefined
|
|
334
|
+
progress?: Progress | undefined
|
|
335
|
+
}
|
|
336
|
+
| {
|
|
337
|
+
phase: 'canceled'
|
|
338
|
+
canceledAt: number
|
|
339
|
+
file?: File | undefined
|
|
340
|
+
intent?: Contracts.Intent.Any<M> | undefined
|
|
341
|
+
cursor?: Contracts.Cursor.Any<C> | undefined
|
|
342
|
+
progress?: Progress | undefined
|
|
343
|
+
attempt?: number | undefined
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Immutable state shape of the upload engine store.
|
|
349
|
+
*/
|
|
350
|
+
export type State<
|
|
351
|
+
M extends Contracts.Intent.Map,
|
|
352
|
+
C extends Contracts.Cursor.Map<M>,
|
|
353
|
+
P extends string,
|
|
354
|
+
R extends Contracts.Result.Base = Contracts.Result.Base,
|
|
355
|
+
> = {
|
|
356
|
+
/** Map of active and historical upload items. */
|
|
357
|
+
items: Map<string, Item<M, C, P, R>>
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Interface for registering lifecycle plugins.
|
|
362
|
+
*/
|
|
363
|
+
export type Plugin<
|
|
364
|
+
M extends Contracts.Intent.Map,
|
|
365
|
+
C extends Contracts.Cursor.Map<M>,
|
|
366
|
+
P extends string,
|
|
367
|
+
R extends Contracts.Result.Base = Contracts.Result.Base,
|
|
368
|
+
> = {
|
|
369
|
+
/** Name of the plugin (for diagnostics). */
|
|
370
|
+
name: string
|
|
371
|
+
/** Sets up subscription listener binds on the store core proxy. */
|
|
372
|
+
setup(store: Pick<Store.UploadStore<M, C, P, R>, 'on' | 'off' | 'dispatch' | 'getSnapshot'>): void
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Events emitted by effects and consumed by the reducer to transition item states.
|
|
268
377
|
*/
|
|
269
|
-
export type
|
|
270
|
-
M extends Contracts.
|
|
271
|
-
C extends Contracts.
|
|
378
|
+
export type Event<
|
|
379
|
+
M extends Contracts.Intent.Map,
|
|
380
|
+
C extends Contracts.Cursor.Map<M>,
|
|
272
381
|
P extends string,
|
|
273
|
-
R extends Contracts.
|
|
382
|
+
R extends Contracts.Result.Base = Contracts.Result.Base,
|
|
274
383
|
> =
|
|
275
384
|
| {
|
|
276
385
|
type: 'files.added'
|
|
@@ -278,87 +387,104 @@ export namespace Engine {
|
|
|
278
387
|
localId: string
|
|
279
388
|
purpose: P
|
|
280
389
|
file: File
|
|
281
|
-
fingerprint: Contracts.
|
|
390
|
+
fingerprint: Contracts.FingerprintFile
|
|
282
391
|
createdAt: number
|
|
392
|
+
meta: Record<string, unknown>
|
|
283
393
|
}>
|
|
284
394
|
}
|
|
285
|
-
| { type: 'fingerprint.updated'; localId: string; fingerprint: Contracts.
|
|
395
|
+
| { type: 'fingerprint.updated'; localId: string; fingerprint: Contracts.FingerprintFile }
|
|
286
396
|
| { type: 'validation.ok'; localId: string }
|
|
287
|
-
| { type: 'validation.failed'; localId: string; reason: Contracts.
|
|
288
|
-
| { type: 'intent.ok'; localId: string; intent: Contracts.
|
|
289
|
-
| { type: 'intent.failed'; localId: string; error:
|
|
397
|
+
| { type: 'validation.failed'; localId: string; reason: Contracts.Validation.Rejection }
|
|
398
|
+
| { type: 'intent.ok'; localId: string; intent: Contracts.Intent.Any<M> }
|
|
399
|
+
| { type: 'intent.failed'; localId: string; error: UploadError; retryable: boolean }
|
|
290
400
|
| { type: 'upload.begin'; localId: string; startedAt: number }
|
|
291
401
|
| { type: 'upload.progress'; localId: string; uploadedBytes: number; totalBytes: number }
|
|
292
|
-
| { type: 'cursor.updated'; localId: string; cursor: Contracts.
|
|
402
|
+
| { type: 'cursor.updated'; localId: string; cursor: Contracts.Cursor.Any<C> }
|
|
293
403
|
| { type: 'upload.ok'; localId: string }
|
|
294
|
-
| { type: 'upload.failed'; localId: string; error:
|
|
404
|
+
| { type: 'upload.failed'; localId: string; error: UploadError; retryable: boolean }
|
|
295
405
|
| { type: 'dedupe.ok'; localId: string; result: R }
|
|
296
406
|
| { type: 'complete.ok'; localId: string; result: R }
|
|
297
|
-
| { type: 'complete.failed'; localId: string; error:
|
|
298
|
-
| { type: 'paused'; localId: string; cursor: Contracts.
|
|
407
|
+
| { type: 'complete.failed'; localId: string; error: UploadError; retryable: boolean }
|
|
408
|
+
| { type: 'paused'; localId: string; cursor: Contracts.Cursor.Any<C>; pausedAt: number }
|
|
299
409
|
| { type: 'canceled'; localId: string; canceledAt: number }
|
|
300
410
|
|
|
301
|
-
// -------------------- Public event map --------------------
|
|
302
|
-
|
|
303
411
|
/**
|
|
304
|
-
* Public event
|
|
305
|
-
*
|
|
306
|
-
* @template M Intent map keyed by strategy id.
|
|
307
|
-
* @template C Cursor map keyed by strategy id.
|
|
308
|
-
* @template P Purpose discriminator string.
|
|
309
|
-
* @template R Backend result payload.
|
|
310
|
-
* @author wildduck2 <https://github.com/wildduck2>
|
|
412
|
+
* Public event registry mapping event names to payloads emitted via `store.on()`.
|
|
311
413
|
*/
|
|
312
414
|
export type EventMap<
|
|
313
|
-
M extends Contracts.
|
|
314
|
-
C extends Contracts.
|
|
415
|
+
M extends Contracts.Intent.Map,
|
|
416
|
+
C extends Contracts.Cursor.Map<M>,
|
|
315
417
|
P extends string,
|
|
316
|
-
R extends Contracts.
|
|
418
|
+
R extends Contracts.Result.Base = Contracts.Result.Base,
|
|
317
419
|
> = {
|
|
318
|
-
|
|
319
|
-
'file.
|
|
420
|
+
/** Emitted when a file is validated and successfully registered in state. */
|
|
421
|
+
'file.added': { localId: string; purpose: P; file: File; fingerprint: Contracts.FingerprintFile }
|
|
422
|
+
/** Emitted when a file fails client-side validation rules. */
|
|
423
|
+
'file.rejected': { file: File; reason: Contracts.Validation.Rejection }
|
|
320
424
|
|
|
425
|
+
/** Emitted when validation succeeds. */
|
|
321
426
|
'validation.ok': { localId: string }
|
|
322
|
-
|
|
427
|
+
/** Emitted when validation fails. */
|
|
428
|
+
'validation.failed': { localId: string; reason: Contracts.Validation.Rejection }
|
|
323
429
|
|
|
430
|
+
/** Emitted when intent lookup/creation is requested. */
|
|
324
431
|
'intent.creating': { localId: string }
|
|
432
|
+
/** Emitted when upload intent is successfully created. */
|
|
325
433
|
'intent.created': { localId: string; intent: M[keyof M] }
|
|
326
|
-
|
|
434
|
+
/** Emitted when intent creation fails. */
|
|
435
|
+
'intent.failed': { localId: string; error: UploadError; retryable: boolean }
|
|
327
436
|
|
|
437
|
+
/** Emitted when an item is queued waiting for concurrency slots. */
|
|
328
438
|
'upload.queued': { localId: string }
|
|
439
|
+
/** Emitted when a paused upload is resumed. */
|
|
329
440
|
'upload.resumed': { localId: string }
|
|
441
|
+
/** Emitted when data transfer starts. */
|
|
330
442
|
'upload.started': { localId: string }
|
|
443
|
+
/** Periodic progress event. */
|
|
331
444
|
'upload.progress': { localId: string; pct: number; uploadedBytes: number; totalBytes: number }
|
|
332
|
-
|
|
333
|
-
'upload.
|
|
445
|
+
/** Emitted when strategy cursor state is updated. */
|
|
446
|
+
'upload.cursor': { localId: string; cursor: Contracts.Cursor.Any<C> }
|
|
447
|
+
/** Emitted when upload is paused. */
|
|
448
|
+
'upload.paused': { localId: string; cursor: Contracts.Cursor.Any<C> }
|
|
449
|
+
/** Emitted when upload is canceled. */
|
|
334
450
|
'upload.canceled': { localId: string }
|
|
335
451
|
|
|
452
|
+
/** Emitted when final completing steps (like completeMultipart or final verification) begin. */
|
|
336
453
|
'upload.completing': { localId: string }
|
|
454
|
+
/** Emitted when a file is completely uploaded and finalized. */
|
|
337
455
|
'upload.completed': { localId: string; result: R; completedBy: CompletionKind }
|
|
338
456
|
|
|
339
|
-
|
|
457
|
+
/** Emitted when transfer hits a retryable or terminal error. */
|
|
458
|
+
'upload.error': { localId: string; error: UploadError; retryable: boolean }
|
|
340
459
|
|
|
341
|
-
/**
|
|
342
|
-
* Emitted whenever an item leaves state. `reason: 'user'` for an
|
|
343
|
-
* explicit `dispatch({ type: 'remove' })`; `reason: 'cleanup'` for
|
|
344
|
-
* an automatic eviction by `completedItemTTL` / `maxItems`. Lets
|
|
345
|
-
* consumers (waitFor, UI listeners, telemetry) react without polling
|
|
346
|
-
* the snapshot.
|
|
347
|
-
*/
|
|
460
|
+
/** Emitted when an item is removed from the store state. */
|
|
348
461
|
'upload.removed': { localId: string; reason: 'user' | 'cleanup' }
|
|
349
462
|
|
|
463
|
+
/** Emitted when a file rebind operation succeeds. */
|
|
350
464
|
'rebind.ok': { localId: string }
|
|
465
|
+
/** Emitted when a file rebind fails. */
|
|
351
466
|
'rebind.failed': { localId: string; reason: RebindReason }
|
|
352
467
|
}
|
|
353
468
|
|
|
354
469
|
/**
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
* @author wildduck2 <https://github.com/wildduck2>
|
|
470
|
+
* Rebind failure classifications.
|
|
358
471
|
*/
|
|
359
472
|
export type RebindReason =
|
|
360
473
|
| { code: 'no_item' }
|
|
361
474
|
| { code: 'wrong_phase'; phase: string }
|
|
362
475
|
| { code: 'already_bound' }
|
|
363
|
-
| { code: 'fingerprint_mismatch'; expected: Contracts.
|
|
476
|
+
| { code: 'fingerprint_mismatch'; expected: Contracts.FingerprintFile; got: Contracts.FingerprintFile }
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Diagnostic hooks for observing engine updates.
|
|
480
|
+
*/
|
|
481
|
+
export type Hooks<
|
|
482
|
+
M extends Contracts.Intent.Map,
|
|
483
|
+
C extends Contracts.Cursor.Map<M>,
|
|
484
|
+
P extends string,
|
|
485
|
+
R extends Contracts.Result.Base = Contracts.Result.Base,
|
|
486
|
+
> = {
|
|
487
|
+
/** Callback invoked on every state-reducing internal event. */
|
|
488
|
+
onInternalEvent?: (event: Event<M, C, P, R>, state: State<M, C, P, R>) => void
|
|
489
|
+
}
|
|
364
490
|
}
|