@gentleduck/upload 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +297 -0
  2. package/dist/core/index.d.ts +2 -3
  3. package/dist/core/index.js +1 -1
  4. package/dist/core-BVgloTiP.js +10 -0
  5. package/dist/index-DCqyBVcx.d.ts +1441 -0
  6. package/dist/index-DsZHPmJm.d.ts +225 -0
  7. package/dist/index-KROXtLQh.d.ts +281 -0
  8. package/dist/index.d.ts +4 -5
  9. package/dist/index.js +1 -1
  10. package/dist/react/index.d.ts +2 -90
  11. package/dist/react/index.js +1 -1
  12. package/dist/react-euPrDv24.js +1 -0
  13. package/dist/strategies/index.d.ts +2 -56
  14. package/dist/strategies/index.js +1 -1
  15. package/dist/strategies-CUF0s68j.js +1 -0
  16. package/package.json +3 -2
  17. package/src/__tests__/emitter.test.ts +85 -0
  18. package/src/__tests__/fingerprint.test.ts +49 -0
  19. package/src/__tests__/guards.test.ts +40 -0
  20. package/src/__tests__/id.test.ts +67 -0
  21. package/src/__tests__/mime-sniff.test.ts +142 -0
  22. package/src/__tests__/persistence.test.ts +191 -0
  23. package/src/__tests__/sanitize-filename.test.ts +109 -0
  24. package/src/__tests__/sleep.test.ts +34 -0
  25. package/src/__tests__/static-analysis.test.ts +145 -0
  26. package/src/__tests__/store-libs.test.ts +192 -0
  27. package/src/__tests__/transport-fetch.test.ts +108 -0
  28. package/src/__tests__/transport-libs.test.ts +42 -0
  29. package/src/__tests__/transport.test.ts +182 -0
  30. package/src/core/__tests__/errors.test.ts +152 -0
  31. package/src/core/client.ts +3 -0
  32. package/src/core/contracts/contracts.types.ts +373 -0
  33. package/src/core/contracts/index.ts +3 -8
  34. package/src/core/contracts/transport/index.ts +3 -2
  35. package/src/core/contracts/transport/transport.fetch.ts +203 -0
  36. package/src/core/contracts/transport/transport.libs.ts +13 -39
  37. package/src/core/contracts/transport/transport.ts +96 -90
  38. package/src/core/contracts/transport/transport.types.ts +71 -35
  39. package/src/core/engine/__tests__/config-nan-bypass.test.ts +40 -0
  40. package/src/core/engine/engine.libs.ts +35 -0
  41. package/src/core/engine/engine.types.ts +490 -0
  42. package/src/core/engine/index.ts +3 -8
  43. package/src/core/engine/reducer.ts +93 -53
  44. package/src/core/engine/store/handlers/add-file.ts +43 -48
  45. package/src/core/engine/store/handlers/cancel.ts +13 -8
  46. package/src/core/engine/store/handlers/clean-up.ts +11 -8
  47. package/src/core/engine/store/handlers/create-intent.ts +82 -27
  48. package/src/core/engine/store/handlers/finalize.ts +30 -13
  49. package/src/core/engine/store/handlers/index.ts +7 -7
  50. package/src/core/engine/store/handlers/pause.ts +8 -6
  51. package/src/core/engine/store/handlers/run-upload.ts +54 -39
  52. package/src/core/engine/store/index.ts +27 -21
  53. package/src/core/engine/store/store.dispatch.ts +10 -8
  54. package/src/core/engine/store/store.libs.ts +197 -94
  55. package/src/core/engine/store/store.runtime.ts +52 -53
  56. package/src/core/engine/store/store.schedule.ts +27 -21
  57. package/src/core/engine/store/store.types.ts +177 -227
  58. package/src/core/engine/validation/__tests__/intent.test.ts +70 -0
  59. package/src/core/engine/validation/file.ts +8 -4
  60. package/src/core/engine/validation/index.ts +1 -0
  61. package/src/core/engine/validation/intent.ts +49 -27
  62. package/src/core/engine/validation/list.ts +14 -15
  63. package/src/core/engine/validation/mime.ts +39 -0
  64. package/src/core/errors.ts +271 -0
  65. package/src/core/index.ts +28 -5
  66. package/src/core/persistence/__tests__/deserialize-nan-bypass.test.ts +100 -0
  67. package/src/core/persistence/__tests__/persistence.test.ts +85 -0
  68. package/src/core/persistence/adapters.indexeddb.ts +9 -3
  69. package/src/core/persistence/adapters.local.ts +9 -3
  70. package/src/core/persistence/adapters.memory.ts +35 -13
  71. package/src/core/persistence/index.ts +5 -5
  72. package/src/core/persistence/persistence.ts +73 -60
  73. package/src/core/persistence/persistence.types.ts +140 -60
  74. package/src/core/utils/__tests__/guards.test.ts +93 -0
  75. package/src/core/utils/__tests__/url-safety.test.ts +112 -0
  76. package/src/core/utils/async.ts +7 -2
  77. package/src/core/utils/constants.ts +3 -9
  78. package/src/core/utils/emitter/emitter.ts +126 -0
  79. package/src/core/utils/emitter/emitter.types.ts +33 -16
  80. package/src/core/utils/emitter/index.ts +2 -74
  81. package/src/core/utils/fingerprint.ts +16 -5
  82. package/src/core/utils/guards.ts +91 -1
  83. package/src/core/utils/hash/__tests__/sha256.test.ts +61 -0
  84. package/src/core/utils/hash/index.ts +126 -0
  85. package/src/core/utils/hash/sha256.ts +179 -0
  86. package/src/core/utils/id.ts +33 -1
  87. package/src/core/utils/index.ts +15 -6
  88. package/src/core/utils/mime-sniff.ts +119 -0
  89. package/src/core/utils/sanitize-filename.ts +85 -0
  90. package/src/core/utils/url-safety.ts +156 -0
  91. package/src/react/__tests__/use-dropzone.test.ts +64 -0
  92. package/src/react/index.ts +5 -2
  93. package/src/react/upload-provider.tsx +59 -95
  94. package/src/react/uploader.types.ts +80 -0
  95. package/src/react/use-dropzone.tsx +226 -0
  96. package/src/react/use-uploader.tsx +97 -93
  97. package/src/strategies/_shared/__tests__/retry.test.ts +120 -0
  98. package/src/strategies/_shared/retry.ts +79 -0
  99. package/src/strategies/index.ts +9 -15
  100. package/src/strategies/multipart/__tests__/multipart.test.ts +229 -0
  101. package/src/strategies/multipart/index.ts +211 -116
  102. package/src/strategies/post/__tests__/post-url-safety.test.ts +119 -0
  103. package/src/strategies/post/index.ts +113 -71
  104. package/src/strategies/put/__tests__/put.test.ts +132 -0
  105. package/src/strategies/put/index.ts +139 -0
  106. package/src/strategies/registry/index.ts +12 -9
  107. package/src/strategies/tus/__tests__/tus.test.ts +159 -0
  108. package/src/strategies/tus/index.ts +211 -0
  109. package/dist/async-DxRWu4le.js +0 -1
  110. package/dist/core-BNxznRif.js +0 -2
  111. package/dist/index-BXg9WIce.d.ts +0 -987
  112. package/dist/strategy.types-gg9ZStRe.d.ts +0 -225
  113. package/src/core/client/config.types.ts +0 -77
  114. package/src/core/client/index.ts +0 -4
  115. package/src/core/client/plugins.types.ts +0 -46
  116. package/src/core/client/retry.types.ts +0 -6
  117. package/src/core/client/uploadClient.ts +0 -19
  118. package/src/core/contracts/backendApi.ts +0 -62
  119. package/src/core/contracts/cursor.types.ts +0 -23
  120. package/src/core/contracts/errors.types.ts +0 -67
  121. package/src/core/contracts/fingerprint.types.ts +0 -18
  122. package/src/core/contracts/intent.types.ts +0 -39
  123. package/src/core/contracts/result.types.ts +0 -11
  124. package/src/core/contracts/strategy.types.ts +0 -45
  125. package/src/core/engine/commands.types.ts +0 -17
  126. package/src/core/engine/event-map.types.ts +0 -45
  127. package/src/core/engine/internal-events.types.ts +0 -222
  128. package/src/core/engine/outcome.types.ts +0 -9
  129. package/src/core/engine/progress.types.ts +0 -15
@@ -0,0 +1,159 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest'
2
+ import { __resetTusWarningsForTests, type TusIntent, TusStrategy } from '../index'
3
+
4
+ type CursorState = { offset: number } | undefined
5
+
6
+ /**
7
+ * Build a fake strategy ctx backed by an in-memory tus server that tracks its
8
+ * own offset and grows it by the size of each accepted PATCH.
9
+ */
10
+ function makeCtx(opts: {
11
+ fileBytes: number
12
+ url?: string
13
+ intentChunkSize?: number
14
+ cursor?: CursorState
15
+ headOffset?: number | null // null = HEAD omitted from transport
16
+ serverStartOffset?: number
17
+ }) {
18
+ const { fileBytes, url = 'https://tus.example.com/files/abc', cursor, headOffset = 0, serverStartOffset = 0 } = opts
19
+
20
+ const file = new File([new Uint8Array(fileBytes)], 'a.bin')
21
+ let persisted: CursorState = cursor
22
+ let serverOffset = serverStartOffset
23
+
24
+ const patch = vi.fn(async (args: { headers?: Record<string, string>; body: Blob }) => {
25
+ const sent = Number(args.headers?.['Upload-Offset'] ?? args.headers?.['upload-offset'])
26
+ // Simulate the server accepting the chunk at the declared offset.
27
+ serverOffset = sent + args.body.size
28
+ return { headers: { 'upload-offset': String(serverOffset) } }
29
+ })
30
+
31
+ const transport: Record<string, unknown> = { patch }
32
+ if (headOffset !== null) {
33
+ transport['head'] = vi.fn(async () => ({
34
+ status: 200,
35
+ headers: { 'upload-offset': String(headOffset) },
36
+ }))
37
+ }
38
+
39
+ const intent: TusIntent = { strategy: 'tus', fileId: 'f', url }
40
+ if (opts.intentChunkSize) intent.chunkSize = opts.intentChunkSize
41
+
42
+ const ctx = {
43
+ intent,
44
+ file,
45
+ signal: new AbortController().signal,
46
+ reportProgress: vi.fn(),
47
+ readCursor: () => persisted,
48
+ persistCursor: (c: CursorState) => {
49
+ persisted = c
50
+ },
51
+ transport,
52
+ get persistedOffset() {
53
+ return persisted?.offset
54
+ },
55
+ }
56
+ return {
57
+ ctx,
58
+ patch,
59
+ get server() {
60
+ return serverOffset
61
+ },
62
+ }
63
+ }
64
+
65
+ describe('TusStrategy', () => {
66
+ beforeEach(() => {
67
+ __resetTusWarningsForTests()
68
+ })
69
+
70
+ test('uploads a file in chunks and completes', async () => {
71
+ const s = TusStrategy({ allowedHosts: ['tus.example.com'], chunkSize: 10 })
72
+ const { ctx, patch } = makeCtx({ fileBytes: 25, intentChunkSize: 10 })
73
+ // biome-ignore lint/suspicious/noExplicitAny: test ctx is a subset of the full strategy ctx
74
+ await expect(s.start(ctx as any)).resolves.toBeUndefined()
75
+ // 25 bytes / 10 per chunk -> 3 PATCHes (10, 10, 5)
76
+ expect(patch).toHaveBeenCalledTimes(3)
77
+ const sizes = patch.mock.calls.map((c) => (c[0] as { body: Blob }).body.size)
78
+ expect(sizes).toEqual([10, 10, 5])
79
+ // biome-ignore lint/suspicious/noExplicitAny: reading test helper getter
80
+ expect((ctx as any).persistedOffset).toBe(25)
81
+ })
82
+
83
+ test('sends the tus protocol headers', async () => {
84
+ const s = TusStrategy({ allowedHosts: ['tus.example.com'] })
85
+ const { ctx, patch } = makeCtx({ fileBytes: 5, intentChunkSize: 10 })
86
+ // biome-ignore lint/suspicious/noExplicitAny: test ctx is a subset of the full strategy ctx
87
+ await s.start(ctx as any)
88
+ const headers = (patch.mock.calls[0]?.[0] as { headers: Record<string, string> }).headers
89
+ expect(headers['Tus-Resumable']).toBe('1.0.0')
90
+ expect(headers['Upload-Offset']).toBe('0')
91
+ expect(headers['Content-Type']).toBe('application/offset+octet-stream')
92
+ })
93
+
94
+ test('resumes from the server HEAD offset, not the start', async () => {
95
+ const s = TusStrategy({ allowedHosts: ['tus.example.com'], chunkSize: 10 })
96
+ // Server already has 20 of 25 bytes.
97
+ const { ctx, patch } = makeCtx({
98
+ fileBytes: 25,
99
+ intentChunkSize: 10,
100
+ headOffset: 20,
101
+ serverStartOffset: 20,
102
+ })
103
+ // biome-ignore lint/suspicious/noExplicitAny: test ctx is a subset of the full strategy ctx
104
+ await s.start(ctx as any)
105
+ // Only the final 5 bytes remain -> one PATCH.
106
+ expect(patch).toHaveBeenCalledTimes(1)
107
+ expect((patch.mock.calls[0]?.[0] as { body: Blob }).body.size).toBe(5)
108
+ const startHeader = (patch.mock.calls[0]?.[0] as { headers: Record<string, string> }).headers['Upload-Offset']
109
+ expect(startHeader).toBe('20')
110
+ })
111
+
112
+ test('falls back to the persisted cursor when no HEAD transport', async () => {
113
+ const s = TusStrategy({ allowedHosts: ['tus.example.com'], chunkSize: 10 })
114
+ const { ctx, patch } = makeCtx({
115
+ fileBytes: 25,
116
+ intentChunkSize: 10,
117
+ headOffset: null, // no head() on transport
118
+ cursor: { offset: 20 },
119
+ serverStartOffset: 20,
120
+ })
121
+ // biome-ignore lint/suspicious/noExplicitAny: test ctx is a subset of the full strategy ctx
122
+ await s.start(ctx as any)
123
+ expect(patch).toHaveBeenCalledTimes(1)
124
+ expect((patch.mock.calls[0]?.[0] as { body: Blob }).body.size).toBe(5)
125
+ })
126
+
127
+ test('is a no-op when already fully uploaded', async () => {
128
+ const s = TusStrategy({ allowedHosts: ['tus.example.com'] })
129
+ const { ctx, patch } = makeCtx({ fileBytes: 25, headOffset: 25, serverStartOffset: 25 })
130
+ // biome-ignore lint/suspicious/noExplicitAny: test ctx is a subset of the full strategy ctx
131
+ await s.start(ctx as any)
132
+ expect(patch).not.toHaveBeenCalled()
133
+ })
134
+
135
+ test('rejects a non-allowlisted host', async () => {
136
+ const s = TusStrategy({ allowedHosts: ['tus.example.com'] })
137
+ const { ctx } = makeCtx({ fileBytes: 5, url: 'https://evil.example.com/f' })
138
+ // biome-ignore lint/suspicious/noExplicitAny: test ctx is a subset of the full strategy ctx
139
+ await expect(s.start(ctx as any)).rejects.toThrow(/allow-list/)
140
+ })
141
+
142
+ test('rejects file:// URL', async () => {
143
+ const s = TusStrategy()
144
+ const { ctx } = makeCtx({ fileBytes: 5, url: 'file:///etc/passwd' })
145
+ // biome-ignore lint/suspicious/noExplicitAny: test ctx is a subset of the full strategy ctx
146
+ await expect(s.start(ctx as any)).rejects.toThrow(/forbidden protocol/)
147
+ })
148
+
149
+ test('bails when the server never advances the offset', async () => {
150
+ const s = TusStrategy({ allowedHosts: ['tus.example.com'], chunkSize: 10 })
151
+ const { ctx } = makeCtx({ fileBytes: 25, intentChunkSize: 10 })
152
+ // Server that always reports offset 0 -> no forward progress.
153
+ ;(ctx.transport as { patch: unknown }).patch = vi.fn(async () => ({
154
+ headers: { 'upload-offset': '0' },
155
+ }))
156
+ // biome-ignore lint/suspicious/noExplicitAny: test ctx is a subset of the full strategy ctx
157
+ await expect(s.start(ctx as any)).rejects.toThrow(/did not advance/)
158
+ })
159
+ })
@@ -0,0 +1,211 @@
1
+ /**
2
+ * tus resumable upload strategy (creation-less).
3
+ *
4
+ * The backend intent hands back a ready tus upload URL (creation is done
5
+ * server-side), so this strategy implements only the transfer half of the
6
+ * protocol: HEAD to read the server's `Upload-Offset`, then a sequence of
7
+ * `PATCH` requests with `application/offset+octet-stream` bodies. The offset is
8
+ * persisted in the cursor so a resumed run continues where it left off, and
9
+ * re-confirmed against the server via HEAD when the transport supports it.
10
+ *
11
+ * @see https://tus.io/protocols/resumable-upload
12
+ */
13
+
14
+ import type { Contracts } from '../../core'
15
+ import { UploadEngineError } from '../../core'
16
+ import { validateUploadUrl } from '../../core/utils/url-safety'
17
+ import { withRetry } from '../_shared/retry'
18
+
19
+ /** tus protocol version this strategy speaks. */
20
+ const TUS_RESUMABLE = '1.0.0'
21
+
22
+ /** Default PATCH chunk size when neither intent nor config specifies one: 8 MiB. */
23
+ const DEFAULT_TUS_CHUNK_SIZE = 8 * 1024 * 1024
24
+
25
+ /**
26
+ * Types for the tus resumable upload strategy.
27
+ * @author wildduck2 <https://github.com/wildduck2>
28
+ */
29
+ export namespace TusStrategy {
30
+ /**
31
+ * Configuration options for the tus strategy.
32
+ */
33
+ export type Config = {
34
+ /** PATCH chunk size in bytes. Overridden by `intent.chunkSize`. Defaults to 8 MiB. */
35
+ chunkSize?: number
36
+ /**
37
+ * Case-insensitive allow-list of host names (with optional port). When set,
38
+ * the tus upload URL must match a listed host or it is rejected.
39
+ */
40
+ allowedHosts?: string[]
41
+ /**
42
+ * When `true`, allow private-network IP literals in the tus URL.
43
+ * Defaults to `false`.
44
+ */
45
+ allowPrivateHosts?: boolean
46
+ /** Retries after the first attempt for transient PATCH/HEAD failures. Defaults to 3. */
47
+ maxRetries?: number
48
+ }
49
+
50
+ /**
51
+ * tus intent payload. The backend has already created the upload; `url` is the
52
+ * ready upload resource to PATCH against.
53
+ */
54
+ export type Intent = {
55
+ /** Discriminant matching strategy configuration registry keys. */
56
+ strategy: 'tus'
57
+ /** Unique database identifier for the file resource. */
58
+ fileId: string
59
+ /** Ready tus upload URL (backend-created via the creation extension). */
60
+ url: string
61
+ /** Optional per-upload chunk size override in bytes. */
62
+ chunkSize?: number
63
+ /** Optional extra headers to send on every request (e.g. auth). */
64
+ headers?: Record<string, string>
65
+ }
66
+
67
+ /**
68
+ * Persisted resume state: the last acknowledged upload offset in bytes.
69
+ */
70
+ export type Cursor = {
71
+ /** Byte offset already accepted by the server. */
72
+ offset: number
73
+ }
74
+ }
75
+
76
+ /** Convenience alias for {@link TusStrategy.Intent}. */
77
+ export type TusIntent = TusStrategy.Intent
78
+
79
+ let warnedMissingAllowedHosts = false
80
+
81
+ /**
82
+ * Reset the warn-once latch. Test-only.
83
+ * @internal
84
+ */
85
+ export function __resetTusWarningsForTests(): void {
86
+ warnedMissingAllowedHosts = false
87
+ }
88
+
89
+ /** Parse a non-negative integer header value, or `null` when absent/invalid. */
90
+ function parseOffsetHeader(headers: Record<string, string> | undefined, name: string): number | null {
91
+ const raw = headers?.[name] ?? headers?.[name.toLowerCase()]
92
+ if (raw === undefined) return null
93
+ const n = Number(raw)
94
+ return Number.isInteger(n) && n >= 0 ? n : null
95
+ }
96
+
97
+ /**
98
+ * tus resumable upload strategy (creation-less).
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * const registry = createStrategyRegistry([
103
+ * TusStrategy({ allowedHosts: ['tus.example.com'], chunkSize: 8 * 1024 * 1024 }),
104
+ * ]);
105
+ * ```
106
+ * @author wildduck2 <https://github.com/wildduck2>
107
+ */
108
+ export function TusStrategy<
109
+ M extends Contracts.Intent.Map & { tus: TusStrategy.Intent },
110
+ C extends Contracts.Cursor.Map<M> & { tus?: TusStrategy.Cursor },
111
+ P extends string = string,
112
+ R extends Contracts.Result.Base = Contracts.Result.Base,
113
+ >(opts: TusStrategy.Config = {}): Contracts.Strategy.Me<M, C, P, R, 'tus'> {
114
+ const allowedHosts = opts.allowedHosts ?? []
115
+ const allowPrivateHosts = opts.allowPrivateHosts === true
116
+ const configChunkSize = opts.chunkSize
117
+ const retryOpts = opts.maxRetries !== undefined ? { maxRetries: opts.maxRetries } : {}
118
+
119
+ return {
120
+ id: 'tus',
121
+ resumable: true,
122
+
123
+ async start(ctx) {
124
+ const intent = ctx.intent
125
+ if (!intent.url) {
126
+ throw new UploadEngineError('validation_failed', { message: 'tus.start: intent missing url' })
127
+ }
128
+
129
+ if (allowedHosts.length === 0 && !warnedMissingAllowedHosts) {
130
+ warnedMissingAllowedHosts = true
131
+ console.warn(
132
+ '[duck-upload] TusStrategy: no `allowedHosts` configured. tus upload URLs will be host-unrestricted. ' +
133
+ 'Set TusStrategy.Config.allowedHosts to lock the upload host.',
134
+ )
135
+ }
136
+
137
+ validateUploadUrl(intent.url, 'tus.intent', { allowedHosts, allowPrivateHosts })
138
+
139
+ const url = intent.url
140
+ const total = ctx.file.size
141
+ const chunkSize = Math.max(1, intent.chunkSize ?? configChunkSize ?? DEFAULT_TUS_CHUNK_SIZE)
142
+ const baseHeaders: Record<string, string> = { 'Tus-Resumable': TUS_RESUMABLE, ...(intent.headers ?? {}) }
143
+
144
+ // Start from the persisted offset, then confirm against the server when
145
+ // the transport can HEAD (server offset is authoritative on resume).
146
+ let offset = ctx.readCursor()?.offset ?? 0
147
+
148
+ if (ctx.transport.head) {
149
+ const head = ctx.transport.head
150
+ try {
151
+ const res = await withRetry(() => head({ url, headers: baseHeaders, signal: ctx.signal }), {
152
+ signal: ctx.signal,
153
+ ...retryOpts,
154
+ })
155
+ const serverOffset = parseOffsetHeader(res.headers, 'upload-offset')
156
+ if (serverOffset !== null) offset = serverOffset
157
+ } catch (err) {
158
+ // Abort must propagate; other HEAD failures fall back to the cursor
159
+ // offset and let the first PATCH surface a real error.
160
+ if (ctx.signal?.aborted || (err instanceof Error && err.name === 'AbortError')) throw err
161
+ }
162
+ }
163
+
164
+ // Clamp a stale/oversized persisted offset back into range.
165
+ if (offset > total) offset = total
166
+
167
+ ctx.reportProgress({ uploadedBytes: offset, totalBytes: total })
168
+
169
+ while (offset < total) {
170
+ if (ctx.signal?.aborted) throw new DOMException('The operation was aborted.', 'AbortError')
171
+
172
+ const start = offset
173
+ const end = Math.min(total, start + chunkSize)
174
+ const chunk = ctx.file.slice(start, end)
175
+
176
+ const res = await withRetry(
177
+ () =>
178
+ ctx.transport.patch({
179
+ url,
180
+ body: chunk,
181
+ headers: {
182
+ ...baseHeaders,
183
+ 'Upload-Offset': String(start),
184
+ 'Content-Type': 'application/offset+octet-stream',
185
+ },
186
+ signal: ctx.signal,
187
+ onProgress: (loaded) => {
188
+ ctx.reportProgress({ uploadedBytes: start + loaded, totalBytes: total })
189
+ },
190
+ }),
191
+ { signal: ctx.signal, ...retryOpts },
192
+ )
193
+
194
+ // Trust the server's acknowledged offset; fall back to the local end.
195
+ const acked = parseOffsetHeader(res.headers, 'upload-offset')
196
+ offset = acked !== null ? acked : end
197
+
198
+ if (offset <= start) {
199
+ // No forward progress -> a broken/misconfigured server. Bail rather
200
+ // than spin forever.
201
+ throw new UploadEngineError('upload_failed', {
202
+ message: `tus: server did not advance Upload-Offset (stuck at ${offset} of ${total}).`,
203
+ })
204
+ }
205
+
206
+ ctx.persistCursor({ offset } as C['tus'])
207
+ ctx.reportProgress({ uploadedBytes: offset, totalBytes: total })
208
+ }
209
+ },
210
+ }
211
+ }
@@ -1 +0,0 @@
1
- function e(e){return new Promise(t=>setTimeout(t,e))}export{e as t};
@@ -1,2 +0,0 @@
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(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 u(e){return!!e&&typeof e==`object`}function d(e,t){if(!u(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(`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 f(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=l(r,t,n);if(e){o.push({file:r,reason:e});continue}a.push(r),--c}return{valid:a,rejected:o}}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=u(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(u(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 u(e)&&e.strategy===`multipart`&&typeof e.fileId==`string`&&typeof e.uploadId==`string`}function x(e){return u(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}=f(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=l(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(u(e)&&`reason`in e){let t=e.reason;if(t===`pause`||t===`cancel`)return t}if(u(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!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};