@gentleduck/upload 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +297 -0
- package/dist/core/index.d.ts +2 -3
- package/dist/core/index.js +1 -1
- package/dist/core-BVgloTiP.js +10 -0
- package/dist/index-DCqyBVcx.d.ts +1441 -0
- package/dist/index-DsZHPmJm.d.ts +225 -0
- package/dist/index-KROXtLQh.d.ts +281 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +2 -90
- package/dist/react/index.js +1 -1
- package/dist/react-euPrDv24.js +1 -0
- package/dist/strategies/index.d.ts +2 -56
- package/dist/strategies/index.js +1 -1
- package/dist/strategies-CUF0s68j.js +1 -0
- package/package.json +3 -2
- package/src/__tests__/emitter.test.ts +85 -0
- package/src/__tests__/fingerprint.test.ts +49 -0
- package/src/__tests__/guards.test.ts +40 -0
- package/src/__tests__/id.test.ts +67 -0
- package/src/__tests__/mime-sniff.test.ts +142 -0
- package/src/__tests__/persistence.test.ts +191 -0
- package/src/__tests__/sanitize-filename.test.ts +109 -0
- package/src/__tests__/sleep.test.ts +34 -0
- package/src/__tests__/static-analysis.test.ts +145 -0
- package/src/__tests__/store-libs.test.ts +192 -0
- package/src/__tests__/transport-fetch.test.ts +108 -0
- package/src/__tests__/transport-libs.test.ts +42 -0
- package/src/__tests__/transport.test.ts +182 -0
- package/src/core/__tests__/errors.test.ts +152 -0
- package/src/core/client.ts +3 -0
- package/src/core/contracts/contracts.types.ts +373 -0
- package/src/core/contracts/index.ts +3 -8
- package/src/core/contracts/transport/index.ts +3 -2
- package/src/core/contracts/transport/transport.fetch.ts +203 -0
- package/src/core/contracts/transport/transport.libs.ts +13 -39
- package/src/core/contracts/transport/transport.ts +96 -90
- package/src/core/contracts/transport/transport.types.ts +71 -35
- package/src/core/engine/__tests__/config-nan-bypass.test.ts +40 -0
- package/src/core/engine/engine.libs.ts +35 -0
- package/src/core/engine/engine.types.ts +490 -0
- package/src/core/engine/index.ts +3 -8
- package/src/core/engine/reducer.ts +93 -53
- package/src/core/engine/store/handlers/add-file.ts +43 -48
- package/src/core/engine/store/handlers/cancel.ts +13 -8
- package/src/core/engine/store/handlers/clean-up.ts +11 -8
- package/src/core/engine/store/handlers/create-intent.ts +82 -27
- package/src/core/engine/store/handlers/finalize.ts +30 -13
- package/src/core/engine/store/handlers/index.ts +7 -7
- package/src/core/engine/store/handlers/pause.ts +8 -6
- package/src/core/engine/store/handlers/run-upload.ts +54 -39
- package/src/core/engine/store/index.ts +27 -21
- package/src/core/engine/store/store.dispatch.ts +10 -8
- package/src/core/engine/store/store.libs.ts +197 -94
- package/src/core/engine/store/store.runtime.ts +52 -53
- package/src/core/engine/store/store.schedule.ts +27 -21
- package/src/core/engine/store/store.types.ts +177 -227
- package/src/core/engine/validation/__tests__/intent.test.ts +70 -0
- package/src/core/engine/validation/file.ts +8 -4
- package/src/core/engine/validation/index.ts +1 -0
- package/src/core/engine/validation/intent.ts +49 -27
- package/src/core/engine/validation/list.ts +14 -15
- package/src/core/engine/validation/mime.ts +39 -0
- package/src/core/errors.ts +271 -0
- package/src/core/index.ts +28 -5
- package/src/core/persistence/__tests__/deserialize-nan-bypass.test.ts +100 -0
- package/src/core/persistence/__tests__/persistence.test.ts +85 -0
- package/src/core/persistence/adapters.indexeddb.ts +9 -3
- package/src/core/persistence/adapters.local.ts +9 -3
- package/src/core/persistence/adapters.memory.ts +35 -13
- package/src/core/persistence/index.ts +5 -5
- package/src/core/persistence/persistence.ts +73 -60
- package/src/core/persistence/persistence.types.ts +140 -60
- package/src/core/utils/__tests__/guards.test.ts +93 -0
- package/src/core/utils/__tests__/url-safety.test.ts +112 -0
- package/src/core/utils/async.ts +7 -2
- package/src/core/utils/constants.ts +3 -9
- package/src/core/utils/emitter/emitter.ts +126 -0
- package/src/core/utils/emitter/emitter.types.ts +33 -16
- package/src/core/utils/emitter/index.ts +2 -74
- package/src/core/utils/fingerprint.ts +16 -5
- package/src/core/utils/guards.ts +91 -1
- package/src/core/utils/hash/__tests__/sha256.test.ts +61 -0
- package/src/core/utils/hash/index.ts +126 -0
- package/src/core/utils/hash/sha256.ts +179 -0
- package/src/core/utils/id.ts +33 -1
- package/src/core/utils/index.ts +15 -6
- package/src/core/utils/mime-sniff.ts +119 -0
- package/src/core/utils/sanitize-filename.ts +85 -0
- package/src/core/utils/url-safety.ts +156 -0
- package/src/react/__tests__/use-dropzone.test.ts +64 -0
- package/src/react/index.ts +5 -2
- package/src/react/upload-provider.tsx +59 -95
- package/src/react/uploader.types.ts +80 -0
- package/src/react/use-dropzone.tsx +226 -0
- package/src/react/use-uploader.tsx +97 -93
- package/src/strategies/_shared/__tests__/retry.test.ts +120 -0
- package/src/strategies/_shared/retry.ts +79 -0
- package/src/strategies/index.ts +9 -15
- package/src/strategies/multipart/__tests__/multipart.test.ts +229 -0
- package/src/strategies/multipart/index.ts +211 -116
- package/src/strategies/post/__tests__/post-url-safety.test.ts +119 -0
- package/src/strategies/post/index.ts +113 -71
- package/src/strategies/put/__tests__/put.test.ts +132 -0
- package/src/strategies/put/index.ts +139 -0
- package/src/strategies/registry/index.ts +12 -9
- package/src/strategies/tus/__tests__/tus.test.ts +159 -0
- package/src/strategies/tus/index.ts +211 -0
- package/dist/async-DxRWu4le.js +0 -1
- package/dist/core-BNxznRif.js +0 -2
- package/dist/index-BXg9WIce.d.ts +0 -987
- package/dist/strategy.types-gg9ZStRe.d.ts +0 -225
- package/src/core/client/config.types.ts +0 -77
- package/src/core/client/index.ts +0 -4
- package/src/core/client/plugins.types.ts +0 -46
- package/src/core/client/retry.types.ts +0 -6
- package/src/core/client/uploadClient.ts +0 -19
- package/src/core/contracts/backendApi.ts +0 -62
- package/src/core/contracts/cursor.types.ts +0 -23
- package/src/core/contracts/errors.types.ts +0 -67
- package/src/core/contracts/fingerprint.types.ts +0 -18
- package/src/core/contracts/intent.types.ts +0 -39
- package/src/core/contracts/result.types.ts +0 -11
- package/src/core/contracts/strategy.types.ts +0 -45
- package/src/core/engine/commands.types.ts +0 -17
- package/src/core/engine/event-map.types.ts +0 -45
- package/src/core/engine/internal-events.types.ts +0 -222
- package/src/core/engine/outcome.types.ts +0 -9
- package/src/core/engine/progress.types.ts +0 -15
package/README.md
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="../../public/logo-dark.svg" alt="@gentleduck/upload" width="120"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@gentleduck/upload</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Headless, framework-agnostic file-upload engine with a typed state machine, pluggable strategies, and React bindings.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="../../LICENSE">MIT</a> -
|
|
13
|
+
<a href="../../CHANGELOG.md">Changelog</a> -
|
|
14
|
+
<a href="../../CONTRIBUTING.md">Contributing</a> -
|
|
15
|
+
<a href="https://gentleduck.org/duck-upload">Docs</a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="https://www.npmjs.com/package/@gentleduck/upload"><img src="https://img.shields.io/npm/v/@gentleduck/upload.svg" alt="npm"/></a>
|
|
20
|
+
<a href="https://www.npmjs.com/package/@gentleduck/upload"><img src="https://img.shields.io/npm/dm/@gentleduck/upload.svg" alt="downloads"/></a>
|
|
21
|
+
<a href="../../LICENSE"><img src="https://img.shields.io/npm/l/@gentleduck/upload.svg" alt="MIT"/></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
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.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
bun add @gentleduck/upload
|
|
35
|
+
# peer deps for the React bindings
|
|
36
|
+
bun add react react-dom
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { createUploadStore } from '@gentleduck/upload/core'
|
|
43
|
+
import { PutStrategy, createStrategyRegistry } from '@gentleduck/upload/strategies'
|
|
44
|
+
|
|
45
|
+
type Intents = { put: PutStrategy.Intent }
|
|
46
|
+
type Cursors = { put?: PutStrategy.Cursor }
|
|
47
|
+
type Purpose = 'attachment'
|
|
48
|
+
type Result = { fileId: string; key: string }
|
|
49
|
+
|
|
50
|
+
const strategies = createStrategyRegistry<Intents, Cursors, Purpose, Result>()
|
|
51
|
+
strategies.set(PutStrategy({ allowedHosts: ['uploads.example.com'] }))
|
|
52
|
+
|
|
53
|
+
const store = createUploadStore<Intents, Cursors, Purpose, Result>({
|
|
54
|
+
strategies,
|
|
55
|
+
api: {
|
|
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? }
|
|
63
|
+
},
|
|
64
|
+
// Finalize once the bytes are transferred.
|
|
65
|
+
async complete({ fileId }) {
|
|
66
|
+
return { fileId, key: `attachments/${fileId}` }
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
config: { maxConcurrentUploads: 3, autoStart: ['attachment'] },
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
store.dispatch({ type: 'addFiles', files, purpose: 'attachment' })
|
|
73
|
+
```
|
|
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
|
+
|
|
112
|
+
## React
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
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>()
|
|
127
|
+
|
|
128
|
+
return (
|
|
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
|
+
/>
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Wrap once so descendants can reach the store.
|
|
143
|
+
<UploadProvider store={store}>
|
|
144
|
+
<Uploader />
|
|
145
|
+
</UploadProvider>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
React exports: `UploadProvider`, `useUploader`, `useUploaderActions`, `useDropzone`,
|
|
149
|
+
`createUploadFactory`, `useUploadStore`, `isUploadStore` (plus the pure helpers
|
|
150
|
+
`fileMatchesAccept`, `matchesAcceptToken`, `selectFiles`).
|
|
151
|
+
|
|
152
|
+
## Subpath Exports
|
|
153
|
+
|
|
154
|
+
```ts
|
|
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'
|
|
158
|
+
```
|
|
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
|
+
|
|
190
|
+
## Persistence
|
|
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
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { createUploadStore, IndexedDBAdapter } from '@gentleduck/upload/core'
|
|
198
|
+
|
|
199
|
+
createUploadStore({
|
|
200
|
+
strategies,
|
|
201
|
+
api,
|
|
202
|
+
persistence: {
|
|
203
|
+
key: 'app:uploads',
|
|
204
|
+
version: 1,
|
|
205
|
+
adapter: IndexedDBAdapter, // or LocalStorageAdapter / createMemoryAdapter()
|
|
206
|
+
isPurpose,
|
|
207
|
+
isIntent,
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
```
|
|
211
|
+
|
|
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.
|
|
216
|
+
|
|
217
|
+
## Writing a custom strategy
|
|
218
|
+
|
|
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.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import type { Contracts } from '@gentleduck/upload/core'
|
|
225
|
+
|
|
226
|
+
type Intents = { myput: { strategy: 'myput'; fileId: string; url: string } }
|
|
227
|
+
type Cursors = { myput?: Record<string, never> }
|
|
228
|
+
type Purpose = 'attachment'
|
|
229
|
+
type Result = { fileId: string; key: string }
|
|
230
|
+
|
|
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
|
+
})
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
strategies.set(myPut)
|
|
245
|
+
```
|
|
246
|
+
|
|
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.
|
|
252
|
+
|
|
253
|
+
## Commands
|
|
254
|
+
|
|
255
|
+
Dispatch via `store.dispatch(cmd)`.
|
|
256
|
+
|
|
257
|
+
| Command | Purpose |
|
|
258
|
+
| --- | --- |
|
|
259
|
+
| `addFiles` | Validate + insert; schedules checksum + intent |
|
|
260
|
+
| `start` / `startAll` | Move ready items to queued (batched) |
|
|
261
|
+
| `pause` / `pauseAll` | Abort inflight, persist cursor (batched) |
|
|
262
|
+
| `resume` | Re-queue a paused item |
|
|
263
|
+
| `cancel` / `cancelAll` | Abort and mark canceled (batched) |
|
|
264
|
+
| `retry` | Re-attempt the failed phase, bumping the attempt counter |
|
|
265
|
+
| `rebind` | Re-attach a fresh `File` after refresh (fingerprint-validated) |
|
|
266
|
+
| `remove` | Drop the item from state |
|
|
267
|
+
|
|
268
|
+
## Events
|
|
269
|
+
|
|
270
|
+
Subscribe via `store.on(type, cb)`; payloads are typed from the engine's event map.
|
|
271
|
+
|
|
272
|
+
- `file.added`, `file.rejected`
|
|
273
|
+
- `validation.ok`, `validation.failed`
|
|
274
|
+
- `intent.creating`, `intent.created`, `intent.failed`
|
|
275
|
+
- `upload.queued`, `upload.started`, `upload.progress`, `upload.cursor`
|
|
276
|
+
- `upload.paused`, `upload.resumed`, `upload.canceled`
|
|
277
|
+
- `upload.completing`, `upload.completed`, `upload.error`
|
|
278
|
+
|
|
279
|
+
## Bundler requirement
|
|
280
|
+
|
|
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.
|
|
285
|
+
|
|
286
|
+
## Tests
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
bun run test
|
|
290
|
+
```
|
|
291
|
+
|
|
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.
|
|
294
|
+
|
|
295
|
+
## License
|
|
296
|
+
|
|
297
|
+
MIT
|
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};
|