@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,225 +0,0 @@
1
- //#region src/core/contracts/intent.types.d.ts
2
- /**
3
- * Base shape for all upload intents.
4
- *
5
- * An "intent" is data provided by your backend that tells the client how to upload a file
6
- * (for example: a presigned POST, presigned PUT, multipart upload IDs, part URLs, headers, etc).
7
- *
8
- * Every intent must include:
9
- * - `strategy`: a discriminant used to select an upload strategy implementation
10
- * - `fileId`: a stable backend identifier for the file being uploaded
11
- *
12
- * @typeParam K - Strategy key / discriminant string (example: `"direct" | "multipart"`).
13
- */
14
- type IntentBase<K extends string = string> = {
15
- /** Strategy name (discriminant). */strategy: K; /** Unique backend file identifier. */
16
- fileId: string;
17
- };
18
- /**
19
- * Registry of intent types mapped by strategy name.
20
- *
21
- * Example:
22
- * ```ts
23
- * type MyIntents = {
24
- * direct: DirectIntent
25
- * multipart: MultipartIntent
26
- * }
27
- * ```
28
- *
29
- * `M[keyof M]` is used throughout the engine as the "any intent" union.
30
- */
31
- type IntentMap = Record<string, IntentBase<string>>;
32
- /** Union of strategy keys for an intent map. */
33
- type StrategyKey<M extends IntentMap> = keyof M & string;
34
- /** Union of all intent variants for an intent map. */
35
- type AnyIntent<M extends IntentMap> = M[StrategyKey<M>];
36
- //#endregion
37
- //#region src/core/contracts/result.types.d.ts
38
- /**
39
- * Base shape for upload completion results returned by your backend.
40
- *
41
- * Extend this with any additional metadata you return (e.g. width/height, variants).
42
- */
43
- type UploadResultBase = {
44
- /** Backend file identifier. */fileId: string; /** Storage key or path used for signed URLs. */
45
- key: string;
46
- };
47
- //#endregion
48
- //#region src/core/contracts/backendApi.d.ts
49
- type WithSignal = {
50
- signal?: AbortSignal;
51
- };
52
- interface UploadApi<M extends IntentMap, P extends string, R extends UploadResultBase = UploadResultBase> {
53
- createIntent(args: {
54
- purpose: P;
55
- contentType: string;
56
- size: number;
57
- filename: string;
58
- checksum?: string;
59
- }, opts?: WithSignal): Promise<M[keyof M]>;
60
- complete(args: {
61
- fileId: string;
62
- }, opts?: WithSignal): Promise<R>;
63
- getSignedPreviewUrl?(args: {
64
- fileId: string;
65
- key: string;
66
- purpose: P;
67
- }, opts?: WithSignal): Promise<string>;
68
- /**
69
- * Optional support for finding existing files by checksum for deduplication.
70
- * If implemented, the upload engine will check for existing files before uploading.
71
- * If a file with the same checksum exists, the upload will be skipped and the existing file will be linked.
72
- */
73
- findByChecksum?(args: {
74
- checksum: string;
75
- purpose: P;
76
- }, opts?: WithSignal): Promise<R | null>;
77
- multipart?: {
78
- signPart(args: {
79
- fileId: string;
80
- uploadId: string;
81
- partNumber: number;
82
- checksum?: string;
83
- }, opts?: WithSignal): Promise<{
84
- url: string;
85
- headers?: Record<string, string>;
86
- }>;
87
- completeMultipart(args: {
88
- fileId: string;
89
- uploadId: string;
90
- parts: Array<{
91
- partNumber: number;
92
- etag: string;
93
- }>;
94
- }, opts?: WithSignal): Promise<unknown>;
95
- listParts?(args: {
96
- fileId: string;
97
- uploadId: string;
98
- }, opts?: WithSignal): Promise<Array<{
99
- partNumber: number;
100
- etag?: string;
101
- size?: number;
102
- }>>;
103
- abort?(args: {
104
- fileId: string;
105
- uploadId: string;
106
- }, opts?: WithSignal): Promise<void>;
107
- };
108
- tus?: {
109
- create(args: {
110
- fileId: string;
111
- size: number;
112
- filename: string;
113
- contentType: string;
114
- }, opts?: WithSignal): Promise<{
115
- uploadUrl: string;
116
- }>;
117
- getOffset(args: {
118
- uploadUrl: string;
119
- }, opts?: WithSignal): Promise<{
120
- offset: number;
121
- }>;
122
- };
123
- }
124
- //#endregion
125
- //#region src/core/contracts/cursor.types.d.ts
126
- /**
127
- * Registry of cursor payload types mapped by strategy name.
128
- *
129
- * A "cursor" is strategy-defined resume state (ex: multipart uploaded parts, next byte offset).
130
- * Use `unknown` here and define concrete cursor payload types in your app.
131
- *
132
- * @typeParam M - The intent map, used to constrain strategy keys.
133
- */
134
- type CursorMap<M extends IntentMap> = Partial<Record<StrategyKey<M>, unknown>>;
135
- /**
136
- * Discriminated union of all cursor variants.
137
- *
138
- * The cursor is always tagged with a `strategy` so it can be routed back to the correct strategy
139
- * on resume/rebind.
140
- *
141
- * @typeParam C - Cursor registry (usually {@link CursorMap}).
142
- */
143
- type AnyCursor<C extends Record<string, unknown>> = { [K in keyof C & string]: {
144
- strategy: K;
145
- value?: C[K];
146
- } }[keyof C & string];
147
- //#endregion
148
- //#region src/core/contracts/transport/transport.types.d.ts
149
- /**
150
- * Abstraction layer for network operations.
151
- * Allows switching between XHR (browser) or other implementations (testing/Node).
152
- *
153
- * Designed to support:
154
- * - Deterministic progress tracking (requires XHR in browsers)
155
- * - Cancellation via AbortSignal
156
- * - Header parsing (ETag extraction)
157
- */
158
- interface UploadTransport {
159
- /**
160
- * Performs an HTTP PUT request (typically for S3 signed URLs).
161
- */
162
- put(args: {
163
- url: string;
164
- body: Blob;
165
- headers?: Record<string, string>;
166
- signal: AbortSignal;
167
- onProgress?: (u: number, t: number) => void;
168
- }): Promise<{
169
- etag?: string;
170
- headers?: Record<string, string>;
171
- }>;
172
- /**
173
- * Performs an HTTP POST multipart request.
174
- */
175
- postForm(args: {
176
- url: string;
177
- fields: Record<string, string>;
178
- file: File | Blob;
179
- filename?: string;
180
- signal: AbortSignal;
181
- onProgress?: (uploadedBytes: number, totalBytes: number) => void;
182
- }): Promise<{
183
- etag?: string;
184
- headers?: Record<string, string>;
185
- }>;
186
- /**
187
- * Performs an HTTP request.
188
- */
189
- patch(args: {
190
- url: string;
191
- body: Blob | ArrayBuffer;
192
- headers?: Record<string, string>;
193
- signal: AbortSignal;
194
- onProgress?: (u: number, t: number) => void;
195
- }): Promise<{
196
- headers?: Record<string, string>;
197
- }>;
198
- }
199
- //#endregion
200
- //#region src/core/contracts/strategy.types.d.ts
201
- type StrategyCtx<M extends IntentMap, C extends CursorMap<M>, P extends string, R extends UploadResultBase, K extends keyof M & string> = {
202
- file: File;
203
- intent: M[K];
204
- signal: AbortSignal;
205
- transport: UploadTransport;
206
- api: UploadApi<M, P, R>;
207
- reportProgress: (p: {
208
- uploadedBytes: number;
209
- totalBytes: number;
210
- }) => void;
211
- readCursor: () => C[K] | undefined;
212
- persistCursor: (cursor: C[K] | undefined) => void;
213
- };
214
- type UploadStrategy<M extends IntentMap, C extends CursorMap<M>, P extends string, R extends UploadResultBase, K extends keyof M & string> = {
215
- id: K;
216
- resumable: boolean;
217
- start(ctx: StrategyCtx<M, C, P, R, K>): Promise<void>;
218
- };
219
- interface StrategyRegistry<M extends IntentMap, C extends CursorMap<M>, P extends string = string, R extends UploadResultBase = UploadResultBase> {
220
- get<K extends keyof M & string>(id: K): UploadStrategy<M, C, P, R, K> | undefined;
221
- has(id: string): id is keyof M & string;
222
- set<K extends keyof M & string>(strategy: UploadStrategy<M, C, P, R, K>): void;
223
- }
224
- //#endregion
225
- export { AnyCursor as a, WithSignal as c, IntentBase as d, IntentMap as f, UploadTransport as i, UploadResultBase as l, StrategyRegistry as n, CursorMap as o, StrategyKey as p, UploadStrategy as r, UploadApi as s, StrategyCtx as t, AnyIntent as u };
@@ -1,5 +0,0 @@
1
- // Tests in this file referenced a planned effect-pool / watchdog / regression
2
- // API that was never ported into `src/`. The intended surface (WaitForError,
3
- // runUpload handler, frozen snapshots, listener-error hooks, queue compaction,
4
- // rebind handler, etc.) does not exist. Deleted until the source-of-truth API
5
- // stabilizes.
@@ -1,14 +0,0 @@
1
- import { describe, expect, test } from 'vitest'
2
- import { generateId } from '../core/utils/id'
3
-
4
- describe('generateId', () => {
5
- test('starts with the `upload_` prefix', () => {
6
- expect(generateId()).toMatch(/^upload_/)
7
- })
8
-
9
- test('produces unique values across many calls', () => {
10
- const set = new Set<string>()
11
- for (let i = 0; i < 10_000; i++) set.add(generateId())
12
- expect(set.size).toBe(10_000)
13
- })
14
- })
@@ -1,5 +0,0 @@
1
- // Tests in this file referenced a planned store API surface (store.flush,
2
- // store.destroy, store.dispose, store.has, store.purge, store.outcomes,
3
- // store.cleanup, store.isDestroyed, store.ready, rebind handler, tombstones,
4
- // onPersistenceLoopStarved, etc.) that does not exist in `src/`. Deleted
5
- // until the source-of-truth API stabilizes.
@@ -1,50 +0,0 @@
1
- import { Defaults } from '../utils/constants'
2
- import type { Client } from './client.types'
3
-
4
- /**
5
- * Apply defaults to a partial {@link Client.IUploadConfig}.
6
- *
7
- * @param input Partial config from the caller. Nested groups
8
- * (e.g. `effectQueueCompaction`) accept their own partial shape.
9
- * @returns Fully populated config with each numeric field clamped to a sane
10
- * minimum.
11
- * @template P Purpose string union.
12
- * @author wildduck2 <https://github.com/wildduck2>
13
- */
14
- export function resolveUploadConfig<P extends string>(input?: Client.UploadConfigInput<P>): Client.IUploadConfig<P> {
15
- return {
16
- maxConcurrentUploads: Math.max(1, input?.maxConcurrentUploads ?? Defaults.MAX_CONCURRENT_UPLOADS),
17
- autoStart: input?.autoStart,
18
- progressThrottleMs: Math.max(0, input?.progressThrottleMs ?? Defaults.PROGRESS_THROTTLE_MS),
19
- validation: input?.validation ?? {},
20
- maxAttempts: Math.max(1, input?.maxAttempts ?? Defaults.MAX_ATTEMPTS),
21
- retryPolicy: input?.retryPolicy,
22
- maxItems: input?.maxItems === null ? null : Math.max(1, input?.maxItems ?? Defaults.MAX_ITEMS),
23
- completedItemTTL: input?.completedItemTTL,
24
- effectConcurrency: Math.max(1, input?.effectConcurrency ?? Defaults.EFFECT_CONCURRENCY),
25
- effectTimeoutMs: Math.max(0, input?.effectTimeoutMs ?? Defaults.EFFECT_TIMEOUT_MS),
26
- errorContextInMessage: input?.errorContextInMessage ?? false,
27
- keepRawCause: input?.keepRawCause ?? false,
28
- effectQueueCompaction: {
29
- minHead: Math.max(1, input?.effectQueueCompaction?.minHead ?? 64),
30
- ratioDenom: Math.max(2, input?.effectQueueCompaction?.ratioDenom ?? 2),
31
- maxQueueLength:
32
- input?.effectQueueCompaction?.maxQueueLength === null
33
- ? null
34
- : Math.max(1, input?.effectQueueCompaction?.maxQueueLength ?? 10_000),
35
- },
36
- checksumMaxSize:
37
- input?.checksumMaxSize === undefined
38
- ? null
39
- : input.checksumMaxSize === null
40
- ? null
41
- : Math.max(0, input.checksumMaxSize),
42
- checksumChunkBytes:
43
- input?.checksumChunkBytes === undefined
44
- ? null
45
- : input.checksumChunkBytes === null
46
- ? null
47
- : Math.max(1, input.checksumChunkBytes),
48
- strictRebindType: input?.strictRebindType ?? false,
49
- }
50
- }
@@ -1,184 +0,0 @@
1
- import type { Contracts } from '../contracts'
2
- import type { Engine } from '../engine/engine.types'
3
- import type { Reducer } from '../engine/reducer'
4
- import type { Store } from '../engine/store'
5
- import type { PersistenceError } from '../persistence/persistence.types'
6
-
7
- /**
8
- * Client-facing configuration, hooks, plugins, and retry policy types.
9
- *
10
- * @author wildduck2 <https://github.com/wildduck2>
11
- */
12
- export namespace Client {
13
- // -------------------- Retry --------------------
14
-
15
- /** Output of the retry policy. `delayMs` is a backoff before re-attempt. */
16
- export type RetryDecision = { retryable: false } | { retryable: true; delayMs: number }
17
-
18
- // -------------------- Config --------------------
19
-
20
- /** Effect-queue compaction tuning knobs. */
21
- export interface IEffectQueueCompaction {
22
- /** Absolute head-count floor before compaction. Default 64. */
23
- minHead: number
24
- /** Triggers compaction when `head * denom > length`. Default 2. */
25
- ratioDenom: number
26
- /**
27
- * Hard upper bound on queue length. New effects past this cap are
28
- * dropped + logged. Defends against a pathological dispatch storm that
29
- * grows the queue faster than compaction drains. `null` disables.
30
- * Default `10_000`.
31
- */
32
- maxQueueLength: number | null
33
- }
34
-
35
- /** Global upload engine configuration. */
36
- export interface IUploadConfig<P extends string> {
37
- /** Maximum concurrent in-flight uploads. Default `3`. */
38
- maxConcurrentUploads: number
39
- /** Auto-start `ready` items. */
40
- autoStart?: readonly P[] | ((purpose: P) => boolean)
41
- /** Throttle for `upload.progress` events in ms. Default `100`. */
42
- progressThrottleMs: number
43
- /** Per-purpose validation rules. */
44
- validation: Partial<Record<P, Contracts.IValidationRules>>
45
- /** Maximum attempts per phase before a failure is final. Default `3`. */
46
- maxAttempts: number
47
- /** Override the default retry policy. */
48
- retryPolicy?: (ctx: {
49
- phase: 'intent' | 'upload' | 'complete'
50
- attempt: number
51
- error: Contracts.Error
52
- }) => RetryDecision
53
- /** Cap on items kept in state. Set to `null` for no cap. Default `100`. */
54
- maxItems: number | null
55
- /** Auto-remove `completed`/`canceled` items after this many ms. */
56
- completedItemTTL?: number
57
- /** Concurrency cap for side-effects. Default `8`. */
58
- effectConcurrency: number
59
- /** Per-effect watchdog timeout in ms. `0` disables. Default `60000`. */
60
- effectTimeoutMs: number
61
- /** Include file context in persisted error messages. Default `false` (PII). */
62
- errorContextInMessage: boolean
63
- /** Keep raw thrown value as `error.cause`. Default `false` (PII). */
64
- keepRawCause: boolean
65
- /** Effect-queue compaction tuning. */
66
- effectQueueCompaction: IEffectQueueCompaction
67
- /**
68
- * Skip `crypto.subtle.digest` (and the dedupe lookup that depends on it)
69
- * for files larger than this byte threshold. The browser has no streaming
70
- * SHA-256, so digesting a 10GB file allocates 10GB of heap; the cap lets
71
- * the consumer trade dedupe coverage for memory. Default `null` (no cap).
72
- */
73
- checksumMaxSize: number | null
74
- /**
75
- * Hash only the first N bytes of each file. Yields a fast approximate
76
- * fingerprint at the cost of strict uniqueness: files that share the
77
- * leading window collide. Combine with `size + lastModified + chunk
78
- * checksum` on the backend to form a unique dedupe key. Default
79
- * `null` (full-file hash, exact match).
80
- *
81
- * @remarks Set to a small value (e.g. `1024 * 64`) for fast
82
- * checksums on very large files where exact dedupe is not
83
- * required.
84
- */
85
- checksumChunkBytes: number | null
86
- /**
87
- * When `true`, `rebind` requires the rebound `File.type` to match the
88
- * paused item's fingerprint type. Default `false` because browsers
89
- * disagree on MIME detection (Safari often omits MIME types Chrome
90
- * attaches). Enable when both sessions are guaranteed to run in the
91
- * same browser/runtime.
92
- */
93
- strictRebindType: boolean
94
- }
95
-
96
- /**
97
- * Partial form accepted by `createUploadStore`. Nested config groups
98
- * (e.g. `effectQueueCompaction`) accept their own partial shape so
99
- * callers can override one knob without restating the rest.
100
- */
101
- export type UploadConfigInput<P extends string> = Partial<Omit<IUploadConfig<P>, 'effectQueueCompaction'>> & {
102
- effectQueueCompaction?: Partial<IEffectQueueCompaction>
103
- }
104
-
105
- // -------------------- Hooks --------------------
106
-
107
- /** Optional observability hooks. Diagnostics / analytics / devtools / Sentry. */
108
- export interface IUploadHooks<
109
- M extends Contracts.IntentMap,
110
- C extends Contracts.CursorMap<M>,
111
- P extends string,
112
- R extends Contracts.IResultBase = Contracts.IResultBase,
113
- > {
114
- /** Fires after every internal event applied by the reducer. */
115
- onInternalEvent?: (event: Engine.InternalEvent<M, C, P, R>, state: Reducer.IState<M, C, P, R>) => void
116
- /** Fires when a persistence adapter operation fails. */
117
- onPersistenceError?: (error: PersistenceError) => void
118
- /** Fires on listener / plugin / emitter / batch throws. */
119
- onListenerError?: (
120
- error: unknown,
121
- context: { kind: 'subscriber' | 'plugin-setup' | 'emitter' | 'batch'; name?: string },
122
- ) => void
123
- /**
124
- * Fires when the effect queue rejects a new effect because it is at
125
- * `effectQueueCompaction.maxQueueLength`. Production telemetry hook so
126
- * dropped effects are not silently swallowed. Passes the current queue
127
- * length (`queueLength`), active in-flight effect count (`active`), the
128
- * hard cap (`cap`), and the optional `localId` of the upload that
129
- * triggered the enqueue (when the caller threaded it through).
130
- */
131
- onEffectDropped?: (context: { queueLength: number; active: number; cap: number; localId?: string }) => void
132
- /**
133
- * Fires when `scheduleWork`'s re-entrant pass loop hits the soft cap
134
- * (default 100). Production telemetry hook so a runaway feedback
135
- * loop is visible even when dev-mode `console.error` is gated out.
136
- */
137
- onScheduleStarved?: (context: { passes: number }) => void
138
- /**
139
- * Fires when `flushPersistence`'s trailing-flush loop hits the soft
140
- * cap. A pathological feedback loop (e.g. `onPersistenceError`
141
- * dispatching state changes that schedule another flush) would
142
- * otherwise spin forever. Defaults to 100 trailing iterations.
143
- */
144
- onPersistenceLoopStarved?: (context: { passes: number }) => void
145
- }
146
-
147
- // -------------------- Plugin --------------------
148
-
149
- /** Store plugin. Subscribed once at construction with a narrow store view. */
150
- export interface IUploadPlugin<
151
- M extends Contracts.IntentMap,
152
- C extends Contracts.CursorMap<M>,
153
- P extends string,
154
- R extends Contracts.IResultBase = Contracts.IResultBase,
155
- > {
156
- /** Plugin name (used in debugging output + `onListenerError` context). */
157
- name: string
158
- /**
159
- * Called once at construction. May return a `Promise` to delay
160
- * `store.ready` until the plugin finishes initializing (e.g. fetching
161
- * remote feature flags). Sync throws and async rejections are both
162
- * routed through `onListenerError`.
163
- *
164
- * Wider surface than R23: plugins now also see `flush` / `cleanup` /
165
- * `purge` / `has` / `waitFor` / `subscribe` so a plugin can await
166
- * durability, do bulk operations, or subscribe via the public
167
- * mechanism instead of holding raw `rt` references.
168
- */
169
- setup: (
170
- ctx: Pick<
171
- Store.IUploadStore<M, C, P, R>,
172
- 'on' | 'off' | 'dispatch' | 'getSnapshot' | 'subscribe' | 'waitFor' | 'flush' | 'cleanup' | 'purge' | 'has'
173
- >,
174
- ) => void | Promise<void>
175
- /**
176
- * Called once during `store.destroy()` so the plugin can release its
177
- * own resources (timers, external subscriptions, network handles).
178
- * May return a `Promise` -- `destroy()` awaits all plugin disposes
179
- * before resolving. Sync throws and async rejections route through
180
- * `onListenerError` and never reject `destroy()`.
181
- */
182
- dispose?: () => void | Promise<void>
183
- }
184
- }
@@ -1,77 +0,0 @@
1
- import type { UploadError, UploadValidationRules } from '../contracts'
2
- import {
3
- DEFAULT_MAX_ATTEMPTS,
4
- DEFAULT_MAX_CONCURRENT_UPLOADS,
5
- DEFAULT_MAX_ITEMS,
6
- DEFAULT_PROGRESS_THROTTLE_MS,
7
- } from '../utils/constants'
8
- import type { RetryDecision } from './retry.types'
9
-
10
- /**
11
- * Global upload engine configuration.
12
- *
13
- * @typeParam P - Union of allowed `purpose` strings for your app.
14
- */
15
- export type UploadConfig<P extends string> = {
16
- /** Maximum concurrent uploads. */
17
- maxConcurrentUploads: number
18
-
19
- /**
20
- * Auto-start uploads for specific purposes.
21
- * - Array form: only those purposes auto-start
22
- * - Function form: dynamic decision per purpose
23
- */
24
- autoStart?: readonly P[] | ((purpose: P) => boolean)
25
-
26
- /** Progress event throttle in milliseconds. */
27
- progressThrottleMs: number
28
-
29
- /** Validation rules keyed by purpose. */
30
- validation: Partial<Record<P, UploadValidationRules>>
31
-
32
- /** Maximum number of retry attempts (per item / per phase depending on implementation). */
33
- maxAttempts: number
34
-
35
- /**
36
- * Retry policy.
37
- * Called whenever a phase fails and the engine needs a decision.
38
- */
39
- retryPolicy?: (ctx: { phase: 'intent' | 'upload' | 'complete'; attempt: number; error: UploadError }) => RetryDecision
40
-
41
- /**
42
- * Maximum number of items to keep in state.
43
- * When exceeded, oldest completed/canceled items are removed automatically.
44
- *
45
- * Set to `undefined` for no limit (not recommended for long-running apps).
46
- * Default often: `100`.
47
- */
48
- maxItems: number
49
-
50
- /**
51
- * Automatically remove completed items after this many milliseconds.
52
- * Set to `undefined` to keep completed items indefinitely.
53
- */
54
- completedItemTTL?: number
55
- }
56
-
57
- /**
58
- * Config input shape accepted by store creation.
59
- * All fields are optional; defaults are applied by {@link resolveUploadConfig}.
60
- */
61
- export type UploadConfigInput<P extends string> = Partial<UploadConfig<P>>
62
-
63
- /**
64
- * Normalizes user config by applying sensible defaults.
65
- */
66
- export function resolveUploadConfig<P extends string>(input?: UploadConfigInput<P>): UploadConfig<P> {
67
- return {
68
- maxConcurrentUploads: Math.max(1, input?.maxConcurrentUploads ?? DEFAULT_MAX_CONCURRENT_UPLOADS),
69
- autoStart: input?.autoStart,
70
- progressThrottleMs: Math.max(0, input?.progressThrottleMs ?? DEFAULT_PROGRESS_THROTTLE_MS),
71
- validation: input?.validation ?? {},
72
- maxAttempts: Math.max(1, input?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS),
73
- retryPolicy: input?.retryPolicy,
74
- maxItems: Math.max(1, input?.maxItems ?? DEFAULT_MAX_ITEMS),
75
- completedItemTTL: input?.completedItemTTL,
76
- }
77
- }
@@ -1,4 +0,0 @@
1
- export * from './config.types'
2
- export * from './plugins.types'
3
- export * from './retry.types'
4
- export * from './uploadClient'
@@ -1,46 +0,0 @@
1
- import type { CursorMap, IntentMap, UploadResultBase } from '../contracts'
2
- import type { InternalEvent } from '../engine/internal-events.types'
3
- import type { UploadStore } from '../engine/store'
4
-
5
- /**
6
- * Optional hooks for observing internal behavior.
7
- *
8
- * Use these for diagnostics, analytics, logging, or devtools.
9
- */
10
- export type UploadHooks<
11
- M extends IntentMap,
12
- C extends CursorMap<M>,
13
- P extends string,
14
- R extends UploadResultBase = UploadResultBase,
15
- > = {
16
- /**
17
- * Called after internal events are processed (depending on your runtime wiring).
18
- * Prefer this over subscribing to public events if you need full fidelity.
19
- */
20
- onInternalEvent?: (
21
- event: InternalEvent<M, C, P, R>,
22
- state: import('../engine/reducer').UploadState<M, C, P, R>,
23
- ) => void
24
- }
25
-
26
- /**
27
- * Store plugins.
28
- *
29
- * Plugins can subscribe to store events and dispatch commands.
30
- * They should not reach into internal state mutably.
31
- */
32
- export type UploadPlugin<
33
- M extends IntentMap,
34
- C extends CursorMap<M>,
35
- P extends string,
36
- R extends UploadResultBase = UploadResultBase,
37
- > = {
38
- /** Plugin name (for debugging). */
39
- name: string
40
-
41
- /**
42
- * Setup callback called once when the store is created.
43
- * The plugin can register event listeners and drive behavior.
44
- */
45
- setup: (ctx: Pick<UploadStore<M, C, P, R>, 'on' | 'dispatch' | 'getSnapshot'>) => void
46
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Output of the retry policy.
3
- *
4
- * `delayMs` should be a backoff delay before re-attempting the failed step.
5
- */
6
- export type RetryDecision = { retryable: false } | { retryable: true; delayMs: number }
@@ -1,19 +0,0 @@
1
- import type { CursorMap, IntentMap, UploadResultBase } from '../contracts'
2
- import type { StoreOptions, UploadStore } from '../engine/store'
3
- import { createUploadStore } from '../engine/store'
4
-
5
- export type UploadClient<
6
- M extends IntentMap,
7
- C extends CursorMap<M>,
8
- P extends string,
9
- R extends UploadResultBase = UploadResultBase,
10
- > = UploadStore<M, C, P, R>
11
-
12
- export function createUploadClient<
13
- M extends IntentMap,
14
- C extends CursorMap<M>,
15
- P extends string,
16
- R extends UploadResultBase = UploadResultBase,
17
- >(opts: StoreOptions<M, C, P, R>): UploadClient<M, C, P, R> {
18
- return createUploadStore(opts)
19
- }