@gentleduck/upload 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -193
- package/dist/core/index.d.ts +2 -3
- package/dist/core/index.js +1 -1
- package/dist/core-BVgloTiP.js +10 -0
- package/dist/index-DCqyBVcx.d.ts +1441 -0
- package/dist/index-DsZHPmJm.d.ts +225 -0
- package/dist/index-KROXtLQh.d.ts +281 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +2 -51
- package/dist/react/index.js +1 -1
- package/dist/react-euPrDv24.js +1 -0
- package/dist/strategies/index.d.ts +2 -56
- package/dist/strategies/index.js +1 -1
- package/dist/strategies-CUF0s68j.js +1 -0
- package/package.json +3 -2
- package/src/{__test__ → __tests__}/emitter.test.ts +4 -4
- package/src/{__test__ → __tests__}/fingerprint.test.ts +1 -1
- package/src/{__test__ → __tests__}/guards.test.ts +15 -5
- package/src/__tests__/id.test.ts +67 -0
- package/src/__tests__/mime-sniff.test.ts +142 -0
- package/src/{__test__ → __tests__}/persistence.test.ts +43 -8
- package/src/__tests__/sanitize-filename.test.ts +109 -0
- package/src/__tests__/sleep.test.ts +34 -0
- package/src/__tests__/static-analysis.test.ts +145 -0
- package/src/{__test__ → __tests__}/store-libs.test.ts +57 -3
- package/src/__tests__/transport-fetch.test.ts +108 -0
- package/src/{__test__ → __tests__}/transport.test.ts +5 -5
- package/src/core/__tests__/errors.test.ts +152 -0
- package/src/core/client.ts +3 -0
- package/src/core/contracts/contracts.types.ts +335 -141
- package/src/core/contracts/index.ts +3 -8
- package/src/core/contracts/transport/index.ts +3 -2
- package/src/core/contracts/transport/transport.fetch.ts +203 -0
- package/src/core/contracts/transport/transport.libs.ts +7 -8
- package/src/core/contracts/transport/transport.ts +91 -83
- package/src/core/contracts/transport/transport.types.ts +71 -35
- package/src/core/engine/__tests__/config-nan-bypass.test.ts +40 -0
- package/src/core/engine/engine.libs.ts +35 -0
- package/src/core/engine/engine.types.ts +367 -241
- package/src/core/engine/index.ts +3 -8
- package/src/core/engine/reducer.ts +89 -43
- package/src/core/engine/store/handlers/add-file.ts +39 -29
- package/src/core/engine/store/handlers/cancel.ts +13 -8
- package/src/core/engine/store/handlers/clean-up.ts +11 -8
- package/src/core/engine/store/handlers/create-intent.ts +82 -27
- package/src/core/engine/store/handlers/finalize.ts +30 -13
- package/src/core/engine/store/handlers/index.ts +7 -7
- package/src/core/engine/store/handlers/pause.ts +8 -6
- package/src/core/engine/store/handlers/run-upload.ts +49 -34
- package/src/core/engine/store/index.ts +27 -21
- package/src/core/engine/store/store.dispatch.ts +10 -8
- package/src/core/engine/store/store.libs.ts +184 -41
- package/src/core/engine/store/store.runtime.ts +45 -36
- package/src/core/engine/store/store.schedule.ts +27 -21
- package/src/core/engine/store/store.types.ts +177 -227
- package/src/core/engine/validation/__tests__/intent.test.ts +70 -0
- package/src/core/engine/validation/file.ts +7 -3
- package/src/core/engine/validation/index.ts +1 -0
- package/src/core/engine/validation/intent.ts +49 -27
- package/src/core/engine/validation/list.ts +14 -15
- package/src/core/engine/validation/mime.ts +39 -0
- package/src/core/errors.ts +271 -0
- package/src/core/index.ts +28 -5
- package/src/core/persistence/__tests__/deserialize-nan-bypass.test.ts +100 -0
- package/src/core/persistence/__tests__/persistence.test.ts +85 -0
- package/src/core/persistence/adapters.indexeddb.ts +9 -3
- package/src/core/persistence/adapters.local.ts +9 -3
- package/src/core/persistence/adapters.memory.ts +35 -13
- package/src/core/persistence/index.ts +5 -5
- package/src/core/persistence/persistence.ts +73 -60
- package/src/core/persistence/persistence.types.ts +140 -60
- package/src/core/utils/__tests__/guards.test.ts +93 -0
- package/src/core/utils/__tests__/url-safety.test.ts +112 -0
- package/src/core/utils/async.ts +7 -2
- package/src/core/utils/constants.ts +3 -9
- package/src/core/utils/emitter/emitter.ts +7 -24
- package/src/core/utils/emitter/emitter.types.ts +33 -16
- package/src/core/utils/emitter/index.ts +2 -74
- package/src/core/utils/fingerprint.ts +16 -5
- package/src/core/utils/guards.ts +91 -1
- package/src/core/utils/hash/__tests__/sha256.test.ts +61 -0
- package/src/core/utils/hash/index.ts +126 -0
- package/src/core/utils/hash/sha256.ts +179 -0
- package/src/core/utils/id.ts +33 -1
- package/src/core/utils/index.ts +15 -6
- package/src/core/utils/mime-sniff.ts +119 -0
- package/src/core/utils/sanitize-filename.ts +85 -0
- package/src/core/utils/url-safety.ts +156 -0
- package/src/react/__tests__/use-dropzone.test.ts +64 -0
- package/src/react/index.ts +5 -2
- package/src/react/upload-provider.tsx +68 -53
- package/src/react/uploader.types.ts +58 -61
- package/src/react/use-dropzone.tsx +226 -0
- package/src/react/use-uploader.tsx +98 -91
- package/src/strategies/_shared/__tests__/retry.test.ts +120 -0
- package/src/strategies/_shared/retry.ts +79 -0
- package/src/strategies/index.ts +9 -15
- package/src/strategies/multipart/__tests__/multipart.test.ts +229 -0
- package/src/strategies/multipart/index.ts +206 -108
- package/src/strategies/post/__tests__/post-url-safety.test.ts +119 -0
- package/src/strategies/post/index.ts +115 -33
- package/src/strategies/put/__tests__/put.test.ts +132 -0
- package/src/strategies/put/index.ts +139 -0
- package/src/strategies/registry/index.ts +12 -9
- package/src/strategies/tus/__tests__/tus.test.ts +159 -0
- package/src/strategies/tus/index.ts +211 -0
- package/dist/async-DxRWu4le.js +0 -1
- package/dist/core-CMkrP9DX.js +0 -2
- package/dist/index-C38Jmizu.d.ts +0 -1005
- package/dist/strategy.types-gg9ZStRe.d.ts +0 -225
- package/src/__test__/effect-pool.test.ts +0 -5
- package/src/__test__/id.test.ts +0 -14
- package/src/__test__/store-integration.test.ts +0 -5
- package/src/core/client/client.ts +0 -50
- package/src/core/client/client.types.ts +0 -184
- package/src/core/client/config.types.ts +0 -77
- package/src/core/client/index.ts +0 -4
- package/src/core/client/plugins.types.ts +0 -46
- package/src/core/client/retry.types.ts +0 -6
- package/src/core/client/uploadClient.ts +0 -19
- package/src/core/contracts/backendApi.ts +0 -62
- package/src/core/contracts/cursor.types.ts +0 -23
- package/src/core/contracts/errors.types.ts +0 -67
- package/src/core/contracts/fingerprint.types.ts +0 -18
- package/src/core/contracts/intent.types.ts +0 -39
- package/src/core/contracts/result.types.ts +0 -11
- package/src/core/contracts/strategy.types.ts +0 -45
- package/src/core/engine/commands.types.ts +0 -17
- package/src/core/engine/event-map.types.ts +0 -45
- package/src/core/engine/internal-events.types.ts +0 -202
- package/src/core/engine/outcome.types.ts +0 -9
- package/src/core/engine/progress.types.ts +0 -15
- package/src/core/engine/store/handlers.ts +0 -819
- package/src/core/engine/validation.ts +0 -216
- /package/src/{__test__ → __tests__}/transport-libs.test.ts +0 -0
package/README.md
CHANGED
|
@@ -23,37 +23,45 @@
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
|
|
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
|
-
|
|
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 = {
|
|
37
|
-
type Cursors = {
|
|
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
|
-
|
|
55
|
-
|
|
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 {
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
<
|
|
78
|
-
{
|
|
79
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
142
|
+
// Wrap once so descendants can reach the store.
|
|
143
|
+
<UploadProvider store={store}>
|
|
144
|
+
<Uploader />
|
|
145
|
+
</UploadProvider>
|
|
146
|
+
```
|
|
89
147
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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 {
|
|
224
|
+
import type { Contracts } from '@gentleduck/upload/core'
|
|
199
225
|
|
|
200
|
-
type Intents = {
|
|
201
|
-
type Cursors = {
|
|
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
|
-
|
|
206
|
-
id: '
|
|
207
|
-
resumable:
|
|
208
|
-
async start(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
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
|
|
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)
|
|
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.
|
|
298
|
-
- `
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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,
|
|
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
|
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
|
|
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 };
|
package/dist/core/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{C as e,S as t,_ as n,a as r,b as i,c as a,
|
|
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};
|