@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.
Files changed (136) hide show
  1. package/README.md +170 -193
  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 -51
  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/{__test__ → __tests__}/emitter.test.ts +4 -4
  18. package/src/{__test__ → __tests__}/fingerprint.test.ts +1 -1
  19. package/src/{__test__ → __tests__}/guards.test.ts +15 -5
  20. package/src/__tests__/id.test.ts +67 -0
  21. package/src/__tests__/mime-sniff.test.ts +142 -0
  22. package/src/{__test__ → __tests__}/persistence.test.ts +43 -8
  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/{__test__ → __tests__}/store-libs.test.ts +57 -3
  27. package/src/__tests__/transport-fetch.test.ts +108 -0
  28. package/src/{__test__ → __tests__}/transport.test.ts +5 -5
  29. package/src/core/__tests__/errors.test.ts +152 -0
  30. package/src/core/client.ts +3 -0
  31. package/src/core/contracts/contracts.types.ts +335 -141
  32. package/src/core/contracts/index.ts +3 -8
  33. package/src/core/contracts/transport/index.ts +3 -2
  34. package/src/core/contracts/transport/transport.fetch.ts +203 -0
  35. package/src/core/contracts/transport/transport.libs.ts +7 -8
  36. package/src/core/contracts/transport/transport.ts +91 -83
  37. package/src/core/contracts/transport/transport.types.ts +71 -35
  38. package/src/core/engine/__tests__/config-nan-bypass.test.ts +40 -0
  39. package/src/core/engine/engine.libs.ts +35 -0
  40. package/src/core/engine/engine.types.ts +367 -241
  41. package/src/core/engine/index.ts +3 -8
  42. package/src/core/engine/reducer.ts +89 -43
  43. package/src/core/engine/store/handlers/add-file.ts +39 -29
  44. package/src/core/engine/store/handlers/cancel.ts +13 -8
  45. package/src/core/engine/store/handlers/clean-up.ts +11 -8
  46. package/src/core/engine/store/handlers/create-intent.ts +82 -27
  47. package/src/core/engine/store/handlers/finalize.ts +30 -13
  48. package/src/core/engine/store/handlers/index.ts +7 -7
  49. package/src/core/engine/store/handlers/pause.ts +8 -6
  50. package/src/core/engine/store/handlers/run-upload.ts +49 -34
  51. package/src/core/engine/store/index.ts +27 -21
  52. package/src/core/engine/store/store.dispatch.ts +10 -8
  53. package/src/core/engine/store/store.libs.ts +184 -41
  54. package/src/core/engine/store/store.runtime.ts +45 -36
  55. package/src/core/engine/store/store.schedule.ts +27 -21
  56. package/src/core/engine/store/store.types.ts +177 -227
  57. package/src/core/engine/validation/__tests__/intent.test.ts +70 -0
  58. package/src/core/engine/validation/file.ts +7 -3
  59. package/src/core/engine/validation/index.ts +1 -0
  60. package/src/core/engine/validation/intent.ts +49 -27
  61. package/src/core/engine/validation/list.ts +14 -15
  62. package/src/core/engine/validation/mime.ts +39 -0
  63. package/src/core/errors.ts +271 -0
  64. package/src/core/index.ts +28 -5
  65. package/src/core/persistence/__tests__/deserialize-nan-bypass.test.ts +100 -0
  66. package/src/core/persistence/__tests__/persistence.test.ts +85 -0
  67. package/src/core/persistence/adapters.indexeddb.ts +9 -3
  68. package/src/core/persistence/adapters.local.ts +9 -3
  69. package/src/core/persistence/adapters.memory.ts +35 -13
  70. package/src/core/persistence/index.ts +5 -5
  71. package/src/core/persistence/persistence.ts +73 -60
  72. package/src/core/persistence/persistence.types.ts +140 -60
  73. package/src/core/utils/__tests__/guards.test.ts +93 -0
  74. package/src/core/utils/__tests__/url-safety.test.ts +112 -0
  75. package/src/core/utils/async.ts +7 -2
  76. package/src/core/utils/constants.ts +3 -9
  77. package/src/core/utils/emitter/emitter.ts +7 -24
  78. package/src/core/utils/emitter/emitter.types.ts +33 -16
  79. package/src/core/utils/emitter/index.ts +2 -74
  80. package/src/core/utils/fingerprint.ts +16 -5
  81. package/src/core/utils/guards.ts +91 -1
  82. package/src/core/utils/hash/__tests__/sha256.test.ts +61 -0
  83. package/src/core/utils/hash/index.ts +126 -0
  84. package/src/core/utils/hash/sha256.ts +179 -0
  85. package/src/core/utils/id.ts +33 -1
  86. package/src/core/utils/index.ts +15 -6
  87. package/src/core/utils/mime-sniff.ts +119 -0
  88. package/src/core/utils/sanitize-filename.ts +85 -0
  89. package/src/core/utils/url-safety.ts +156 -0
  90. package/src/react/__tests__/use-dropzone.test.ts +64 -0
  91. package/src/react/index.ts +5 -2
  92. package/src/react/upload-provider.tsx +68 -53
  93. package/src/react/uploader.types.ts +58 -61
  94. package/src/react/use-dropzone.tsx +226 -0
  95. package/src/react/use-uploader.tsx +98 -91
  96. package/src/strategies/_shared/__tests__/retry.test.ts +120 -0
  97. package/src/strategies/_shared/retry.ts +79 -0
  98. package/src/strategies/index.ts +9 -15
  99. package/src/strategies/multipart/__tests__/multipart.test.ts +229 -0
  100. package/src/strategies/multipart/index.ts +206 -108
  101. package/src/strategies/post/__tests__/post-url-safety.test.ts +119 -0
  102. package/src/strategies/post/index.ts +115 -33
  103. package/src/strategies/put/__tests__/put.test.ts +132 -0
  104. package/src/strategies/put/index.ts +139 -0
  105. package/src/strategies/registry/index.ts +12 -9
  106. package/src/strategies/tus/__tests__/tus.test.ts +159 -0
  107. package/src/strategies/tus/index.ts +211 -0
  108. package/dist/async-DxRWu4le.js +0 -1
  109. package/dist/core-CMkrP9DX.js +0 -2
  110. package/dist/index-C38Jmizu.d.ts +0 -1005
  111. package/dist/strategy.types-gg9ZStRe.d.ts +0 -225
  112. package/src/__test__/effect-pool.test.ts +0 -5
  113. package/src/__test__/id.test.ts +0 -14
  114. package/src/__test__/store-integration.test.ts +0 -5
  115. package/src/core/client/client.ts +0 -50
  116. package/src/core/client/client.types.ts +0 -184
  117. package/src/core/client/config.types.ts +0 -77
  118. package/src/core/client/index.ts +0 -4
  119. package/src/core/client/plugins.types.ts +0 -46
  120. package/src/core/client/retry.types.ts +0 -6
  121. package/src/core/client/uploadClient.ts +0 -19
  122. package/src/core/contracts/backendApi.ts +0 -62
  123. package/src/core/contracts/cursor.types.ts +0 -23
  124. package/src/core/contracts/errors.types.ts +0 -67
  125. package/src/core/contracts/fingerprint.types.ts +0 -18
  126. package/src/core/contracts/intent.types.ts +0 -39
  127. package/src/core/contracts/result.types.ts +0 -11
  128. package/src/core/contracts/strategy.types.ts +0 -45
  129. package/src/core/engine/commands.types.ts +0 -17
  130. package/src/core/engine/event-map.types.ts +0 -45
  131. package/src/core/engine/internal-events.types.ts +0 -202
  132. package/src/core/engine/outcome.types.ts +0 -9
  133. package/src/core/engine/progress.types.ts +0 -15
  134. package/src/core/engine/store/handlers.ts +0 -819
  135. package/src/core/engine/validation.ts +0 -216
  136. /package/src/{__test__ → __tests__}/transport-libs.test.ts +0 -0
package/README.md CHANGED
@@ -23,37 +23,45 @@
23
23
 
24
24
  ---
25
25
 
26
- Headless, framework-agnostic file-upload engine with a typed state machine, pluggable strategies, and React bindings.
26
+ A pure-reducer core, persistence-aware and resume-after-refresh, with bounded upload
27
+ concurrency and zero coupling to a specific backend or UI library. The engine stays
28
+ protocol-agnostic: your backend returns an intent, the engine dispatches on its `strategy` field
29
+ to a registered strategy that moves the bytes.
27
30
 
28
- Pure-reducer core, persistence-aware, resume-after-refresh, bounded effect concurrency, and zero coupling to a specific backend or UI library.
31
+ ## Install
32
+
33
+ ```sh
34
+ bun add @gentleduck/upload
35
+ # peer deps for the React bindings
36
+ bun add react react-dom
37
+ ```
29
38
 
30
39
  ## Quick Start
31
40
 
32
41
  ```ts
33
42
  import { createUploadStore } from '@gentleduck/upload/core'
34
- import { createStrategyRegistry } from '@gentleduck/upload/strategies'
43
+ import { PutStrategy, createStrategyRegistry } from '@gentleduck/upload/strategies'
35
44
 
36
- type Intents = { post: { strategy: 'post'; fileId: string; url: string; fields: Record<string, string> } }
37
- type Cursors = { post: { offset: number } }
45
+ type Intents = { put: PutStrategy.Intent }
46
+ type Cursors = { put?: PutStrategy.Cursor }
38
47
  type Purpose = 'attachment'
39
48
  type Result = { fileId: string; key: string }
40
49
 
41
50
  const strategies = createStrategyRegistry<Intents, Cursors, Purpose, Result>()
42
- strategies.set({
43
- id: 'post',
44
- resumable: false,
45
- async start({ file, intent, transport, signal, reportProgress }) {
46
- // your upload logic (PUT/POST/multipart/tus)
47
- reportProgress({ uploadedBytes: file.size, totalBytes: file.size })
48
- },
49
- })
51
+ strategies.set(PutStrategy({ allowedHosts: ['uploads.example.com'] }))
50
52
 
51
53
  const store = createUploadStore<Intents, Cursors, Purpose, Result>({
52
54
  strategies,
53
55
  api: {
54
- async createIntent({ filename }) {
55
- return { strategy: 'post', fileId: filename, url: '/sign', fields: {} }
56
+ // Ask your backend to presign the upload.
57
+ async createIntent({ filename, contentType, size }) {
58
+ const res = await fetch('/api/sign', {
59
+ method: 'POST',
60
+ body: JSON.stringify({ filename, contentType, size }),
61
+ })
62
+ return res.json() // -> { strategy: 'put', fileId, url, headers? }
56
63
  },
64
+ // Finalize once the bytes are transferred.
57
65
  async complete({ fileId }) {
58
66
  return { fileId, key: `attachments/${fileId}` }
59
67
  },
@@ -64,248 +72,216 @@ const store = createUploadStore<Intents, Cursors, Purpose, Result>({
64
72
  store.dispatch({ type: 'addFiles', files, purpose: 'attachment' })
65
73
  ```
66
74
 
75
+ The `createXHRTransport()` browser transport is installed automatically. Pass
76
+ `transport: createFetchTransport()` to run in Node / SSR / edge / workers.
77
+
78
+ ## Strategies
79
+
80
+ A strategy is the thin layer that turns an intent into network calls. Four ship built in;
81
+ register the ones you need and let `createIntent` pick per file.
82
+
83
+ | Strategy | Factory | Use case | Resumable |
84
+ | --- | --- | --- | --- |
85
+ | PUT | `PutStrategy(opts?)` | Single presigned PUT (S3 `putObject`, signed PUT) | No |
86
+ | POST | `PostStrategy(opts?)` | Presigned `multipart/form-data` (S3 POST policy) | No |
87
+ | Multipart | `multipartStrategy(opts?)` | Large files, concurrent parts, per-part resume | Yes |
88
+ | tus | `TusStrategy(opts?)` | Resumable uploads over the tus protocol | Yes |
89
+
90
+ ```ts
91
+ import {
92
+ PutStrategy,
93
+ PostStrategy,
94
+ multipartStrategy,
95
+ TusStrategy,
96
+ createStrategyRegistry,
97
+ } from '@gentleduck/upload/strategies'
98
+
99
+ const strategies = createStrategyRegistry<Intents, Cursors, Purpose, Result>()
100
+ strategies.set(PutStrategy({ allowedHosts: ['uploads.example.com'] }))
101
+ strategies.set(PostStrategy())
102
+ strategies.set(multipartStrategy({ maxPartConcurrency: 4 }))
103
+ strategies.set(TusStrategy({ allowedHosts: ['tus.example.com'] }))
104
+ ```
105
+
106
+ Every backend-supplied URL (PUT / POST / multipart part / tus) runs through the `validateUploadUrl`
107
+ SSRF guard before any byte leaves the client. Set `allowedHosts` to lock the host; keep
108
+ `allowPrivateHosts` at its default `false` to block loopback / RFC1918 / cloud-metadata targets.
109
+ `PutStrategy`, `PostStrategy`, and `TusStrategy` retry transient network failures
110
+ (timeouts / 5xx / `ECONNRESET`) with exponential backoff.
111
+
67
112
  ## React
68
113
 
69
114
  ```tsx
70
- import { UploadProvider, useUploader, createUploadFactory } from '@gentleduck/upload/react'
71
-
72
- const useUploads = createUploadFactory(store)
115
+ import {
116
+ UploadProvider,
117
+ useUploader,
118
+ useDropzone,
119
+ } from '@gentleduck/upload/react'
120
+
121
+ function Uploader() {
122
+ const { getRootProps, getInputProps, isDragging } = useDropzone({
123
+ purpose: 'attachment',
124
+ accept: 'image/*,.pdf',
125
+ })
126
+ const { items, dispatch } = useUploader<Intents, Cursors, Purpose, Result>()
73
127
 
74
- function FileList() {
75
- const { items, uploading, dispatch } = useUploads()
76
128
  return (
77
- <UploadProvider store={store}>
78
- {items.map(item => (
79
- <Row key={item.localId} item={item} onCancel={() => dispatch({ type: 'cancel', localId: item.localId })} />
129
+ <div {...getRootProps()} data-active={isDragging}>
130
+ <input {...getInputProps()} />
131
+ {items.map((item) => (
132
+ <Row
133
+ key={item.localId}
134
+ item={item}
135
+ onCancel={() => dispatch({ type: 'cancel', localId: item.localId })}
136
+ />
80
137
  ))}
81
- </UploadProvider>
138
+ </div>
82
139
  )
83
140
  }
84
- ```
85
-
86
- A full working demo lives at `packages/registry-examples/src/upload/upload-1.tsx`.
87
141
 
88
- ## Features
142
+ // Wrap once so descendants can reach the store.
143
+ <UploadProvider store={store}>
144
+ <Uploader />
145
+ </UploadProvider>
146
+ ```
89
147
 
90
- - **Pure reducer** - command/event-driven state machine; effects are isolated
91
- - **Typed end-to-end** - intents, cursors, purposes, and results are quad-generics threaded through the engine
92
- - **Strategy registry** - ship POST/multipart out of the box, add TUS or your own
93
- - **Resume after refresh** - IndexedDB / LocalStorage / Memory persistence adapters; cursor-aware re-binding
94
- - **Retry policy** - exponential backoff with attempt escalation per phase (intent / upload / complete)
95
- - **Bounded effect concurrency** - `effectConcurrency` separates side-effect parallelism from upload-byte parallelism
96
- - **Batched bulk dispatch** - `startAll` / `pauseAll` / `cancelAll` collapse into a single reducer pass and notification
97
- - **Rebind** - rebind a paused, persisted item to a fresh `File` after refresh, with fingerprint validation
98
- - **Plugins + hooks** - `onInternalEvent`, `onPersistenceError`, custom plugins
99
- - **No DOM coupling** - works in Workers, Node 22+, or the browser
148
+ React exports: `UploadProvider`, `useUploader`, `useUploaderActions`, `useDropzone`,
149
+ `createUploadFactory`, `useUploadStore`, `isUploadStore` (plus the pure helpers
150
+ `fileMatchesAccept`, `matchesAcceptToken`, `selectFiles`).
100
151
 
101
152
  ## Subpath Exports
102
153
 
103
154
  ```ts
104
- import { createUploadStore } from '@gentleduck/upload/core'
105
- import { UploadProvider, useUploader } from '@gentleduck/upload/react'
106
- import { createStrategyRegistry } from '@gentleduck/upload/strategies'
155
+ import { createUploadStore, createFetchTransport } from '@gentleduck/upload/core'
156
+ import { UploadProvider, useUploader, useDropzone } from '@gentleduck/upload/react'
157
+ import { PutStrategy, TusStrategy, createStrategyRegistry } from '@gentleduck/upload/strategies'
107
158
  ```
108
159
 
160
+ ## Features
161
+
162
+ - **Pure reducer** — command/event-driven state machine; effects are isolated.
163
+ - **Typed end-to-end** — intents, cursors, purposes, and results are quad-generics threaded
164
+ through the engine (`M`, `C`, `P`, `R`).
165
+ - **Four built-in strategies** — PUT, POST, multipart, tus — plus a registry for your own.
166
+ - **Two transports** — `createXHRTransport()` (browser, real upload progress) and
167
+ `createFetchTransport()` (Node / SSR / edge / workers), with optional `get()` streaming
168
+ download and `head()`.
169
+ - **Resume after refresh** — Memory / LocalStorage / IndexedDB persistence adapters; cursor-aware
170
+ re-binding via the `rebind` command.
171
+ - **Retry policy** — exponential backoff with per-phase attempt escalation (intent / upload /
172
+ complete), overridable via `config.retryPolicy`.
173
+ - **Bounded concurrency** — `config.maxConcurrentUploads` caps parallel uploads; queued items
174
+ wait for a free slot.
175
+ - **Content dedupe, off-thread** — SHA-256 checksums hash inline for small files and
176
+ incrementally in a Web Worker for large ones, so `checksumMaxSize` is a boundary, not a skip —
177
+ big files stay deduplicated without freezing the UI.
178
+ - **Headless React** — `useDropzone` gives drag-and-drop + file-picker prop getters with zero
179
+ markup or styling.
180
+ - **SSRF guard built in** — allowlist + protocol + IPv4/IPv6 private + cloud-metadata + NAT64 +
181
+ 6to4 + IPv4-mapped checks on every upload URL.
182
+ - **Filename + MIME hardening** — NFKC normalize, control-char strip, Windows reserved-name +
183
+ 255-char + path-sep rejection; magic-byte sniff cross-checks the claimed `file.type`.
184
+ - **NaN-safe configs** — every numeric option clamps `NaN` / `Infinity` / negative to its
185
+ default; persisted snapshots reject `NaN` fields.
186
+ - **No DOM coupling** — runs in Workers, Node 22+, or the browser.
187
+ - **Threat model + audit** — STRIDE-mapped `THREAT-MODEL.md` and re-runnable `AUDIT-RESULTS.md`
188
+ checked into the package.
189
+
109
190
  ## Persistence
110
191
 
192
+ Persistence lets a paused or in-flight upload survive a refresh: the snapshot is stored, and a
193
+ `rebind` re-attaches a fresh `File` (validated by fingerprint) so a resumable strategy continues
194
+ from its cursor.
195
+
111
196
  ```ts
112
- import { createIndexedDBAdapter } from '@gentleduck/upload/core'
197
+ import { createUploadStore, IndexedDBAdapter } from '@gentleduck/upload/core'
113
198
 
114
199
  createUploadStore({
200
+ strategies,
201
+ api,
115
202
  persistence: {
116
203
  key: 'app:uploads',
117
204
  version: 1,
118
- adapter: createIndexedDBAdapter(),
205
+ adapter: IndexedDBAdapter, // or LocalStorageAdapter / createMemoryAdapter()
119
206
  isPurpose,
120
207
  isIntent,
121
208
  },
122
- hooks: { onPersistenceError: (err) => toast.error(err.message) },
123
- ...
124
209
  })
125
210
  ```
126
211
 
127
- Use `createIndexedDBAdapter()` / `createMemoryAdapter()` factories so each store owns its own connection.
128
-
129
- Adapters throw a typed `PersistenceError` on failure (`quota_exceeded`, `unavailable`, `serialization_failed`, `transaction_failed`, `unknown`) that is routed to the optional `onPersistenceError` hook.
130
-
131
- ### Cross-tab IndexedDB contract
132
-
133
- When a second tab opens the same database at a higher `persistence.version`, the existing tab's connection receives `onversionchange` and closes. The adapter resets its memoized handle so the next call reopens -- but at the OLD version. IndexedDB rejects that with `VersionError`, which the adapter surfaces as `PersistenceError('unavailable', operation, ...)`.
134
-
135
- Application policy:
136
-
137
- - Bump `persistence.version` in every tab/deployment in lockstep.
138
- - Treat `onPersistenceError({ code: 'unavailable' })` after a known multi-tab scenario as a signal to reload the tab. The adapter is fixed at construction; the store does not support runtime adapter swap. If you want graceful fallback to in-memory persistence, the consumer must tear the store down and reconstruct with `createMemoryAdapter()`.
139
- - Snapshot version mismatches at load time also surface as `PersistenceError('unavailable')` (via `deserializeSnapshot`'s `expectedVersion` fence).
140
-
141
- ### Handling persistence errors
142
-
143
- ```ts
144
- import { createIndexedDBAdapter, type PersistenceError } from '@gentleduck/upload/core'
145
-
146
- createUploadStore({
147
- persistence: { key: 'app:uploads', version: 1, adapter: createIndexedDBAdapter(), isPurpose, isIntent },
148
- hooks: {
149
- onPersistenceError(err: PersistenceError) {
150
- switch (err.code) {
151
- case 'quota_exceeded':
152
- toast.error('Storage full -- drop some uploads and try again.')
153
- break
154
- case 'unavailable':
155
- // Private mode / SecurityError / no IndexedDB. Fall back to memory.
156
- telemetry.warn('upload.persistence.unavailable', { op: err.operation })
157
- break
158
- case 'serialization_failed':
159
- // Snapshot was tampered with; drop the bad key and start over.
160
- adapter.clear('app:uploads').catch(() => undefined)
161
- break
162
- case 'transaction_failed':
163
- case 'unknown':
164
- telemetry.error('upload.persistence.error', { op: err.operation, message: err.message })
165
- break
166
- }
167
- },
168
- },
169
- ...
170
- })
171
- ```
172
-
173
- The engine never aborts uploads on a persistence error -- it continues operating in-memory. Use the hook to surface the failure to the user or to swap adapters at runtime.
174
-
175
- The hook may be sync or `async`; the engine tracks the returned promise and keeps the re-entrancy guard active until it settles, so dispatches issued inside an async hook cannot loop the failure path.
176
-
177
- ## Effect concurrency and watchdog
178
-
179
- Side-effects (intent creation, finalize, checksum, retry sleep, multipart abort) run in a bounded worker pool:
180
-
181
- ```ts
182
- createUploadStore({
183
- config: {
184
- effectConcurrency: 8, // default: 8. Set to 1 for strict-sequential.
185
- effectTimeoutMs: 60_000, // default: 60s per effect. 0 disables the watchdog.
186
- },
187
- ...
188
- })
189
- ```
190
-
191
- Every effect receives an `AbortSignal`. When the watchdog fires the signal is aborted so well-behaved effects can exit cleanly. Effects that ignore the signal keep running in the background; the slot is released either way so the pool keeps draining. Retry-sleep effects that get cut short by the watchdog still dispatch the retry as long as the item is still in `error` -- the watchdog cannot strand an item permanently.
212
+ Adapters: `IndexedDBAdapter` and `LocalStorageAdapter` (shared singletons), `MemoryAdapter`
213
+ (shared) or `createMemoryAdapter()` (isolated per store). Bump `persistence.version` in lockstep
214
+ across tabs/deployments; a snapshot at a different version is rejected by `deserializeSnapshot`'s
215
+ version fence rather than loaded.
192
216
 
193
217
  ## Writing a custom strategy
194
218
 
195
- A strategy is the thin layer that knows how to actually move bytes from a `File` to your storage using the intent shape your backend returns. Every strategy implements one `start()` and shares the same `Strategy.ICtx` contract:
219
+ A strategy implements one `start()` against the `Contracts.Strategy.Me` contract. `id` must equal
220
+ the intent's `strategy` field; `resumable` is metadata for your UI. Honor `signal`, call
221
+ `reportProgress`, and — if resumable — `persistCursor` at each safe boundary.
196
222
 
197
223
  ```ts
198
- import type { Strategy } from '@gentleduck/upload/core'
224
+ import type { Contracts } from '@gentleduck/upload/core'
199
225
 
200
- type Intents = { tus: { strategy: 'tus'; fileId: string; uploadUrl: string } }
201
- type Cursors = { tus: { offset: number } }
226
+ type Intents = { myput: { strategy: 'myput'; fileId: string; url: string } }
227
+ type Cursors = { myput?: Record<string, never> }
202
228
  type Purpose = 'attachment'
203
229
  type Result = { fileId: string; key: string }
204
230
 
205
- export const tusStrategy: Strategy.IStrategy<Intents, Cursors, Purpose, Result, 'tus'> = {
206
- id: 'tus',
207
- resumable: true,
208
- async start({ file, intent, signal, transport, readCursor, persistCursor, reportProgress }) {
209
- // 1. Resume from the last persisted cursor if there is one.
210
- let offset = readCursor()?.offset ?? 0
211
-
212
- while (offset < file.size) {
213
- if (signal.aborted) throw { code: 'aborted', reason: signal.reason }
214
-
215
- const chunk = file.slice(offset, offset + 1024 * 1024)
216
- const res = await transport.send({
217
- url: intent.uploadUrl,
218
- method: 'PATCH',
219
- headers: {
220
- 'Upload-Offset': String(offset),
221
- 'Content-Type': 'application/offset+octet-stream',
222
- },
223
- body: chunk,
224
- signal,
225
- onProgress: ({ uploadedBytes }) => {
226
- reportProgress({ uploadedBytes: offset + uploadedBytes, totalBytes: file.size })
227
- },
228
- })
229
-
230
- offset += chunk.size
231
-
232
- // 2. Persist resumable progress so a refresh can rebind + resume.
233
- persistCursor({ offset })
234
-
235
- if (!res.ok) throw new Error(`TUS PATCH failed: ${res.status}`)
236
- }
231
+ const myPut: Contracts.Strategy.Me<Intents, Cursors, Purpose, Result, 'myput'> = {
232
+ id: 'myput',
233
+ resumable: false,
234
+ async start(ctx) {
235
+ await ctx.transport.put({
236
+ url: ctx.intent.url,
237
+ body: ctx.file,
238
+ signal: ctx.signal,
239
+ onProgress: (uploadedBytes, totalBytes) => ctx.reportProgress({ uploadedBytes, totalBytes }),
240
+ })
237
241
  },
238
242
  }
239
- ```
240
-
241
- Contract:
242
243
 
243
- - **`id`** must match the `intent.strategy` your backend returns.
244
- - **`resumable`** is metadata for UIs; the engine doesn't enforce it.
245
- - **`start()`** is awaited; resolve normally → engine transitions to `completing`. Throw → engine routes the error through `errorNormalizer` + `retryPolicy`.
246
- - Honor **`signal`** -- abort cleanly when it fires (throw `{ code: 'aborted', reason: signal.reason }`).
247
- - Call **`reportProgress`** as often as makes sense; it is throttled by `config.progressThrottleMs`.
248
- - Call **`persistCursor`** at every safe resume boundary; the value is restored on the next `rebind` after refresh.
249
- - Use **`api`** for backend round-trips that depend on the intent (e.g. multipart `signPart` / `completeMultipart`).
250
-
251
- Register the strategy and reference it from `createIntent`:
252
-
253
- ```ts
254
- const strategies = createStrategyRegistry<Intents, Cursors, Purpose, Result>()
255
- strategies.set(tusStrategy)
244
+ strategies.set(myPut)
256
245
  ```
257
246
 
258
- ## Architecture
259
-
260
- ```
261
- @gentleduck/upload
262
- ├── core/
263
- │ ├── client/ - Client.IUploadConfig, Client.IUploadHooks, Client.IUploadPlugin
264
- │ ├── contracts/ - intents, cursors, transport, errors, strategy
265
- │ ├── engine/ - reducer + commands + internal events
266
- │ │ └── store/ - runtime, dispatch, scheduler, handlers
267
- │ ├── persistence/ - Memory / LocalStorage / IndexedDB adapters + serializer
268
- │ └── utils/ - emitter, id, fingerprint, guards, async
269
- ├── strategies/
270
- │ ├── post/ - single-PUT/POST strategy
271
- │ ├── multipart/ - S3-style multipart with partSize + ETag tracking
272
- │ └── registry/ - createStrategyRegistry
273
- └── react/ - UploadProvider, useUploader, createUploadFactory
274
- ```
247
+ `start()` is awaited: resolve → the engine transitions to `completing`; throw → the error routes
248
+ through `errorNormalizer` + `retryPolicy`. The `ctx` also carries `intent`, `api`, `readCursor`,
249
+ `persistCursor`, `attempt`, and the injected `transport` (`put` / `postForm` / `patch` and the
250
+ optional `get` / `head`). Use `api.multipart.signPart` / `completeMultipart` for backend
251
+ round-trips a strategy needs mid-flight.
275
252
 
276
253
  ## Commands
277
254
 
255
+ Dispatch via `store.dispatch(cmd)`.
256
+
278
257
  | Command | Purpose |
279
- |---|---|
258
+ | --- | --- |
280
259
  | `addFiles` | Validate + insert; schedules checksum + intent |
281
260
  | `start` / `startAll` | Move ready items to queued (batched) |
282
261
  | `pause` / `pauseAll` | Abort inflight, persist cursor (batched) |
283
262
  | `resume` | Re-queue a paused item |
284
- | `cancel` / `cancelAll` | Abort everything and mark canceled (batched) |
263
+ | `cancel` / `cancelAll` | Abort and mark canceled (batched) |
285
264
  | `retry` | Re-attempt the failed phase, bumping the attempt counter |
286
- | `rebind` | Re-attach a fresh `File` after refresh (validated by fingerprint) |
265
+ | `rebind` | Re-attach a fresh `File` after refresh (fingerprint-validated) |
287
266
  | `remove` | Drop the item from state |
288
267
 
289
268
  ## Events
290
269
 
291
- Subscribe via `store.on(type, cb)`. Highlights:
270
+ Subscribe via `store.on(type, cb)`; payloads are typed from the engine's event map.
292
271
 
293
272
  - `file.added`, `file.rejected`
294
273
  - `validation.ok`, `validation.failed`
295
274
  - `intent.creating`, `intent.created`, `intent.failed`
296
275
  - `upload.queued`, `upload.started`, `upload.progress`, `upload.cursor`
297
- - `upload.paused`, `upload.canceled`, `upload.completing`, `upload.completed`, `upload.error`
298
- - `rebind.ok`, `rebind.failed`
276
+ - `upload.paused`, `upload.resumed`, `upload.canceled`
277
+ - `upload.completing`, `upload.completed`, `upload.error`
299
278
 
300
279
  ## Bundler requirement
301
280
 
302
- The engine references `process.env.NODE_ENV` to gate dev-only invariant
303
- warnings (reducer no-op check, listener-throw fallback logging, strategy-
304
- overwrite warn). Vite / Webpack / Rspack / esbuild / Rollup replace this at
305
- build time and ship a single branch into production. Pure-ESM consumers that
306
- load the source directly without a bundler must polyfill
307
- `globalThis.process = { env: { NODE_ENV: 'production' } }` before importing
308
- the package, otherwise `process` is undefined at module load.
281
+ The engine references `process.env.NODE_ENV` to gate dev-only invariant warnings. Vite / Webpack /
282
+ Rspack / esbuild / Rollup replace this at build time and ship a single branch to production.
283
+ Pure-ESM consumers loading the source without a bundler must set
284
+ `globalThis.process = { env: { NODE_ENV: 'production' } }` before importing the package.
309
285
 
310
286
  ## Tests
311
287
 
@@ -313,7 +289,8 @@ the package, otherwise `process` is undefined at module load.
313
289
  bun run test
314
290
  ```
315
291
 
316
- Vitest suite covers the reducer state machine, the persistence layer, all utility modules, the rebind handler, and a full smoke upload through the React-less store API.
292
+ The Vitest suite covers the reducer state machine, persistence, every strategy and transport, the
293
+ incremental hasher, the SSRF/filename/MIME guards, and the React helpers.
317
294
 
318
295
  ## License
319
296
 
@@ -1,3 +1,2 @@
1
- import { a as AnyCursor, c as WithSignal, d as IntentBase, f as IntentMap, i as UploadTransport, l as UploadResultBase, n as StrategyRegistry, o as CursorMap, p as StrategyKey, r as UploadStrategy, s as UploadApi, t as StrategyCtx, u as AnyIntent } from "../strategy.types-gg9ZStRe.js";
2
- import { $ as RejectReason, A as serializeSnapshot, B as UploadItem, C as StoreOptions, D as createTypedEmitter, E as UploadEventMap, F as PersistedUploadItem, G as UploadCommand, H as UploadProgress, I as PersistenceAdapter, J as resolveUploadConfig, K as UploadConfig, L as UploadState, M as LocalStorageAdapter, N as IndexedDBAdapter, O as TypedEmitter, P as PersistedSnapshot, Q as BuiltInUploadError, R as createReducer, S as PersistenceOptions, T as UploadStore, U as UploadCompletionKind, V as UploadPhase, W as UploadOutcome, X as createXHRTransport, Y as RetryDecision, Z as FileFingerprint, _ as UploadHooks, a as DEFAULT_MAX_ATTEMPTS, b as DeserializeContext, c as DEFAULT_PROGRESS_THROTTLE_MS, d as sleep, et as UploadError, f as validateFile, g as createUploadClient, h as UploadClient, i as fingerprintMatches, j as MemoryAdapter, k as deserializeSnapshot, l as DEFAULT_RETRY_DELAY_BASE_MS, m as validateIntent, n as isRecord, nt as UploadValidationRules, o as DEFAULT_MAX_CONCURRENT_UPLOADS, p as validateFileList, q as UploadConfigInput, r as computeFingerprint, s as DEFAULT_MAX_ITEMS, t as generateId, tt as UploadErrorBase, u as DEFAULT_RETRY_DELAY_MAX_MS, v as UploadPlugin, w as StoreRuntime, x as InflightUpload, y as createUploadStore, z as InternalEvent } from "../index-C38Jmizu.js";
3
- export { AnyCursor, AnyIntent, BuiltInUploadError, CursorMap, DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_CONCURRENT_UPLOADS, DEFAULT_MAX_ITEMS, DEFAULT_PROGRESS_THROTTLE_MS, DEFAULT_RETRY_DELAY_BASE_MS, DEFAULT_RETRY_DELAY_MAX_MS, DeserializeContext, FileFingerprint, IndexedDBAdapter, InflightUpload, IntentBase, IntentMap, InternalEvent, LocalStorageAdapter, MemoryAdapter, PersistedSnapshot, PersistedUploadItem, PersistenceAdapter, PersistenceOptions, RejectReason, RetryDecision, StoreOptions, StoreRuntime, StrategyCtx, StrategyKey, StrategyRegistry, TypedEmitter, UploadApi, UploadClient, UploadCommand, UploadCompletionKind, UploadConfig, UploadConfigInput, UploadError, UploadErrorBase, UploadEventMap, UploadHooks, UploadItem, UploadOutcome, UploadPhase, UploadPlugin, UploadProgress, UploadResultBase, UploadState, UploadStore, UploadStrategy, UploadTransport, UploadValidationRules, WithSignal, computeFingerprint, createReducer, createTypedEmitter, createUploadClient, createUploadStore, createXHRTransport, deserializeSnapshot, fingerprintMatches, generateId, isRecord, resolveUploadConfig, serializeSnapshot, sleep, validateFile, validateFileList, validateIntent };
1
+ import { C as UploadUnknownError, D as Transport, E as createXHRTransport, O as Contracts, S as UploadTimeoutError, T as createFetchTransport, _ as UploadHttpError, a as deserializeSnapshot, b as UploadServerError, c as createMemoryAdapter, d as UploadPersistence, f as UploadAbortError, g as UploadErrorContext, h as UploadError, i as Store, l as LocalStorageAdapter, m as UploadEngineError, n as Engine, o as serializeSnapshot, p as UploadAuthError, r as createUploadStore, s as MemoryAdapter, t as resolveUploadConfig, u as IndexedDBAdapter, v as UploadNetworkError, w as UploadValidationError, x as UploadStrategyMissingError, y as UploadRateLimitError } from "../index-DCqyBVcx.js";
2
+ export { Contracts, Engine, IndexedDBAdapter, LocalStorageAdapter, MemoryAdapter, Store, Transport, UploadAbortError, UploadAuthError, UploadEngineError, UploadError, UploadErrorContext, UploadHttpError, UploadNetworkError, UploadPersistence, UploadRateLimitError, UploadServerError, UploadStrategyMissingError, UploadTimeoutError, UploadUnknownError, UploadValidationError, createFetchTransport, createMemoryAdapter, createUploadStore, createXHRTransport, deserializeSnapshot, resolveUploadConfig, serializeSnapshot };
@@ -1 +1 @@
1
- import{C as e,S as t,_ as n,a as r,b as i,c as a,d as o,f as s,g as c,h as l,i as u,l as d,m as f,n as p,o as m,p as h,r as g,s as _,t as v,u as y,v as b,w as x,x as S,y as C}from"../core-CMkrP9DX.js";import{t as w}from"../async-DxRWu4le.js";export{C as DEFAULT_MAX_ATTEMPTS,i as DEFAULT_MAX_CONCURRENT_UPLOADS,S as DEFAULT_MAX_ITEMS,t as DEFAULT_PROGRESS_THROTTLE_MS,e as DEFAULT_RETRY_DELAY_BASE_MS,x as DEFAULT_RETRY_DELAY_MAX_MS,d as IndexedDBAdapter,a as LocalStorageAdapter,_ as MemoryAdapter,o as computeFingerprint,g as createReducer,u as createTypedEmitter,v as createUploadClient,p as createUploadStore,y as createXHRTransport,r as deserializeSnapshot,s as fingerprintMatches,n as generateId,c as isRecord,b as resolveUploadConfig,m as serializeSnapshot,w as sleep,h as validateFile,f as validateFileList,l as validateIntent};
1
+ import{C as e,S as t,_ as n,a as r,b as i,c as a,f as o,g as s,h as c,i as l,l as u,m as d,n as f,o as p,p as m,r as h,s as g,t as _,u as v,v as y,x as b,y as x}from"../core-BVgloTiP.js";export{_ as Engine,a as IndexedDBAdapter,g as LocalStorageAdapter,r as MemoryAdapter,o as UploadAbortError,m as UploadAuthError,d as UploadEngineError,c as UploadHttpError,s as UploadNetworkError,n as UploadRateLimitError,y as UploadServerError,x as UploadStrategyMissingError,i as UploadTimeoutError,b as UploadUnknownError,t as UploadValidationError,u as createFetchTransport,p as createMemoryAdapter,f as createUploadStore,v as createXHRTransport,h as deserializeSnapshot,e as resolveUploadConfig,l as serializeSnapshot};
@@ -0,0 +1,10 @@
1
+ function e(e,t,n=1){return typeof e!=`number`||!Number.isFinite(e)?t:Math.max(n,e)}function t(t){return{maxConcurrentUploads:e(t?.maxConcurrentUploads,3),autoStart:t?.autoStart,progressThrottleMs:e(t?.progressThrottleMs,100,0),validation:t?.validation??{},maxAttempts:e(t?.maxAttempts,3),retryPolicy:t?.retryPolicy,maxItems:e(t?.maxItems,100),completedItemTTL:t?.completedItemTTL,strictMimeMatch:t?.strictMimeMatch??!1,checksumMaxSize:t?.checksumMaxSize??null}}const n=new Set([`__proto__`,`constructor`,`prototype`]);function r(e){if(typeof e!=`object`||!e)return!1;let t=Object.getPrototypeOf(e);return t===null||t===Object.prototype}function i(){return typeof window<`u`&&typeof process<`u`&&process.env?.NODE_ENV===`development`}function a(e){if(typeof e!=`object`||!e)return e;let t=[{value:e,depth:0}],r=new WeakSet;for(;t.length>0;){let e=t.pop();if(!e)break;let{value:i,depth:a}=e;if(r.has(i)||(r.add(i),a>=16))continue;if(Array.isArray(i)){for(let e of i)typeof e==`object`&&e&&t.push({value:e,depth:a+1});continue}let o=Object.getPrototypeOf(i);if(o!==null&&o!==Object.prototype)continue;let s=i;for(let e of Object.keys(s)){if(n.has(e)){delete s[e];continue}let r=s[e];typeof r==`object`&&r&&t.push({value:r,depth:a+1})}}return e}function o(){return`upload_${Date.now()}_${s()}`}function s(){let e=globalThis.crypto;if(e&&typeof e.randomUUID==`function`)return e.randomUUID();if(e&&typeof e.getRandomValues==`function`){let t=new Uint8Array(16);e.getRandomValues(t);let n=t[6]??0,r=t[8]??0;t[6]=n&15|64,t[8]=r&63|128;let i=[];for(let e=0;e<t.length;e++)i.push((t[e]??0).toString(16).padStart(2,`0`));return`${i.slice(0,4).join(``)}-${i.slice(4,6).join(``)}-${i.slice(6,8).join(``)}-${i.slice(8,10).join(``)}-${i.slice(10,16).join(``)}`}throw Error(`[duck-upload] No CSPRNG available: crypto.randomUUID and crypto.getRandomValues both missing`)}function c(e,t,n){let r=n.validation?.[t];if(!r)return null;if(e.size===0)return{code:`empty_file`};if(r.maxSizeBytes!==void 0&&e.size>r.maxSizeBytes)return{code:`file_too_large`,maxBytes:r.maxSizeBytes,size:e.size};if(r.minSizeBytes!==void 0&&e.size<r.minSizeBytes)return{code:`empty_file`};if(r.allowedTypes||r.allowedExtensions){let t=r.allowedTypes||[],n=r.allowedExtensions||[],i=e.name.split(`.`).pop()?.toLowerCase(),a=(e.type||``).toLowerCase(),o=t.length===0?!1:t.some(e=>{if(e.endsWith(`/*`)){let t=e.slice(0,-2).toLowerCase();return a.startsWith(`${t}/`)}return a===e.toLowerCase()}),s=i&&n.length>0?n.some(e=>e.toLowerCase()===i):!1,c=t.length>0,l=n.length>0;if(!(c&&l?o||s:(!c||o)&&(!l||s)))return{code:`type_not_allowed`,allowed:[...t,...n],got:e.type||e.name}}return null}function l(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}const u={intent_failed:`upload intent request failed`,upload_failed:`upload transfer failed`,complete_failed:`upload finalize failed`,validation_failed:`file validation failed`,strategy_missing:`upload strategy not found`,aborted:`upload aborted`,network:`network error occurred`,http:`http request failed`,timeout:`request timed out`,auth:`authentication failed`,rate_limit:`rate limit exceeded`,server:`server error occurred`,unknown:`unknown upload error`};var d=class extends Error{constructor(e,t){let n=t?.message??u[e]??`upload error (${e})`;super(n),l(this,`code`,void 0),l(this,`context`,void 0),l(this,`cause`,void 0),l(this,`retryable`,void 0),this.name=`UploadEngineError`,this.code=e,this.retryable=t?.retryable??!1,t?.context&&(this.context=t.context),t?.cause!==void 0&&(this.cause=t.cause)}},f=class extends d{constructor(e,t,n){super(`validation_failed`,{message:t??`File validation failed: ${e.code}`,retryable:!1,cause:n?.cause,context:n?.context}),l(this,`reason`,void 0),this.name=`UploadValidationError`,this.reason=e}},p=class extends d{constructor(e,t,n){super(`strategy_missing`,{message:t??`Upload strategy "${e}" is not registered`,retryable:!1,cause:n?.cause,context:n?.context}),l(this,`strategy`,void 0),this.name=`UploadStrategyMissingError`,this.strategy=e}},m=class extends d{constructor(e,t,n){super(`aborted`,{message:t??`Upload aborted`,retryable:!1,cause:n?.cause,context:n?.context}),l(this,`reason`,void 0),this.name=`UploadAbortError`,this.reason=e}},h=class extends d{constructor(e,t){super(`network`,{message:e??`Network error occurred`,retryable:!0,cause:t?.cause,context:t?.context}),this.name=`UploadNetworkError`}},g=class extends d{constructor(e,t,n){let r=n?.retryable??(e>=500||e===429);super(`http`,{message:t??`HTTP request failed with status ${e}`,retryable:r,cause:n?.cause,context:n?.context}),l(this,`status`,void 0),l(this,`statusText`,void 0),this.name=`UploadHttpError`,this.status=e,n?.statusText&&(this.statusText=n.statusText)}},_=class extends d{constructor(e,t){super(`timeout`,{message:e??`Request timed out`,retryable:!0,cause:t?.cause,context:t?.context}),this.name=`UploadTimeoutError`}},v=class extends d{constructor(e,t){super(`auth`,{message:e??`Authentication failed`,retryable:!1,cause:t?.cause,context:t?.context}),this.name=`UploadAuthError`}},y=class extends d{constructor(e,t){super(`rate_limit`,{message:e??`Rate limit exceeded`,retryable:!0,cause:t?.cause,context:t?.context}),l(this,`retryAfterMs`,void 0),this.name=`UploadRateLimitError`,t?.retryAfterMs!==void 0&&(this.retryAfterMs=t.retryAfterMs)}},b=class extends d{constructor(e,t){super(`server`,{message:e??`Server error occurred`,retryable:!0,cause:t?.cause,context:t?.context}),l(this,`serverCode`,void 0),this.name=`UploadServerError`,t?.serverCode&&(this.serverCode=t.serverCode)}},x=class extends d{constructor(e,t){super(`unknown`,{message:e??`Unknown upload error`,retryable:!1,cause:t?.cause,context:t?.context}),this.name=`UploadUnknownError`}};function S(e){return typeof e==`number`&&Number.isFinite(e)&&e>0}function C(e,t){if(!r(e))return new d(`validation_failed`,{message:`Invalid intent: must be an object`});if(typeof e.strategy!=`string`)return new d(`validation_failed`,{message:`Invalid intent: missing or invalid strategy field`});if(e.strategy!==t)return new d(`validation_failed`,{message:`Invalid intent: strategy mismatch (expected ${t}, got ${e.strategy})`});if(typeof e.fileId!=`string`||!e.fileId)return new d(`validation_failed`,{message:`Invalid intent: missing or invalid fileId`});if(t===`post`){if(typeof e.url!=`string`||!e.url)return new d(`validation_failed`,{message:`Invalid post intent: missing or invalid url`});try{let t=new URL(e.url);if(![`http:`,`https:`].includes(t.protocol))return new d(`validation_failed`,{message:`Invalid post intent: url must use http or https protocol`})}catch{return new d(`validation_failed`,{message:`Invalid post intent: url is not a valid URL`})}if(!e.fields||typeof e.fields!=`object`)return new d(`validation_failed`,{message:`Invalid post intent: missing or invalid fields`})}else if(t===`multipart`){if(typeof e.uploadId!=`string`||!e.uploadId)return new d(`validation_failed`,{message:`Invalid multipart intent: missing or invalid uploadId`});if(!S(e.partSize))return new d(`validation_failed`,{message:`Invalid multipart intent: missing or invalid partSize`});if(`parts`in e&&e.parts!==void 0&&!Array.isArray(e.parts))return new d(`validation_failed`,{message:`Invalid multipart intent: parts must be an array if provided`})}return null}function w(e,t,n,r=0){let i=n.validation?.[t],a=[],o=[],s=i?.maxFiles,l=s===void 0?1/0:Math.max(0,s-r);for(let r of e){if(l<=0){let e=s??0;o.push({file:r,reason:{code:`too_many_files`,max:e}});continue}let e=c(r,t,n);if(e){o.push({file:r,reason:e});continue}a.push(r),--l}return{valid:a,rejected:o}}function ee(e){let t=e.length;return t>=3&&e[0]===255&&e[1]===216&&e[2]===255?`image/jpeg`:t>=8&&e[0]===137&&e[1]===80&&e[2]===78&&e[3]===71&&e[4]===13&&e[5]===10&&e[6]===26&&e[7]===10?`image/png`:t>=4&&e[0]===71&&e[1]===73&&e[2]===70&&e[3]===56?`image/gif`:t>=4&&e[0]===37&&e[1]===80&&e[2]===68&&e[3]===70?`application/pdf`:t>=12&&e[0]===82&&e[1]===73&&e[2]===70&&e[3]===70&&e[8]===87&&e[9]===69&&e[10]===66&&e[11]===80?`image/webp`:t>=8&&e[4]===102&&e[5]===116&&e[6]===121&&e[7]===112?`video/mp4`:t>=4&&e[0]===26&&e[1]===69&&e[2]===223&&e[3]===163?`video/webm`:t>=4&&e[0]===80&&e[1]===75&&e[2]===3&&e[3]===4?`application/zip`:t>=3&&e[0]===73&&e[1]===68&&e[2]===51||t>=2&&e[0]===255&&(e[1]===251||e[1]===243||e[1]===242)?`audio/mpeg`:null}function te(e,t){if(!t)return!0;let n=e.toLowerCase().trim(),r=t.toLowerCase().trim();return!!(n===r||n===``||n===`application/octet-stream`||r===`application/zip`&&(n.startsWith(`application/vnd.openxmlformats-officedocument.`)||n.startsWith(`application/vnd.oasis.opendocument.`)||n===`application/epub+zip`||n===`application/java-archive`||n===`application/vnd.android.package-archive`)||r===`image/jpeg`&&(n===`image/jpg`||n===`image/pjpeg`))}const T=new Set;async function ne(e,t){if(e.size===0)return null;let n=await e.slice(0,16).arrayBuffer(),r=ee(new Uint8Array(n));if(r===null)return null;let i=e.type||``;if(te(i,r))return null;if(t)return{code:`mime_mismatch`,claimed:i||`<empty>`,sniffed:r};let a=`${i||`<empty>`}::${r}`;return T.has(a)||(T.add(a),console.warn(`[duck-upload] MIME mismatch: claimed "${i||`<empty>`}" but bytes look like "${r}". Set \`strictMimeMatch: true\` to reject these files.`)),null}function E(e){return{name:e.name,size:e.size,type:re(e.type),lastModified:e.lastModified}}function re(e){let t=e.indexOf(`;`);return t===-1?e.trim():e.slice(0,t).trim()}function ie(e,t){return e.name===t.name&&e.size===t.size&&e.lastModified===t.lastModified}async function D(e,t){let n=t?.chunkSize&&t.chunkSize>0?t.chunkSize:8*1024*1024,r=t?.signal,i=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),a=1779033703,o=3144134277,s=1013904242,c=2773480762,l=1359893119,u=2600822924,d=528734635,f=1541459225,p=new Uint32Array(64),m=new Uint8Array(64),h=0,g=0,_=(e,t)=>e>>>t|e<<32-t,v=()=>{for(let e=0;e<16;e++){let t=e*4;p[e]=(m[t]<<24|m[t+1]<<16|m[t+2]<<8|m[t+3])>>>0}for(let e=16;e<64;e++){let t=p[e-15],n=p[e-2],r=_(t,7)^_(t,18)^t>>>3,i=_(n,17)^_(n,19)^n>>>10;p[e]=p[e-16]+r+p[e-7]+i>>>0}let e=a,t=o,n=s,r=c,h=l,g=u,v=d,y=f;for(let a=0;a<64;a++){let o=_(h,6)^_(h,11)^_(h,25),s=h&g^~h&v,c=y+o+s+i[a]+p[a]>>>0,l=(_(e,2)^_(e,13)^_(e,22))+(e&t^e&n^t&n)>>>0;y=v,v=g,g=h,h=r+c>>>0,r=n,n=t,t=e,e=c+l>>>0}a=a+e>>>0,o=o+t>>>0,s=s+n>>>0,c=c+r>>>0,l=l+h>>>0,u=u+g>>>0,d=d+v>>>0,f=f+y>>>0},y=e=>{g+=e.length;let t=0;if(h>0){let n=Math.min(64-h,e.length);m.set(e.subarray(0,n),h),h+=n,t=n,h===64&&(v(),h=0)}for(;e.length-t>=64;)m.set(e.subarray(t,t+64),0),v(),t+=64;if(t<e.length){let n=e.subarray(t);m.set(n,0),h=n.length}},b=e.size;for(let t=0;t<b;t+=n){if(r?.aborted)throw new DOMException(`The operation was aborted.`,`AbortError`);let i=Math.min(b,t+n),a=await e.slice(t,i).arrayBuffer();y(new Uint8Array(a))}let x=g*8,S=Math.floor(x/4294967296)>>>0,C=x>>>0;if(m[h++]=128,h>56){for(;h<64;)m[h++]=0;v(),h=0}for(;h<56;)m[h++]=0;m[56]=S>>>24&255,m[57]=S>>>16&255,m[58]=S>>>8&255,m[59]=S&255,m[60]=C>>>24&255,m[61]=C>>>16&255,m[62]=C>>>8&255,m[63]=C&255,v();let w=e=>(e>>>0).toString(16).padStart(8,`0`);return w(a)+w(o)+w(s)+w(c)+w(l)+w(u)+w(d)+w(f)}async function ae(e){let t=await e.arrayBuffer(),n=await crypto.subtle.digest(`SHA-256`,t),r=new Uint8Array(n),i=``;for(let e of r)i+=e.toString(16).padStart(2,`0`);return i}function oe(){return typeof Worker<`u`&&typeof Blob<`u`&&typeof URL<`u`&&typeof URL.createObjectURL==`function`}let O=null;function se(){if(O)return O;let e=`const sha256HexOfBlob = ${D.toString()};
2
+ self.onmessage = async (e) => {
3
+ try {
4
+ const hex = await sha256HexOfBlob(e.data.blob, { chunkSize: e.data.chunkSize });
5
+ self.postMessage({ ok: true, hex });
6
+ } catch (err) {
7
+ self.postMessage({ ok: false, error: String((err && err.message) || err) });
8
+ }
9
+ };`,t=new Blob([e],{type:`application/javascript`});return O=URL.createObjectURL(t),O}function ce(e,t){return new Promise((n,r)=>{let i;try{i=new Worker(se())}catch(e){r(e instanceof Error?e:Error(String(e)));return}let a=e=>{i.onmessage=null,i.onerror=null,i.terminate(),e()};i.onmessage=e=>{let t=e.data;t.ok&&typeof t.hex==`string`?a(()=>n(t.hex)):a(()=>r(Error(t.error??`hash worker failed`)))},i.onerror=e=>{a(()=>r(Error(e.message||`hash worker error`)))},i.postMessage({blob:e,chunkSize:t})})}async function le(e,t){let n=typeof t?.inlineMaxSize==`number`&&Number.isFinite(t.inlineMaxSize)&&t.inlineMaxSize>0?t.inlineMaxSize:8388608,r=t?.chunkSize??8388608;if(e.size<=n&&typeof crypto<`u`&&crypto.subtle)return ae(e);if(oe()&&!t?.signal)try{return await ce(e,r)}catch{}return t?.signal?D(e,{chunkSize:r,signal:t.signal}):D(e,{chunkSize:r})}function k(e){let t=Number.isFinite(e)&&e>0?Math.min(e,864e5):0;return new Promise(e=>setTimeout(e,t))}function A(e,t={}){let n=[...e.steps].reverse().find(e=>e.error!==void 0);return{localId:e.localId,purpose:e.purpose,fingerprint:e.fingerprint,attempt:e.attempt??1,createdAt:e.createdAt,steps:e.steps,meta:e.meta,lastError:n?.error,file:`file`in e?e.file:void 0,intent:`intent`in e?e.intent:void 0,...t}}async function ue(e,t=null){return le(e,{inlineMaxSize:typeof t==`number`&&Number.isFinite(t)&&t>0?t:67108864})}function de(e,t){let n=e.config?.autoStart;if(n===void 0)return!1;if(Array.isArray(n))return n.includes(t);if(typeof n==`function`)try{return n(t)===!0}catch{return!1}return!1}async function j(e,t,n,r,a){if(!n||!e.opts.api.findByChecksum)return!1;let o=e.state.items.get(t);if(!o||!a.includes(o.phase)||!o.file)return!1;try{let i=await e.opts.api.findByChecksum({checksum:n,purpose:r},{...A(o),file:o.file});if(!i)return!1;let s=e.state.items.get(t);return!s||!a.includes(s.phase)?!1:(e.applyInternal({type:`dedupe.ok`,localId:t,result:i}),!0)}catch(e){return i()&&console.warn(`[UploadEngine] Failed to check for existing file:`,e),!1}}function fe(e){if(!r(e))return null;let t=e.strategy;if(!(e.dedupe===!0||e.existing===!0||e.skipUpload===!0||t===`dedupe`))return null;let n=typeof e.fileId==`string`?e.fileId:typeof e.id==`string`?e.id:null,i=typeof e.key==`string`?e.key:null;return!n||!i?null:{fileId:n,key:i}}function M(e,t){if(t)return t(e);if(e instanceof d)return e;if(he(e))return new m(e.reason===`pause`||e.reason===`cancel`?e.reason:`unknown`,void 0,{cause:e});let n=r(e)&&typeof e.message==`string`?e.message:`Unknown error`;return n.toLowerCase().includes(`network`)||n.toLowerCase().includes(`fetch`)?new h(n,{cause:e}):r(e)&&(typeof e.status==`number`||typeof e.statusCode==`number`)?new g(typeof e.status==`number`?e.status:e.statusCode,n,{cause:e,statusText:typeof e.statusText==`string`?e.statusText:void 0}):new x(n,{cause:e})}function N(e){return`intent`in e&&!!e.intent}function pe(e){return`file`in e&&!!e.file}function P(e){return`cursor`in e}function me(e){return r(e)&&e.strategy===`multipart`&&typeof e.fileId==`string`&&typeof e.uploadId==`string`}function he(e){return r(e)&&e.code===`aborted`}function F(e,t){if(e.retryPolicy)return e.retryPolicy(t);if(t.error.code===`auth`||t.error.code===`validation_failed`||t.error.code===`strategy_missing`||t.error.code===`aborted`)return{retryable:!1};let n=e.maxAttempts;return t.attempt<n?{retryable:!0,delayMs:Math.min(1e4,500*2**(t.attempt-1))}:{retryable:!1}}function ge(e,t,n,r={}){let a=Array.from(e.state.items.values()).filter(e=>e.purpose===n&&e.phase!==`canceled`).length,{valid:s,rejected:l}=w(t,n,e.opts.config,a);for(let{file:t,reason:n}of l)e.emitter.emit(`file.rejected`,{reason:n,file:t});let u=Date.now(),d=[];for(let t of s){let a=o(),s=(e.opts.fingerprint??E)(t);d.push({localId:a,purpose:n,file:t,fingerprint:s,createdAt:u,meta:r}),e.enqueueEffect(async()=>{let r;try{r=await ue(t,e.opts.config.checksumMaxSize);let n={...s,checksum:r};e.applyInternal({type:`fingerprint.updated`,localId:a,fingerprint:n})}catch(e){i()&&console.warn(`[UploadEngine] Failed to calculate checksum:`,e)}if(await j(e,a,r,n,[`validating`]))return;let o=e.opts.validateFile?.(t,n);if(o){e.applyInternal({type:`validation.failed`,localId:a,reason:o});return}let l=c(t,n,e.opts.config);if(l){e.applyInternal({type:`validation.failed`,localId:a,reason:l});return}try{let n=await ne(t,e.opts.config.strictMimeMatch===!0);if(n){e.applyInternal({type:`validation.failed`,localId:a,reason:n});return}}catch(e){i()&&console.warn(`[UploadEngine] MIME sniff failed:`,e)}e.applyInternal({type:`validation.ok`,localId:a})})}d.length>0&&e.applyInternal({type:`files.added`,items:d})}function _e(e,t){let n=e.state.items.get(t),r=e.inflightIntents.get(t);r&&(r.abort({reason:`cancel`}),e.inflightIntents.delete(t));let i=e.inflightCompletes.get(t);i&&(i.abort({reason:`cancel`}),e.inflightCompletes.delete(t));let a=e.inflightUploads.get(t);if(a&&(a.mode=`cancel`,a.controller.abort({reason:`cancel`})),n&&N(n)&&n.intent.strategy===`multipart`&&e.opts.api.multipart?.abort){let t=n.intent;me(t)&&e.enqueueEffect(async()=>{try{let r=e.opts.api.multipart?.abort;r&&await r({fileId:t.fileId,uploadId:t.uploadId},A(n))}catch{}})}}function ve(e,t){let n=e.inflightUploads.get(t);if(n){n.mode=`pause`,n.controller.abort({reason:`pause`});return}}function ye(e,t){if(t.type===`startAll`){let n=t.purpose;Array.from(e.state.items.values()).filter(e=>e.phase===`ready`&&(!n||e.purpose===n)).forEach(t=>{e.dispatch({type:`start`,localId:t.localId})});return}if(t.type===`pauseAll`){let n=t.purpose;Array.from(e.state.items.values()).filter(e=>e.phase===`uploading`&&(!n||e.purpose===n)).forEach(t=>{e.dispatch({type:`pause`,localId:t.localId})});return}if(t.type===`cancelAll`){let n=t.purpose;Array.from(e.state.items.values()).filter(e=>e.phase!==`completed`&&e.phase!==`canceled`&&(!n||e.purpose===n)).forEach(t=>{e.dispatch({type:`cancel`,localId:t.localId})});return}if(t.type===`addFiles`){ge(e,t.files,t.purpose,t.meta??{});return}if(t.type===`pause`){ve(e,t.localId),e.applyCommand(t);return}if(t.type===`cancel`){_e(e,t.localId),e.applyCommand(t);return}if(t.type===`start`){e.applyCommand(t);return}if(t.type===`resume`){e.applyCommand(t);return}e.applyCommand(t)}function I(e){return e?.reason}function be(e){if(!e)return`unknown`;if(typeof e==`string`)return e===`pause`||e===`cancel`?e:`unknown`;if(r(e)&&`reason`in e){let t=e.reason;if(t===`pause`||t===`cancel`)return t}if(r(e)&&`kind`in e){let t=e.kind;if(t===`pause`||t===`cancel`)return t}return`unknown`}var xe=class extends m{constructor(e){let t=be(e);super(t)}};function L(e){return new xe(e)}function Se(e,t){return e.status===0?Error(`CORS error: The upload server does not allow requests from this origin. Please check CORS configuration.`):Error(`${t} (status: ${e.status})`)}function Ce(e){let t={};return e.getAllResponseHeaders().split(`\r
10
+ `).map(e=>e.trim()).filter(Boolean).forEach(e=>{let n=e.indexOf(`:`);if(n<=0)return;let r=e.slice(0,n).trim().toLowerCase(),i=e.slice(n+1).trim();r&&(t[r]=i)}),t}function we(e){return/^https?:\/\//i.test(e)}function Te(e){let t=e.getResponseHeader(`ETag`)??e.getResponseHeader(`etag`);if(t)return t.replace(/"/g,``)}function R(e){return new Promise((t,n)=>{if(!e.url||typeof e.url!=`string`){n(Error(`UploadTransport: missing url`));return}if(!we(e.url)){n(Error(`UploadTransport: expected absolute URL, got: ${String(e.url)}`));return}let r=new XMLHttpRequest,i=!1,a=e=>{i||(i=!0,s(),e())},o=()=>{try{r.abort()}finally{a(()=>n(L(I(e.signal))))}},s=()=>{e.signal&&e.signal.removeEventListener(`abort`,o),r.upload.onprogress=null,r.onload=null,r.onerror=null,r.onabort=null};if(e.signal?.aborted){a(()=>n(L(I(e.signal))));return}if(e.signal&&e.signal.addEventListener(`abort`,o,{once:!0}),e.onProgress&&(r.upload.onprogress=t=>{t.lengthComputable&&e.onProgress?.(t.loaded,t.total)}),r.onload=()=>{let e=r.status;a(e>=200&&e<300?()=>t({status:e,headers:Ce(r),etag:Te(r)}):()=>n(Error(`Upload failed with status ${e}`)))},r.onerror=()=>a(()=>n(Se(r,`Network error during upload`))),r.onabort=()=>a(()=>n(L(e.signal?.aborted?I(e.signal):`unknown`))),r.open(e.method,e.url),e.headers)for(let[t,n]of Object.entries(e.headers))r.setRequestHeader(t,n);r.send(e.body)})}function z(){return{async postForm(e){let t=new FormData;for(let[n,r]of Object.entries(e.fields))t.append(n,r);return t.append(`file`,e.file,e.filename??`file`),{headers:(await R({method:`POST`,url:e.url,body:t,signal:e.signal,onProgress:e.onProgress??(()=>{})})).headers}},async put(e){let t=await R({method:`PUT`,url:e.url,body:e.body,signal:e.signal,headers:e.headers??{},onProgress:e.onProgress??(()=>{})});return{headers:t.headers,etag:t.etag}},async patch(e){return{headers:(await R({method:`PATCH`,url:e.url,body:e.body,signal:e.signal??new AbortController().signal,headers:e.headers??{},onProgress:e.onProgress??(()=>{})})).headers}},async head(e){let t=await R({method:`HEAD`,url:e.url,body:null,signal:e.signal,headers:e.headers??{}});return{status:t.status,headers:t.headers}}}}function B(e){return/^https?:\/\//i.test(e)}function Ee(e){let t=e.get(`etag`)??e.get(`ETag`);if(t)return t.replace(/"/g,``)}function V(e){let t={};return e.forEach((e,n)=>{t[n.toLowerCase()]=e}),t}async function H(e){if(!e.url||!B(e.url))throw Error(`UploadTransport(fetch): expected absolute URL, got: ${String(e.url)}`);if(e.signal.aborted)throw L(I(e.signal));let t={method:e.method,body:e.body,signal:e.signal};e.headers&&(t.headers=e.headers);let n;try{n=await fetch(e.url,t)}catch(t){throw e.signal.aborted||t instanceof Error&&t.name===`AbortError`?L(I(e.signal)):t instanceof Error?t:Error(String(t))}if(n.status<200||n.status>=300)throw Error(`Upload failed with status ${n.status}`);return e.onProgress?.(e.byteLength,e.byteLength),{headers:V(n.headers),etag:Ee(n.headers)}}function De(){return{async postForm(e){let t=new FormData;for(let[n,r]of Object.entries(e.fields))t.append(n,r);t.append(`file`,e.file,e.filename??`file`);let n=await H({method:`POST`,url:e.url,body:t,signal:e.signal,byteLength:e.file.size,onProgress:e.onProgress});return n.etag===void 0?{headers:n.headers}:{headers:n.headers,etag:n.etag}},async put(e){let t=await H({method:`PUT`,url:e.url,body:e.body,signal:e.signal,headers:e.headers??{},byteLength:e.body.size,onProgress:e.onProgress});return{headers:t.headers,etag:t.etag}},async patch(e){let t=e.body instanceof Blob?e.body:new Blob([e.body]);return{headers:(await H({method:`PATCH`,url:e.url,body:t,signal:e.signal,headers:e.headers??{},byteLength:t.size,onProgress:e.onProgress})).headers}},async get(e){if(!e.url||!B(e.url))throw Error(`UploadTransport(fetch): expected absolute URL, got: ${String(e.url)}`);if(e.signal.aborted)throw L(I(e.signal));let t={method:`GET`,signal:e.signal};e.headers&&(t.headers=e.headers);let n;try{n=await fetch(e.url,t)}catch(t){throw e.signal.aborted||t instanceof Error&&t.name===`AbortError`?L(I(e.signal)):t instanceof Error?t:Error(String(t))}if(n.status<200||n.status>=300)throw Error(`Download failed with status ${n.status}`);let r=V(n.headers),i=Number(n.headers.get(`content-length`))||0;if(n.body&&typeof n.body.getReader==`function`){let t=n.body.getReader(),a=[],o=0;for(;;){let{done:n,value:r}=await t.read();if(n)break;r&&(a.push(r),o+=r.byteLength,e.onProgress?.(o,i||o))}return{blob:new Blob(a,{type:n.headers.get(`content-type`)??`application/octet-stream`}),status:n.status,headers:r}}let a=await n.blob();return e.onProgress?.(a.size,i||a.size),{blob:a,status:n.status,headers:r}},async head(e){if(!e.url||!B(e.url))throw Error(`UploadTransport(fetch): expected absolute URL, got: ${String(e.url)}`);if(e.signal.aborted)throw L(I(e.signal));let t={method:`HEAD`,signal:e.signal};e.headers&&(t.headers=e.headers);let n;try{n=await fetch(e.url,t)}catch(t){throw e.signal.aborted||t instanceof Error&&t.name===`AbortError`?L(I(e.signal)):t instanceof Error?t:Error(String(t))}return{status:n.status,headers:V(n.headers)}}}}const U=`snapshots`;function W(){return new Promise((e,t)=>{let n=indexedDB.open(`upload-engine`,1);n.onupgradeneeded=()=>{let e=n.result;e.objectStoreNames.contains(U)||e.createObjectStore(U)},n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error)})}function Oe(e){return new Promise((t,n)=>{e.onsuccess=()=>t(e.result),e.onerror=()=>n(e.error)})}function G(e){return new Promise((t,n)=>{e.oncomplete=()=>t(),e.onabort=()=>n(e.error??Error(`IndexedDB transaction aborted`)),e.onerror=()=>n(e.error??Error(`IndexedDB transaction error`))})}const ke={async load(e){if(typeof indexedDB>`u`)return null;let t=(await W()).transaction(U,`readonly`),n=t.objectStore(U);try{let r=await Oe(n.get(e));return await G(t),r==null?null:a(r)}catch{try{await G(t)}catch{}return null}},async save(e,t){if(typeof indexedDB>`u`)return;let n=(await W()).transaction(U,`readwrite`),r=n.objectStore(U);try{r.put(t,e),await G(n)}catch{try{n.abort()}catch{}}},async clear(e){if(typeof indexedDB>`u`)return;let t=(await W()).transaction(U,`readwrite`),n=t.objectStore(U);try{n.delete(e),await G(t)}catch{try{t.abort()}catch{}}}},Ae={load(e){if(typeof localStorage>`u`)return null;let t=localStorage.getItem(e);if(!t)return null;try{return a(JSON.parse(t))}catch{return null}},save(e,t){typeof localStorage>`u`||localStorage.setItem(e,JSON.stringify(t))},clear(e){typeof localStorage>`u`||localStorage.removeItem(e)}};function K(){let e=new Map;return{load(t){return e.get(t)??null},save(t,n){e.set(t,n)},clear(t){e.delete(t)}}}const je=K();function q(e,t){let n={};for(let t of e.items.values()){if(!N(t))continue;let e=t.phase;if(e===`completed`||e===`canceled`||e===`error`)continue;let r=P(t)?t.cursor:void 0,i=`progress`in t&&t.progress?{uploadedBytes:t.progress.uploadedBytes,totalBytes:t.progress.totalBytes,pct:t.progress.pct}:void 0,a={id:t.localId,purpose:t.purpose,status:t.phase,file:{name:t.fingerprint.name,size:t.fingerprint.size,type:t.fingerprint.type,lastModified:t.fingerprint.lastModified,checksum:t.fingerprint.checksum},intent:t.intent,cursor:r,progress:i};n[a.id]=a}return{version:t,createdAt:Date.now(),items:n}}function J(e,t){if(!t.isPurpose||!t.isIntent||!t.hasStrategy||!Me(e))return null;let n=new Map;for(let i of Object.values(e.items)){let a=Ne(i);if(!a||!t.isPurpose(a.purpose)||!t.isIntent(a.intent))continue;let o=a.intent;if(!r(o))continue;let s=o.strategy;if(typeof s!=`string`||!t.hasStrategy(s)||!a.cursor||!Fe(a.cursor,t.hasStrategy)||a.cursor.strategy!==s)continue;let c=a.progress?.totalBytes??a.file.size,l=a.progress?.uploadedBytes??0,u=typeof a.progress?.pct==`number`?a.progress.pct:c>0?Math.min(100,Math.max(0,l/c*100)):0;n.set(a.id,{phase:`paused`,localId:a.id,purpose:a.purpose,fingerprint:{name:a.file.name,size:a.file.size,type:a.file.type,lastModified:a.file.lastModified,checksum:a.file.checksum},intent:a.intent,cursor:a.cursor,progress:{uploadedBytes:l,totalBytes:c,pct:u},pausedAt:Date.now(),createdAt:e.createdAt??Date.now(),file:void 0,steps:[],meta:{}})}return{items:n}}function Y(e){return typeof e==`number`&&Number.isFinite(e)}function Me(e){return!r(e)||!Y(e.version)||!Y(e.createdAt)?!1:r(e.items)}function Ne(e){if(!r(e))return null;let t=typeof e.id==`string`?e.id:null,n=typeof e.purpose==`string`?e.purpose:null,i=typeof e.status==`string`?e.status:null,a=e.intent;if(!t||!n||!i)return null;let o=e.file;if(!r(o))return null;let s=typeof o.name==`string`?o.name:null,c=o.size,l=Y(c)&&c>=0?c:null,u=typeof o.type==`string`?o.type:null,d=o.lastModified,f=Y(d)?d:null,p=typeof o.checksum==`string`?o.checksum:void 0;if(!s||l===null||u===null||f===null)return null;let m=Pe(e.progress),h=e.cursor;return{id:t,purpose:n,status:i,file:{name:s,size:l,type:u,lastModified:f,checksum:p},intent:a,cursor:h,progress:m}}function Pe(e){if(!r(e))return;let t=e.uploadedBytes,n=e.totalBytes;if(!Y(t)||!Y(n)||t<0||n<0)return;let i=e.pct;return{uploadedBytes:t,totalBytes:n,pct:Y(i)?i:void 0}}function Fe(e,t){if(!r(e))return!1;let n=e.strategy;return!(typeof n!=`string`||!t(n))}function Ie(e){let t={},n=new Map,r=0,a=!1,o=e=>{n.size>0&&n.delete(e)};return{on(e,n){let r=t[e];return r||(r=new Set,t[e]=r),r.add(n),o(e),()=>{let r=t[e];r&&(r.delete(n),o(e),r.size===0&&delete t[e])}},off(e,n){let r=t[e];r&&(r.delete(n),o(e),r.size===0&&delete t[e])},emit(o,s){let c=t[o];if(!c||c.size===0)return;let l=t=>{try{t(s)}catch(n){if(e){try{e(o,n,t)}catch{}return}i()&&console.error(`[UploadEngine] Error in event listener for "${o}":`,n)}};r++;try{if(r>50&&!a&&i()&&(a=!0,console.warn(`[UploadEngine] emit depth exceeded 50 for "${o}". A listener is likely dispatching a command that re-emits, forming a feedback loop. Defer the dispatch with \`queueMicrotask\` to break the chain.`)),c.size===1){let[e]=c;e&&l(e);return}let e=n.get(o);e||(e=Array.from(c),n.set(o,e));for(let t of e)l(t)}finally{r--,r===0&&(a=!1)}}}}function X(e){let t={code:e.code,message:e.message};return e.retryable!==void 0&&(t.retryable=e.retryable),t}function Le(e,t){if(!e.length)return e;let n=e[e.length-1];if(!n||n.leftAt!==void 0)return e;let r={phase:n.phase,enteredAt:n.enteredAt,leftAt:t,attempt:n.attempt,...n.error===void 0?{}:{error:n.error}};return[...e.slice(0,-1),r]}function Z(e,t,n,r,i){return[...Le(e,r),{phase:t,enteredAt:r,attempt:n,error:i}]}function Re(){return function(e,t){let n=new Map(e.items),r=(e,t)=>n.set(e,t),i=Date.now();if((e=>`type`in e&&typeof e.type==`string`&&!e.type.includes(`.`))(t)){switch(t.type){case`addFiles`:break;case`start`:{let e=n.get(t.localId);if(!e||e.phase!==`ready`)break;r(t.localId,{...e,phase:`queued`,requestedAt:i,steps:Z(e.steps,`queued`,e.attempt??1,i)});break}case`resume`:{let e=n.get(t.localId);if(!e||e.phase!==`paused`||!e.file)break;r(t.localId,{...e,file:e.file,phase:`queued`,requestedAt:i,steps:Z(e.steps,`queued`,e.attempt??1,i)});break}case`pause`:{let e=n.get(t.localId);if(!e)break;e.phase===`queued`&&r(t.localId,{...e,phase:`ready`,steps:Z(e.steps,`ready`,e.attempt??1,i)});break}case`cancel`:{let e=n.get(t.localId);if(!e||e.phase===`completed`||e.phase===`canceled`)break;r(t.localId,Q(e,i));break}case`retry`:{let e=n.get(t.localId);if(!e||e.phase!==`error`||!e.retryable||!e.file)break;if(e.intent)e.progress&&e.progress.pct===100?r(t.localId,{...e,progress:e.progress,intent:e.intent,file:e.file,phase:`completing`,completingAt:i,steps:Z(e.steps,`completing`,e.attempt??1,i)}):r(t.localId,{...e,file:e.file,intent:e.intent,phase:`ready`,steps:Z(e.steps,`ready`,e.attempt??1,i)});else{let n=(e.attempt??1)+1;r(t.localId,{...e,phase:`creating_intent`,file:e.file,attempt:n,steps:Z(e.steps,`creating_intent`,n,i)})}break}case`rebind`:{let e=n.get(t.localId);if(!e)break;e.phase===`paused`&&!e.file&&ie(E(t.file),e.fingerprint)&&r(t.localId,{...e,file:t.file});break}case`remove`:n.delete(t.localId);break;case`startAll`:case`pauseAll`:case`cancelAll`:break}return{items:n}}switch(t.type){case`files.added`:{let e=t;for(let t of e.items)n.has(t.localId)||r(t.localId,{phase:`validating`,localId:t.localId,purpose:t.purpose,file:t.file,fingerprint:t.fingerprint,createdAt:t.createdAt,meta:t.meta,steps:[{phase:`validating`,enteredAt:i,attempt:1}]});break}case`fingerprint.updated`:{let e=t,i=n.get(e.localId);if(!i)break;r(e.localId,{...i,fingerprint:e.fingerprint});break}case`validation.ok`:{let e=n.get(t.localId);if(e?.phase!==`validating`)break;r(t.localId,{...e,phase:`creating_intent`,attempt:1,steps:Z(e.steps,`creating_intent`,1,i)});break}case`validation.failed`:{let e=t,a=n.get(e.localId);if(a?.phase!==`validating`)break;let o=new f(e.reason);r(e.localId,{...a,phase:`error`,error:o,retryable:!1,attempt:1,failedAt:i,steps:Z(a.steps,`error`,1,i,X(o))});break}case`intent.ok`:{let e=t,a=n.get(e.localId);if(a?.phase!==`creating_intent`)break;r(e.localId,{...a,phase:`ready`,intent:e.intent,steps:Z(a.steps,`ready`,a.attempt??1,i)});break}case`intent.failed`:{let e=t,a=n.get(e.localId);if(a?.phase!==`creating_intent`)break;r(e.localId,{...a,phase:`error`,error:e.error,retryable:e.retryable,failedAt:i,steps:Z(a.steps,`error`,a.attempt??1,i,X(e.error))});break}case`upload.begin`:{let e=t,i=n.get(e.localId);if(i?.phase!==`queued`)break;let a=i.file.size,o=i.progress,s=o?{...o,totalBytes:a,pct:$(o.uploadedBytes,a)}:{uploadedBytes:0,totalBytes:a,pct:0};r(e.localId,{...i,phase:`uploading`,progress:s,startedAt:e.startedAt,steps:Z(i.steps,`uploading`,i.attempt??1,e.startedAt)});break}case`upload.progress`:{let e=t,i=n.get(e.localId);if(i?.phase!==`uploading`)break;let a={uploadedBytes:e.uploadedBytes,totalBytes:e.totalBytes,pct:$(e.uploadedBytes,e.totalBytes)};r(e.localId,{...i,progress:a});break}case`cursor.updated`:{let e=t,i=n.get(e.localId);if(!i)break;(i.phase===`uploading`||i.phase===`queued`||i.phase===`paused`)&&r(e.localId,{...i,cursor:e.cursor});break}case`upload.ok`:{let e=t,a=n.get(e.localId);if(a?.phase!==`uploading`)break;r(e.localId,{...a,phase:`completing`,progress:{uploadedBytes:a.file.size,totalBytes:a.file.size,pct:100},completingAt:i,steps:Z(a.steps,`completing`,a.attempt??1,i)});break}case`upload.failed`:{let e=t,a=n.get(e.localId);if(a?.phase!==`uploading`)break;r(e.localId,{...a,phase:`error`,error:e.error,retryable:e.retryable,attempt:1,failedAt:i,steps:Z(a.steps,`error`,a.attempt??1,i,X(e.error))});break}case`paused`:{let e=t,i=n.get(e.localId);if(i?.phase!==`uploading`)break;r(e.localId,{...i,phase:`paused`,cursor:e.cursor,pausedAt:e.pausedAt,steps:Z(i.steps,`paused`,i.attempt??1,e.pausedAt)});break}case`canceled`:{let e=t,i=n.get(e.localId);if(!i)break;r(e.localId,Q(i,e.canceledAt));break}case`complete.ok`:{let e=t,a=n.get(e.localId);if(a?.phase!==`completing`)break;r(e.localId,{...a,phase:`completed`,completedBy:`upload`,result:e.result,completedAt:i,steps:Z(a.steps,`completed`,a.attempt??1,i)});break}case`dedupe.ok`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`validating`&&a.phase!==`creating_intent`)break;r(e.localId,{phase:`completed`,localId:a.localId,file:`file`in a?a.file:void 0,purpose:a.purpose,fingerprint:a.fingerprint,result:e.result,completedBy:`dedupe`,completedAt:i,createdAt:a.createdAt,meta:a.meta,steps:Z(a.steps,`completed`,a.attempt??1,i)});break}case`complete.failed`:{let e=t,a=n.get(e.localId);if(a?.phase!==`completing`)break;let o=(a.attempt??1)+1;r(e.localId,{...a,phase:`error`,error:e.error,retryable:e.retryable,attempt:o,failedAt:i,steps:Z(a.steps,`error`,o,i,X(e.error))});break}}return{items:n}}}function Q(e,t){let n=(`attempt`in e?e.attempt:void 0)??1;return{phase:`canceled`,localId:e.localId,purpose:e.purpose,fingerprint:e.fingerprint,canceledAt:t,createdAt:e.createdAt,file:`file`in e?e.file:void 0,intent:`intent`in e?e.intent:void 0,cursor:`cursor`in e?e.cursor:void 0,progress:`progress`in e?e.progress:void 0,attempt:`attempt`in e?e.attempt:void 0,meta:e.meta,steps:Z(e.steps,`canceled`,n,t)}}function $(e,t){return t<=0?0:Math.min(100,e/t*100)}function ze(e,t){let n=e.config?.maxItems,r=e.config?.completedItemTTL,i=Date.now(),a=[],o=Array.from(t.items.entries());if(r!==void 0)for(let[e,t]of o)((t.phase===`completed`||t.phase===`canceled`)&&`completedAt`in t&&typeof t.completedAt==`number`&&i-t.completedAt>r||t.phase===`canceled`&&`canceledAt`in t&&typeof t.canceledAt==`number`&&i-t.canceledAt>r)&&a.push(e);let s=o.filter(([e])=>!a.includes(e));if(n!==void 0&&s.length>n){let e=s.filter(([,e])=>e.phase===`completed`||e.phase===`canceled`).sort(([,e],[,t])=>(`completedAt`in e?e.completedAt:`canceledAt`in e?e.canceledAt:0)-(`completedAt`in t?t.completedAt:`canceledAt`in t?t.canceledAt:0)),t=s.length-n;for(let n=0;n<t&&n<e.length;n++){let t=e[n];t&&a.push(t[0])}}if(a.length===0)return null;let c=new Map(t.items);for(let e of a)c.delete(e);return{...t,items:c}}const Be=new Set([`CON`,`PRN`,`AUX`,`NUL`,`COM1`,`COM2`,`COM3`,`COM4`,`COM5`,`COM6`,`COM7`,`COM8`,`COM9`,`LPT1`,`LPT2`,`LPT3`,`LPT4`,`LPT5`,`LPT6`,`LPT7`,`LPT8`,`LPT9`]);function Ve(e){let t=``;for(let n=0;n<e.length;n++){let r=e.charCodeAt(n);r>=32&&r!==127&&(t+=e[n])}return t}function He(e){let t=e.normalize(`NFKC`);for(t=Ve(t);t.startsWith(`-`);)t=t.slice(1);for(;t.length>0&&(t.endsWith(`.`)||t.endsWith(` `));)t=t.slice(0,-1);if(t.length===0)return{safe:!1,normalised:t,reason:`empty`};if(t.includes(`/`)||t.includes(`\\`))return{safe:!1,normalised:t,reason:`path-sep`};if(t.length>255)return{safe:!1,normalised:t,reason:`too-long`};let n=t.indexOf(`.`),r=(n===-1?t:t.slice(0,n)).toUpperCase();return Be.has(r)?{safe:!1,normalised:t,reason:`reserved`}:{safe:!0,normalised:t}}async function Ue(e,t){let n=e.state.items.get(t);if(n?.phase!==`creating_intent`||e.inflightIntents.has(t))return;let i=He(n.file.name);if(!i.safe){let r=new f({code:`filename_rejected`,reason:i.reason},`filename rejected`,{context:{original:n.file.name,reason:i.reason}});e.applyInternal({type:`intent.failed`,localId:t,error:r,retryable:!1});return}let a=new AbortController;e.inflightIntents.set(t,a);try{if(await j(e,t,n.fingerprint.checksum,n.purpose,[`creating_intent`]))return;let o={purpose:n.purpose,contentType:n.file.type||`application/octet-stream`,size:n.file.size,filename:i.normalised,attempt:n.attempt??1};n.fingerprint.checksum!==void 0&&(o.checksum=n.fingerprint.checksum);let s=await e.opts.api.createIntent(o,A(n,{signal:a.signal}));if(e.inflightIntents.delete(t),e.state.items.get(t)?.phase!==`creating_intent`)return;let c=fe(s);if(c){e.applyInternal({type:`dedupe.ok`,localId:t,result:c});return}let l=C(s,r(s)&&typeof s.strategy==`string`?s.strategy:``);if(l){e.applyInternal({type:`intent.failed`,localId:t,error:l,retryable:!1});return}e.applyInternal({type:`intent.ok`,localId:t,intent:s})}catch(r){if(e.inflightIntents.delete(t),a.signal.aborted)return;let i=M(r,e.opts.errorNormalizer),o={...i,context:{...i.context??{},filename:n.fingerprint.name,size:n.file.size,purpose:n.purpose}},s=n.attempt??1,c=F(e.opts.config,{phase:`intent`,attempt:s,error:o});e.applyInternal({type:`intent.failed`,localId:t,error:o,retryable:c.retryable}),c.retryable&&c.delayMs!==void 0&&e.enqueueEffect(async()=>{await k(c.delayMs),e.dispatch({type:`retry`,localId:t})})}}function We(e){return typeof e==`object`&&!!e&&`context`in e&&typeof e.context==`object`&&e.context!==null}async function Ge(e,t){let n=e.state.items.get(t);if(n?.phase!==`completing`||e.inflightCompletes.has(t))return;let r=new AbortController;e.inflightCompletes.set(t,r);try{let i=await e.opts.api.complete({fileId:n.intent.fileId,filename:n.fingerprint.name,contentType:n.file.type||`application/octet-stream`,size:n.fingerprint.size,...n.fingerprint.checksum===void 0?{}:{checksum:n.fingerprint.checksum},attempt:n.attempt??1},A(n,{signal:r.signal}));if(e.inflightCompletes.delete(t),e.state.items.get(t)?.phase!==`completing`)return;e.applyInternal({type:`complete.ok`,localId:t,result:i})}catch(n){if(e.inflightCompletes.delete(t),r.signal.aborted)return;let i=M(n,e.opts.errorNormalizer),a=e.state.items.get(t),o=a?{...i,context:{...We(i)?i.context:{},filename:a.fingerprint.name,fileId:(N(a)&&a.intent?.fileId)??`unknown`}}:i,s=a&&`attempt`in a?a.attempt??1:1,c=F(e.opts.config,{phase:`complete`,attempt:s,error:o});e.applyInternal({type:`complete.failed`,localId:t,error:o,retryable:c.retryable}),c.retryable&&c.delayMs!==void 0&&e.enqueueEffect(async()=>{await k(c.delayMs),e.dispatch({type:`retry`,localId:t})})}}async function Ke(e,t){let n=e.state.items.get(t);if(n?.phase!==`uploading`||!N(n)){e.inflightUploads.delete(t);return}let r=e.inflightUploads.get(t);if(r||(r={controller:new AbortController,mode:`normal`,started:!1},e.inflightUploads.set(t,r)),r.started)return;r.started=!0;let i=r.controller,a=()=>{let n=e.state.items.get(t);!n||n.phase===`canceled`||e.applyInternal({type:`canceled`,localId:t,canceledAt:Date.now()})},o=n=>{let r=e.state.items.get(t);!r||r.phase===`paused`||e.applyInternal({type:`paused`,localId:t,cursor:n,pausedAt:Date.now()})},s=n.intent.strategy;if(i.signal.aborted){if(e.inflightUploads.delete(t),r.mode===`pause`){let n=e.state.items.get(t),r=(n&&P(n)?n.cursor:void 0)||{strategy:s,value:void 0};r&&o(r);return}if(r.mode===`cancel`){a();return}return}if(!e.opts.strategies.has(s)){e.inflightUploads.delete(t);let n=new p(String(s));e.applyInternal({type:`upload.failed`,localId:t,error:n,retryable:!1});return}let c=e.opts.strategies.get(s);if(!c){e.inflightUploads.delete(t);let r=new p(String(n.intent.strategy));e.applyInternal({type:`upload.failed`,localId:t,error:r,retryable:!1});return}let l=0,u=e.opts.config.progressThrottleMs;try{if(await c.start({localId:t,purpose:n.purpose,file:n.file,intent:n.intent,signal:i.signal,transport:e.opts.transport,api:e.opts.api,fingerprint:n.fingerprint,attempt:n.attempt??1,createdAt:n.createdAt,steps:n.steps,meta:n.meta,lastError:[...n.steps].reverse().find(e=>e.error)?.error,readCursor:()=>{let n=e.state.items.get(t);if(!(!n||!P(n)||!n.cursor)&&n.cursor.strategy===s)return n.cursor.value},persistCursor:n=>{let r={strategy:s,value:n};e.applyInternal({type:`cursor.updated`,localId:t,cursor:r})},reportProgress:n=>{let r=Date.now();r-l<u||(e.applyInternal({type:`upload.progress`,localId:t,uploadedBytes:n.uploadedBytes,totalBytes:n.totalBytes}),l=r)}}),e.inflightUploads.delete(t),r?.mode===`cancel`){a();return}if(i.signal.aborted&&r?.mode===`pause`){let n=e.state.items.get(t);o((n&&P(n)?n.cursor:void 0)||{strategy:s,value:void 0});return}e.applyInternal({type:`upload.ok`,localId:t})}catch(n){if(e.inflightUploads.delete(t),i.signal.aborted){if(r?.mode===`pause`){let n=e.state.items.get(t);o((n&&P(n)?n.cursor:void 0)||{strategy:s,value:void 0});return}if(r?.mode===`cancel`){a();return}a();return}let c=M(n,e.opts.errorNormalizer),l=e.state.items.get(t),u=l?{...c,context:{...c.context??{},filename:l.fingerprint.name,size:(pe(l)&&l.file?.size)??0,phase:l.phase}}:c,d=F(e.opts.config,{phase:`upload`,attempt:1,error:u});e.applyInternal({type:`upload.failed`,localId:t,error:u,retryable:d.retryable}),d.retryable&&d.delayMs!==void 0&&e.enqueueEffect(async()=>{await k(d.delayMs),e.dispatch({type:`retry`,localId:t})})}}function qe(e){if(!e.scheduling){e.scheduling=!0;try{Je(e),Xe(e),Ye(e)}finally{e.scheduling=!1}}}function Je(e){for(let t of e.state.items.values())t.phase===`creating_intent`&&(e.inflightIntents.has(t.localId)||e.enqueueEffect(()=>Ue(e,t.localId)))}function Ye(e){for(let t of e.state.items.values())t.phase===`completing`&&(e.inflightCompletes.has(t.localId)||e.enqueueEffect(()=>Ge(e,t.localId)))}function Xe(e){let t=Math.max(1,e.opts.config.maxConcurrentUploads),n=e.inflightUploads.size;for(let t of e.state.items.values())t.phase===`ready`&&de(e.opts,t.purpose)&&e.applyCommand({type:`start`,localId:t.localId});if(n>=t)return;let r=Array.from(e.state.items.values()).filter(Ze);if(r.length===0)return;let i=Math.max(0,t-n),a=r.slice(0,i);for(let t of a){if(e.inflightUploads.has(t.localId))continue;let n=new AbortController;e.inflightUploads.set(t.localId,{controller:n,mode:`normal`,started:!1}),e.applyInternal({type:`upload.begin`,localId:t.localId,startedAt:Date.now()}),Ke(e,t.localId)}}function Ze(e){return e.phase===`queued`}function Qe(e){let t=$e(e),n=t.persistence,r=null,a=Math.max(0,n?.debounceMs??200),o=async()=>{if(n)try{let e=(n.serialize??q)(c.state,n.version);if(!e.items||Object.keys(e.items).length===0){await n.adapter.clear(n.key);return}await n.adapter.save(n.key,e)}catch(e){i()&&console.warn(`[UploadEngine] persistence flush failed:`,e)}},s=()=>{if(n){if(a===0){o();return}r&&clearTimeout(r),r=setTimeout(()=>void o(),a)}},c={opts:t,state:t.initialState??{items:new Map},listeners:new Set,emitter:Ie(),reduce:Re(),inflightUploads:new Map,inflightIntents:new Map,inflightCompletes:new Map,effectQueue:[],processingEffects:!1,scheduling:!1,notify(){c.listeners.forEach(e=>{e()})},scheduleWork(){qe(c)},applyInternal(e){let t=c.state;c.state=c.reduce(t,e),c.opts.hooks?.onInternalEvent?.(e,c.state),et(c,e);let n=ze(c.opts,c.state);n&&(c.state=n),s(),c.notify(),c.scheduleWork()},applyCommand(e){let t=c.state,n=c.reduce(t,e);c.state=n,tt(c,e,t,n);let r=ze(c.opts,c.state);r&&(c.state=r),s(),c.notify(),c.scheduleWork()},enqueueEffect(e){c.effectQueue.push(e),c.processingEffects||c.processEffects()},async processEffects(){for(c.processingEffects=!0;c.effectQueue.length>0;){let e=c.effectQueue.shift();if(e)try{await e()}catch(e){if(i()){let t=e instanceof Error?e:Error(String(e)),n={queueLength:c.effectQueue.length,processing:c.processingEffects,timestamp:new Date().toISOString()};console.error(`[UploadEngine] Effect error:`,t,n)}}}c.processingEffects=!1},dispatch:()=>{throw Error(`[UploadEngine] dispatch not initialized`)}};if(n&&!t.initialState&&typeof window<`u`)try{let e=n.adapter.load(n.key);Promise.resolve(e).then(e=>{if(!e)return;let r=(n.deserialize??J)(e,{isPurpose:n.isPurpose,isIntent:n.isIntent,hasStrategy:t.strategies.has});if(r){if(c.state.items.size===0)c.state=r;else{let e=new Map(c.state.items);for(let[t,n]of r.items.entries())e.has(t)||e.set(t,n);c.state={...c.state,items:e}}c.notify(),c.scheduleWork()}}).catch(()=>void 0)}catch{}return c}function $e(e){return{...e,config:t(e.config),transport:e.transport??z()}}function et(e,t){switch(t.type){case`files.added`:for(let n of t.items)e.emitter.emit(`file.added`,{localId:n.localId,purpose:n.purpose,file:n.file,fingerprint:n.fingerprint});break;case`validation.ok`:e.emitter.emit(`validation.ok`,{localId:t.localId}),e.emitter.emit(`intent.creating`,{localId:t.localId});break;case`validation.failed`:e.emitter.emit(`validation.failed`,{localId:t.localId,reason:t.reason});break;case`intent.ok`:e.emitter.emit(`intent.created`,{localId:t.localId,intent:t.intent});break;case`intent.failed`:e.emitter.emit(`intent.failed`,{localId:t.localId,error:t.error,retryable:t.retryable});break;case`upload.begin`:e.emitter.emit(`upload.started`,{localId:t.localId});break;case`upload.progress`:{let n=t.totalBytes>0?Math.min(100,t.uploadedBytes/t.totalBytes*100):0;e.emitter.emit(`upload.progress`,{localId:t.localId,pct:n,uploadedBytes:t.uploadedBytes,totalBytes:t.totalBytes});break}case`cursor.updated`:e.emitter.emit(`upload.cursor`,{localId:t.localId,cursor:t.cursor});break;case`upload.ok`:e.emitter.emit(`upload.completing`,{localId:t.localId});break;case`upload.failed`:e.emitter.emit(`upload.error`,{localId:t.localId,error:t.error,retryable:t.retryable});break;case`dedupe.ok`:e.emitter.emit(`upload.completed`,{localId:t.localId,result:t.result,completedBy:`dedupe`});break;case`complete.ok`:e.emitter.emit(`upload.completed`,{localId:t.localId,result:t.result,completedBy:`upload`});break;case`complete.failed`:e.emitter.emit(`upload.error`,{localId:t.localId,error:t.error,retryable:t.retryable});break;case`paused`:e.emitter.emit(`upload.paused`,{localId:t.localId,cursor:t.cursor});break;case`canceled`:e.emitter.emit(`upload.canceled`,{localId:t.localId});break;case`fingerprint.updated`:break}}function tt(e,t,n,r){if(t.type===`start`||t.type===`resume`){let i=n.items.get(t.localId),a=r.items.get(t.localId);if(!i||!a||a.phase!==`queued`)return;i.phase===`paused`?e.emitter.emit(`upload.resumed`,{localId:t.localId}):e.emitter.emit(`upload.queued`,{localId:t.localId})}if(t.type===`cancel`){let i=n.items.get(t.localId),a=r.items.get(t.localId);if(!i||!a)return;a.phase===`canceled`&&i.phase!==`canceled`&&e.emitter.emit(`upload.canceled`,{localId:t.localId})}}function nt(e){let t=Qe(e);t.dispatch=e=>ye(t,e);let n={on:t.emitter.on.bind(t.emitter),off:t.emitter.off.bind(t.emitter),dispatch:t.dispatch,getSnapshot:()=>t.state};for(let t of e.plugins||[])try{t.setup(n)}catch(e){i()&&console.error(`[UploadEngine] Plugin "${t.name}" failed to setup:`,e)}return{...n,subscribe:e=>(t.listeners.add(e),()=>t.listeners.delete(e)),waitFor(e){return rt(t,e)}}}function rt(e,t){if(t.length===0)return Promise.resolve([]);let n=new Set(t),r=new Map,i=()=>{for(let t of n){let i=e.state.items.get(t);if(!i){r.set(t,{localId:t,status:`missing`,reason:`never-existed`}),n.delete(t);continue}if(i.phase===`completed`){r.set(t,{localId:t,status:`completed`,completedBy:i.completedBy,result:i.result}),n.delete(t);continue}if(i.phase===`error`){r.set(t,{localId:t,status:`error`,error:i.error}),n.delete(t);continue}i.phase===`canceled`&&(r.set(t,{localId:t,status:`canceled`}),n.delete(t))}};return i(),n.size===0?Promise.resolve(t.map(e=>r.get(e)??{localId:e,status:`missing`,reason:`never-existed`})):new Promise(a=>{let o=()=>{i(),n.size===0&&(e.listeners.delete(o),a(t.map(e=>r.get(e)??{localId:e,status:`missing`,reason:`never-existed`})))};e.listeners.add(o)})}(function(e){e.Phases={validating:`validating`,creating_intent:`creating_intent`,ready:`ready`,queued:`queued`,uploading:`uploading`,paused:`paused`,completing:`completing`,completed:`completed`,error:`error`,canceled:`canceled`}})(it||={});var it;export{t as C,f as S,y as _,je as a,_ as b,ke as c,k as d,m as f,h as g,g as h,q as i,De as l,d as m,nt as n,K as o,v as p,J as r,Ae as s,it as t,z as u,b as v,x,p as y};