@gentleduck/upload 0.2.5 → 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 +297 -0
- 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 -90
- 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/__tests__/emitter.test.ts +85 -0
- package/src/__tests__/fingerprint.test.ts +49 -0
- package/src/__tests__/guards.test.ts +40 -0
- package/src/__tests__/id.test.ts +67 -0
- package/src/__tests__/mime-sniff.test.ts +142 -0
- package/src/__tests__/persistence.test.ts +191 -0
- 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/__tests__/store-libs.test.ts +192 -0
- package/src/__tests__/transport-fetch.test.ts +108 -0
- package/src/__tests__/transport-libs.test.ts +42 -0
- package/src/__tests__/transport.test.ts +182 -0
- package/src/core/__tests__/errors.test.ts +152 -0
- package/src/core/client.ts +3 -0
- package/src/core/contracts/contracts.types.ts +373 -0
- 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 +13 -39
- package/src/core/contracts/transport/transport.ts +96 -90
- 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 +490 -0
- package/src/core/engine/index.ts +3 -8
- package/src/core/engine/reducer.ts +93 -53
- package/src/core/engine/store/handlers/add-file.ts +43 -48
- 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 +54 -39
- 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 +197 -94
- package/src/core/engine/store/store.runtime.ts +52 -53
- 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 +8 -4
- 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 +126 -0
- 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 +59 -95
- package/src/react/uploader.types.ts +80 -0
- package/src/react/use-dropzone.tsx +226 -0
- package/src/react/use-uploader.tsx +97 -93
- 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 +211 -116
- package/src/strategies/post/__tests__/post-url-safety.test.ts +119 -0
- package/src/strategies/post/index.ts +113 -71
- 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-BNxznRif.js +0 -2
- package/dist/index-BXg9WIce.d.ts +0 -987
- package/dist/strategy.types-gg9ZStRe.d.ts +0 -225
- 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 -222
- package/src/core/engine/outcome.types.ts +0 -9
- package/src/core/engine/progress.types.ts +0 -15
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import { abortReason, createNetworkError, makeAbortError, parseHeaders } from './transport.libs'
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
type XhrCommonArgs = {
|
|
5
|
-
url: string
|
|
6
|
-
signal?: AbortSignal
|
|
7
|
-
headers?: Record<string, string>
|
|
8
|
-
onProgress?: (loaded: number, total: number) => void
|
|
9
|
-
}
|
|
2
|
+
import type { Transport } from './transport.types'
|
|
10
3
|
|
|
11
4
|
function isAbsoluteHttpUrl(url: string) {
|
|
12
5
|
return /^https?:\/\//i.test(url)
|
|
@@ -20,107 +13,110 @@ function readEtag(xhr: XMLHttpRequest) {
|
|
|
20
13
|
}
|
|
21
14
|
|
|
22
15
|
function xhrRequest(args: {
|
|
23
|
-
method: 'POST' | 'PUT' | 'PATCH'
|
|
16
|
+
method: 'POST' | 'PUT' | 'PATCH' | 'HEAD'
|
|
24
17
|
url: string
|
|
25
18
|
body: Document | BodyInit | null
|
|
26
19
|
signal?: AbortSignal
|
|
27
20
|
headers?: Record<string, string>
|
|
28
21
|
onProgress?: (loaded: number, total: number) => void
|
|
29
22
|
}) {
|
|
30
|
-
return new Promise<{ headers: Record<string, string>; etag?: string }>(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const xhr = new XMLHttpRequest()
|
|
41
|
-
let settled = false
|
|
42
|
-
|
|
43
|
-
const settle = (fn: () => void) => {
|
|
44
|
-
if (settled) return
|
|
45
|
-
settled = true
|
|
46
|
-
cleanup()
|
|
47
|
-
fn()
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const onAbort = () => {
|
|
51
|
-
try {
|
|
52
|
-
xhr.abort()
|
|
53
|
-
} finally {
|
|
54
|
-
settle(() => reject(makeAbortError(abortReason(args.signal))))
|
|
23
|
+
return new Promise<{ status: number; headers: Record<string, string>; etag?: string | undefined }>(
|
|
24
|
+
(resolve, reject) => {
|
|
25
|
+
if (!args.url || typeof args.url !== 'string') {
|
|
26
|
+
reject(new Error(`UploadTransport: missing url`))
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
if (!isAbsoluteHttpUrl(args.url)) {
|
|
30
|
+
reject(new Error(`UploadTransport: expected absolute URL, got: ${String(args.url)}`))
|
|
31
|
+
return
|
|
55
32
|
}
|
|
56
|
-
}
|
|
57
33
|
|
|
58
|
-
|
|
59
|
-
|
|
34
|
+
const xhr = new XMLHttpRequest()
|
|
35
|
+
let settled = false
|
|
60
36
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
37
|
+
const settle = (fn: () => void) => {
|
|
38
|
+
if (settled) return
|
|
39
|
+
settled = true
|
|
40
|
+
cleanup()
|
|
41
|
+
fn()
|
|
42
|
+
}
|
|
66
43
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
44
|
+
const onAbort = () => {
|
|
45
|
+
try {
|
|
46
|
+
xhr.abort()
|
|
47
|
+
} finally {
|
|
48
|
+
settle(() => reject(makeAbortError(abortReason(args.signal))))
|
|
49
|
+
}
|
|
50
|
+
}
|
|
71
51
|
|
|
72
|
-
|
|
52
|
+
const cleanup = () => {
|
|
53
|
+
if (args.signal) args.signal.removeEventListener('abort', onAbort)
|
|
73
54
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
55
|
+
xhr.upload.onprogress = null
|
|
56
|
+
xhr.onload = null
|
|
57
|
+
xhr.onerror = null
|
|
58
|
+
xhr.onabort = null
|
|
77
59
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// S3 multipart UploadPart returns 200, tus PATCH returns 204 typically.
|
|
83
|
-
if (status >= 200 && status < 300) {
|
|
84
|
-
settle(() =>
|
|
85
|
-
resolve({
|
|
86
|
-
headers: parseHeaders(xhr),
|
|
87
|
-
etag: readEtag(xhr),
|
|
88
|
-
}),
|
|
89
|
-
)
|
|
90
|
-
} else {
|
|
91
|
-
settle(() => reject(new Error(`Upload failed with status ${status}`)))
|
|
60
|
+
|
|
61
|
+
if (args.signal?.aborted) {
|
|
62
|
+
settle(() => reject(makeAbortError(abortReason(args.signal))))
|
|
63
|
+
return
|
|
92
64
|
}
|
|
93
|
-
}
|
|
94
65
|
|
|
95
|
-
|
|
66
|
+
if (args.signal) args.signal.addEventListener('abort', onAbort, { once: true })
|
|
96
67
|
|
|
97
|
-
|
|
98
|
-
|
|
68
|
+
if (args.onProgress) {
|
|
69
|
+
xhr.upload.onprogress = (e) => {
|
|
70
|
+
if (e.lengthComputable) args.onProgress?.(e.loaded, e.total)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
99
73
|
|
|
100
|
-
|
|
74
|
+
xhr.onload = () => {
|
|
75
|
+
const status = xhr.status
|
|
76
|
+
// S3 multipart UploadPart returns 200, tus PATCH returns 204 typically.
|
|
77
|
+
if (status >= 200 && status < 300) {
|
|
78
|
+
settle(() =>
|
|
79
|
+
resolve({
|
|
80
|
+
status,
|
|
81
|
+
headers: parseHeaders(xhr),
|
|
82
|
+
etag: readEtag(xhr),
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
} else {
|
|
86
|
+
settle(() => reject(new Error(`Upload failed with status ${status}`)))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
xhr.onerror = () => settle(() => reject(createNetworkError(xhr, 'Network error during upload')))
|
|
91
|
+
|
|
92
|
+
xhr.onabort = () =>
|
|
93
|
+
settle(() => reject(makeAbortError(args.signal?.aborted ? abortReason(args.signal) : 'unknown')))
|
|
101
94
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
95
|
+
xhr.open(args.method, args.url)
|
|
96
|
+
|
|
97
|
+
if (args.headers) {
|
|
98
|
+
for (const [k, v] of Object.entries(args.headers)) {
|
|
99
|
+
xhr.setRequestHeader(k, v)
|
|
100
|
+
}
|
|
105
101
|
}
|
|
106
|
-
}
|
|
107
102
|
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
xhr.send(args.body as XMLHttpRequestBodyInit | Document | null)
|
|
104
|
+
},
|
|
105
|
+
)
|
|
110
106
|
}
|
|
111
107
|
|
|
112
108
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
109
|
+
* Browser-native XHR transport. Uses `XMLHttpRequest` over `fetch` because
|
|
110
|
+
* `fetch` lacks cross-browser upload-progress events.
|
|
111
|
+
* @author wildduck2 <https://github.com/wildduck2>
|
|
116
112
|
*/
|
|
117
|
-
export function createXHRTransport():
|
|
118
|
-
|
|
119
|
-
patch?: (args:
|
|
113
|
+
export function createXHRTransport(): Transport.Options & {
|
|
114
|
+
/** Optional PATCH for tus-style strategies. */
|
|
115
|
+
patch?: (args: Transport.XhrArgs & { body: Blob | ArrayBuffer }) => Promise<{ headers: Record<string, string> }>
|
|
120
116
|
} {
|
|
121
117
|
return {
|
|
122
118
|
async postForm(args) {
|
|
123
|
-
//
|
|
119
|
+
// Presigned POST URLs are absolute (MinIO/S3).
|
|
124
120
|
const form = new FormData()
|
|
125
121
|
for (const [k, v] of Object.entries(args.fields)) form.append(k, v)
|
|
126
122
|
form.append('file', args.file, args.filename ?? 'file')
|
|
@@ -130,7 +126,7 @@ export function createXHRTransport(): UploadTransport & {
|
|
|
130
126
|
url: args.url,
|
|
131
127
|
body: form,
|
|
132
128
|
signal: args.signal,
|
|
133
|
-
onProgress: args.onProgress,
|
|
129
|
+
onProgress: args.onProgress ?? (() => {}),
|
|
134
130
|
})
|
|
135
131
|
|
|
136
132
|
return { headers: out.headers }
|
|
@@ -142,25 +138,35 @@ export function createXHRTransport(): UploadTransport & {
|
|
|
142
138
|
url: args.url,
|
|
143
139
|
body: args.body,
|
|
144
140
|
signal: args.signal,
|
|
145
|
-
headers: args.headers,
|
|
146
|
-
onProgress: args.onProgress,
|
|
141
|
+
headers: args.headers ?? {},
|
|
142
|
+
onProgress: args.onProgress ?? (() => {}),
|
|
147
143
|
})
|
|
148
144
|
|
|
149
|
-
// multipart needs ETag from each part
|
|
145
|
+
// S3 multipart needs the ETag from each part response.
|
|
150
146
|
return { headers: out.headers, etag: out.etag }
|
|
151
147
|
},
|
|
152
148
|
|
|
153
|
-
// Optional for tus strategy (PATCH)
|
|
154
149
|
async patch(args) {
|
|
155
150
|
const out = await xhrRequest({
|
|
156
151
|
method: 'PATCH',
|
|
157
152
|
url: args.url,
|
|
158
153
|
body: args.body,
|
|
159
|
-
signal: args.signal,
|
|
160
|
-
headers: args.headers,
|
|
161
|
-
onProgress: args.onProgress,
|
|
154
|
+
signal: args.signal ?? new AbortController().signal,
|
|
155
|
+
headers: args.headers ?? {},
|
|
156
|
+
onProgress: args.onProgress ?? (() => {}),
|
|
162
157
|
})
|
|
163
158
|
return { headers: out.headers }
|
|
164
159
|
},
|
|
160
|
+
|
|
161
|
+
async head(args) {
|
|
162
|
+
const out = await xhrRequest({
|
|
163
|
+
method: 'HEAD',
|
|
164
|
+
url: args.url,
|
|
165
|
+
body: null,
|
|
166
|
+
signal: args.signal,
|
|
167
|
+
headers: args.headers ?? {},
|
|
168
|
+
})
|
|
169
|
+
return { status: out.status, headers: out.headers }
|
|
170
|
+
},
|
|
165
171
|
}
|
|
166
172
|
}
|
|
@@ -1,44 +1,80 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Designed to support:
|
|
6
|
-
* - Deterministic progress tracking (requires XHR in browsers)
|
|
7
|
-
* - Cancellation via AbortSignal
|
|
8
|
-
* - Header parsing (ETag extraction)
|
|
2
|
+
* Transport contracts: the network layer the engine uses to move bytes.
|
|
3
|
+
* @author wildduck2 <https://github.com/wildduck2>
|
|
9
4
|
*/
|
|
10
|
-
export
|
|
5
|
+
export namespace Transport {
|
|
11
6
|
/**
|
|
12
|
-
*
|
|
7
|
+
* Abstraction layer for network operations.
|
|
8
|
+
* Allows switching between XHR (browser) or other implementations (testing/Node).
|
|
9
|
+
*
|
|
10
|
+
* Designed to support:
|
|
11
|
+
* - Deterministic progress tracking (requires XHR in browsers)
|
|
12
|
+
* - Cancellation via AbortSignal
|
|
13
|
+
* - Header parsing (ETag extraction)
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
export type Options = {
|
|
16
|
+
/**
|
|
17
|
+
* Performs an HTTP PUT request (typically for S3 signed URLs).
|
|
18
|
+
*/
|
|
19
|
+
put(args: {
|
|
20
|
+
url: string
|
|
21
|
+
body: Blob
|
|
22
|
+
headers?: Record<string, string>
|
|
23
|
+
signal: AbortSignal
|
|
24
|
+
onProgress?: (u: number, t: number) => void
|
|
25
|
+
}): Promise<{ etag?: string | undefined; headers?: Record<string, string> }>
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Performs an HTTP POST multipart request.
|
|
29
|
+
*/
|
|
30
|
+
postForm(args: {
|
|
31
|
+
url: string
|
|
32
|
+
fields: Record<string, string>
|
|
33
|
+
file: File | Blob
|
|
34
|
+
filename?: string
|
|
35
|
+
signal: AbortSignal
|
|
36
|
+
onProgress?: (uploadedBytes: number, totalBytes: number) => void
|
|
37
|
+
}): Promise<{ etag?: string; headers?: Record<string, string> }>
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Performs an HTTP request.
|
|
41
|
+
*/
|
|
42
|
+
patch(args: {
|
|
43
|
+
url: string
|
|
44
|
+
body: Blob | ArrayBuffer
|
|
45
|
+
headers?: Record<string, string>
|
|
46
|
+
signal: AbortSignal
|
|
47
|
+
onProgress?: (u: number, t: number) => void
|
|
48
|
+
}): Promise<{ headers?: Record<string, string> }>
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Optional HTTP GET download. Streams the response so `onProgress` can
|
|
52
|
+
* report bytes as they arrive. Not every transport implements it — check
|
|
53
|
+
* for presence before calling.
|
|
54
|
+
*/
|
|
55
|
+
get?(args: {
|
|
56
|
+
url: string
|
|
57
|
+
headers?: Record<string, string>
|
|
58
|
+
signal: AbortSignal
|
|
59
|
+
onProgress?: (loaded: number, total: number) => void
|
|
60
|
+
}): Promise<{ blob: Blob; status: number; headers?: Record<string, string> }>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Optional HTTP HEAD. Used by resumable strategies (tus) to read the
|
|
64
|
+
* server's current `Upload-Offset` before resuming. Not every transport
|
|
65
|
+
* implements it — check for presence before calling.
|
|
66
|
+
*/
|
|
67
|
+
head?(args: {
|
|
68
|
+
url: string
|
|
69
|
+
headers?: Record<string, string>
|
|
70
|
+
signal: AbortSignal
|
|
71
|
+
}): Promise<{ status: number; headers: Record<string, string> }>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type XhrArgs = {
|
|
38
75
|
url: string
|
|
39
|
-
|
|
76
|
+
signal?: AbortSignal
|
|
40
77
|
headers?: Record<string, string>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}): Promise<{ headers?: Record<string, string> }>
|
|
78
|
+
onProgress?: (loaded: number, total: number) => void
|
|
79
|
+
}
|
|
44
80
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { resolveUploadConfig } from '../engine.libs'
|
|
3
|
+
|
|
4
|
+
describe('resolveUploadConfig - NaN-bypass defense', () => {
|
|
5
|
+
test('NaN maxAttempts falls back to default (does NOT propagate NaN)', () => {
|
|
6
|
+
const cfg = resolveUploadConfig({ maxAttempts: Number.NaN })
|
|
7
|
+
expect(Number.isFinite(cfg.maxAttempts)).toBe(true)
|
|
8
|
+
expect(cfg.maxAttempts).toBeGreaterThanOrEqual(1)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('Infinity maxAttempts falls back to default (no infinite retry loop)', () => {
|
|
12
|
+
const cfg = resolveUploadConfig({ maxAttempts: Number.POSITIVE_INFINITY })
|
|
13
|
+
expect(Number.isFinite(cfg.maxAttempts)).toBe(true)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('NaN maxConcurrentUploads falls back', () => {
|
|
17
|
+
const cfg = resolveUploadConfig({ maxConcurrentUploads: Number.NaN })
|
|
18
|
+
expect(Number.isFinite(cfg.maxConcurrentUploads)).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('Infinity progressThrottleMs falls back (no scheduler wedge)', () => {
|
|
22
|
+
const cfg = resolveUploadConfig({ progressThrottleMs: Number.POSITIVE_INFINITY })
|
|
23
|
+
expect(Number.isFinite(cfg.progressThrottleMs)).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('NaN maxItems falls back', () => {
|
|
27
|
+
const cfg = resolveUploadConfig({ maxItems: Number.NaN })
|
|
28
|
+
expect(Number.isFinite(cfg.maxItems)).toBe(true)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('negative maxAttempts clamps to 1', () => {
|
|
32
|
+
const cfg = resolveUploadConfig({ maxAttempts: -5 })
|
|
33
|
+
expect(cfg.maxAttempts).toBe(1)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('finite positive maxAttempts is preserved', () => {
|
|
37
|
+
const cfg = resolveUploadConfig({ maxAttempts: 7 })
|
|
38
|
+
expect(cfg.maxAttempts).toBe(7)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_MAX_ATTEMPTS,
|
|
3
|
+
DEFAULT_MAX_CONCURRENT_UPLOADS,
|
|
4
|
+
DEFAULT_MAX_ITEMS,
|
|
5
|
+
DEFAULT_PROGRESS_THROTTLE_MS,
|
|
6
|
+
} from '../utils/constants'
|
|
7
|
+
import type { Engine } from './engine.types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Clamp a numeric config field, rejecting non-finite values (NaN, Infinity)
|
|
11
|
+
* that would otherwise slip past a plain `Math.max`.
|
|
12
|
+
*/
|
|
13
|
+
function finitePositive(input: number | undefined, fallback: number, min = 1): number {
|
|
14
|
+
if (typeof input !== 'number' || !Number.isFinite(input)) return fallback
|
|
15
|
+
return Math.max(min, input)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Normalizes user config by applying sensible defaults.
|
|
20
|
+
* @author wildduck2 <https://github.com/wildduck2>
|
|
21
|
+
*/
|
|
22
|
+
export function resolveUploadConfig<P extends string>(input?: Partial<Engine.Config<P>>): Engine.Config<P> {
|
|
23
|
+
return {
|
|
24
|
+
maxConcurrentUploads: finitePositive(input?.maxConcurrentUploads, DEFAULT_MAX_CONCURRENT_UPLOADS),
|
|
25
|
+
autoStart: input?.autoStart,
|
|
26
|
+
progressThrottleMs: finitePositive(input?.progressThrottleMs, DEFAULT_PROGRESS_THROTTLE_MS, 0),
|
|
27
|
+
validation: input?.validation ?? {},
|
|
28
|
+
maxAttempts: finitePositive(input?.maxAttempts, DEFAULT_MAX_ATTEMPTS),
|
|
29
|
+
retryPolicy: input?.retryPolicy,
|
|
30
|
+
maxItems: finitePositive(input?.maxItems, DEFAULT_MAX_ITEMS),
|
|
31
|
+
completedItemTTL: input?.completedItemTTL,
|
|
32
|
+
strictMimeMatch: input?.strictMimeMatch ?? false,
|
|
33
|
+
checksumMaxSize: input?.checksumMaxSize ?? null,
|
|
34
|
+
}
|
|
35
|
+
}
|