@gentleduck/upload 0.2.5 → 0.2.6
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 +320 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/{core-BNxznRif.js → core-CMkrP9DX.js} +2 -2
- package/dist/{index-BXg9WIce.d.ts → index-C38Jmizu.d.ts} +51 -33
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +8 -47
- package/dist/react/index.js +1 -1
- package/dist/strategies/index.d.ts +5 -5
- package/package.json +1 -1
- package/src/__test__/effect-pool.test.ts +5 -0
- package/src/__test__/emitter.test.ts +85 -0
- package/src/__test__/fingerprint.test.ts +49 -0
- package/src/__test__/guards.test.ts +30 -0
- package/src/__test__/id.test.ts +14 -0
- package/src/__test__/persistence.test.ts +156 -0
- package/src/__test__/store-integration.test.ts +5 -0
- package/src/__test__/store-libs.test.ts +138 -0
- package/src/__test__/transport-libs.test.ts +42 -0
- package/src/__test__/transport.test.ts +182 -0
- package/src/core/client/client.ts +50 -0
- package/src/core/client/client.types.ts +184 -0
- package/src/core/contracts/contracts.types.ts +179 -0
- package/src/core/contracts/transport/transport.libs.ts +7 -32
- package/src/core/contracts/transport/transport.ts +5 -7
- package/src/core/engine/engine.types.ts +364 -0
- package/src/core/engine/internal-events.types.ts +3 -23
- package/src/core/engine/reducer.ts +5 -11
- package/src/core/engine/store/handlers/add-file.ts +6 -21
- package/src/core/engine/store/handlers/run-upload.ts +7 -7
- package/src/core/engine/store/handlers.ts +819 -0
- package/src/core/engine/store/store.libs.ts +17 -57
- package/src/core/engine/store/store.runtime.ts +10 -20
- package/src/core/engine/validation/file.ts +1 -1
- package/src/core/engine/validation.ts +216 -0
- package/src/core/utils/emitter/emitter.ts +143 -0
- package/src/react/upload-provider.tsx +7 -58
- package/src/react/uploader.types.ts +83 -0
- package/src/react/use-uploader.tsx +2 -5
- package/src/strategies/multipart/index.ts +9 -12
- package/src/strategies/post/index.ts +6 -46
package/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
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
|
+
Headless, framework-agnostic file-upload engine with a typed state machine, pluggable strategies, and React bindings.
|
|
27
|
+
|
|
28
|
+
Pure-reducer core, persistence-aware, resume-after-refresh, bounded effect concurrency, and zero coupling to a specific backend or UI library.
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { createUploadStore } from '@gentleduck/upload/core'
|
|
34
|
+
import { createStrategyRegistry } from '@gentleduck/upload/strategies'
|
|
35
|
+
|
|
36
|
+
type Intents = { post: { strategy: 'post'; fileId: string; url: string; fields: Record<string, string> } }
|
|
37
|
+
type Cursors = { post: { offset: number } }
|
|
38
|
+
type Purpose = 'attachment'
|
|
39
|
+
type Result = { fileId: string; key: string }
|
|
40
|
+
|
|
41
|
+
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
|
+
})
|
|
50
|
+
|
|
51
|
+
const store = createUploadStore<Intents, Cursors, Purpose, Result>({
|
|
52
|
+
strategies,
|
|
53
|
+
api: {
|
|
54
|
+
async createIntent({ filename }) {
|
|
55
|
+
return { strategy: 'post', fileId: filename, url: '/sign', fields: {} }
|
|
56
|
+
},
|
|
57
|
+
async complete({ fileId }) {
|
|
58
|
+
return { fileId, key: `attachments/${fileId}` }
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
config: { maxConcurrentUploads: 3, autoStart: ['attachment'] },
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
store.dispatch({ type: 'addFiles', files, purpose: 'attachment' })
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## React
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { UploadProvider, useUploader, createUploadFactory } from '@gentleduck/upload/react'
|
|
71
|
+
|
|
72
|
+
const useUploads = createUploadFactory(store)
|
|
73
|
+
|
|
74
|
+
function FileList() {
|
|
75
|
+
const { items, uploading, dispatch } = useUploads()
|
|
76
|
+
return (
|
|
77
|
+
<UploadProvider store={store}>
|
|
78
|
+
{items.map(item => (
|
|
79
|
+
<Row key={item.localId} item={item} onCancel={() => dispatch({ type: 'cancel', localId: item.localId })} />
|
|
80
|
+
))}
|
|
81
|
+
</UploadProvider>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A full working demo lives at `packages/registry-examples/src/upload/upload-1.tsx`.
|
|
87
|
+
|
|
88
|
+
## Features
|
|
89
|
+
|
|
90
|
+
- **Pure reducer** - command/event-driven state machine; effects are isolated
|
|
91
|
+
- **Typed end-to-end** - intents, cursors, purposes, and results are quad-generics threaded through the engine
|
|
92
|
+
- **Strategy registry** - ship POST/multipart out of the box, add TUS or your own
|
|
93
|
+
- **Resume after refresh** - IndexedDB / LocalStorage / Memory persistence adapters; cursor-aware re-binding
|
|
94
|
+
- **Retry policy** - exponential backoff with attempt escalation per phase (intent / upload / complete)
|
|
95
|
+
- **Bounded effect concurrency** - `effectConcurrency` separates side-effect parallelism from upload-byte parallelism
|
|
96
|
+
- **Batched bulk dispatch** - `startAll` / `pauseAll` / `cancelAll` collapse into a single reducer pass and notification
|
|
97
|
+
- **Rebind** - rebind a paused, persisted item to a fresh `File` after refresh, with fingerprint validation
|
|
98
|
+
- **Plugins + hooks** - `onInternalEvent`, `onPersistenceError`, custom plugins
|
|
99
|
+
- **No DOM coupling** - works in Workers, Node 22+, or the browser
|
|
100
|
+
|
|
101
|
+
## Subpath Exports
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { createUploadStore } from '@gentleduck/upload/core'
|
|
105
|
+
import { UploadProvider, useUploader } from '@gentleduck/upload/react'
|
|
106
|
+
import { createStrategyRegistry } from '@gentleduck/upload/strategies'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Persistence
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { createIndexedDBAdapter } from '@gentleduck/upload/core'
|
|
113
|
+
|
|
114
|
+
createUploadStore({
|
|
115
|
+
persistence: {
|
|
116
|
+
key: 'app:uploads',
|
|
117
|
+
version: 1,
|
|
118
|
+
adapter: createIndexedDBAdapter(),
|
|
119
|
+
isPurpose,
|
|
120
|
+
isIntent,
|
|
121
|
+
},
|
|
122
|
+
hooks: { onPersistenceError: (err) => toast.error(err.message) },
|
|
123
|
+
...
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Use `createIndexedDBAdapter()` / `createMemoryAdapter()` factories so each store owns its own connection.
|
|
128
|
+
|
|
129
|
+
Adapters throw a typed `PersistenceError` on failure (`quota_exceeded`, `unavailable`, `serialization_failed`, `transaction_failed`, `unknown`) that is routed to the optional `onPersistenceError` hook.
|
|
130
|
+
|
|
131
|
+
### Cross-tab IndexedDB contract
|
|
132
|
+
|
|
133
|
+
When a second tab opens the same database at a higher `persistence.version`, the existing tab's connection receives `onversionchange` and closes. The adapter resets its memoized handle so the next call reopens -- but at the OLD version. IndexedDB rejects that with `VersionError`, which the adapter surfaces as `PersistenceError('unavailable', operation, ...)`.
|
|
134
|
+
|
|
135
|
+
Application policy:
|
|
136
|
+
|
|
137
|
+
- Bump `persistence.version` in every tab/deployment in lockstep.
|
|
138
|
+
- Treat `onPersistenceError({ code: 'unavailable' })` after a known multi-tab scenario as a signal to reload the tab. The adapter is fixed at construction; the store does not support runtime adapter swap. If you want graceful fallback to in-memory persistence, the consumer must tear the store down and reconstruct with `createMemoryAdapter()`.
|
|
139
|
+
- Snapshot version mismatches at load time also surface as `PersistenceError('unavailable')` (via `deserializeSnapshot`'s `expectedVersion` fence).
|
|
140
|
+
|
|
141
|
+
### Handling persistence errors
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { createIndexedDBAdapter, type PersistenceError } from '@gentleduck/upload/core'
|
|
145
|
+
|
|
146
|
+
createUploadStore({
|
|
147
|
+
persistence: { key: 'app:uploads', version: 1, adapter: createIndexedDBAdapter(), isPurpose, isIntent },
|
|
148
|
+
hooks: {
|
|
149
|
+
onPersistenceError(err: PersistenceError) {
|
|
150
|
+
switch (err.code) {
|
|
151
|
+
case 'quota_exceeded':
|
|
152
|
+
toast.error('Storage full -- drop some uploads and try again.')
|
|
153
|
+
break
|
|
154
|
+
case 'unavailable':
|
|
155
|
+
// Private mode / SecurityError / no IndexedDB. Fall back to memory.
|
|
156
|
+
telemetry.warn('upload.persistence.unavailable', { op: err.operation })
|
|
157
|
+
break
|
|
158
|
+
case 'serialization_failed':
|
|
159
|
+
// Snapshot was tampered with; drop the bad key and start over.
|
|
160
|
+
adapter.clear('app:uploads').catch(() => undefined)
|
|
161
|
+
break
|
|
162
|
+
case 'transaction_failed':
|
|
163
|
+
case 'unknown':
|
|
164
|
+
telemetry.error('upload.persistence.error', { op: err.operation, message: err.message })
|
|
165
|
+
break
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
...
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The engine never aborts uploads on a persistence error -- it continues operating in-memory. Use the hook to surface the failure to the user or to swap adapters at runtime.
|
|
174
|
+
|
|
175
|
+
The hook may be sync or `async`; the engine tracks the returned promise and keeps the re-entrancy guard active until it settles, so dispatches issued inside an async hook cannot loop the failure path.
|
|
176
|
+
|
|
177
|
+
## Effect concurrency and watchdog
|
|
178
|
+
|
|
179
|
+
Side-effects (intent creation, finalize, checksum, retry sleep, multipart abort) run in a bounded worker pool:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
createUploadStore({
|
|
183
|
+
config: {
|
|
184
|
+
effectConcurrency: 8, // default: 8. Set to 1 for strict-sequential.
|
|
185
|
+
effectTimeoutMs: 60_000, // default: 60s per effect. 0 disables the watchdog.
|
|
186
|
+
},
|
|
187
|
+
...
|
|
188
|
+
})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Every effect receives an `AbortSignal`. When the watchdog fires the signal is aborted so well-behaved effects can exit cleanly. Effects that ignore the signal keep running in the background; the slot is released either way so the pool keeps draining. Retry-sleep effects that get cut short by the watchdog still dispatch the retry as long as the item is still in `error` -- the watchdog cannot strand an item permanently.
|
|
192
|
+
|
|
193
|
+
## Writing a custom strategy
|
|
194
|
+
|
|
195
|
+
A strategy is the thin layer that knows how to actually move bytes from a `File` to your storage using the intent shape your backend returns. Every strategy implements one `start()` and shares the same `Strategy.ICtx` contract:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import type { Strategy } from '@gentleduck/upload/core'
|
|
199
|
+
|
|
200
|
+
type Intents = { tus: { strategy: 'tus'; fileId: string; uploadUrl: string } }
|
|
201
|
+
type Cursors = { tus: { offset: number } }
|
|
202
|
+
type Purpose = 'attachment'
|
|
203
|
+
type Result = { fileId: string; key: string }
|
|
204
|
+
|
|
205
|
+
export const tusStrategy: Strategy.IStrategy<Intents, Cursors, Purpose, Result, 'tus'> = {
|
|
206
|
+
id: 'tus',
|
|
207
|
+
resumable: true,
|
|
208
|
+
async start({ file, intent, signal, transport, readCursor, persistCursor, reportProgress }) {
|
|
209
|
+
// 1. Resume from the last persisted cursor if there is one.
|
|
210
|
+
let offset = readCursor()?.offset ?? 0
|
|
211
|
+
|
|
212
|
+
while (offset < file.size) {
|
|
213
|
+
if (signal.aborted) throw { code: 'aborted', reason: signal.reason }
|
|
214
|
+
|
|
215
|
+
const chunk = file.slice(offset, offset + 1024 * 1024)
|
|
216
|
+
const res = await transport.send({
|
|
217
|
+
url: intent.uploadUrl,
|
|
218
|
+
method: 'PATCH',
|
|
219
|
+
headers: {
|
|
220
|
+
'Upload-Offset': String(offset),
|
|
221
|
+
'Content-Type': 'application/offset+octet-stream',
|
|
222
|
+
},
|
|
223
|
+
body: chunk,
|
|
224
|
+
signal,
|
|
225
|
+
onProgress: ({ uploadedBytes }) => {
|
|
226
|
+
reportProgress({ uploadedBytes: offset + uploadedBytes, totalBytes: file.size })
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
offset += chunk.size
|
|
231
|
+
|
|
232
|
+
// 2. Persist resumable progress so a refresh can rebind + resume.
|
|
233
|
+
persistCursor({ offset })
|
|
234
|
+
|
|
235
|
+
if (!res.ok) throw new Error(`TUS PATCH failed: ${res.status}`)
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Contract:
|
|
242
|
+
|
|
243
|
+
- **`id`** must match the `intent.strategy` your backend returns.
|
|
244
|
+
- **`resumable`** is metadata for UIs; the engine doesn't enforce it.
|
|
245
|
+
- **`start()`** is awaited; resolve normally → engine transitions to `completing`. Throw → engine routes the error through `errorNormalizer` + `retryPolicy`.
|
|
246
|
+
- Honor **`signal`** -- abort cleanly when it fires (throw `{ code: 'aborted', reason: signal.reason }`).
|
|
247
|
+
- Call **`reportProgress`** as often as makes sense; it is throttled by `config.progressThrottleMs`.
|
|
248
|
+
- Call **`persistCursor`** at every safe resume boundary; the value is restored on the next `rebind` after refresh.
|
|
249
|
+
- Use **`api`** for backend round-trips that depend on the intent (e.g. multipart `signPart` / `completeMultipart`).
|
|
250
|
+
|
|
251
|
+
Register the strategy and reference it from `createIntent`:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const strategies = createStrategyRegistry<Intents, Cursors, Purpose, Result>()
|
|
255
|
+
strategies.set(tusStrategy)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Architecture
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
@gentleduck/upload
|
|
262
|
+
├── core/
|
|
263
|
+
│ ├── client/ - Client.IUploadConfig, Client.IUploadHooks, Client.IUploadPlugin
|
|
264
|
+
│ ├── contracts/ - intents, cursors, transport, errors, strategy
|
|
265
|
+
│ ├── engine/ - reducer + commands + internal events
|
|
266
|
+
│ │ └── store/ - runtime, dispatch, scheduler, handlers
|
|
267
|
+
│ ├── persistence/ - Memory / LocalStorage / IndexedDB adapters + serializer
|
|
268
|
+
│ └── utils/ - emitter, id, fingerprint, guards, async
|
|
269
|
+
├── strategies/
|
|
270
|
+
│ ├── post/ - single-PUT/POST strategy
|
|
271
|
+
│ ├── multipart/ - S3-style multipart with partSize + ETag tracking
|
|
272
|
+
│ └── registry/ - createStrategyRegistry
|
|
273
|
+
└── react/ - UploadProvider, useUploader, createUploadFactory
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Commands
|
|
277
|
+
|
|
278
|
+
| Command | Purpose |
|
|
279
|
+
|---|---|
|
|
280
|
+
| `addFiles` | Validate + insert; schedules checksum + intent |
|
|
281
|
+
| `start` / `startAll` | Move ready items to queued (batched) |
|
|
282
|
+
| `pause` / `pauseAll` | Abort inflight, persist cursor (batched) |
|
|
283
|
+
| `resume` | Re-queue a paused item |
|
|
284
|
+
| `cancel` / `cancelAll` | Abort everything and mark canceled (batched) |
|
|
285
|
+
| `retry` | Re-attempt the failed phase, bumping the attempt counter |
|
|
286
|
+
| `rebind` | Re-attach a fresh `File` after refresh (validated by fingerprint) |
|
|
287
|
+
| `remove` | Drop the item from state |
|
|
288
|
+
|
|
289
|
+
## Events
|
|
290
|
+
|
|
291
|
+
Subscribe via `store.on(type, cb)`. Highlights:
|
|
292
|
+
|
|
293
|
+
- `file.added`, `file.rejected`
|
|
294
|
+
- `validation.ok`, `validation.failed`
|
|
295
|
+
- `intent.creating`, `intent.created`, `intent.failed`
|
|
296
|
+
- `upload.queued`, `upload.started`, `upload.progress`, `upload.cursor`
|
|
297
|
+
- `upload.paused`, `upload.canceled`, `upload.completing`, `upload.completed`, `upload.error`
|
|
298
|
+
- `rebind.ok`, `rebind.failed`
|
|
299
|
+
|
|
300
|
+
## Bundler requirement
|
|
301
|
+
|
|
302
|
+
The engine references `process.env.NODE_ENV` to gate dev-only invariant
|
|
303
|
+
warnings (reducer no-op check, listener-throw fallback logging, strategy-
|
|
304
|
+
overwrite warn). Vite / Webpack / Rspack / esbuild / Rollup replace this at
|
|
305
|
+
build time and ship a single branch into production. Pure-ESM consumers that
|
|
306
|
+
load the source directly without a bundler must polyfill
|
|
307
|
+
`globalThis.process = { env: { NODE_ENV: 'production' } }` before importing
|
|
308
|
+
the package, otherwise `process` is undefined at module load.
|
|
309
|
+
|
|
310
|
+
## Tests
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
bun run test
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Vitest suite covers the reducer state machine, the persistence layer, all utility modules, the rebind handler, and a full smoke upload through the React-less store API.
|
|
317
|
+
|
|
318
|
+
## License
|
|
319
|
+
|
|
320
|
+
MIT
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { a as AnyCursor, c as WithSignal, d as IntentBase, f as IntentMap, i as UploadTransport, l as UploadResultBase, n as StrategyRegistry, o as CursorMap, p as StrategyKey, r as UploadStrategy, s as UploadApi, t as StrategyCtx, u as AnyIntent } from "../strategy.types-gg9ZStRe.js";
|
|
2
|
-
import { $ as RejectReason, A as serializeSnapshot, B as UploadItem, C as StoreOptions, D as createTypedEmitter, E as UploadEventMap, F as PersistedUploadItem, G as UploadCommand, H as UploadProgress, I as PersistenceAdapter, J as resolveUploadConfig, K as UploadConfig, L as UploadState, M as LocalStorageAdapter, N as IndexedDBAdapter, O as TypedEmitter, P as PersistedSnapshot, Q as BuiltInUploadError, R as createReducer, S as PersistenceOptions, T as UploadStore, U as UploadCompletionKind, V as UploadPhase, W as UploadOutcome, X as createXHRTransport, Y as RetryDecision, Z as FileFingerprint, _ as UploadHooks, a as DEFAULT_MAX_ATTEMPTS, b as DeserializeContext, c as DEFAULT_PROGRESS_THROTTLE_MS, d as sleep, et as UploadError, f as
|
|
2
|
+
import { $ as RejectReason, A as serializeSnapshot, B as UploadItem, C as StoreOptions, D as createTypedEmitter, E as UploadEventMap, F as PersistedUploadItem, G as UploadCommand, H as UploadProgress, I as PersistenceAdapter, J as resolveUploadConfig, K as UploadConfig, L as UploadState, M as LocalStorageAdapter, N as IndexedDBAdapter, O as TypedEmitter, P as PersistedSnapshot, Q as BuiltInUploadError, R as createReducer, S as PersistenceOptions, T as UploadStore, U as UploadCompletionKind, V as UploadPhase, W as UploadOutcome, X as createXHRTransport, Y as RetryDecision, Z as FileFingerprint, _ as UploadHooks, a as DEFAULT_MAX_ATTEMPTS, b as DeserializeContext, c as DEFAULT_PROGRESS_THROTTLE_MS, d as sleep, et as UploadError, f as validateFile, g as createUploadClient, h as UploadClient, i as fingerprintMatches, j as MemoryAdapter, k as deserializeSnapshot, l as DEFAULT_RETRY_DELAY_BASE_MS, m as validateIntent, n as isRecord, nt as UploadValidationRules, o as DEFAULT_MAX_CONCURRENT_UPLOADS, p as validateFileList, q as UploadConfigInput, r as computeFingerprint, s as DEFAULT_MAX_ITEMS, t as generateId, tt as UploadErrorBase, u as DEFAULT_RETRY_DELAY_MAX_MS, v as UploadPlugin, w as StoreRuntime, x as InflightUpload, y as createUploadStore, z as InternalEvent } from "../index-C38Jmizu.js";
|
|
3
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 };
|
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,d as o,f as s,g as c,h as l,i as u,l as d,m as f,n as p,o as m,p as h,r as g,s as _,t as v,u as y,v as b,w as x,x as S,y as C}from"../core-
|
|
1
|
+
import{C as e,S as t,_ as n,a as r,b as i,c as a,d as o,f as s,g as c,h as l,i as u,l as d,m as f,n as p,o as m,p as h,r as g,s as _,t as v,u as y,v as b,w as x,x as S,y as C}from"../core-CMkrP9DX.js";import{t as w}from"../async-DxRWu4le.js";export{C as DEFAULT_MAX_ATTEMPTS,i as DEFAULT_MAX_CONCURRENT_UPLOADS,S as DEFAULT_MAX_ITEMS,t as DEFAULT_PROGRESS_THROTTLE_MS,e as DEFAULT_RETRY_DELAY_BASE_MS,x as DEFAULT_RETRY_DELAY_MAX_MS,d as IndexedDBAdapter,a as LocalStorageAdapter,_ as MemoryAdapter,o as computeFingerprint,g as createReducer,u as createTypedEmitter,v as createUploadClient,p as createUploadStore,y as createXHRTransport,r as deserializeSnapshot,s as fingerprintMatches,n as generateId,c as isRecord,b as resolveUploadConfig,m as serializeSnapshot,w as sleep,h as validateFile,f as validateFileList,l as validateIntent};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./async-DxRWu4le.js";const t=500,n=1e4,r=100,i=3,a=3,o=100;function s(e){return{maxConcurrentUploads:Math.max(1,e?.maxConcurrentUploads??3),autoStart:e?.autoStart,progressThrottleMs:Math.max(0,e?.progressThrottleMs??100),validation:e?.validation??{},maxAttempts:Math.max(1,e?.maxAttempts??3),retryPolicy:e?.retryPolicy,maxItems:Math.max(1,e?.maxItems??100),completedItemTTL:e?.completedItemTTL}}function c(){return`upload_${Date.now()}_${Math.random().toString(36).slice(2)}`}function l(e,t,n){let r=n.validation?.[t];if(!r)return null;if(
|
|
2
|
-
`).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 ie(e){return/^https?:\/\//i.test(e)}function ae(e){let t=e.getResponseHeader(`ETag`)??e.getResponseHeader(`etag`);if(t)return t.replace(/"/g,``)}function j(e){return new Promise((t,n)=>{if(!e.url||typeof e.url!=`string`){n(Error(`UploadTransport: missing url`));return}if(!ie(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(A(O(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(A(O(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({headers:re(r),etag:ae(r)}):()=>n(Error(`Upload failed with status ${e}`)))},r.onerror=()=>a(()=>n(ne(r,`Network error during upload`))),r.onabort=()=>a(()=>n(A(e.signal?.aborted?O(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 M(){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 j({method:`POST`,url:e.url,body:t,signal:e.signal,onProgress:e.onProgress})).headers}},async put(e){let t=await j({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 j({method:`PATCH`,url:e.url,body:e.body,signal:e.signal,headers:e.headers,onProgress:e.onProgress})).headers}}}}const N=`snapshots`;function P(){return new Promise((e,t)=>{let n=indexedDB.open(`upload-engine`,1);n.onupgradeneeded=()=>{let e=n.result;e.objectStoreNames.contains(N)||e.createObjectStore(N)},n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error)})}function F(e){return new Promise((t,n)=>{e.onsuccess=()=>t(e.result),e.onerror=()=>n(e.error)})}function I(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 L={async load(e){if(typeof indexedDB>`u`)return null;let t=(await P()).transaction(N,`readonly`),n=t.objectStore(N);try{let r=await F(n.get(e));return await I(t),r??null}catch{try{await I(t)}catch{}return null}},async save(e,t){if(typeof indexedDB>`u`)return;let n=(await P()).transaction(N,`readwrite`),r=n.objectStore(N);try{r.put(t,e),await I(n)}catch{try{n.abort()}catch{}}},async clear(e){if(typeof indexedDB>`u`)return;let t=(await P()).transaction(N,`readwrite`),n=t.objectStore(N);try{n.delete(e),await I(t)}catch{try{t.abort()}catch{}}}},R={load(e){if(typeof localStorage>`u`)return null;let t=localStorage.getItem(e);if(!t)return null;try{return 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)}},z=new Map,B={load(e){return z.get(e)??null},save(e,t){z.set(e,t)},clear(e){z.delete(e)}};function V(e,t){let n={};for(let t of e.items.values()){if(!v(t)||t.phase===`completed`||t.phase===`canceled`||t.phase===`error`)continue;let e=b(t)?t.cursor:void 0,r=`progress`in t&&t.progress?{uploadedBytes:t.progress.uploadedBytes,totalBytes:t.progress.totalBytes,pct:t.progress.pct}:void 0,i={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:e,progress:r};n[i.id]=i}return{version:t,createdAt:Date.now(),items:n}}function H(e,t){if(!t.isPurpose||!t.isIntent||!U(e))return null;let n=new Map;for(let r of Object.values(e.items)){let i=W(r);if(!i||!t.isPurpose(i.purpose)||!t.isIntent(i.intent))continue;let a=i.intent.strategy;if(typeof a!=`string`||!t.hasStrategy(a)||!i.cursor||!K(i.cursor,t.hasStrategy)||i.cursor.strategy!==a)continue;let o=i.progress?.totalBytes??i.file.size,s=i.progress?.uploadedBytes??0,c=typeof i.progress?.pct==`number`?i.progress.pct:o>0?Math.min(100,Math.max(0,s/o*100)):0;n.set(i.id,{phase:`paused`,localId:i.id,purpose:i.purpose,fingerprint:{name:i.file.name,size:i.file.size,type:i.file.type,lastModified:i.file.lastModified,checksum:i.file.checksum},intent:i.intent,cursor:i.cursor,progress:{uploadedBytes:s,totalBytes:o,pct:c},pausedAt:Date.now(),createdAt:e.createdAt??Date.now(),file:void 0})}return{items:n}}function U(e){return!u(e)||typeof e.version!=`number`||typeof e.createdAt!=`number`?!1:u(e.items)}function W(e){if(!u(e))return null;let t=typeof e.id==`string`?e.id:null,n=typeof e.purpose==`string`?e.purpose:null,r=typeof e.status==`string`?e.status:null,i=e.intent;if(!t||!n||!r||!u(e.file))return null;let a=typeof e.file.name==`string`?e.file.name:null,o=typeof e.file.size==`number`?e.file.size:null,s=typeof e.file.type==`string`?e.file.type:null,c=typeof e.file.lastModified==`number`?e.file.lastModified:null,l=typeof e.file.checksum==`string`?e.file.checksum:void 0;if(!a||o===null||!s||c===null)return null;let d=G(e.progress);return{id:t,purpose:n,status:r,file:{name:a,size:o,type:s,lastModified:c,checksum:l},intent:i,cursor:e.cursor,progress:d}}function G(e){if(!u(e)||typeof e.uploadedBytes!=`number`||typeof e.totalBytes!=`number`)return;let t=typeof e.pct==`number`?e.pct:void 0;return{uploadedBytes:e.uploadedBytes,totalBytes:e.totalBytes,pct:t}}function K(e,t){return!(!u(e)||typeof e.strategy!=`string`||!t(e.strategy))}function q(){let e={};return{on(t,n){let r=e[t];return r||(r=new Set,e[t]=r),r.add(n),()=>{let r=e[t];r&&(r.delete(n),r.size===0&&delete e[t])}},off(t,n){let r=e[t];return r?(r.delete(n),r.size===0&&delete e[t],()=>{}):()=>{}},emit(t,n){let r=e[t];r&&r.forEach(e=>{try{e(n)}catch(e){typeof window<`u`&&process.env.NODE_ENV===`development`&&console.error(`[UploadEngine] Error in event listener for "${t}":`,e)}})}}}function J(){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});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});break}case`pause`:{let e=n.get(t.localId);if(!e)break;e.phase===`queued`&&r(t.localId,{...e,phase:`ready`});break}case`cancel`:{let e=n.get(t.localId);if(!e||e.phase===`completed`||e.phase===`canceled`)break;r(t.localId,Y(e,i));break}case`retry`:{let e=n.get(t.localId);if(!e||e.phase!==`error`||!e.retryable||!e.file)break;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}):r(t.localId,{...e,file:e.file,intent:e.intent,phase:`ready`}):r(t.localId,{...e,phase:`creating_intent`,file:e.file,attempt:(e.attempt??1)+1});break}case`rebind`:{let e=n.get(t.localId);if(!e)break;e.phase===`paused`&&!e.file&&m(p(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});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||e.phase!==`validating`)break;r(t.localId,{...e,phase:`creating_intent`,attempt:1});break}case`validation.failed`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`validating`)break;r(e.localId,{...a,phase:`error`,error:{code:`validation_failed`,reason:e.reason,message:String(e.reason)},retryable:!1,attempt:1,failedAt:i});break}case`intent.ok`:{let e=t,i=n.get(e.localId);if(!i||i.phase!==`creating_intent`)break;r(e.localId,{...i,phase:`ready`,intent:e.intent});break}case`intent.failed`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`creating_intent`)break;r(e.localId,{...a,phase:`error`,error:e.error,retryable:e.retryable,failedAt:i});break}case`upload.begin`:{let e=t,i=n.get(e.localId);if(!i||i.phase!==`queued`)break;let a=i.file.size,o=i.progress,s=o?{...o,totalBytes:a,pct:X(o.uploadedBytes,a)}:{uploadedBytes:0,totalBytes:a,pct:0};r(e.localId,{...i,phase:`uploading`,progress:s,startedAt:e.startedAt});break}case`upload.progress`:{let e=t,i=n.get(e.localId);if(!i||i.phase!==`uploading`)break;let a={uploadedBytes:e.uploadedBytes,totalBytes:e.totalBytes,pct:X(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||a.phase!==`uploading`)break;r(e.localId,{...a,phase:`completing`,progress:{uploadedBytes:a.file.size,totalBytes:a.file.size,pct:100},completingAt:i});break}case`upload.failed`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`uploading`)break;r(e.localId,{...a,phase:`error`,error:e.error,retryable:e.retryable,attempt:1,failedAt:i});break}case`paused`:{let e=t,i=n.get(e.localId);if(!i||i.phase!==`uploading`)break;r(e.localId,{...i,phase:`paused`,cursor:e.cursor,pausedAt:e.pausedAt});break}case`canceled`:{let e=t,i=n.get(e.localId);if(!i)break;r(e.localId,Y(i,e.canceledAt));break}case`complete.ok`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`completing`)break;r(e.localId,{...a,phase:`completed`,completedBy:`upload`,result:e.result,completedAt:i});break}case`dedupe.ok`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`validating`)break;r(e.localId,{phase:`completed`,localId:a.localId,file:a.file,purpose:a.purpose,fingerprint:a.fingerprint,result:e.result,completedBy:`dedupe`,completedAt:i,createdAt:a.createdAt});break}case`complete.failed`:{let e=t,a=n.get(e.localId);if(!a||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});break}}return{items:n}}}function Y(e,t){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}}function X(e,t){return t<=0?0:Math.min(100,e/t*100)}function Z(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++)a.push(e[n][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}}async function oe(t,n){let r=t.state.items.get(n);if(!r||r.phase!==`creating_intent`||t.inflightIntents.has(n))return;let i=new AbortController;t.inflightIntents.set(n,i);try{let e=await t.opts.api.createIntent({purpose:r.purpose,contentType:r.file.type||`application/octet-stream`,size:r.file.size,filename:r.file.name,checksum:r.fingerprint.checksum},{signal:i.signal});t.inflightIntents.delete(n);let a=t.state.items.get(n);if(!a||a.phase!==`creating_intent`)return;let o=d(e,e.strategy);if(o){let e={code:`validation_failed`,message:`Invalid intent from backend: ${o.message}`,cause:o,retryable:!1};t.applyInternal({type:`intent.failed`,localId:n,error:e,retryable:!1});return}t.applyInternal({type:`intent.ok`,localId:n,intent:e})}catch(a){if(t.inflightIntents.delete(n),i.signal.aborted)return;let o=_(a,t.opts.errorNormalizer),s={...o,message:`${o.message} (file: ${r.fingerprint.name}, size: ${r.file.size} bytes, purpose: ${r.purpose})`},c=S(t.opts.config,{phase:`intent`,attempt:r.attempt,error:s});t.applyInternal({type:`intent.failed`,localId:n,error:s,retryable:c.retryable}),c.retryable&&c.delayMs!==void 0&&t.enqueueEffect(async()=>{await e(c.delayMs),t.dispatch({type:`retry`,localId:n})})}}async function se(t,n){let r=t.state.items.get(n);if(!r||r.phase!==`completing`||t.inflightCompletes.has(n))return;let i=new AbortController;t.inflightCompletes.set(n,i);try{let e=await t.opts.api.complete({fileId:r.intent.fileId},{signal:i.signal});t.inflightCompletes.delete(n);let a=t.state.items.get(n);if(!a||a.phase!==`completing`)return;t.applyInternal({type:`complete.ok`,localId:n,result:e})}catch(r){if(t.inflightCompletes.delete(n),i.signal.aborted)return;let a=_(r,t.opts.errorNormalizer),o=t.state.items.get(n),s=o?{...a,message:`${a.message} (file: ${o.fingerprint.name}, fileId: ${(v(o)&&o.intent?.fileId)??`unknown`})`}:a,c=o&&`attempt`in o?o.attempt??1:1,l=S(t.opts.config,{phase:`complete`,attempt:c,error:s});t.applyInternal({type:`complete.failed`,localId:n,error:s,retryable:l.retryable}),l.retryable&&l.delayMs!==void 0&&t.enqueueEffect(async()=>{await e(l.delayMs),t.dispatch({type:`retry`,localId:n})})}}async function ce(t,n){let r=t.state.items.get(n);if(!r||r.phase!==`uploading`||!v(r)){t.inflightUploads.delete(n);return}let i=t.inflightUploads.get(n);if(i||(i={controller:new AbortController,mode:`normal`,started:!1},t.inflightUploads.set(n,i)),i.started)return;i.started=!0;let a=i.controller,o=()=>{let e=t.state.items.get(n);!e||e.phase===`canceled`||t.applyInternal({type:`canceled`,localId:n,canceledAt:Date.now()})},s=e=>{let r=t.state.items.get(n);!r||r.phase===`paused`||t.applyInternal({type:`paused`,localId:n,cursor:e,pausedAt:Date.now()})};if(a.signal.aborted){if(t.inflightUploads.delete(n),i.mode===`pause`){let e=t.state.items.get(n),i;r&&v(r)&&(i=r.intent.strategy);let a=(e&&b(e)?e.cursor:void 0)||(i?{strategy:i,value:void 0}:void 0);a&&s(a);return}if(i.mode===`cancel`){o();return}return}let c=r.intent.strategy;if(!t.opts.strategies.has(c)){t.inflightUploads.delete(n);let e={code:`strategy_missing`,message:`Strategy not found: ${c}`,strategy:String(c),retryable:!1};t.applyInternal({type:`upload.failed`,localId:n,error:e,retryable:!1});return}let l=t.opts.strategies.get(c);if(!l){t.inflightUploads.delete(n);let e={code:`strategy_missing`,message:`Strategy not found: ${r.intent.strategy}`,strategy:String(r.intent.strategy),retryable:!1};t.applyInternal({type:`upload.failed`,localId:n,error:e,retryable:!1});return}let u=0,d=t.opts.config.progressThrottleMs;try{if(await l.start({file:r.file,intent:r.intent,signal:a.signal,transport:t.opts.transport,api:t.opts.api,readCursor:()=>{let e=t.state.items.get(n);if(!(!e||!b(e)||!e.cursor)&&e.cursor.strategy===c)return e.cursor.value},persistCursor:e=>{let r={strategy:c,value:e};t.applyInternal({type:`cursor.updated`,localId:n,cursor:r})},reportProgress:e=>{let r=Date.now();r-u<d||(t.applyInternal({type:`upload.progress`,localId:n,uploadedBytes:e.uploadedBytes,totalBytes:e.totalBytes}),u=r)}}),t.inflightUploads.delete(n),i?.mode===`cancel`){o();return}if(a.signal.aborted&&i?.mode===`pause`){let e=t.state.items.get(n);s((e&&b(e)?e.cursor:void 0)||{strategy:c,value:void 0});return}t.applyInternal({type:`upload.ok`,localId:n})}catch(r){if(t.inflightUploads.delete(n),a.signal.aborted){if(i?.mode===`pause`){let e=t.state.items.get(n);s((e&&b(e)?e.cursor:void 0)||{strategy:c,value:void 0});return}if(i?.mode===`cancel`){o();return}o();return}let l=_(r,t.opts.errorNormalizer),u=t.state.items.get(n),d=u?{...l,message:`${l.message} (file: ${u.fingerprint.name}, size: ${(y(u)&&u.file?.size)??0} bytes, phase: ${u.phase})`}:l,f=S(t.opts.config,{phase:`upload`,attempt:1,error:d});t.applyInternal({type:`upload.failed`,localId:n,error:d,retryable:f.retryable}),f.retryable&&f.delayMs!==void 0&&t.enqueueEffect(async()=>{await e(f.delayMs),t.dispatch({type:`retry`,localId:n})})}}function le(e){if(!e.scheduling){e.scheduling=!0;try{ue(e),fe(e),de(e)}finally{e.scheduling=!1}}}function ue(e){for(let t of e.state.items.values())t.phase===`creating_intent`&&(e.inflightIntents.has(t.localId)||e.enqueueEffect(()=>oe(e,t.localId)))}function de(e){for(let t of e.state.items.values())t.phase===`completing`&&(e.inflightCompletes.has(t.localId)||e.enqueueEffect(()=>se(e,t.localId)))}function fe(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`&&g(e.opts,t.purpose)&&e.applyCommand({type:`start`,localId:t.localId});if(n>=t)return;let r=Array.from(e.state.items.values()).filter(pe);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()}),ce(e,t.localId)}}function pe(e){return e.phase===`queued`}function me(e){let t=Q(e),n=t.persistence,r=null,i=Math.max(0,n?.debounceMs??200),a=async()=>{if(n)try{let e=(n.serialize??V)(s.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){typeof window<`u`&&process.env.NODE_ENV===`development`&&console.warn(`[UploadEngine] persistence flush failed:`,e)}},o=()=>{if(n){if(i===0){a();return}r&&clearTimeout(r),r=setTimeout(()=>void a(),i)}},s={opts:t,state:t.initialState??{items:new Map},listeners:new Set,emitter:q(),reduce:J(),inflightUploads:new Map,inflightIntents:new Map,inflightCompletes:new Map,effectQueue:[],processingEffects:!1,scheduling:!1,notify(){s.listeners.forEach(e=>{e()})},scheduleWork(){le(s)},applyInternal(e){let t=s.state;s.state=s.reduce(t,e),s.opts.hooks?.onInternalEvent?.(e,s.state),he(s,e);let n=Z(s.opts,s.state);n&&(s.state=n),o(),s.notify(),s.scheduleWork()},applyCommand(e){let t=s.state,n=s.reduce(t,e);s.state=n,ge(s,e,t,n);let r=Z(s.opts,s.state);r&&(s.state=r),o(),s.notify(),s.scheduleWork()},enqueueEffect(e){s.effectQueue.push(e),s.processingEffects||s.processEffects()},async processEffects(){for(s.processingEffects=!0;s.effectQueue.length>0;){let e=s.effectQueue.shift();if(e)try{await e()}catch(e){if(typeof window<`u`&&process.env.NODE_ENV===`development`){let t=e instanceof Error?e:Error(String(e)),n={queueLength:s.effectQueue.length,processing:s.processingEffects,timestamp:new Date().toISOString()};console.error(`[UploadEngine] Effect error:`,t,n)}}}s.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??H)(e,{isPurpose:n.isPurpose,isIntent:n.isIntent,hasStrategy:t.strategies.has});if(r){if(s.state.items.size===0)s.state=r;else{let e=new Map(s.state.items);for(let[t,n]of r.items.entries())e.has(t)||e.set(t,n);s.state={...s.state,items:e}}s.notify(),s.scheduleWork()}}).catch(()=>void 0)}catch{}return s}function Q(e){return{...e,config:s(e.config),transport:e.transport??M()}}function he(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 ge(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 $(e){let t=me(e);t.dispatch=e=>E(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){typeof window<`u`&&process.env.NODE_ENV===`development`&&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 _e(t,e)}}}function _e(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`}),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`})):new Promise(a=>{let o=()=>{i(),n.size===0&&(e.listeners.delete(o),a(t.map(e=>r.get(e)??{localId:e,status:`missing`})))};e.listeners.add(o)})}function ve(e){return $(e)}export{t as C,r as S,c as _,H as a,a as b,R as c,p as d,m as f,l as g,u as h,q as i,L as l,d as m,$ as n,V as o,f as p,J as r,B as s,ve as t,M as u,s as v,n as w,o as x,i as y};
|
|
1
|
+
import{t as e}from"./async-DxRWu4le.js";const t=500,n=1e4,r=100,i=3,a=3,o=100;function s(e){return{maxConcurrentUploads:Math.max(1,e?.maxConcurrentUploads??3),autoStart:e?.autoStart,progressThrottleMs:Math.max(0,e?.progressThrottleMs??100),validation:e?.validation??{},maxAttempts:Math.max(1,e?.maxAttempts??3),retryPolicy:e?.retryPolicy,maxItems:Math.max(1,e?.maxItems??100),completedItemTTL:e?.completedItemTTL}}function c(){return`upload_${Date.now()}_${Math.random().toString(36).slice(2)}`}function l(e){return!!e&&typeof e==`object`}function u(e,t,n){if(e.size===0)return{code:`empty_file`};let r=n.validation?.[t];if(!r)return null;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 d(e,t,n,r=0){let i=n.validation?.[t],a=[],o=[],s=i?.maxFiles,c=s===void 0?1/0:Math.max(0,s-r);for(let r of e){if(c<=0){let e=s??0;o.push({file:r,reason:{code:`too_many_files`,max:e}});continue}let e=u(r,t,n);if(e){o.push({file:r,reason:e});continue}a.push(r),--c}return{valid:a,rejected:o}}function f(e,t,n){if(!l(e))return Error(`Invalid intent: must be an object`);if(typeof e.strategy!=`string`)return Error(`Invalid intent: missing or invalid strategy field`);if(e.strategy!==t)return Error(`Invalid intent: strategy mismatch (expected ${t}, got ${e.strategy})`);if(typeof e.fileId!=`string`||!e.fileId)return Error(`Invalid intent: missing or invalid fileId`);if(t===`post`){if(typeof e.url!=`string`||!e.url)return Error(`Invalid post intent: missing or invalid url`);try{let t=new URL(e.url);if(![`http:`,`https:`].includes(t.protocol))return Error(`Invalid post intent: url must use http or https protocol`)}catch{return Error(`Invalid post intent: url is not a valid URL`)}if(!e.fields||typeof e.fields!=`object`)return Error(`Invalid post intent: missing or invalid fields`)}else if(t===`multipart`){if(typeof e.uploadId!=`string`||!e.uploadId)return Error(`Invalid multipart intent: missing or invalid uploadId`);if(typeof e.partSize!=`number`||e.partSize<=0)return Error(`Invalid multipart intent: missing or invalid partSize`);if(`partCount`in e&&e.partCount!==void 0&&(typeof e.partCount!=`number`||!Number.isFinite(e.partCount)||e.partCount<=0))return Error(`Invalid multipart intent: partCount must be a positive number`);if(typeof n==`number`&&typeof e.partCount==`number`&&e.partCount>0){let t=e.partSize,r=e.partCount,i=r*t,a=(r-1)*t;if(i<n)return Error(`Invalid multipart intent: partCount * partSize (${i}) is smaller than file size (${n})`);if(a>=n&&r>1)return Error(`Invalid multipart intent: partCount (${r}) exceeds what file size (${n}) needs at partSize ${t}`)}if(`parts`in e&&e.parts!==void 0&&!Array.isArray(e.parts))return Error(`Invalid multipart intent: parts must be an array if provided`)}return null}function p(e){return{name:e.name,size:e.size,type:e.type,lastModified:e.lastModified}}function m(e,t){return e.name===t.name&&e.size===t.size&&e.lastModified===t.lastModified}async function h(e){let t=await e.arrayBuffer(),n=await crypto.subtle.digest(`SHA-256`,t);return Array.from(new Uint8Array(n)).map(e=>e.toString(16).padStart(2,`0`)).join(``)}function g(e,t){let n=e.config?.autoStart;return n===void 0?!1:Array.isArray(n)?n.includes(t):typeof n==`function`?n(t):!1}function _(e,t){if(t)return t(e);if(x(e))return{code:`aborted`,message:`Upload aborted`,reason:String(e.reason??`unknown`),retryable:!1};let n=l(e)&&typeof e.message==`string`?e.message:`Unknown error`;if(String(n).toLowerCase().includes(`network`)||String(n).toLowerCase().includes(`fetch`))return{code:`network`,message:String(n),cause:e,retryable:!0};if(l(e)&&(typeof e.status==`number`||typeof e.statusCode==`number`)){let t=typeof e.status==`number`?e.status:e.statusCode,r=t>=500||t===429;return{code:`http`,status:t,message:String(n),cause:e,retryable:r}}return{code:`unknown`,message:String(n),cause:e,retryable:!1}}function v(e){return`intent`in e&&!!e.intent}function y(e){return`file`in e&&!!e.file}function b(e){return`cursor`in e}function ee(e){return l(e)&&e.strategy===`multipart`&&typeof e.fileId==`string`&&typeof e.uploadId==`string`}function x(e){return l(e)&&e.code===`aborted`}function S(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 r=e.maxAttempts;return t.attempt<r?{retryable:!0,delayMs:Math.min(n,500*2**(t.attempt-1))}:{retryable:!1}}function C(e,t,n){let r=Array.from(e.state.items.values()).filter(e=>e.purpose===n&&e.phase!==`canceled`).length,{valid:i,rejected:a}=d(t,n,e.opts.config,r);for(let{file:t,reason:n}of a)e.emitter.emit(`file.rejected`,{reason:n,file:t});let o=Date.now(),s=[];for(let t of i){let r=c(),i=(e.opts.fingerprint??p)(t);s.push({localId:r,purpose:n,file:t,fingerprint:i,createdAt:o}),e.enqueueEffect(async()=>{let a;try{a=await h(t);let n={...i,checksum:a};e.applyInternal({type:`fingerprint.updated`,localId:r,fingerprint:n})}catch(e){typeof window<`u`&&process.env.NODE_ENV===`development`&&console.warn(`[UploadEngine] Failed to calculate checksum:`,e)}if(a&&e.opts.api.findByChecksum)try{let t=await e.opts.api.findByChecksum({checksum:a,purpose:n});if(t){let n=e.state.items.get(r);if(n&&n.phase===`validating`){e.applyInternal({type:`dedupe.ok`,localId:r,result:t});return}}}catch(e){typeof window<`u`&&process.env.NODE_ENV===`development`&&console.warn(`[UploadEngine] Failed to check for existing file:`,e)}let o=e.opts.validateFile?.(t,n);if(o){e.applyInternal({type:`validation.failed`,localId:r,reason:o});return}let s=u(t,n,e.opts.config);if(s){e.applyInternal({type:`validation.failed`,localId:r,reason:s});return}e.applyInternal({type:`validation.ok`,localId:r})})}s.length>0&&e.applyInternal({type:`files.added`,items:s})}function w(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&&v(n)&&n.intent.strategy===`multipart`&&e.opts.api.multipart?.abort){let t=n.intent;ee(t)&&e.enqueueEffect(async()=>{try{let n=e.opts.api.multipart?.abort;n&&await n({fileId:t.fileId,uploadId:t.uploadId})}catch{}})}}function T(e,t){let n=e.inflightUploads.get(t);if(n){n.mode=`pause`,n.controller.abort({reason:`pause`});return}}function E(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`){C(e,t.files,t.purpose);return}if(t.type===`pause`){T(e,t.localId),e.applyCommand(t);return}if(t.type===`cancel`){w(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 D(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function O(e){return e?.reason}function te(e){if(!e)return`unknown`;if(typeof e==`string`)return e===`pause`||e===`cancel`?e:`unknown`;if(l(e)&&`reason`in e){let t=e.reason;if(t===`pause`||t===`cancel`)return t}if(l(e)&&`kind`in e){let t=e.kind;if(t===`pause`||t===`cancel`)return t}return`unknown`}var k=class extends Error{constructor(e){super(`Upload aborted`),D(this,`code`,`aborted`),D(this,`reason`,void 0),this.reason=te(e)}};function A(e){return new k(e)}function ne(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 re(e){let t={};return e.getAllResponseHeaders().split(`\r
|
|
2
|
+
`).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 ie(e){return/^https?:\/\//i.test(e)}function ae(e){let t=e.getResponseHeader(`ETag`)??e.getResponseHeader(`etag`);if(t)return t.replace(/"/g,``)}function j(e){return new Promise((t,n)=>{if(!e.url||typeof e.url!=`string`){n(Error(`UploadTransport: missing url`));return}if(!ie(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(A(O(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(A(O(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({headers:re(r),etag:ae(r)}):()=>n(Error(`Upload failed with status ${e}`)))},r.onerror=()=>a(()=>n(ne(r,`Network error during upload`))),r.onabort=()=>a(()=>n(A(e.signal?.aborted?O(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 M(){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 j({method:`POST`,url:e.url,body:t,signal:e.signal,onProgress:e.onProgress})).headers}},async put(e){let t=await j({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 j({method:`PATCH`,url:e.url,body:e.body,signal:e.signal,headers:e.headers,onProgress:e.onProgress})).headers}}}}const N=`snapshots`;function P(){return new Promise((e,t)=>{let n=indexedDB.open(`upload-engine`,1);n.onupgradeneeded=()=>{let e=n.result;e.objectStoreNames.contains(N)||e.createObjectStore(N)},n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error)})}function F(e){return new Promise((t,n)=>{e.onsuccess=()=>t(e.result),e.onerror=()=>n(e.error)})}function I(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 L={async load(e){if(typeof indexedDB>`u`)return null;let t=(await P()).transaction(N,`readonly`),n=t.objectStore(N);try{let r=await F(n.get(e));return await I(t),r??null}catch{try{await I(t)}catch{}return null}},async save(e,t){if(typeof indexedDB>`u`)return;let n=(await P()).transaction(N,`readwrite`),r=n.objectStore(N);try{r.put(t,e),await I(n)}catch{try{n.abort()}catch{}}},async clear(e){if(typeof indexedDB>`u`)return;let t=(await P()).transaction(N,`readwrite`),n=t.objectStore(N);try{n.delete(e),await I(t)}catch{try{t.abort()}catch{}}}},R={load(e){if(typeof localStorage>`u`)return null;let t=localStorage.getItem(e);if(!t)return null;try{return 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)}},z=new Map,B={load(e){return z.get(e)??null},save(e,t){z.set(e,t)},clear(e){z.delete(e)}};function V(e,t){let n={};for(let t of e.items.values()){if(!v(t)||t.phase===`completed`||t.phase===`canceled`||t.phase===`error`)continue;let e=b(t)?t.cursor:void 0,r=`progress`in t&&t.progress?{uploadedBytes:t.progress.uploadedBytes,totalBytes:t.progress.totalBytes,pct:t.progress.pct}:void 0,i={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:e,progress:r};n[i.id]=i}return{version:t,createdAt:Date.now(),items:n}}function H(e,t){if(!t.isPurpose||!t.isIntent||!U(e))return null;let n=new Map;for(let r of Object.values(e.items)){let i=W(r);if(!i||!t.isPurpose(i.purpose)||!t.isIntent(i.intent))continue;let a=i.intent.strategy;if(typeof a!=`string`||!t.hasStrategy(a)||!i.cursor||!K(i.cursor,t.hasStrategy)||i.cursor.strategy!==a)continue;let o=i.progress?.totalBytes??i.file.size,s=i.progress?.uploadedBytes??0,c=typeof i.progress?.pct==`number`?i.progress.pct:o>0?Math.min(100,Math.max(0,s/o*100)):0;n.set(i.id,{phase:`paused`,localId:i.id,purpose:i.purpose,fingerprint:{name:i.file.name,size:i.file.size,type:i.file.type,lastModified:i.file.lastModified,checksum:i.file.checksum},intent:i.intent,cursor:i.cursor,progress:{uploadedBytes:s,totalBytes:o,pct:c},pausedAt:Date.now(),createdAt:e.createdAt??Date.now(),file:void 0})}return{items:n}}function U(e){return!l(e)||typeof e.version!=`number`||typeof e.createdAt!=`number`?!1:l(e.items)}function W(e){if(!l(e))return null;let t=typeof e.id==`string`?e.id:null,n=typeof e.purpose==`string`?e.purpose:null,r=typeof e.status==`string`?e.status:null,i=e.intent;if(!t||!n||!r||!l(e.file))return null;let a=typeof e.file.name==`string`?e.file.name:null,o=typeof e.file.size==`number`?e.file.size:null,s=typeof e.file.type==`string`?e.file.type:null,c=typeof e.file.lastModified==`number`?e.file.lastModified:null,u=typeof e.file.checksum==`string`?e.file.checksum:void 0;if(!a||o===null||!s||c===null)return null;let d=G(e.progress);return{id:t,purpose:n,status:r,file:{name:a,size:o,type:s,lastModified:c,checksum:u},intent:i,cursor:e.cursor,progress:d}}function G(e){if(!l(e)||typeof e.uploadedBytes!=`number`||typeof e.totalBytes!=`number`)return;let t=typeof e.pct==`number`?e.pct:void 0;return{uploadedBytes:e.uploadedBytes,totalBytes:e.totalBytes,pct:t}}function K(e,t){return!(!l(e)||typeof e.strategy!=`string`||!t(e.strategy))}function q(){let e={};return{on(t,n){let r=e[t];return r||(r=new Set,e[t]=r),r.add(n),()=>{let r=e[t];r&&(r.delete(n),r.size===0&&delete e[t])}},off(t,n){let r=e[t];return r?(r.delete(n),r.size===0&&delete e[t],()=>{}):()=>{}},emit(t,n){let r=e[t];r&&r.forEach(e=>{try{e(n)}catch(e){typeof window<`u`&&process.env.NODE_ENV===`development`&&console.error(`[UploadEngine] Error in event listener for "${t}":`,e)}})}}}function J(){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});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});break}case`pause`:{let e=n.get(t.localId);if(!e)break;e.phase===`queued`&&r(t.localId,{...e,phase:`ready`});break}case`cancel`:{let e=n.get(t.localId);if(!e||e.phase===`completed`||e.phase===`canceled`)break;r(t.localId,Y(e,i));break}case`retry`:{let e=n.get(t.localId);if(!e||e.phase!==`error`||!e.retryable||!e.file)break;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}):r(t.localId,{...e,file:e.file,intent:e.intent,phase:`ready`}):r(t.localId,{...e,phase:`creating_intent`,file:e.file,attempt:(e.attempt??1)+1});break}case`rebind`:{let e=n.get(t.localId);if(!e)break;e.phase===`paused`&&!e.file&&m(p(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});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||e.phase!==`validating`)break;r(t.localId,{...e,phase:`creating_intent`,attempt:1});break}case`validation.failed`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`validating`)break;r(e.localId,{...a,phase:`error`,error:{code:`validation_failed`,reason:e.reason,message:String(e.reason)},retryable:!1,attempt:1,failedAt:i});break}case`intent.ok`:{let e=t,i=n.get(e.localId);if(!i||i.phase!==`creating_intent`)break;r(e.localId,{...i,phase:`ready`,intent:e.intent});break}case`intent.failed`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`creating_intent`)break;r(e.localId,{...a,phase:`error`,error:e.error,retryable:e.retryable,failedAt:i});break}case`upload.begin`:{let e=t,i=n.get(e.localId);if(!i||i.phase!==`queued`)break;let a=i.file.size,o=i.progress,s=o?{...o,totalBytes:a,pct:X(o.uploadedBytes,a)}:{uploadedBytes:0,totalBytes:a,pct:0};r(e.localId,{...i,phase:`uploading`,progress:s,startedAt:e.startedAt});break}case`upload.progress`:{let e=t,i=n.get(e.localId);if(!i||i.phase!==`uploading`)break;let a={uploadedBytes:e.uploadedBytes,totalBytes:e.totalBytes,pct:X(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||a.phase!==`uploading`)break;r(e.localId,{...a,phase:`completing`,progress:{uploadedBytes:a.file.size,totalBytes:a.file.size,pct:100},completingAt:i});break}case`upload.failed`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`uploading`)break;r(e.localId,{...a,phase:`error`,error:e.error,retryable:e.retryable,attempt:1,failedAt:i});break}case`paused`:{let e=t,i=n.get(e.localId);if(!i||i.phase!==`uploading`)break;r(e.localId,{...i,phase:`paused`,cursor:e.cursor,pausedAt:e.pausedAt});break}case`canceled`:{let e=t,i=n.get(e.localId);if(!i)break;r(e.localId,Y(i,e.canceledAt));break}case`complete.ok`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`completing`)break;r(e.localId,{...a,phase:`completed`,completedBy:`upload`,result:e.result,completedAt:i});break}case`dedupe.ok`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`validating`)break;r(e.localId,{phase:`completed`,localId:a.localId,file:a.file,purpose:a.purpose,fingerprint:a.fingerprint,result:e.result,completedBy:`dedupe`,completedAt:i,createdAt:a.createdAt});break}case`complete.failed`:{let e=t,a=n.get(e.localId);if(!a||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});break}}return{items:n}}}function Y(e,t){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}}function X(e,t){return t<=0?0:Math.min(100,e/t*100)}function Z(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++)a.push(e[n][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}}async function oe(t,n){let r=t.state.items.get(n);if(!r||r.phase!==`creating_intent`||t.inflightIntents.has(n))return;let i=new AbortController;t.inflightIntents.set(n,i);try{let e=await t.opts.api.createIntent({purpose:r.purpose,contentType:r.file.type||`application/octet-stream`,size:r.file.size,filename:r.file.name,checksum:r.fingerprint.checksum},{signal:i.signal});t.inflightIntents.delete(n);let a=t.state.items.get(n);if(!a||a.phase!==`creating_intent`)return;let o=f(e,e.strategy);if(o){let e={code:`validation_failed`,message:`Invalid intent from backend: ${o.message}`,cause:o,retryable:!1};t.applyInternal({type:`intent.failed`,localId:n,error:e,retryable:!1});return}t.applyInternal({type:`intent.ok`,localId:n,intent:e})}catch(a){if(t.inflightIntents.delete(n),i.signal.aborted)return;let o=_(a,t.opts.errorNormalizer),s={...o,message:`${o.message} (file: ${r.fingerprint.name}, size: ${r.file.size} bytes, purpose: ${r.purpose})`},c=S(t.opts.config,{phase:`intent`,attempt:r.attempt,error:s});t.applyInternal({type:`intent.failed`,localId:n,error:s,retryable:c.retryable}),c.retryable&&c.delayMs!==void 0&&t.enqueueEffect(async()=>{await e(c.delayMs),t.dispatch({type:`retry`,localId:n})})}}async function se(t,n){let r=t.state.items.get(n);if(!r||r.phase!==`completing`||t.inflightCompletes.has(n))return;let i=new AbortController;t.inflightCompletes.set(n,i);try{let e=await t.opts.api.complete({fileId:r.intent.fileId},{signal:i.signal});t.inflightCompletes.delete(n);let a=t.state.items.get(n);if(!a||a.phase!==`completing`)return;t.applyInternal({type:`complete.ok`,localId:n,result:e})}catch(r){if(t.inflightCompletes.delete(n),i.signal.aborted)return;let a=_(r,t.opts.errorNormalizer),o=t.state.items.get(n),s=o?{...a,message:`${a.message} (file: ${o.fingerprint.name}, fileId: ${(v(o)&&o.intent?.fileId)??`unknown`})`}:a,c=o&&`attempt`in o?o.attempt??1:1,l=S(t.opts.config,{phase:`complete`,attempt:c,error:s});t.applyInternal({type:`complete.failed`,localId:n,error:s,retryable:l.retryable}),l.retryable&&l.delayMs!==void 0&&t.enqueueEffect(async()=>{await e(l.delayMs),t.dispatch({type:`retry`,localId:n})})}}async function ce(t,n){let r=t.state.items.get(n);if(!r||r.phase!==`uploading`||!v(r)){t.inflightUploads.delete(n);return}let i=t.inflightUploads.get(n);if(i||(i={controller:new AbortController,mode:`normal`,started:!1},t.inflightUploads.set(n,i)),i.started)return;i.started=!0;let a=i.controller,o=()=>{let e=t.state.items.get(n);!e||e.phase===`canceled`||t.applyInternal({type:`canceled`,localId:n,canceledAt:Date.now()})},s=e=>{let r=t.state.items.get(n);!r||r.phase===`paused`||t.applyInternal({type:`paused`,localId:n,cursor:e,pausedAt:Date.now()})};if(a.signal.aborted){if(t.inflightUploads.delete(n),i.mode===`pause`){let e=t.state.items.get(n),i;r&&v(r)&&(i=r.intent.strategy);let a=(e&&b(e)?e.cursor:void 0)||(i?{strategy:i,value:void 0}:void 0);a&&s(a);return}if(i.mode===`cancel`){o();return}return}let c=r.intent.strategy;if(!t.opts.strategies.has(c)){t.inflightUploads.delete(n);let e={code:`strategy_missing`,message:`Strategy not found: ${c}`,strategy:String(c),retryable:!1};t.applyInternal({type:`upload.failed`,localId:n,error:e,retryable:!1});return}let l=t.opts.strategies.get(c);if(!l){t.inflightUploads.delete(n);let e={code:`strategy_missing`,message:`Strategy not found: ${r.intent.strategy}`,strategy:String(r.intent.strategy),retryable:!1};t.applyInternal({type:`upload.failed`,localId:n,error:e,retryable:!1});return}let u=0,d=t.opts.config.progressThrottleMs;try{if(await l.start({file:r.file,intent:r.intent,signal:a.signal,transport:t.opts.transport,api:t.opts.api,readCursor:()=>{let e=t.state.items.get(n);if(!(!e||!b(e)||!e.cursor)&&e.cursor.strategy===c)return e.cursor.value},persistCursor:e=>{let r={strategy:c,value:e};t.applyInternal({type:`cursor.updated`,localId:n,cursor:r})},reportProgress:e=>{let r=Date.now();r-u<d||(t.applyInternal({type:`upload.progress`,localId:n,uploadedBytes:e.uploadedBytes,totalBytes:e.totalBytes}),u=r)}}),t.inflightUploads.delete(n),i?.mode===`cancel`){o();return}if(a.signal.aborted&&i?.mode===`pause`){let e=t.state.items.get(n);s((e&&b(e)?e.cursor:void 0)||{strategy:c,value:void 0});return}t.applyInternal({type:`upload.ok`,localId:n})}catch(r){if(t.inflightUploads.delete(n),a.signal.aborted){if(i?.mode===`pause`){let e=t.state.items.get(n);s((e&&b(e)?e.cursor:void 0)||{strategy:c,value:void 0});return}if(i?.mode===`cancel`){o();return}o();return}let l=_(r,t.opts.errorNormalizer),u=t.state.items.get(n),d=u?{...l,message:`${l.message} (file: ${u.fingerprint.name}, size: ${(y(u)&&u.file?.size)??0} bytes, phase: ${u.phase})`}:l,f=S(t.opts.config,{phase:`upload`,attempt:1,error:d});t.applyInternal({type:`upload.failed`,localId:n,error:d,retryable:f.retryable}),f.retryable&&f.delayMs!==void 0&&t.enqueueEffect(async()=>{await e(f.delayMs),t.dispatch({type:`retry`,localId:n})})}}function le(e){if(!e.scheduling){e.scheduling=!0;try{ue(e),fe(e),de(e)}finally{e.scheduling=!1}}}function ue(e){for(let t of e.state.items.values())t.phase===`creating_intent`&&(e.inflightIntents.has(t.localId)||e.enqueueEffect(()=>oe(e,t.localId)))}function de(e){for(let t of e.state.items.values())t.phase===`completing`&&(e.inflightCompletes.has(t.localId)||e.enqueueEffect(()=>se(e,t.localId)))}function fe(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`&&g(e.opts,t.purpose)&&e.applyCommand({type:`start`,localId:t.localId});if(n>=t)return;let r=Array.from(e.state.items.values()).filter(pe);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()}),ce(e,t.localId)}}function pe(e){return e.phase===`queued`}function me(e){let t=Q(e),n=t.persistence,r=null,i=Math.max(0,n?.debounceMs??200),a=async()=>{if(n)try{let e=(n.serialize??V)(s.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){typeof window<`u`&&process.env.NODE_ENV===`development`&&console.warn(`[UploadEngine] persistence flush failed:`,e)}},o=()=>{if(n){if(i===0){a();return}r&&clearTimeout(r),r=setTimeout(()=>void a(),i)}},s={opts:t,state:t.initialState??{items:new Map},listeners:new Set,emitter:q(),reduce:J(),inflightUploads:new Map,inflightIntents:new Map,inflightCompletes:new Map,effectQueue:[],processingEffects:!1,scheduling:!1,notify(){s.listeners.forEach(e=>{e()})},scheduleWork(){le(s)},applyInternal(e){let t=s.state;s.state=s.reduce(t,e),s.opts.hooks?.onInternalEvent?.(e,s.state),he(s,e);let n=Z(s.opts,s.state);n&&(s.state=n),o(),s.notify(),s.scheduleWork()},applyCommand(e){let t=s.state,n=s.reduce(t,e);s.state=n,ge(s,e,t,n);let r=Z(s.opts,s.state);r&&(s.state=r),o(),s.notify(),s.scheduleWork()},enqueueEffect(e){s.effectQueue.push(e),s.processingEffects||s.processEffects()},async processEffects(){for(s.processingEffects=!0;s.effectQueue.length>0;){let e=s.effectQueue.shift();if(e)try{await e()}catch(e){if(typeof window<`u`&&process.env.NODE_ENV===`development`){let t=e instanceof Error?e:Error(String(e)),n={queueLength:s.effectQueue.length,processing:s.processingEffects,timestamp:new Date().toISOString()};console.error(`[UploadEngine] Effect error:`,t,n)}}}s.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??H)(e,{isPurpose:n.isPurpose,isIntent:n.isIntent,hasStrategy:t.strategies.has});if(r){if(s.state.items.size===0)s.state=r;else{let e=new Map(s.state.items);for(let[t,n]of r.items.entries())e.has(t)||e.set(t,n);s.state={...s.state,items:e}}s.notify(),s.scheduleWork()}}).catch(()=>void 0)}catch{}return s}function Q(e){return{...e,config:s(e.config),transport:e.transport??M()}}function he(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 ge(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 $(e){let t=me(e);t.dispatch=e=>E(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){typeof window<`u`&&process.env.NODE_ENV===`development`&&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 _e(t,e)}}}function _e(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`}),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`})):new Promise(a=>{let o=()=>{i(),n.size===0&&(e.listeners.delete(o),a(t.map(e=>r.get(e)??{localId:e,status:`missing`})))};e.listeners.add(o)})}function ve(e){return $(e)}export{t as C,r as S,c as _,H as a,a as b,R as c,p as d,m as f,l as g,f as h,q as i,L as l,d as m,$ as n,V as o,u as p,J as r,B as s,ve as t,M as u,s as v,n as w,o as x,i as y};
|
|
@@ -137,12 +137,11 @@ type XhrCommonArgs = {
|
|
|
137
137
|
onProgress?: (loaded: number, total: number) => void;
|
|
138
138
|
};
|
|
139
139
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* upload progress events in a standard cross-browser way.
|
|
140
|
+
* Browser-native XHR transport. Uses `XMLHttpRequest` over `fetch` because
|
|
141
|
+
* `fetch` lacks cross-browser upload-progress events.
|
|
143
142
|
*/
|
|
144
143
|
declare function createXHRTransport(): UploadTransport & {
|
|
145
|
-
patch?: (args: XhrCommonArgs & {
|
|
144
|
+
/** Optional PATCH for tus-style strategies. */patch?: (args: XhrCommonArgs & {
|
|
146
145
|
body: Blob | ArrayBuffer;
|
|
147
146
|
}) => Promise<{
|
|
148
147
|
headers: Record<string, string>;
|
|
@@ -291,11 +290,7 @@ type UploadProgress = {
|
|
|
291
290
|
//#region src/core/engine/internal-events.types.d.ts
|
|
292
291
|
/**
|
|
293
292
|
* Internal events emitted by effects and consumed by the reducer.
|
|
294
|
-
*
|
|
295
|
-
* These are not the same as public events. They are designed for correctness and state transitions.
|
|
296
|
-
*
|
|
297
|
-
* @typeParam M - Intent map
|
|
298
|
-
* @typeParam C - Cursor map
|
|
293
|
+
* Distinct from the public event surface — these drive state transitions.
|
|
299
294
|
*/
|
|
300
295
|
type InternalEvent<M extends IntentMap, C extends CursorMap<M>, P extends string, R extends UploadResultBase = UploadResultBase> = {
|
|
301
296
|
type: 'files.added';
|
|
@@ -370,21 +365,9 @@ type InternalEvent<M extends IntentMap, C extends CursorMap<M>, P extends string
|
|
|
370
365
|
localId: string;
|
|
371
366
|
canceledAt: number;
|
|
372
367
|
};
|
|
373
|
-
/**
|
|
374
|
-
* All possible upload phases.
|
|
375
|
-
*
|
|
376
|
-
* Useful for constraints and UI filtering.
|
|
377
|
-
*/
|
|
368
|
+
/** Every upload phase; the `phase` discriminator of {@link UploadItem}. */
|
|
378
369
|
type UploadPhase = 'validating' | 'creating_intent' | 'ready' | 'queued' | 'uploading' | 'paused' | 'completing' | 'completed' | 'error' | 'canceled';
|
|
379
|
-
/**
|
|
380
|
-
* State of a single upload item.
|
|
381
|
-
*
|
|
382
|
-
* This is a discriminated union on `phase`.
|
|
383
|
-
*
|
|
384
|
-
* @typeParam M - Intent map
|
|
385
|
-
* @typeParam C - Cursor map
|
|
386
|
-
* @typeParam P - Purpose union
|
|
387
|
-
*/
|
|
370
|
+
/** Discriminated union on `phase` describing a single upload's state. */
|
|
388
371
|
type UploadItem<M extends IntentMap, C extends CursorMap<M>, P extends string, R extends UploadResultBase = UploadResultBase> = {
|
|
389
372
|
/** Validating file against rules. */phase: 'validating';
|
|
390
373
|
localId: string;
|
|
@@ -921,20 +904,55 @@ type UploadPlugin<M extends IntentMap, C extends CursorMap<M>, P extends string,
|
|
|
921
904
|
type UploadClient<M extends IntentMap, C extends CursorMap<M>, P extends string, R extends UploadResultBase = UploadResultBase> = UploadStore<M, C, P, R>;
|
|
922
905
|
declare function createUploadClient<M extends IntentMap, C extends CursorMap<M>, P extends string, R extends UploadResultBase = UploadResultBase>(opts: StoreOptions<M, C, P, R>): UploadClient<M, C, P, R>;
|
|
923
906
|
//#endregion
|
|
924
|
-
//#region src/core/engine/validation
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
907
|
+
//#region src/core/engine/validation.d.ts
|
|
908
|
+
/**
|
|
909
|
+
* Validate one file against `config.validation[purpose]` rules.
|
|
910
|
+
*
|
|
911
|
+
* Empty files are always rejected.
|
|
912
|
+
*
|
|
913
|
+
* @param file File to validate.
|
|
914
|
+
* @param purpose Purpose key used to look up rules.
|
|
915
|
+
* @param config Resolved upload config.
|
|
916
|
+
* @template P Purpose discriminator string.
|
|
917
|
+
* @returns `null` when valid, otherwise a {@link Contracts.RejectReason}.
|
|
918
|
+
* @author wildduck2 <https://github.com/wildduck2>
|
|
919
|
+
*/
|
|
920
|
+
declare function validateFile<P extends string>(file: File, purpose: P, config: Client.IUploadConfig<P>): Contracts.RejectReason | null;
|
|
921
|
+
/**
|
|
922
|
+
* Validate a batch of files and split into accepted vs rejected.
|
|
923
|
+
*
|
|
924
|
+
* Enforces `maxFiles` against the existing count plus the running additions
|
|
925
|
+
* so a single `addFiles` call cannot overshoot.
|
|
926
|
+
*
|
|
927
|
+
* @param files Files to validate.
|
|
928
|
+
* @param purpose Purpose key used to look up rules.
|
|
929
|
+
* @param config Resolved upload config.
|
|
930
|
+
* @param existingCount Items already in the store for this purpose.
|
|
931
|
+
* @template P Purpose discriminator string.
|
|
932
|
+
* @returns Object with `valid` files and `rejected` files plus reasons.
|
|
933
|
+
* @author wildduck2 <https://github.com/wildduck2>
|
|
934
|
+
*/
|
|
935
|
+
declare function validateFileList<P extends string>(files: File[], purpose: P, config: Client.IUploadConfig<P>, existingCount?: number): {
|
|
932
936
|
valid: File[];
|
|
933
937
|
rejected: Array<{
|
|
934
938
|
file: File;
|
|
935
|
-
reason: RejectReason;
|
|
939
|
+
reason: Contracts.RejectReason;
|
|
936
940
|
}>;
|
|
937
941
|
};
|
|
942
|
+
/**
|
|
943
|
+
* Validate an intent payload returned by the backend before the engine
|
|
944
|
+
* commits to it.
|
|
945
|
+
*
|
|
946
|
+
* Rejects malformed shapes, protocol-foreign URLs, and cross-field
|
|
947
|
+
* inconsistencies (e.g. `partCount * partSize` cannot cover the file).
|
|
948
|
+
*
|
|
949
|
+
* @param intent Raw intent value from `createIntent`.
|
|
950
|
+
* @param strategy Expected `strategy` discriminator.
|
|
951
|
+
* @param fileSize File byte length; enables multipart cross-checks.
|
|
952
|
+
* @returns `Error` describing the failure, or `null` if valid.
|
|
953
|
+
* @author wildduck2 <https://github.com/wildduck2>
|
|
954
|
+
*/
|
|
955
|
+
declare function validateIntent(intent: unknown, strategy: string, fileSize?: number): Error | null;
|
|
938
956
|
//#endregion
|
|
939
957
|
//#region src/core/utils/async.d.ts
|
|
940
958
|
declare function sleep(ms: number): Promise<void>;
|
|
@@ -984,4 +1002,4 @@ declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
|
984
1002
|
//#region src/core/utils/id.d.ts
|
|
985
1003
|
declare function generateId(): string;
|
|
986
1004
|
//#endregion
|
|
987
|
-
export { RejectReason as $, serializeSnapshot as A, UploadItem as B, StoreOptions as C, createTypedEmitter as D, UploadEventMap as E, PersistedUploadItem as F, UploadCommand as G, UploadProgress as H, PersistenceAdapter as I, resolveUploadConfig as J, UploadConfig as K, UploadState as L, LocalStorageAdapter as M, IndexedDBAdapter as N, TypedEmitter as O, PersistedSnapshot as P, BuiltInUploadError as Q, createReducer as R, PersistenceOptions as S, UploadStore as T, UploadCompletionKind as U, UploadPhase as V, UploadOutcome as W, createXHRTransport as X, RetryDecision as Y, FileFingerprint as Z, UploadHooks as _, DEFAULT_MAX_ATTEMPTS as a, DeserializeContext as b, DEFAULT_PROGRESS_THROTTLE_MS as c, sleep as d, UploadError as et,
|
|
1005
|
+
export { RejectReason as $, serializeSnapshot as A, UploadItem as B, StoreOptions as C, createTypedEmitter as D, UploadEventMap as E, PersistedUploadItem as F, UploadCommand as G, UploadProgress as H, PersistenceAdapter as I, resolveUploadConfig as J, UploadConfig as K, UploadState as L, LocalStorageAdapter as M, IndexedDBAdapter as N, TypedEmitter as O, PersistedSnapshot as P, BuiltInUploadError as Q, createReducer as R, PersistenceOptions as S, UploadStore as T, UploadCompletionKind as U, UploadPhase as V, UploadOutcome as W, createXHRTransport as X, RetryDecision as Y, FileFingerprint as Z, UploadHooks as _, DEFAULT_MAX_ATTEMPTS as a, DeserializeContext as b, DEFAULT_PROGRESS_THROTTLE_MS as c, sleep as d, UploadError as et, validateFile as f, createUploadClient as g, UploadClient as h, fingerprintMatches as i, MemoryAdapter as j, deserializeSnapshot as k, DEFAULT_RETRY_DELAY_BASE_MS as l, validateIntent as m, isRecord as n, UploadValidationRules as nt, DEFAULT_MAX_CONCURRENT_UPLOADS as o, validateFileList as p, UploadConfigInput as q, computeFingerprint as r, DEFAULT_MAX_ITEMS as s, generateId as t, UploadErrorBase as tt, DEFAULT_RETRY_DELAY_MAX_MS as u, UploadPlugin as v, StoreRuntime as w, InflightUpload as x, createUploadStore as y, InternalEvent as z };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as AnyCursor, c as WithSignal, d as IntentBase, f as IntentMap, i as UploadTransport, l as UploadResultBase, n as StrategyRegistry, o as CursorMap, p as StrategyKey, r as UploadStrategy, s as UploadApi, t as StrategyCtx, u as AnyIntent } from "./strategy.types-gg9ZStRe.js";
|
|
2
|
-
import { $ as RejectReason, A as serializeSnapshot, B as UploadItem, C as StoreOptions, D as createTypedEmitter, E as UploadEventMap, F as PersistedUploadItem, G as UploadCommand, H as UploadProgress, I as PersistenceAdapter, J as resolveUploadConfig, K as UploadConfig, L as UploadState, M as LocalStorageAdapter, N as IndexedDBAdapter, O as TypedEmitter, P as PersistedSnapshot, Q as BuiltInUploadError, R as createReducer, S as PersistenceOptions, T as UploadStore, U as UploadCompletionKind, V as UploadPhase, W as UploadOutcome, X as createXHRTransport, Y as RetryDecision, Z as FileFingerprint, _ as UploadHooks, a as DEFAULT_MAX_ATTEMPTS, b as DeserializeContext, c as DEFAULT_PROGRESS_THROTTLE_MS, d as sleep, et as UploadError, f as
|
|
2
|
+
import { $ as RejectReason, A as serializeSnapshot, B as UploadItem, C as StoreOptions, D as createTypedEmitter, E as UploadEventMap, F as PersistedUploadItem, G as UploadCommand, H as UploadProgress, I as PersistenceAdapter, J as resolveUploadConfig, K as UploadConfig, L as UploadState, M as LocalStorageAdapter, N as IndexedDBAdapter, O as TypedEmitter, P as PersistedSnapshot, Q as BuiltInUploadError, R as createReducer, S as PersistenceOptions, T as UploadStore, U as UploadCompletionKind, V as UploadPhase, W as UploadOutcome, X as createXHRTransport, Y as RetryDecision, Z as FileFingerprint, _ as UploadHooks, a as DEFAULT_MAX_ATTEMPTS, b as DeserializeContext, c as DEFAULT_PROGRESS_THROTTLE_MS, d as sleep, et as UploadError, f as validateFile, g as createUploadClient, h as UploadClient, i as fingerprintMatches, j as MemoryAdapter, k as deserializeSnapshot, l as DEFAULT_RETRY_DELAY_BASE_MS, m as validateIntent, n as isRecord, nt as UploadValidationRules, o as DEFAULT_MAX_CONCURRENT_UPLOADS, p as validateFileList, q as UploadConfigInput, r as computeFingerprint, s as DEFAULT_MAX_ITEMS, t as generateId, tt as UploadErrorBase, u as DEFAULT_RETRY_DELAY_MAX_MS, v as UploadPlugin, w as StoreRuntime, x as InflightUpload, y as createUploadStore, z as InternalEvent } from "./index-C38Jmizu.js";
|
|
3
3
|
import { PickItemsByPhase, UploadProvider, UploaderActions, createUploadFactory, useOptionalUploadStore, useUploadStore, useUploader, useUploaderActions } from "./react/index.js";
|
|
4
4
|
import { MultipartCursor, MultipartIntent, PostCursor, PostIntent, PostStrategy, createStrategyRegistry, multipartStrategy } from "./strategies/index.js";
|
|
5
5
|
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, MultipartCursor, MultipartIntent, PersistedSnapshot, PersistedUploadItem, PersistenceAdapter, PersistenceOptions, PickItemsByPhase, PostCursor, PostIntent, PostStrategy, RejectReason, RetryDecision, StoreOptions, StoreRuntime, StrategyCtx, StrategyKey, StrategyRegistry, TypedEmitter, UploadApi, UploadClient, UploadCommand, UploadCompletionKind, UploadConfig, UploadConfigInput, UploadError, UploadErrorBase, UploadEventMap, UploadHooks, UploadItem, UploadOutcome, UploadPhase, UploadPlugin, UploadProgress, UploadProvider, UploadResultBase, UploadState, UploadStore, UploadStrategy, UploadTransport, UploadValidationRules, UploaderActions, WithSignal, computeFingerprint, createReducer, createStrategyRegistry, createTypedEmitter, createUploadClient, createUploadFactory, createUploadStore, createXHRTransport, deserializeSnapshot, fingerprintMatches, generateId, isRecord, multipartStrategy, resolveUploadConfig, serializeSnapshot, sleep, useOptionalUploadStore, useUploadStore, useUploader, useUploaderActions, validateFile, validateFileList, validateIntent };
|