@gentleduck/upload 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +170 -193
  2. package/dist/core/index.d.ts +2 -3
  3. package/dist/core/index.js +1 -1
  4. package/dist/core-BVgloTiP.js +10 -0
  5. package/dist/index-DCqyBVcx.d.ts +1441 -0
  6. package/dist/index-DsZHPmJm.d.ts +225 -0
  7. package/dist/index-KROXtLQh.d.ts +281 -0
  8. package/dist/index.d.ts +4 -5
  9. package/dist/index.js +1 -1
  10. package/dist/react/index.d.ts +2 -51
  11. package/dist/react/index.js +1 -1
  12. package/dist/react-euPrDv24.js +1 -0
  13. package/dist/strategies/index.d.ts +2 -56
  14. package/dist/strategies/index.js +1 -1
  15. package/dist/strategies-CUF0s68j.js +1 -0
  16. package/package.json +3 -2
  17. package/src/{__test__ → __tests__}/emitter.test.ts +4 -4
  18. package/src/{__test__ → __tests__}/fingerprint.test.ts +1 -1
  19. package/src/{__test__ → __tests__}/guards.test.ts +15 -5
  20. package/src/__tests__/id.test.ts +67 -0
  21. package/src/__tests__/mime-sniff.test.ts +142 -0
  22. package/src/{__test__ → __tests__}/persistence.test.ts +43 -8
  23. package/src/__tests__/sanitize-filename.test.ts +109 -0
  24. package/src/__tests__/sleep.test.ts +34 -0
  25. package/src/__tests__/static-analysis.test.ts +145 -0
  26. package/src/{__test__ → __tests__}/store-libs.test.ts +57 -3
  27. package/src/__tests__/transport-fetch.test.ts +108 -0
  28. package/src/{__test__ → __tests__}/transport.test.ts +5 -5
  29. package/src/core/__tests__/errors.test.ts +152 -0
  30. package/src/core/client.ts +3 -0
  31. package/src/core/contracts/contracts.types.ts +335 -141
  32. package/src/core/contracts/index.ts +3 -8
  33. package/src/core/contracts/transport/index.ts +3 -2
  34. package/src/core/contracts/transport/transport.fetch.ts +203 -0
  35. package/src/core/contracts/transport/transport.libs.ts +7 -8
  36. package/src/core/contracts/transport/transport.ts +91 -83
  37. package/src/core/contracts/transport/transport.types.ts +71 -35
  38. package/src/core/engine/__tests__/config-nan-bypass.test.ts +40 -0
  39. package/src/core/engine/engine.libs.ts +35 -0
  40. package/src/core/engine/engine.types.ts +367 -241
  41. package/src/core/engine/index.ts +3 -8
  42. package/src/core/engine/reducer.ts +89 -43
  43. package/src/core/engine/store/handlers/add-file.ts +39 -29
  44. package/src/core/engine/store/handlers/cancel.ts +13 -8
  45. package/src/core/engine/store/handlers/clean-up.ts +11 -8
  46. package/src/core/engine/store/handlers/create-intent.ts +82 -27
  47. package/src/core/engine/store/handlers/finalize.ts +30 -13
  48. package/src/core/engine/store/handlers/index.ts +7 -7
  49. package/src/core/engine/store/handlers/pause.ts +8 -6
  50. package/src/core/engine/store/handlers/run-upload.ts +49 -34
  51. package/src/core/engine/store/index.ts +27 -21
  52. package/src/core/engine/store/store.dispatch.ts +10 -8
  53. package/src/core/engine/store/store.libs.ts +184 -41
  54. package/src/core/engine/store/store.runtime.ts +45 -36
  55. package/src/core/engine/store/store.schedule.ts +27 -21
  56. package/src/core/engine/store/store.types.ts +177 -227
  57. package/src/core/engine/validation/__tests__/intent.test.ts +70 -0
  58. package/src/core/engine/validation/file.ts +7 -3
  59. package/src/core/engine/validation/index.ts +1 -0
  60. package/src/core/engine/validation/intent.ts +49 -27
  61. package/src/core/engine/validation/list.ts +14 -15
  62. package/src/core/engine/validation/mime.ts +39 -0
  63. package/src/core/errors.ts +271 -0
  64. package/src/core/index.ts +28 -5
  65. package/src/core/persistence/__tests__/deserialize-nan-bypass.test.ts +100 -0
  66. package/src/core/persistence/__tests__/persistence.test.ts +85 -0
  67. package/src/core/persistence/adapters.indexeddb.ts +9 -3
  68. package/src/core/persistence/adapters.local.ts +9 -3
  69. package/src/core/persistence/adapters.memory.ts +35 -13
  70. package/src/core/persistence/index.ts +5 -5
  71. package/src/core/persistence/persistence.ts +73 -60
  72. package/src/core/persistence/persistence.types.ts +140 -60
  73. package/src/core/utils/__tests__/guards.test.ts +93 -0
  74. package/src/core/utils/__tests__/url-safety.test.ts +112 -0
  75. package/src/core/utils/async.ts +7 -2
  76. package/src/core/utils/constants.ts +3 -9
  77. package/src/core/utils/emitter/emitter.ts +7 -24
  78. package/src/core/utils/emitter/emitter.types.ts +33 -16
  79. package/src/core/utils/emitter/index.ts +2 -74
  80. package/src/core/utils/fingerprint.ts +16 -5
  81. package/src/core/utils/guards.ts +91 -1
  82. package/src/core/utils/hash/__tests__/sha256.test.ts +61 -0
  83. package/src/core/utils/hash/index.ts +126 -0
  84. package/src/core/utils/hash/sha256.ts +179 -0
  85. package/src/core/utils/id.ts +33 -1
  86. package/src/core/utils/index.ts +15 -6
  87. package/src/core/utils/mime-sniff.ts +119 -0
  88. package/src/core/utils/sanitize-filename.ts +85 -0
  89. package/src/core/utils/url-safety.ts +156 -0
  90. package/src/react/__tests__/use-dropzone.test.ts +64 -0
  91. package/src/react/index.ts +5 -2
  92. package/src/react/upload-provider.tsx +68 -53
  93. package/src/react/uploader.types.ts +58 -61
  94. package/src/react/use-dropzone.tsx +226 -0
  95. package/src/react/use-uploader.tsx +98 -91
  96. package/src/strategies/_shared/__tests__/retry.test.ts +120 -0
  97. package/src/strategies/_shared/retry.ts +79 -0
  98. package/src/strategies/index.ts +9 -15
  99. package/src/strategies/multipart/__tests__/multipart.test.ts +229 -0
  100. package/src/strategies/multipart/index.ts +206 -108
  101. package/src/strategies/post/__tests__/post-url-safety.test.ts +119 -0
  102. package/src/strategies/post/index.ts +115 -33
  103. package/src/strategies/put/__tests__/put.test.ts +132 -0
  104. package/src/strategies/put/index.ts +139 -0
  105. package/src/strategies/registry/index.ts +12 -9
  106. package/src/strategies/tus/__tests__/tus.test.ts +159 -0
  107. package/src/strategies/tus/index.ts +211 -0
  108. package/dist/async-DxRWu4le.js +0 -1
  109. package/dist/core-CMkrP9DX.js +0 -2
  110. package/dist/index-C38Jmizu.d.ts +0 -1005
  111. package/dist/strategy.types-gg9ZStRe.d.ts +0 -225
  112. package/src/__test__/effect-pool.test.ts +0 -5
  113. package/src/__test__/id.test.ts +0 -14
  114. package/src/__test__/store-integration.test.ts +0 -5
  115. package/src/core/client/client.ts +0 -50
  116. package/src/core/client/client.types.ts +0 -184
  117. package/src/core/client/config.types.ts +0 -77
  118. package/src/core/client/index.ts +0 -4
  119. package/src/core/client/plugins.types.ts +0 -46
  120. package/src/core/client/retry.types.ts +0 -6
  121. package/src/core/client/uploadClient.ts +0 -19
  122. package/src/core/contracts/backendApi.ts +0 -62
  123. package/src/core/contracts/cursor.types.ts +0 -23
  124. package/src/core/contracts/errors.types.ts +0 -67
  125. package/src/core/contracts/fingerprint.types.ts +0 -18
  126. package/src/core/contracts/intent.types.ts +0 -39
  127. package/src/core/contracts/result.types.ts +0 -11
  128. package/src/core/contracts/strategy.types.ts +0 -45
  129. package/src/core/engine/commands.types.ts +0 -17
  130. package/src/core/engine/event-map.types.ts +0 -45
  131. package/src/core/engine/internal-events.types.ts +0 -202
  132. package/src/core/engine/outcome.types.ts +0 -9
  133. package/src/core/engine/progress.types.ts +0 -15
  134. package/src/core/engine/store/handlers.ts +0 -819
  135. package/src/core/engine/validation.ts +0 -216
  136. /package/src/{__test__ → __tests__}/transport-libs.test.ts +0 -0
@@ -1,19 +1,114 @@
1
- import type { Contracts } from '../contracts'
1
+ import type { Contracts } from '../contracts/contracts.types'
2
+ import type { UploadError } from '../errors'
3
+ import type { Store } from './store'
2
4
 
3
5
  /**
4
- * Engine-level type surface.
5
- *
6
+ * Engine-level type surface containing runtime state models, commands, progress,
7
+ * and configuration options.
6
8
  * @author wildduck2 <https://github.com/wildduck2>
7
9
  */
8
10
  export namespace Engine {
9
- // -------------------- Progress --------------------
11
+ /**
12
+ * Return value from a retry policy determining if and when a task should be re-scheduled.
13
+ */
14
+ export type RetryDecision =
15
+ | {
16
+ /** The task has failed terminal or cannot recover. */
17
+ retryable: false
18
+ }
19
+ | {
20
+ /** The task is recoverable and should retry. */
21
+ retryable: true
22
+ /** Delay duration in milliseconds before invoking the next attempt. */
23
+ delayMs: number
24
+ }
10
25
 
11
26
  /**
12
- * Upload progress snapshot with `pct` clamped to 0..100.
27
+ * Global upload engine configuration.
28
+ *
29
+ * Coordinates concurrency limits, retry strategies, constraints, and validation.
13
30
  *
14
- * @author wildduck2 <https://github.com/wildduck2>
31
+ * @typeParam P - Union of allowed `purpose` strings for your app (e.g. `"avatar" | "attachment"`).
32
+ */
33
+ export type Config<P extends string> = {
34
+ /**
35
+ * Maximum number of parallel uploads active at any time.
36
+ * Queued items wait until active uploads complete or fail.
37
+ */
38
+ maxConcurrentUploads: number
39
+
40
+ /**
41
+ * Auto-start uploads for specific purposes.
42
+ * - Array form: only files with these purposes automatically begin uploading on registration.
43
+ * - Function form: dynamic callback determining whether to auto-start.
44
+ */
45
+ autoStart?: (readonly P[] | ((purpose: P) => boolean)) | undefined
46
+
47
+ /**
48
+ * Throttle delay in milliseconds for progress events.
49
+ * Limits rendering updates and state changes under busy progress flows.
50
+ */
51
+ progressThrottleMs: number
52
+
53
+ /**
54
+ * Validation rules keyed by purpose.
55
+ */
56
+ validation: Partial<Record<P, Contracts.ValidationRules>>
57
+
58
+ /**
59
+ * Maximum number of retry attempts permitted for upload/intent actions.
60
+ */
61
+ maxAttempts: number
62
+
63
+ /**
64
+ * Custom retry policy handler.
65
+ * Overrides default exponential backoff calculation.
66
+ */
67
+ retryPolicy?:
68
+ | ((ctx: {
69
+ /** The active execution phase. */
70
+ phase: 'intent' | 'upload' | 'complete'
71
+ /** Current attempt number (starting at 1). */
72
+ attempt: number
73
+ /** The normalized error structure. */
74
+ error: UploadError
75
+ }) => RetryDecision)
76
+ | undefined
77
+
78
+ /**
79
+ * Maximum number of items to keep in memory/state.
80
+ * When exceeded, oldest completed/canceled items are evicted automatically.
81
+ *
82
+ * Set to `undefined` for no limit (not recommended for long-running single-page apps).
83
+ */
84
+ maxItems: number
85
+
86
+ /**
87
+ * Duration in milliseconds after which completed items are evicted from the store.
88
+ * Set to `undefined` to preserve completed items indefinitely.
89
+ */
90
+ completedItemTTL?: number | undefined
91
+
92
+ /**
93
+ * Set to `true` to validate files by sniffing magic bytes/mime signature
94
+ * and rejecting files whose suffix does not match structural magic numbers.
95
+ */
96
+ strictMimeMatch?: boolean | undefined
97
+
98
+ /**
99
+ * File-size boundary in bytes between the two checksum paths. At or below
100
+ * it, files hash inline with native Web Crypto (fastest). Above it, files
101
+ * hash incrementally in bounded slices — off the main thread via a Web
102
+ * Worker when available — so large files no longer freeze the UI and are
103
+ * still deduplicated. Set to `null` or `undefined` for the default (64 MiB).
104
+ */
105
+ checksumMaxSize?: number | null | undefined
106
+ }
107
+
108
+ /**
109
+ * Upload progress snapshot.
15
110
  */
16
- export interface IUploadProgress {
111
+ export type Progress = {
17
112
  /** Bytes uploaded so far. */
18
113
  uploadedBytes: number
19
114
  /** Total bytes to upload. */
@@ -22,255 +117,269 @@ export namespace Engine {
22
117
  pct: number
23
118
  }
24
119
 
25
- // -------------------- Outcome --------------------
26
-
27
120
  /**
28
- * How a `completed` item finished.
29
- *
30
- * @author wildduck2 <https://github.com/wildduck2>
121
+ * Describes how a `completed` upload item resolved.
122
+ * - `'upload'`: standard byte transfer.
123
+ * - `'dedupe'`: server matched existing file checksum, skipping byte transfer.
31
124
  */
32
125
  export type CompletionKind = 'upload' | 'dedupe'
33
126
 
34
127
  /**
35
- * Terminal outcome returned by `Store.IUploadStore.waitFor`.
128
+ * Terminal outcome returned by `waitFor`.
36
129
  *
37
130
  * @template R Result type carried on successful completion.
38
- * @author wildduck2 <https://github.com/wildduck2>
39
131
  */
40
- export type Outcome<R extends Contracts.IResultBase> =
41
- | { localId: string; status: 'completed'; completedBy: CompletionKind; result: R }
42
- | { localId: string; status: 'error'; error: Contracts.Error }
43
- | { localId: string; status: 'canceled' }
132
+ export type Outcome<R extends Contracts.Result.Base> =
133
+ | {
134
+ localId: string
135
+ status: 'completed'
136
+ completedBy: CompletionKind
137
+ result: R
138
+ }
139
+ | {
140
+ localId: string
141
+ status: 'error'
142
+ error: UploadError
143
+ }
144
+ | {
145
+ localId: string
146
+ status: 'canceled'
147
+ }
44
148
  | {
45
149
  localId: string
46
150
  status: 'missing'
47
151
  /**
48
- * Why the id is missing. Required so consumers exhaustively
49
- * handle every cause via switch.
50
- * - `'removed'`: explicit `dispatch({ type: 'remove' })`.
51
- * - `'evicted'`: cleanup pass (`maxItems` / `completedItemTTL`).
52
- * - `'never-existed'`: the id was never registered in this store.
53
- * - `'destroyed'`: `store.destroy()` ran while the wait was pending.
152
+ * Why the id is missing.
153
+ * - `'removed'`: explicit user removal.
154
+ * - `'evicted'`: automatically removed (maxItems/TTL rules).
155
+ * - `'never-existed'`: id was never registered.
156
+ * - `'destroyed'`: store was destroyed.
54
157
  */
55
158
  reason: 'removed' | 'evicted' | 'never-existed' | 'destroyed'
56
159
  }
57
160
 
58
- // -------------------- Commands --------------------
59
-
60
161
  /**
61
- * Public commands accepted by `store.dispatch`.
162
+ * Public commands dispatched to the upload store.
62
163
  *
63
164
  * @template P Purpose discriminator string.
64
- * @author wildduck2 <https://github.com/wildduck2>
65
165
  */
66
166
  export type Command<P extends string> =
67
- | { type: 'addFiles'; files: File[]; purpose: P }
68
- | { type: 'start'; localId: string }
69
- | { type: 'startAll'; purpose?: P }
70
- | { type: 'pause'; localId: string }
71
- | { type: 'pauseAll'; purpose?: P }
72
- | { type: 'resume'; localId: string }
73
- | { type: 'cancel'; localId: string }
74
- | { type: 'cancelAll'; purpose?: P }
75
- | { type: 'retry'; localId: string }
76
- | { type: 'rebind'; localId: string; file: File }
77
- | { type: 'remove'; localId: string }
78
-
79
- // -------------------- Phases / Items --------------------
80
-
81
- /**
82
- * Every upload phase; the `phase` discriminator of {@link Engine.Item}.
83
- *
84
- * @author wildduck2 <https://github.com/wildduck2>
85
- */
86
- export type Phase =
87
- | 'validating'
88
- | 'creating_intent'
89
- | 'ready'
90
- | 'queued'
91
- | 'uploading'
92
- | 'paused'
93
- | 'completing'
94
- | 'completed'
95
- | 'error'
96
- | 'canceled'
97
-
98
- /**
99
- * Enum-style const map of every {@link Phase} value.
100
- *
101
- * Reading `Phases.completing` matches the string literal at compile time
102
- * AND gives consumers a single import point instead of N magic-string
103
- * checks. Use whichever style fits the call site.
104
- *
105
- * @author wildduck2 <https://github.com/wildduck2>
106
- */
107
- export const Phases = {
108
- validating: 'validating',
109
- creating_intent: 'creating_intent',
110
- ready: 'ready',
111
- queued: 'queued',
112
- uploading: 'uploading',
113
- paused: 'paused',
114
- completing: 'completing',
115
- completed: 'completed',
116
- error: 'error',
117
- canceled: 'canceled',
118
- } as const satisfies Record<Phase, Phase>
119
-
120
- /**
121
- * Discriminated union on `phase` describing a single upload's state.
122
- *
123
- * @template M Intent map keyed by strategy id.
124
- * @template C Cursor map keyed by strategy id.
125
- * @template P Purpose discriminator string.
126
- * @template R Backend result payload.
127
- * @author wildduck2 <https://github.com/wildduck2>
128
- */
129
- export type Item<
130
- M extends Contracts.IntentMap,
131
- C extends Contracts.CursorMap<M>,
132
- P extends string,
133
- R extends Contracts.IResultBase = Contracts.IResultBase,
134
- > =
135
167
  | {
136
- phase: 'validating'
137
- localId: string
168
+ /** Register files into the upload store for a specific purpose. */
169
+ type: 'addFiles'
170
+ files: File[]
138
171
  purpose: P
139
- fingerprint: Contracts.IFileFingerprint
140
- file: File
141
- createdAt: number
172
+ /** Optional caller metadata forwarded to every backend API call for this file. */
173
+ meta?: Record<string, unknown> | undefined
142
174
  }
143
175
  | {
144
- phase: 'creating_intent'
176
+ /** Start uploading a specific file. */
177
+ type: 'start'
145
178
  localId: string
146
- purpose: P
147
- fingerprint: Contracts.IFileFingerprint
148
- file: File
149
- attempt: number
150
- createdAt: number
151
179
  }
152
180
  | {
153
- phase: 'ready'
154
- localId: string
155
- purpose: P
156
- fingerprint: Contracts.IFileFingerprint
157
- file: File
158
- intent: Contracts.AnyIntent<M>
159
- createdAt: number
160
- cursor?: Contracts.AnyCursor<C>
161
- progress?: IUploadProgress
162
- attempt?: number
181
+ /** Start all queued files, optionally filtering by purpose. */
182
+ type: 'startAll'
183
+ purpose?: P
163
184
  }
164
185
  | {
165
- phase: 'queued'
186
+ /** Pause an active upload. */
187
+ type: 'pause'
166
188
  localId: string
167
- purpose: P
168
- fingerprint: Contracts.IFileFingerprint
169
- file: File
170
- intent: Contracts.AnyIntent<M>
171
- requestedAt: number
172
- createdAt: number
173
- cursor?: Contracts.AnyCursor<C>
174
- progress?: IUploadProgress
175
- attempt?: number
176
189
  }
177
190
  | {
178
- phase: 'uploading'
179
- localId: string
180
- purpose: P
181
- fingerprint: Contracts.IFileFingerprint
182
- file: File
183
- intent: Contracts.AnyIntent<M>
184
- startedAt: number
185
- progress: IUploadProgress
186
- createdAt: number
187
- cursor?: Contracts.AnyCursor<C>
188
- attempt?: number
191
+ /** Pause all active uploads, optionally filtering by purpose. */
192
+ type: 'pauseAll'
193
+ purpose?: P
189
194
  }
190
195
  | {
191
- phase: 'paused'
196
+ /** Resume a paused upload. */
197
+ type: 'resume'
192
198
  localId: string
193
- purpose: P
194
- fingerprint: Contracts.IFileFingerprint
195
- intent: Contracts.AnyIntent<M>
196
- cursor: Contracts.AnyCursor<C>
197
- progress: IUploadProgress
198
- pausedAt: number
199
- createdAt: number
200
- file?: File
201
- attempt?: number
202
199
  }
203
200
  | {
204
- phase: 'completing'
201
+ /** Cancel a pending or active upload. */
202
+ type: 'cancel'
205
203
  localId: string
206
- purpose: P
207
- fingerprint: Contracts.IFileFingerprint
208
- file: File
209
- intent: Contracts.AnyIntent<M>
210
- progress: IUploadProgress
211
- completingAt: number
212
- createdAt: number
213
- attempt?: number
214
204
  }
215
205
  | {
216
- phase: 'completed'
206
+ /** Cancel all pending and active uploads, optionally filtering by purpose. */
207
+ type: 'cancelAll'
208
+ purpose?: P
209
+ }
210
+ | {
211
+ /** Retry a failed upload item. */
212
+ type: 'retry'
217
213
  localId: string
218
- file?: File
219
- purpose: P
220
- fingerprint: Contracts.IFileFingerprint
221
- intent?: Contracts.AnyIntent<M>
222
- completedBy: CompletionKind
223
- result: R
224
- completedAt: number
225
- createdAt: number
226
- attempt?: number
227
214
  }
228
215
  | {
229
- phase: 'error'
216
+ /** Re-bind a new browser `File` object to a resumed/paused item missing a file reference. */
217
+ type: 'rebind'
230
218
  localId: string
231
- purpose: P
232
- fingerprint: Contracts.IFileFingerprint
233
- error: Contracts.Error
234
- retryable: boolean
235
- attempt: number
236
- failedAt: number
237
- createdAt: number
238
- file?: File
239
- intent?: Contracts.AnyIntent<M>
240
- cursor?: Contracts.AnyCursor<C>
241
- progress?: IUploadProgress
219
+ file: File
242
220
  }
243
221
  | {
244
- phase: 'canceled'
222
+ /** Remove an item from the store state entirely. */
223
+ type: 'remove'
245
224
  localId: string
246
- purpose: P
247
- fingerprint: Contracts.IFileFingerprint
248
- canceledAt: number
249
- createdAt: number
250
- file?: File
251
- intent?: Contracts.AnyIntent<M>
252
- cursor?: Contracts.AnyCursor<C>
253
- progress?: IUploadProgress
254
- attempt?: number
255
225
  }
256
226
 
257
- // -------------------- Internal events --------------------
227
+ /**
228
+ * Const mapping of upload stages.
229
+ */
230
+ export const Phases = {
231
+ /** Initial validation checks are running. */
232
+ validating: 'validating',
233
+ /** Fetching signed URLs and strategies from backend. */
234
+ creating_intent: 'creating_intent',
235
+ /** Ready to start uploading bytes. */
236
+ ready: 'ready',
237
+ /** Waiting in queue (concurrency cap reached). */
238
+ queued: 'queued',
239
+ /** Actively transferring bytes. */
240
+ uploading: 'uploading',
241
+ /** Upload paused; resumable state preserved. */
242
+ paused: 'paused',
243
+ /** Triggering backend finalization. */
244
+ completing: 'completing',
245
+ /** File is fully completed and registered on backend. */
246
+ completed: 'completed',
247
+ /** Execution hit a terminal error or maximum retry cap. */
248
+ error: 'error',
249
+ /** Upload was canceled by the user. */
250
+ canceled: 'canceled',
251
+ } as const
252
+
253
+ /** Every upload phase; the `phase` discriminator of {@link Item}. */
254
+ export type Phase = keyof typeof Phases
255
+
256
+ interface BaseItem<P extends string> {
257
+ /** Unique client-side identifier for this file item. */
258
+ localId: string
259
+ /** Target upload purpose. */
260
+ purpose: P
261
+ /** Metadata properties identifying the file. */
262
+ fingerprint: Contracts.FingerprintFile
263
+ /** Epoch timestamp in ms when the item was registered. */
264
+ createdAt: number
265
+ attempt?: number | undefined
266
+ file?: File | undefined
267
+ /** Full phase history. Each entry records when a phase was entered/left and any stripped error. */
268
+ steps: Contracts.UploadStep[]
269
+ /** Arbitrary caller-supplied metadata from the addFiles command. Never persisted. */
270
+ meta: Record<string, unknown>
271
+ }
258
272
 
259
273
  /**
260
- * Events emitted by effects and consumed by the reducer. Distinct from the
261
- * public event surface -- these drive state transitions.
262
- *
263
- * @template M Intent map keyed by strategy id.
264
- * @template C Cursor map keyed by strategy id.
265
- * @template P Purpose discriminator string.
266
- * @template R Backend result payload.
267
- * @author wildduck2 <https://github.com/wildduck2>
274
+ * Representational union of upload items in their respective lifecycle phases.
275
+ */
276
+ export type Item<
277
+ M extends Contracts.Intent.Map,
278
+ C extends Contracts.Cursor.Map<M>,
279
+ P extends string,
280
+ R extends Contracts.Result.Base = Contracts.Result.Base,
281
+ > = BaseItem<P> &
282
+ (
283
+ | { phase: 'validating'; file: File }
284
+ | { phase: 'creating_intent'; file: File; attempt: number }
285
+ | {
286
+ phase: 'ready'
287
+ file: File
288
+ intent: Contracts.Intent.Any<M>
289
+ cursor?: Contracts.Cursor.Any<C> | undefined
290
+ progress?: Progress | undefined
291
+ }
292
+ | {
293
+ phase: 'queued'
294
+ file: File
295
+ intent: Contracts.Intent.Any<M>
296
+ requestedAt: number
297
+ cursor?: Contracts.Cursor.Any<C> | undefined
298
+ progress?: Progress | undefined
299
+ }
300
+ | {
301
+ phase: 'uploading'
302
+ file: File
303
+ intent: Contracts.Intent.Any<M>
304
+ startedAt: number
305
+ progress: Progress
306
+ cursor?: Contracts.Cursor.Any<C> | undefined
307
+ }
308
+ | {
309
+ phase: 'paused'
310
+ intent: Contracts.Intent.Any<M>
311
+ cursor: Contracts.Cursor.Any<C>
312
+ progress: Progress
313
+ pausedAt: number
314
+ file?: File | undefined
315
+ }
316
+ | { phase: 'completing'; file: File; intent: Contracts.Intent.Any<M>; progress: Progress; completingAt: number }
317
+ | {
318
+ phase: 'completed'
319
+ completedBy: CompletionKind
320
+ result: R
321
+ completedAt: number
322
+ intent?: Contracts.Intent.Any<M> | undefined
323
+ file?: File | undefined
324
+ }
325
+ | {
326
+ phase: 'error'
327
+ error: UploadError
328
+ retryable: boolean
329
+ attempt: number
330
+ failedAt: number
331
+ file?: File | undefined
332
+ intent?: Contracts.Intent.Any<M> | undefined
333
+ cursor?: Contracts.Cursor.Any<C> | undefined
334
+ progress?: Progress | undefined
335
+ }
336
+ | {
337
+ phase: 'canceled'
338
+ canceledAt: number
339
+ file?: File | undefined
340
+ intent?: Contracts.Intent.Any<M> | undefined
341
+ cursor?: Contracts.Cursor.Any<C> | undefined
342
+ progress?: Progress | undefined
343
+ attempt?: number | undefined
344
+ }
345
+ )
346
+
347
+ /**
348
+ * Immutable state shape of the upload engine store.
349
+ */
350
+ export type State<
351
+ M extends Contracts.Intent.Map,
352
+ C extends Contracts.Cursor.Map<M>,
353
+ P extends string,
354
+ R extends Contracts.Result.Base = Contracts.Result.Base,
355
+ > = {
356
+ /** Map of active and historical upload items. */
357
+ items: Map<string, Item<M, C, P, R>>
358
+ }
359
+
360
+ /**
361
+ * Interface for registering lifecycle plugins.
362
+ */
363
+ export type Plugin<
364
+ M extends Contracts.Intent.Map,
365
+ C extends Contracts.Cursor.Map<M>,
366
+ P extends string,
367
+ R extends Contracts.Result.Base = Contracts.Result.Base,
368
+ > = {
369
+ /** Name of the plugin (for diagnostics). */
370
+ name: string
371
+ /** Sets up subscription listener binds on the store core proxy. */
372
+ setup(store: Pick<Store.UploadStore<M, C, P, R>, 'on' | 'off' | 'dispatch' | 'getSnapshot'>): void
373
+ }
374
+
375
+ /**
376
+ * Events emitted by effects and consumed by the reducer to transition item states.
268
377
  */
269
- export type InternalEvent<
270
- M extends Contracts.IntentMap,
271
- C extends Contracts.CursorMap<M>,
378
+ export type Event<
379
+ M extends Contracts.Intent.Map,
380
+ C extends Contracts.Cursor.Map<M>,
272
381
  P extends string,
273
- R extends Contracts.IResultBase = Contracts.IResultBase,
382
+ R extends Contracts.Result.Base = Contracts.Result.Base,
274
383
  > =
275
384
  | {
276
385
  type: 'files.added'
@@ -278,87 +387,104 @@ export namespace Engine {
278
387
  localId: string
279
388
  purpose: P
280
389
  file: File
281
- fingerprint: Contracts.IFileFingerprint
390
+ fingerprint: Contracts.FingerprintFile
282
391
  createdAt: number
392
+ meta: Record<string, unknown>
283
393
  }>
284
394
  }
285
- | { type: 'fingerprint.updated'; localId: string; fingerprint: Contracts.IFileFingerprint }
395
+ | { type: 'fingerprint.updated'; localId: string; fingerprint: Contracts.FingerprintFile }
286
396
  | { type: 'validation.ok'; localId: string }
287
- | { type: 'validation.failed'; localId: string; reason: Contracts.RejectReason }
288
- | { type: 'intent.ok'; localId: string; intent: Contracts.AnyIntent<M> }
289
- | { type: 'intent.failed'; localId: string; error: Contracts.Error; retryable: boolean }
397
+ | { type: 'validation.failed'; localId: string; reason: Contracts.Validation.Rejection }
398
+ | { type: 'intent.ok'; localId: string; intent: Contracts.Intent.Any<M> }
399
+ | { type: 'intent.failed'; localId: string; error: UploadError; retryable: boolean }
290
400
  | { type: 'upload.begin'; localId: string; startedAt: number }
291
401
  | { type: 'upload.progress'; localId: string; uploadedBytes: number; totalBytes: number }
292
- | { type: 'cursor.updated'; localId: string; cursor: Contracts.AnyCursor<C> }
402
+ | { type: 'cursor.updated'; localId: string; cursor: Contracts.Cursor.Any<C> }
293
403
  | { type: 'upload.ok'; localId: string }
294
- | { type: 'upload.failed'; localId: string; error: Contracts.Error; retryable: boolean }
404
+ | { type: 'upload.failed'; localId: string; error: UploadError; retryable: boolean }
295
405
  | { type: 'dedupe.ok'; localId: string; result: R }
296
406
  | { type: 'complete.ok'; localId: string; result: R }
297
- | { type: 'complete.failed'; localId: string; error: Contracts.Error; retryable: boolean }
298
- | { type: 'paused'; localId: string; cursor: Contracts.AnyCursor<C>; pausedAt: number }
407
+ | { type: 'complete.failed'; localId: string; error: UploadError; retryable: boolean }
408
+ | { type: 'paused'; localId: string; cursor: Contracts.Cursor.Any<C>; pausedAt: number }
299
409
  | { type: 'canceled'; localId: string; canceledAt: number }
300
410
 
301
- // -------------------- Public event map --------------------
302
-
303
411
  /**
304
- * Public event map emitted to consumers via `store.on(type, cb)`.
305
- *
306
- * @template M Intent map keyed by strategy id.
307
- * @template C Cursor map keyed by strategy id.
308
- * @template P Purpose discriminator string.
309
- * @template R Backend result payload.
310
- * @author wildduck2 <https://github.com/wildduck2>
412
+ * Public event registry mapping event names to payloads emitted via `store.on()`.
311
413
  */
312
414
  export type EventMap<
313
- M extends Contracts.IntentMap,
314
- C extends Contracts.CursorMap<M>,
415
+ M extends Contracts.Intent.Map,
416
+ C extends Contracts.Cursor.Map<M>,
315
417
  P extends string,
316
- R extends Contracts.IResultBase = Contracts.IResultBase,
418
+ R extends Contracts.Result.Base = Contracts.Result.Base,
317
419
  > = {
318
- 'file.added': { localId: string; purpose: P; file: File; fingerprint: Contracts.IFileFingerprint }
319
- 'file.rejected': { file: File; reason: Contracts.RejectReason }
420
+ /** Emitted when a file is validated and successfully registered in state. */
421
+ 'file.added': { localId: string; purpose: P; file: File; fingerprint: Contracts.FingerprintFile }
422
+ /** Emitted when a file fails client-side validation rules. */
423
+ 'file.rejected': { file: File; reason: Contracts.Validation.Rejection }
320
424
 
425
+ /** Emitted when validation succeeds. */
321
426
  'validation.ok': { localId: string }
322
- 'validation.failed': { localId: string; reason: Contracts.RejectReason }
427
+ /** Emitted when validation fails. */
428
+ 'validation.failed': { localId: string; reason: Contracts.Validation.Rejection }
323
429
 
430
+ /** Emitted when intent lookup/creation is requested. */
324
431
  'intent.creating': { localId: string }
432
+ /** Emitted when upload intent is successfully created. */
325
433
  'intent.created': { localId: string; intent: M[keyof M] }
326
- 'intent.failed': { localId: string; error: Contracts.Error; retryable: boolean }
434
+ /** Emitted when intent creation fails. */
435
+ 'intent.failed': { localId: string; error: UploadError; retryable: boolean }
327
436
 
437
+ /** Emitted when an item is queued waiting for concurrency slots. */
328
438
  'upload.queued': { localId: string }
439
+ /** Emitted when a paused upload is resumed. */
329
440
  'upload.resumed': { localId: string }
441
+ /** Emitted when data transfer starts. */
330
442
  'upload.started': { localId: string }
443
+ /** Periodic progress event. */
331
444
  'upload.progress': { localId: string; pct: number; uploadedBytes: number; totalBytes: number }
332
- 'upload.cursor': { localId: string; cursor: Contracts.AnyCursor<C> }
333
- 'upload.paused': { localId: string; cursor: Contracts.AnyCursor<C> }
445
+ /** Emitted when strategy cursor state is updated. */
446
+ 'upload.cursor': { localId: string; cursor: Contracts.Cursor.Any<C> }
447
+ /** Emitted when upload is paused. */
448
+ 'upload.paused': { localId: string; cursor: Contracts.Cursor.Any<C> }
449
+ /** Emitted when upload is canceled. */
334
450
  'upload.canceled': { localId: string }
335
451
 
452
+ /** Emitted when final completing steps (like completeMultipart or final verification) begin. */
336
453
  'upload.completing': { localId: string }
454
+ /** Emitted when a file is completely uploaded and finalized. */
337
455
  'upload.completed': { localId: string; result: R; completedBy: CompletionKind }
338
456
 
339
- 'upload.error': { localId: string; error: Contracts.Error; retryable: boolean }
457
+ /** Emitted when transfer hits a retryable or terminal error. */
458
+ 'upload.error': { localId: string; error: UploadError; retryable: boolean }
340
459
 
341
- /**
342
- * Emitted whenever an item leaves state. `reason: 'user'` for an
343
- * explicit `dispatch({ type: 'remove' })`; `reason: 'cleanup'` for
344
- * an automatic eviction by `completedItemTTL` / `maxItems`. Lets
345
- * consumers (waitFor, UI listeners, telemetry) react without polling
346
- * the snapshot.
347
- */
460
+ /** Emitted when an item is removed from the store state. */
348
461
  'upload.removed': { localId: string; reason: 'user' | 'cleanup' }
349
462
 
463
+ /** Emitted when a file rebind operation succeeds. */
350
464
  'rebind.ok': { localId: string }
465
+ /** Emitted when a file rebind fails. */
351
466
  'rebind.failed': { localId: string; reason: RebindReason }
352
467
  }
353
468
 
354
469
  /**
355
- * Why a `rebind` command did not bind a file.
356
- *
357
- * @author wildduck2 <https://github.com/wildduck2>
470
+ * Rebind failure classifications.
358
471
  */
359
472
  export type RebindReason =
360
473
  | { code: 'no_item' }
361
474
  | { code: 'wrong_phase'; phase: string }
362
475
  | { code: 'already_bound' }
363
- | { code: 'fingerprint_mismatch'; expected: Contracts.IFileFingerprint; got: Contracts.IFileFingerprint }
476
+ | { code: 'fingerprint_mismatch'; expected: Contracts.FingerprintFile; got: Contracts.FingerprintFile }
477
+
478
+ /**
479
+ * Diagnostic hooks for observing engine updates.
480
+ */
481
+ export type Hooks<
482
+ M extends Contracts.Intent.Map,
483
+ C extends Contracts.Cursor.Map<M>,
484
+ P extends string,
485
+ R extends Contracts.Result.Base = Contracts.Result.Base,
486
+ > = {
487
+ /** Callback invoked on every state-reducing internal event. */
488
+ onInternalEvent?: (event: Event<M, C, P, R>, state: State<M, C, P, R>) => void
489
+ }
364
490
  }