@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
package/src/core/engine/index.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export * from './outcome.types'
|
|
5
|
-
export * from './progress.types'
|
|
6
|
-
export * from './reducer'
|
|
7
|
-
export * from './store'
|
|
8
|
-
export * from './validation'
|
|
1
|
+
export { Engine } from './engine.types'
|
|
2
|
+
export type { Store } from './store'
|
|
3
|
+
export { createUploadStore } from './store'
|
|
@@ -1,37 +1,56 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Contracts } from '../contracts'
|
|
2
|
+
import { UploadValidationError } from '../errors'
|
|
2
3
|
import { computeFingerprint, fingerprintMatches } from '../utils/fingerprint'
|
|
3
|
-
import type {
|
|
4
|
-
import type { InternalEvent, UploadItem } from './internal-events.types'
|
|
5
|
-
import type { UploadProgress } from './progress.types'
|
|
4
|
+
import type { Engine } from './engine.types'
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
function stripError(err: { code: string; message: string; retryable?: boolean }): Contracts.StepError {
|
|
7
|
+
const result: Contracts.StepError = { code: err.code, message: err.message }
|
|
8
|
+
if (err.retryable !== undefined) result.retryable = err.retryable
|
|
9
|
+
return result
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function closeStep(steps: Contracts.UploadStep[], leftAt: number): Contracts.UploadStep[] {
|
|
13
|
+
if (!steps.length) return steps
|
|
14
|
+
const last = steps[steps.length - 1]
|
|
15
|
+
if (!last || last.leftAt !== undefined) return steps
|
|
16
|
+
const closed: Contracts.UploadStep = {
|
|
17
|
+
phase: last.phase,
|
|
18
|
+
enteredAt: last.enteredAt,
|
|
19
|
+
leftAt,
|
|
20
|
+
attempt: last.attempt,
|
|
21
|
+
...(last.error !== undefined ? { error: last.error } : {}),
|
|
22
|
+
}
|
|
23
|
+
return [...steps.slice(0, -1), closed]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function openStep(
|
|
27
|
+
steps: Contracts.UploadStep[],
|
|
28
|
+
phase: string,
|
|
29
|
+
attempt: number,
|
|
30
|
+
enteredAt: number,
|
|
31
|
+
error?: Contracts.StepError,
|
|
32
|
+
): Contracts.UploadStep[] {
|
|
33
|
+
return [...closeStep(steps, enteredAt), { phase, enteredAt, attempt, error }]
|
|
15
34
|
}
|
|
16
35
|
|
|
17
36
|
export function createReducer<
|
|
18
|
-
M extends
|
|
19
|
-
C extends
|
|
37
|
+
M extends Contracts.Intent.Map,
|
|
38
|
+
C extends Contracts.Cursor.Map<M>,
|
|
20
39
|
P extends string,
|
|
21
|
-
R extends
|
|
40
|
+
R extends Contracts.Result.Base = Contracts.Result.Base,
|
|
22
41
|
>() {
|
|
23
42
|
return function reduce(
|
|
24
|
-
state:
|
|
25
|
-
event:
|
|
26
|
-
):
|
|
43
|
+
state: Engine.State<M, C, P, R>,
|
|
44
|
+
event: Engine.Command<P> | Engine.Event<M, C, P, R>,
|
|
45
|
+
): Engine.State<M, C, P, R> {
|
|
27
46
|
const items = new Map(state.items)
|
|
28
47
|
|
|
29
|
-
const set = (localId: string, next:
|
|
48
|
+
const set = (localId: string, next: Engine.Item<M, C, P, R>) => items.set(localId, next)
|
|
30
49
|
|
|
31
50
|
const now = Date.now()
|
|
32
51
|
|
|
33
52
|
// Commands have no dots in their `type`; internal events do (e.g. 'intent.ok').
|
|
34
|
-
const isCommand = (e:
|
|
53
|
+
const isCommand = (e: Engine.Command<P> | Engine.Event<M, C, P, R>): e is Engine.Command<P> => {
|
|
35
54
|
return 'type' in e && typeof e.type === 'string' && !e.type.includes('.')
|
|
36
55
|
}
|
|
37
56
|
|
|
@@ -51,6 +70,7 @@ export function createReducer<
|
|
|
51
70
|
...item,
|
|
52
71
|
phase: 'queued',
|
|
53
72
|
requestedAt: now,
|
|
73
|
+
steps: openStep(item.steps, 'queued', item.attempt ?? 1, now),
|
|
54
74
|
})
|
|
55
75
|
break
|
|
56
76
|
}
|
|
@@ -66,6 +86,7 @@ export function createReducer<
|
|
|
66
86
|
file: item.file,
|
|
67
87
|
phase: 'queued',
|
|
68
88
|
requestedAt: now,
|
|
89
|
+
steps: openStep(item.steps, 'queued', item.attempt ?? 1, now),
|
|
69
90
|
})
|
|
70
91
|
break
|
|
71
92
|
}
|
|
@@ -80,6 +101,7 @@ export function createReducer<
|
|
|
80
101
|
set(event.localId, {
|
|
81
102
|
...item,
|
|
82
103
|
phase: 'ready',
|
|
104
|
+
steps: openStep(item.steps, 'ready', item.attempt ?? 1, now),
|
|
83
105
|
})
|
|
84
106
|
}
|
|
85
107
|
break
|
|
@@ -101,7 +123,7 @@ export function createReducer<
|
|
|
101
123
|
if (!item.file) break
|
|
102
124
|
|
|
103
125
|
if (item.intent) {
|
|
104
|
-
// Progress at 100% with intent means we failed in 'completing'
|
|
126
|
+
// Progress at 100% with intent means we failed in 'completing' retry that phase.
|
|
105
127
|
if (item.progress && item.progress.pct === 100) {
|
|
106
128
|
set(event.localId, {
|
|
107
129
|
...item,
|
|
@@ -110,6 +132,7 @@ export function createReducer<
|
|
|
110
132
|
file: item.file,
|
|
111
133
|
phase: 'completing',
|
|
112
134
|
completingAt: now,
|
|
135
|
+
steps: openStep(item.steps, 'completing', item.attempt ?? 1, now),
|
|
113
136
|
})
|
|
114
137
|
} else {
|
|
115
138
|
set(event.localId, {
|
|
@@ -117,15 +140,18 @@ export function createReducer<
|
|
|
117
140
|
file: item.file,
|
|
118
141
|
intent: item.intent,
|
|
119
142
|
phase: 'ready',
|
|
143
|
+
steps: openStep(item.steps, 'ready', item.attempt ?? 1, now),
|
|
120
144
|
})
|
|
121
145
|
}
|
|
122
146
|
} else {
|
|
123
147
|
// Intent-creation failure path; bump attempt counter.
|
|
148
|
+
const nextAttempt = (item.attempt ?? 1) + 1
|
|
124
149
|
set(event.localId, {
|
|
125
150
|
...item,
|
|
126
151
|
phase: 'creating_intent',
|
|
127
152
|
file: item.file,
|
|
128
|
-
attempt:
|
|
153
|
+
attempt: nextAttempt,
|
|
154
|
+
steps: openStep(item.steps, 'creating_intent', nextAttempt, now),
|
|
129
155
|
})
|
|
130
156
|
}
|
|
131
157
|
break
|
|
@@ -172,6 +198,8 @@ export function createReducer<
|
|
|
172
198
|
file: item.file,
|
|
173
199
|
fingerprint: item.fingerprint,
|
|
174
200
|
createdAt: item.createdAt,
|
|
201
|
+
meta: item.meta,
|
|
202
|
+
steps: [{ phase: 'validating', enteredAt: now, attempt: 1 }],
|
|
175
203
|
})
|
|
176
204
|
}
|
|
177
205
|
}
|
|
@@ -189,12 +217,13 @@ export function createReducer<
|
|
|
189
217
|
|
|
190
218
|
case 'validation.ok': {
|
|
191
219
|
const item = items.get(event.localId)
|
|
192
|
-
if (
|
|
220
|
+
if (item?.phase !== 'validating') break
|
|
193
221
|
|
|
194
222
|
set(event.localId, {
|
|
195
223
|
...item,
|
|
196
224
|
phase: 'creating_intent',
|
|
197
225
|
attempt: 1,
|
|
226
|
+
steps: openStep(item.steps, 'creating_intent', 1, now),
|
|
198
227
|
})
|
|
199
228
|
break
|
|
200
229
|
}
|
|
@@ -202,15 +231,17 @@ export function createReducer<
|
|
|
202
231
|
case 'validation.failed': {
|
|
203
232
|
const ev = event
|
|
204
233
|
const item = items.get(ev.localId)
|
|
205
|
-
if (
|
|
234
|
+
if (item?.phase !== 'validating') break
|
|
206
235
|
|
|
236
|
+
const err = new UploadValidationError(ev.reason)
|
|
207
237
|
set(ev.localId, {
|
|
208
238
|
...item,
|
|
209
239
|
phase: 'error',
|
|
210
|
-
error:
|
|
240
|
+
error: err,
|
|
211
241
|
retryable: false,
|
|
212
242
|
attempt: 1,
|
|
213
243
|
failedAt: now,
|
|
244
|
+
steps: openStep(item.steps, 'error', 1, now, stripError(err)),
|
|
214
245
|
})
|
|
215
246
|
break
|
|
216
247
|
}
|
|
@@ -218,12 +249,13 @@ export function createReducer<
|
|
|
218
249
|
case 'intent.ok': {
|
|
219
250
|
const ev = event
|
|
220
251
|
const item = items.get(ev.localId)
|
|
221
|
-
if (
|
|
252
|
+
if (item?.phase !== 'creating_intent') break
|
|
222
253
|
|
|
223
254
|
set(ev.localId, {
|
|
224
255
|
...item,
|
|
225
256
|
phase: 'ready',
|
|
226
257
|
intent: ev.intent,
|
|
258
|
+
steps: openStep(item.steps, 'ready', item.attempt ?? 1, now),
|
|
227
259
|
})
|
|
228
260
|
break
|
|
229
261
|
}
|
|
@@ -231,7 +263,7 @@ export function createReducer<
|
|
|
231
263
|
case 'intent.failed': {
|
|
232
264
|
const ev = event
|
|
233
265
|
const item = items.get(ev.localId)
|
|
234
|
-
if (
|
|
266
|
+
if (item?.phase !== 'creating_intent') break
|
|
235
267
|
|
|
236
268
|
set(ev.localId, {
|
|
237
269
|
...item,
|
|
@@ -239,6 +271,7 @@ export function createReducer<
|
|
|
239
271
|
error: ev.error,
|
|
240
272
|
retryable: ev.retryable,
|
|
241
273
|
failedAt: now,
|
|
274
|
+
steps: openStep(item.steps, 'error', item.attempt ?? 1, now, stripError(ev.error)),
|
|
242
275
|
})
|
|
243
276
|
break
|
|
244
277
|
}
|
|
@@ -246,12 +279,12 @@ export function createReducer<
|
|
|
246
279
|
case 'upload.begin': {
|
|
247
280
|
const ev = event
|
|
248
281
|
const item = items.get(ev.localId)
|
|
249
|
-
if (
|
|
282
|
+
if (item?.phase !== 'queued') break
|
|
250
283
|
|
|
251
284
|
const total = item.file.size
|
|
252
|
-
const carried:
|
|
285
|
+
const carried: Engine.Progress | undefined = item.progress
|
|
253
286
|
|
|
254
|
-
const progress:
|
|
287
|
+
const progress: Engine.Progress = carried
|
|
255
288
|
? { ...carried, totalBytes: total, pct: pct(carried.uploadedBytes, total) }
|
|
256
289
|
: { uploadedBytes: 0, totalBytes: total, pct: 0 }
|
|
257
290
|
|
|
@@ -260,6 +293,7 @@ export function createReducer<
|
|
|
260
293
|
phase: 'uploading',
|
|
261
294
|
progress,
|
|
262
295
|
startedAt: ev.startedAt,
|
|
296
|
+
steps: openStep(item.steps, 'uploading', item.attempt ?? 1, ev.startedAt),
|
|
263
297
|
})
|
|
264
298
|
break
|
|
265
299
|
}
|
|
@@ -267,9 +301,9 @@ export function createReducer<
|
|
|
267
301
|
case 'upload.progress': {
|
|
268
302
|
const ev = event
|
|
269
303
|
const item = items.get(ev.localId)
|
|
270
|
-
if (
|
|
304
|
+
if (item?.phase !== 'uploading') break
|
|
271
305
|
|
|
272
|
-
const progress:
|
|
306
|
+
const progress: Engine.Progress = {
|
|
273
307
|
uploadedBytes: ev.uploadedBytes,
|
|
274
308
|
totalBytes: ev.totalBytes,
|
|
275
309
|
pct: pct(ev.uploadedBytes, ev.totalBytes),
|
|
@@ -297,13 +331,14 @@ export function createReducer<
|
|
|
297
331
|
case 'upload.ok': {
|
|
298
332
|
const ev = event
|
|
299
333
|
const item = items.get(ev.localId)
|
|
300
|
-
if (
|
|
334
|
+
if (item?.phase !== 'uploading') break
|
|
301
335
|
|
|
302
336
|
set(ev.localId, {
|
|
303
337
|
...item,
|
|
304
338
|
phase: 'completing',
|
|
305
339
|
progress: { uploadedBytes: item.file.size, totalBytes: item.file.size, pct: 100 },
|
|
306
340
|
completingAt: now,
|
|
341
|
+
steps: openStep(item.steps, 'completing', item.attempt ?? 1, now),
|
|
307
342
|
})
|
|
308
343
|
break
|
|
309
344
|
}
|
|
@@ -311,7 +346,7 @@ export function createReducer<
|
|
|
311
346
|
case 'upload.failed': {
|
|
312
347
|
const ev = event
|
|
313
348
|
const item = items.get(ev.localId)
|
|
314
|
-
if (
|
|
349
|
+
if (item?.phase !== 'uploading') break
|
|
315
350
|
|
|
316
351
|
set(ev.localId, {
|
|
317
352
|
...item,
|
|
@@ -320,6 +355,7 @@ export function createReducer<
|
|
|
320
355
|
retryable: ev.retryable,
|
|
321
356
|
attempt: 1,
|
|
322
357
|
failedAt: now,
|
|
358
|
+
steps: openStep(item.steps, 'error', item.attempt ?? 1, now, stripError(ev.error)),
|
|
323
359
|
})
|
|
324
360
|
break
|
|
325
361
|
}
|
|
@@ -327,13 +363,14 @@ export function createReducer<
|
|
|
327
363
|
case 'paused': {
|
|
328
364
|
const ev = event
|
|
329
365
|
const item = items.get(ev.localId)
|
|
330
|
-
if (
|
|
366
|
+
if (item?.phase !== 'uploading') break
|
|
331
367
|
|
|
332
368
|
set(ev.localId, {
|
|
333
369
|
...item,
|
|
334
370
|
phase: 'paused',
|
|
335
371
|
cursor: ev.cursor,
|
|
336
372
|
pausedAt: ev.pausedAt,
|
|
373
|
+
steps: openStep(item.steps, 'paused', item.attempt ?? 1, ev.pausedAt),
|
|
337
374
|
})
|
|
338
375
|
break
|
|
339
376
|
}
|
|
@@ -350,7 +387,7 @@ export function createReducer<
|
|
|
350
387
|
case 'complete.ok': {
|
|
351
388
|
const ev = event
|
|
352
389
|
const item = items.get(ev.localId)
|
|
353
|
-
if (
|
|
390
|
+
if (item?.phase !== 'completing') break
|
|
354
391
|
|
|
355
392
|
set(ev.localId, {
|
|
356
393
|
...item,
|
|
@@ -358,6 +395,7 @@ export function createReducer<
|
|
|
358
395
|
completedBy: 'upload',
|
|
359
396
|
result: ev.result,
|
|
360
397
|
completedAt: now,
|
|
398
|
+
steps: openStep(item.steps, 'completed', item.attempt ?? 1, now),
|
|
361
399
|
})
|
|
362
400
|
break
|
|
363
401
|
}
|
|
@@ -365,18 +403,20 @@ export function createReducer<
|
|
|
365
403
|
case 'dedupe.ok': {
|
|
366
404
|
const ev = event
|
|
367
405
|
const item = items.get(ev.localId)
|
|
368
|
-
if (!item || item.phase !== 'validating') break
|
|
406
|
+
if (!item || (item.phase !== 'validating' && item.phase !== 'creating_intent')) break
|
|
369
407
|
|
|
370
408
|
set(ev.localId, {
|
|
371
409
|
phase: 'completed',
|
|
372
410
|
localId: item.localId,
|
|
373
|
-
file: item.file,
|
|
411
|
+
file: 'file' in item ? item.file : undefined,
|
|
374
412
|
purpose: item.purpose,
|
|
375
413
|
fingerprint: item.fingerprint,
|
|
376
414
|
result: ev.result,
|
|
377
415
|
completedBy: 'dedupe',
|
|
378
416
|
completedAt: now,
|
|
379
417
|
createdAt: item.createdAt,
|
|
418
|
+
meta: item.meta,
|
|
419
|
+
steps: openStep(item.steps, 'completed', item.attempt ?? 1, now),
|
|
380
420
|
})
|
|
381
421
|
break
|
|
382
422
|
}
|
|
@@ -384,7 +424,7 @@ export function createReducer<
|
|
|
384
424
|
case 'complete.failed': {
|
|
385
425
|
const ev = event
|
|
386
426
|
const item = items.get(ev.localId)
|
|
387
|
-
if (
|
|
427
|
+
if (item?.phase !== 'completing') break
|
|
388
428
|
|
|
389
429
|
const nextAttempt = (item.attempt ?? 1) + 1
|
|
390
430
|
|
|
@@ -395,6 +435,7 @@ export function createReducer<
|
|
|
395
435
|
retryable: ev.retryable,
|
|
396
436
|
attempt: nextAttempt,
|
|
397
437
|
failedAt: now,
|
|
438
|
+
steps: openStep(item.steps, 'error', nextAttempt, now, stripError(ev.error)),
|
|
398
439
|
})
|
|
399
440
|
break
|
|
400
441
|
}
|
|
@@ -404,10 +445,13 @@ export function createReducer<
|
|
|
404
445
|
}
|
|
405
446
|
}
|
|
406
447
|
|
|
407
|
-
function toCanceled<
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
448
|
+
function toCanceled<
|
|
449
|
+
M extends Contracts.Intent.Map,
|
|
450
|
+
C extends Contracts.Cursor.Map<M>,
|
|
451
|
+
P extends string,
|
|
452
|
+
R extends Contracts.Result.Base,
|
|
453
|
+
>(item: Engine.Item<M, C, P, R>, canceledAt: number): Engine.Item<M, C, P, R> {
|
|
454
|
+
const attempt = ('attempt' in item ? item.attempt : undefined) ?? 1
|
|
411
455
|
return {
|
|
412
456
|
phase: 'canceled',
|
|
413
457
|
localId: item.localId,
|
|
@@ -420,6 +464,8 @@ function toCanceled<M extends IntentMap, C extends CursorMap<M>, P extends strin
|
|
|
420
464
|
cursor: 'cursor' in item ? item.cursor : undefined,
|
|
421
465
|
progress: 'progress' in item ? item.progress : undefined,
|
|
422
466
|
attempt: 'attempt' in item ? item.attempt : undefined,
|
|
467
|
+
meta: item.meta,
|
|
468
|
+
steps: openStep(item.steps, 'canceled', attempt, canceledAt),
|
|
423
469
|
}
|
|
424
470
|
}
|
|
425
471
|
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Contracts } from '../../../contracts'
|
|
2
|
+
import { isDevEnv } from '../../../utils/guards'
|
|
2
3
|
import { generateId } from '../../../utils/id'
|
|
3
|
-
import { validateFile, validateFileList } from '../../validation'
|
|
4
|
-
import { calculateFileChecksum, computeFingerprint } from '../store.libs'
|
|
5
|
-
import type {
|
|
4
|
+
import { validateFile, validateFileList, validateMimeSignature } from '../../validation'
|
|
5
|
+
import { calculateFileChecksum, computeFingerprint, tryDedupeByChecksum } from '../store.libs'
|
|
6
|
+
import type { Store } from '../store.types'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Sync: validate + insert. Async (queued): checksum, dedupe lookup,
|
|
9
10
|
* and the final validation transition.
|
|
10
11
|
*/
|
|
11
12
|
export function handleAddFiles<
|
|
12
|
-
M extends
|
|
13
|
-
C extends
|
|
13
|
+
M extends Contracts.Intent.Map,
|
|
14
|
+
C extends Contracts.Cursor.Map<M>,
|
|
14
15
|
P extends string,
|
|
15
|
-
R extends
|
|
16
|
-
>(rt:
|
|
16
|
+
R extends Contracts.Result.Base,
|
|
17
|
+
>(rt: Store.Runtime<M, C, P, R>, files: File[], purpose: P, meta: Record<string, unknown> = {}) {
|
|
17
18
|
const existingCount = Array.from(rt.state.items.values()).filter(
|
|
18
19
|
(item) => item.purpose === purpose && item.phase !== 'canceled',
|
|
19
20
|
).length
|
|
@@ -25,44 +26,40 @@ export function handleAddFiles<
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const now = Date.now()
|
|
28
|
-
const toAdd: Array<{
|
|
29
|
+
const toAdd: Array<{
|
|
30
|
+
localId: string
|
|
31
|
+
purpose: P
|
|
32
|
+
file: File
|
|
33
|
+
fingerprint: Contracts.FingerprintFile
|
|
34
|
+
createdAt: number
|
|
35
|
+
meta: Record<string, unknown>
|
|
36
|
+
}> = []
|
|
29
37
|
|
|
30
38
|
for (const file of valid) {
|
|
31
39
|
const localId = generateId()
|
|
32
40
|
const fingerprint = (rt.opts.fingerprint ?? computeFingerprint)(file)
|
|
33
41
|
|
|
34
|
-
toAdd.push({ localId, purpose, file, fingerprint, createdAt: now })
|
|
42
|
+
toAdd.push({ localId, purpose, file, fingerprint, createdAt: now, meta })
|
|
35
43
|
|
|
36
44
|
rt.enqueueEffect(async () => {
|
|
37
45
|
let checksum: string | undefined
|
|
38
46
|
try {
|
|
39
|
-
|
|
47
|
+
// Hashes files of any size (small: native Web Crypto; large:
|
|
48
|
+
// incremental, off-thread when possible). Failures fall through to the
|
|
49
|
+
// catch and simply skip dedupe.
|
|
50
|
+
checksum = await calculateFileChecksum(file, rt.opts.config.checksumMaxSize)
|
|
40
51
|
const updatedFingerprint = { ...fingerprint, checksum }
|
|
41
52
|
rt.applyInternal({ type: 'fingerprint.updated', localId, fingerprint: updatedFingerprint })
|
|
42
53
|
} catch (err) {
|
|
43
|
-
// Checksum failure is non-fatal
|
|
44
|
-
if (
|
|
54
|
+
// Checksum failure is non-fatal continue without dedupe.
|
|
55
|
+
if (isDevEnv()) {
|
|
45
56
|
console.warn('[UploadEngine] Failed to calculate checksum:', err)
|
|
46
57
|
}
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
// Dedupe: skip upload entirely when the backend already has the file.
|
|
50
|
-
if (checksum
|
|
51
|
-
|
|
52
|
-
const existingFile = await rt.opts.api.findByChecksum({ checksum, purpose })
|
|
53
|
-
if (existingFile) {
|
|
54
|
-
const currentItem = rt.state.items.get(localId)
|
|
55
|
-
if (currentItem && currentItem.phase === 'validating') {
|
|
56
|
-
rt.applyInternal({ type: 'dedupe.ok', localId, result: existingFile })
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
} catch (err) {
|
|
61
|
-
// Dedupe failure is non-fatal — fall through to normal upload.
|
|
62
|
-
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') {
|
|
63
|
-
console.warn('[UploadEngine] Failed to check for existing file:', err)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
61
|
+
if (await tryDedupeByChecksum(rt, localId, checksum, purpose, ['validating'])) {
|
|
62
|
+
return
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
const reason = rt.opts.validateFile?.(file, purpose)
|
|
@@ -78,6 +75,19 @@ export function handleAddFiles<
|
|
|
78
75
|
return
|
|
79
76
|
}
|
|
80
77
|
|
|
78
|
+
try {
|
|
79
|
+
const mimeReason = await validateMimeSignature(file, rt.opts.config.strictMimeMatch === true)
|
|
80
|
+
if (mimeReason) {
|
|
81
|
+
rt.applyInternal({ type: 'validation.failed', localId, reason: mimeReason })
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// Sniff failure is non-fatal fall through.
|
|
86
|
+
if (isDevEnv()) {
|
|
87
|
+
console.warn('[UploadEngine] MIME sniff failed:', err)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
rt.applyInternal({ type: 'validation.ok', localId })
|
|
82
92
|
})
|
|
83
93
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { hasIntent, isMultipartIntent } from '../store.libs'
|
|
3
|
-
import type {
|
|
1
|
+
import type { Contracts } from '../../../contracts'
|
|
2
|
+
import { buildApiContext, hasIntent, isMultipartIntent } from '../store.libs'
|
|
3
|
+
import type { Store } from '../store.types'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Cancels a single upload item and aborts related inflight operations.
|
|
@@ -8,10 +8,12 @@ import type { StoreRuntime } from '../store.types'
|
|
|
8
8
|
* This function is intentionally best-effort: abort calls can throw or be unsupported,
|
|
9
9
|
* and server-side multipart abort is attempted only when the adapter provides it.
|
|
10
10
|
*/
|
|
11
|
-
export function handleCancel<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
export function handleCancel<
|
|
12
|
+
M extends Contracts.Intent.Map,
|
|
13
|
+
C extends Contracts.Cursor.Map<M>,
|
|
14
|
+
P extends string,
|
|
15
|
+
R extends Contracts.Result.Base,
|
|
16
|
+
>(rt: Store.Runtime<M, C, P, R>, localId: string) {
|
|
15
17
|
const item = rt.state.items.get(localId)
|
|
16
18
|
|
|
17
19
|
// Abort inflight intent
|
|
@@ -43,7 +45,10 @@ export function handleCancel<M extends IntentMap, C extends CursorMap<M>, P exte
|
|
|
43
45
|
try {
|
|
44
46
|
const abort = rt.opts.api.multipart?.abort
|
|
45
47
|
if (abort) {
|
|
46
|
-
await abort(
|
|
48
|
+
await abort(
|
|
49
|
+
{ fileId: intent.fileId, uploadId: intent.uploadId },
|
|
50
|
+
buildApiContext(item) as Contracts.Api.AbortContext<P, M>,
|
|
51
|
+
)
|
|
47
52
|
}
|
|
48
53
|
} catch {
|
|
49
54
|
// ignore
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
1
|
+
import type { Contracts } from '../../../contracts'
|
|
2
|
+
import type { Engine } from '../../engine.types'
|
|
3
|
+
import type { Store } from '../store.types'
|
|
4
4
|
|
|
5
5
|
export function cleanupOldItems<
|
|
6
|
-
M extends
|
|
7
|
-
C extends
|
|
6
|
+
M extends Contracts.Intent.Map,
|
|
7
|
+
C extends Contracts.Cursor.Map<M>,
|
|
8
8
|
P extends string,
|
|
9
|
-
R extends
|
|
10
|
-
>(opts:
|
|
9
|
+
R extends Contracts.Result.Base,
|
|
10
|
+
>(opts: Store.Options<M, C, P, R>, state: Engine.State<M, C, P, R>): Engine.State<M, C, P, R> | null {
|
|
11
11
|
const maxItems = opts.config?.maxItems
|
|
12
12
|
const completedTTL = opts.config?.completedItemTTL
|
|
13
13
|
const now = Date.now()
|
|
@@ -50,7 +50,10 @@ export function cleanupOldItems<
|
|
|
50
50
|
|
|
51
51
|
const excess = remainingItems.length - maxItems
|
|
52
52
|
for (let i = 0; i < excess && i < completedOrCanceled.length; i++) {
|
|
53
|
-
|
|
53
|
+
const entry = completedOrCanceled[i]
|
|
54
|
+
if (entry) {
|
|
55
|
+
toRemove.push(entry[0])
|
|
56
|
+
}
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
|