@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.
Files changed (129) hide show
  1. package/README.md +297 -0
  2. package/dist/core/index.d.ts +2 -3
  3. package/dist/core/index.js +1 -1
  4. package/dist/core-BVgloTiP.js +10 -0
  5. package/dist/index-DCqyBVcx.d.ts +1441 -0
  6. package/dist/index-DsZHPmJm.d.ts +225 -0
  7. package/dist/index-KROXtLQh.d.ts +281 -0
  8. package/dist/index.d.ts +4 -5
  9. package/dist/index.js +1 -1
  10. package/dist/react/index.d.ts +2 -90
  11. package/dist/react/index.js +1 -1
  12. package/dist/react-euPrDv24.js +1 -0
  13. package/dist/strategies/index.d.ts +2 -56
  14. package/dist/strategies/index.js +1 -1
  15. package/dist/strategies-CUF0s68j.js +1 -0
  16. package/package.json +3 -2
  17. package/src/__tests__/emitter.test.ts +85 -0
  18. package/src/__tests__/fingerprint.test.ts +49 -0
  19. package/src/__tests__/guards.test.ts +40 -0
  20. package/src/__tests__/id.test.ts +67 -0
  21. package/src/__tests__/mime-sniff.test.ts +142 -0
  22. package/src/__tests__/persistence.test.ts +191 -0
  23. package/src/__tests__/sanitize-filename.test.ts +109 -0
  24. package/src/__tests__/sleep.test.ts +34 -0
  25. package/src/__tests__/static-analysis.test.ts +145 -0
  26. package/src/__tests__/store-libs.test.ts +192 -0
  27. package/src/__tests__/transport-fetch.test.ts +108 -0
  28. package/src/__tests__/transport-libs.test.ts +42 -0
  29. package/src/__tests__/transport.test.ts +182 -0
  30. package/src/core/__tests__/errors.test.ts +152 -0
  31. package/src/core/client.ts +3 -0
  32. package/src/core/contracts/contracts.types.ts +373 -0
  33. package/src/core/contracts/index.ts +3 -8
  34. package/src/core/contracts/transport/index.ts +3 -2
  35. package/src/core/contracts/transport/transport.fetch.ts +203 -0
  36. package/src/core/contracts/transport/transport.libs.ts +13 -39
  37. package/src/core/contracts/transport/transport.ts +96 -90
  38. package/src/core/contracts/transport/transport.types.ts +71 -35
  39. package/src/core/engine/__tests__/config-nan-bypass.test.ts +40 -0
  40. package/src/core/engine/engine.libs.ts +35 -0
  41. package/src/core/engine/engine.types.ts +490 -0
  42. package/src/core/engine/index.ts +3 -8
  43. package/src/core/engine/reducer.ts +93 -53
  44. package/src/core/engine/store/handlers/add-file.ts +43 -48
  45. package/src/core/engine/store/handlers/cancel.ts +13 -8
  46. package/src/core/engine/store/handlers/clean-up.ts +11 -8
  47. package/src/core/engine/store/handlers/create-intent.ts +82 -27
  48. package/src/core/engine/store/handlers/finalize.ts +30 -13
  49. package/src/core/engine/store/handlers/index.ts +7 -7
  50. package/src/core/engine/store/handlers/pause.ts +8 -6
  51. package/src/core/engine/store/handlers/run-upload.ts +54 -39
  52. package/src/core/engine/store/index.ts +27 -21
  53. package/src/core/engine/store/store.dispatch.ts +10 -8
  54. package/src/core/engine/store/store.libs.ts +197 -94
  55. package/src/core/engine/store/store.runtime.ts +52 -53
  56. package/src/core/engine/store/store.schedule.ts +27 -21
  57. package/src/core/engine/store/store.types.ts +177 -227
  58. package/src/core/engine/validation/__tests__/intent.test.ts +70 -0
  59. package/src/core/engine/validation/file.ts +8 -4
  60. package/src/core/engine/validation/index.ts +1 -0
  61. package/src/core/engine/validation/intent.ts +49 -27
  62. package/src/core/engine/validation/list.ts +14 -15
  63. package/src/core/engine/validation/mime.ts +39 -0
  64. package/src/core/errors.ts +271 -0
  65. package/src/core/index.ts +28 -5
  66. package/src/core/persistence/__tests__/deserialize-nan-bypass.test.ts +100 -0
  67. package/src/core/persistence/__tests__/persistence.test.ts +85 -0
  68. package/src/core/persistence/adapters.indexeddb.ts +9 -3
  69. package/src/core/persistence/adapters.local.ts +9 -3
  70. package/src/core/persistence/adapters.memory.ts +35 -13
  71. package/src/core/persistence/index.ts +5 -5
  72. package/src/core/persistence/persistence.ts +73 -60
  73. package/src/core/persistence/persistence.types.ts +140 -60
  74. package/src/core/utils/__tests__/guards.test.ts +93 -0
  75. package/src/core/utils/__tests__/url-safety.test.ts +112 -0
  76. package/src/core/utils/async.ts +7 -2
  77. package/src/core/utils/constants.ts +3 -9
  78. package/src/core/utils/emitter/emitter.ts +126 -0
  79. package/src/core/utils/emitter/emitter.types.ts +33 -16
  80. package/src/core/utils/emitter/index.ts +2 -74
  81. package/src/core/utils/fingerprint.ts +16 -5
  82. package/src/core/utils/guards.ts +91 -1
  83. package/src/core/utils/hash/__tests__/sha256.test.ts +61 -0
  84. package/src/core/utils/hash/index.ts +126 -0
  85. package/src/core/utils/hash/sha256.ts +179 -0
  86. package/src/core/utils/id.ts +33 -1
  87. package/src/core/utils/index.ts +15 -6
  88. package/src/core/utils/mime-sniff.ts +119 -0
  89. package/src/core/utils/sanitize-filename.ts +85 -0
  90. package/src/core/utils/url-safety.ts +156 -0
  91. package/src/react/__tests__/use-dropzone.test.ts +64 -0
  92. package/src/react/index.ts +5 -2
  93. package/src/react/upload-provider.tsx +59 -95
  94. package/src/react/uploader.types.ts +80 -0
  95. package/src/react/use-dropzone.tsx +226 -0
  96. package/src/react/use-uploader.tsx +97 -93
  97. package/src/strategies/_shared/__tests__/retry.test.ts +120 -0
  98. package/src/strategies/_shared/retry.ts +79 -0
  99. package/src/strategies/index.ts +9 -15
  100. package/src/strategies/multipart/__tests__/multipart.test.ts +229 -0
  101. package/src/strategies/multipart/index.ts +211 -116
  102. package/src/strategies/post/__tests__/post-url-safety.test.ts +119 -0
  103. package/src/strategies/post/index.ts +113 -71
  104. package/src/strategies/put/__tests__/put.test.ts +132 -0
  105. package/src/strategies/put/index.ts +139 -0
  106. package/src/strategies/registry/index.ts +12 -9
  107. package/src/strategies/tus/__tests__/tus.test.ts +159 -0
  108. package/src/strategies/tus/index.ts +211 -0
  109. package/dist/async-DxRWu4le.js +0 -1
  110. package/dist/core-BNxznRif.js +0 -2
  111. package/dist/index-BXg9WIce.d.ts +0 -987
  112. package/dist/strategy.types-gg9ZStRe.d.ts +0 -225
  113. package/src/core/client/config.types.ts +0 -77
  114. package/src/core/client/index.ts +0 -4
  115. package/src/core/client/plugins.types.ts +0 -46
  116. package/src/core/client/retry.types.ts +0 -6
  117. package/src/core/client/uploadClient.ts +0 -19
  118. package/src/core/contracts/backendApi.ts +0 -62
  119. package/src/core/contracts/cursor.types.ts +0 -23
  120. package/src/core/contracts/errors.types.ts +0 -67
  121. package/src/core/contracts/fingerprint.types.ts +0 -18
  122. package/src/core/contracts/intent.types.ts +0 -39
  123. package/src/core/contracts/result.types.ts +0 -11
  124. package/src/core/contracts/strategy.types.ts +0 -45
  125. package/src/core/engine/commands.types.ts +0 -17
  126. package/src/core/engine/event-map.types.ts +0 -45
  127. package/src/core/engine/internal-events.types.ts +0 -222
  128. package/src/core/engine/outcome.types.ts +0 -9
  129. 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 { UploadTransport } from './transport.types'
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 }>((resolve, reject) => {
31
- if (!args.url || typeof args.url !== 'string') {
32
- reject(new Error(`UploadTransport: missing url`))
33
- return
34
- }
35
- if (!isAbsoluteHttpUrl(args.url)) {
36
- reject(new Error(`UploadTransport: expected absolute URL, got: ${String(args.url)}`))
37
- return
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
- const cleanup = () => {
59
- if (args.signal) args.signal.removeEventListener('abort', onAbort)
34
+ const xhr = new XMLHttpRequest()
35
+ let settled = false
60
36
 
61
- xhr.upload.onprogress = null
62
- xhr.onload = null
63
- xhr.onerror = null
64
- xhr.onabort = null
65
- }
37
+ const settle = (fn: () => void) => {
38
+ if (settled) return
39
+ settled = true
40
+ cleanup()
41
+ fn()
42
+ }
66
43
 
67
- if (args.signal?.aborted) {
68
- settle(() => reject(makeAbortError(abortReason(args.signal))))
69
- return
70
- }
44
+ const onAbort = () => {
45
+ try {
46
+ xhr.abort()
47
+ } finally {
48
+ settle(() => reject(makeAbortError(abortReason(args.signal))))
49
+ }
50
+ }
71
51
 
72
- if (args.signal) args.signal.addEventListener('abort', onAbort, { once: true })
52
+ const cleanup = () => {
53
+ if (args.signal) args.signal.removeEventListener('abort', onAbort)
73
54
 
74
- if (args.onProgress) {
75
- xhr.upload.onprogress = (e) => {
76
- if (e.lengthComputable) args.onProgress?.(e.loaded, e.total)
55
+ xhr.upload.onprogress = null
56
+ xhr.onload = null
57
+ xhr.onerror = null
58
+ xhr.onabort = null
77
59
  }
78
- }
79
-
80
- xhr.onload = () => {
81
- const status = xhr.status
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
- xhr.onerror = () => settle(() => reject(createNetworkError(xhr, 'Network error during upload')))
66
+ if (args.signal) args.signal.addEventListener('abort', onAbort, { once: true })
96
67
 
97
- xhr.onabort = () =>
98
- settle(() => reject(makeAbortError(args.signal?.aborted ? abortReason(args.signal) : 'unknown')))
68
+ if (args.onProgress) {
69
+ xhr.upload.onprogress = (e) => {
70
+ if (e.lengthComputable) args.onProgress?.(e.loaded, e.total)
71
+ }
72
+ }
99
73
 
100
- xhr.open(args.method, args.url)
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
- if (args.headers) {
103
- for (const [k, v] of Object.entries(args.headers)) {
104
- xhr.setRequestHeader(k, v)
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
- xhr.send(args.body as XMLHttpRequestBodyInit | Document | null)
109
- })
103
+ xhr.send(args.body as XMLHttpRequestBodyInit | Document | null)
104
+ },
105
+ )
110
106
  }
111
107
 
112
108
  /**
113
- * Creates a browser-native XHR transport.
114
- * We use XMLHttpRequest instead of fetch because fetch does not yet support
115
- * upload progress events in a standard cross-browser way.
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(): UploadTransport & {
118
- // Optional: for tus strategies (PATCH)
119
- patch?: (args: XhrCommonArgs & { body: Blob | ArrayBuffer }) => Promise<{ headers: Record<string, string> }>
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
- // Note: presigned POST is usually absolute already (MinIO/S3 URL)
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
- * Abstraction layer for network operations.
3
- * Allows switching between XHR (browser) or other implementations (testing/Node).
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 interface UploadTransport {
5
+ export namespace Transport {
11
6
  /**
12
- * Performs an HTTP PUT request (typically for S3 signed URLs).
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
- put(args: {
15
- url: string
16
- body: Blob
17
- headers?: Record<string, string>
18
- signal: AbortSignal
19
- onProgress?: (u: number, t: number) => void
20
- }): Promise<{ etag?: string; headers?: Record<string, string> }>
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
- * Performs an HTTP POST multipart request.
24
- */
25
- postForm(args: {
26
- url: string
27
- fields: Record<string, string>
28
- file: File | Blob
29
- filename?: string
30
- signal: AbortSignal
31
- onProgress?: (uploadedBytes: number, totalBytes: number) => void
32
- }): Promise<{ etag?: string; headers?: Record<string, string> }>
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
- * Performs an HTTP request.
36
- */
37
- patch(args: {
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
- body: Blob | ArrayBuffer
76
+ signal?: AbortSignal
40
77
  headers?: Record<string, string>
41
- signal: AbortSignal
42
- onProgress?: (u: number, t: number) => void
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
+ }