@gentleduck/upload 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +297 -0
- package/dist/core/index.d.ts +2 -3
- package/dist/core/index.js +1 -1
- package/dist/core-BVgloTiP.js +10 -0
- package/dist/index-DCqyBVcx.d.ts +1441 -0
- package/dist/index-DsZHPmJm.d.ts +225 -0
- package/dist/index-KROXtLQh.d.ts +281 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +2 -90
- package/dist/react/index.js +1 -1
- package/dist/react-euPrDv24.js +1 -0
- package/dist/strategies/index.d.ts +2 -56
- package/dist/strategies/index.js +1 -1
- package/dist/strategies-CUF0s68j.js +1 -0
- package/package.json +3 -2
- package/src/__tests__/emitter.test.ts +85 -0
- package/src/__tests__/fingerprint.test.ts +49 -0
- package/src/__tests__/guards.test.ts +40 -0
- package/src/__tests__/id.test.ts +67 -0
- package/src/__tests__/mime-sniff.test.ts +142 -0
- package/src/__tests__/persistence.test.ts +191 -0
- package/src/__tests__/sanitize-filename.test.ts +109 -0
- package/src/__tests__/sleep.test.ts +34 -0
- package/src/__tests__/static-analysis.test.ts +145 -0
- package/src/__tests__/store-libs.test.ts +192 -0
- package/src/__tests__/transport-fetch.test.ts +108 -0
- package/src/__tests__/transport-libs.test.ts +42 -0
- package/src/__tests__/transport.test.ts +182 -0
- package/src/core/__tests__/errors.test.ts +152 -0
- package/src/core/client.ts +3 -0
- package/src/core/contracts/contracts.types.ts +373 -0
- package/src/core/contracts/index.ts +3 -8
- package/src/core/contracts/transport/index.ts +3 -2
- package/src/core/contracts/transport/transport.fetch.ts +203 -0
- package/src/core/contracts/transport/transport.libs.ts +13 -39
- package/src/core/contracts/transport/transport.ts +96 -90
- package/src/core/contracts/transport/transport.types.ts +71 -35
- package/src/core/engine/__tests__/config-nan-bypass.test.ts +40 -0
- package/src/core/engine/engine.libs.ts +35 -0
- package/src/core/engine/engine.types.ts +490 -0
- package/src/core/engine/index.ts +3 -8
- package/src/core/engine/reducer.ts +93 -53
- package/src/core/engine/store/handlers/add-file.ts +43 -48
- package/src/core/engine/store/handlers/cancel.ts +13 -8
- package/src/core/engine/store/handlers/clean-up.ts +11 -8
- package/src/core/engine/store/handlers/create-intent.ts +82 -27
- package/src/core/engine/store/handlers/finalize.ts +30 -13
- package/src/core/engine/store/handlers/index.ts +7 -7
- package/src/core/engine/store/handlers/pause.ts +8 -6
- package/src/core/engine/store/handlers/run-upload.ts +54 -39
- package/src/core/engine/store/index.ts +27 -21
- package/src/core/engine/store/store.dispatch.ts +10 -8
- package/src/core/engine/store/store.libs.ts +197 -94
- package/src/core/engine/store/store.runtime.ts +52 -53
- package/src/core/engine/store/store.schedule.ts +27 -21
- package/src/core/engine/store/store.types.ts +177 -227
- package/src/core/engine/validation/__tests__/intent.test.ts +70 -0
- package/src/core/engine/validation/file.ts +8 -4
- package/src/core/engine/validation/index.ts +1 -0
- package/src/core/engine/validation/intent.ts +49 -27
- package/src/core/engine/validation/list.ts +14 -15
- package/src/core/engine/validation/mime.ts +39 -0
- package/src/core/errors.ts +271 -0
- package/src/core/index.ts +28 -5
- package/src/core/persistence/__tests__/deserialize-nan-bypass.test.ts +100 -0
- package/src/core/persistence/__tests__/persistence.test.ts +85 -0
- package/src/core/persistence/adapters.indexeddb.ts +9 -3
- package/src/core/persistence/adapters.local.ts +9 -3
- package/src/core/persistence/adapters.memory.ts +35 -13
- package/src/core/persistence/index.ts +5 -5
- package/src/core/persistence/persistence.ts +73 -60
- package/src/core/persistence/persistence.types.ts +140 -60
- package/src/core/utils/__tests__/guards.test.ts +93 -0
- package/src/core/utils/__tests__/url-safety.test.ts +112 -0
- package/src/core/utils/async.ts +7 -2
- package/src/core/utils/constants.ts +3 -9
- package/src/core/utils/emitter/emitter.ts +126 -0
- package/src/core/utils/emitter/emitter.types.ts +33 -16
- package/src/core/utils/emitter/index.ts +2 -74
- package/src/core/utils/fingerprint.ts +16 -5
- package/src/core/utils/guards.ts +91 -1
- package/src/core/utils/hash/__tests__/sha256.test.ts +61 -0
- package/src/core/utils/hash/index.ts +126 -0
- package/src/core/utils/hash/sha256.ts +179 -0
- package/src/core/utils/id.ts +33 -1
- package/src/core/utils/index.ts +15 -6
- package/src/core/utils/mime-sniff.ts +119 -0
- package/src/core/utils/sanitize-filename.ts +85 -0
- package/src/core/utils/url-safety.ts +156 -0
- package/src/react/__tests__/use-dropzone.test.ts +64 -0
- package/src/react/index.ts +5 -2
- package/src/react/upload-provider.tsx +59 -95
- package/src/react/uploader.types.ts +80 -0
- package/src/react/use-dropzone.tsx +226 -0
- package/src/react/use-uploader.tsx +97 -93
- package/src/strategies/_shared/__tests__/retry.test.ts +120 -0
- package/src/strategies/_shared/retry.ts +79 -0
- package/src/strategies/index.ts +9 -15
- package/src/strategies/multipart/__tests__/multipart.test.ts +229 -0
- package/src/strategies/multipart/index.ts +211 -116
- package/src/strategies/post/__tests__/post-url-safety.test.ts +119 -0
- package/src/strategies/post/index.ts +113 -71
- package/src/strategies/put/__tests__/put.test.ts +132 -0
- package/src/strategies/put/index.ts +139 -0
- package/src/strategies/registry/index.ts +12 -9
- package/src/strategies/tus/__tests__/tus.test.ts +159 -0
- package/src/strategies/tus/index.ts +211 -0
- package/dist/async-DxRWu4le.js +0 -1
- package/dist/core-BNxznRif.js +0 -2
- package/dist/index-BXg9WIce.d.ts +0 -987
- package/dist/strategy.types-gg9ZStRe.d.ts +0 -225
- package/src/core/client/config.types.ts +0 -77
- package/src/core/client/index.ts +0 -4
- package/src/core/client/plugins.types.ts +0 -46
- package/src/core/client/retry.types.ts +0 -6
- package/src/core/client/uploadClient.ts +0 -19
- package/src/core/contracts/backendApi.ts +0 -62
- package/src/core/contracts/cursor.types.ts +0 -23
- package/src/core/contracts/errors.types.ts +0 -67
- package/src/core/contracts/fingerprint.types.ts +0 -18
- package/src/core/contracts/intent.types.ts +0 -39
- package/src/core/contracts/result.types.ts +0 -11
- package/src/core/contracts/strategy.types.ts +0 -45
- package/src/core/engine/commands.types.ts +0 -17
- package/src/core/engine/event-map.types.ts +0 -45
- package/src/core/engine/internal-events.types.ts +0 -222
- package/src/core/engine/outcome.types.ts +0 -9
- package/src/core/engine/progress.types.ts +0 -15
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { Contracts } from '../../contracts'
|
|
2
|
+
import type { Engine } from '../engine.types'
|
|
3
3
|
import { createIntent } from './handlers/create-intent'
|
|
4
4
|
import { finalizeUpload } from './handlers/finalize'
|
|
5
5
|
import { runUpload } from './handlers/run-upload'
|
|
6
6
|
import { isAutoStart } from './store.libs'
|
|
7
|
-
import type {
|
|
7
|
+
import type { Store } from './store.types'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Central scheduler that ensures the system is always making progress towards a consistent state.
|
|
@@ -15,9 +15,12 @@ import type { StoreRuntime } from './store.types'
|
|
|
15
15
|
*
|
|
16
16
|
* This function is idempotent and safe to call multiple times.
|
|
17
17
|
*/
|
|
18
|
-
export function scheduleWork<
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export function scheduleWork<
|
|
19
|
+
M extends Contracts.Intent.Map,
|
|
20
|
+
C extends Contracts.Cursor.Map<M>,
|
|
21
|
+
P extends string,
|
|
22
|
+
R extends Contracts.Result.Base,
|
|
23
|
+
>(rt: Store.Runtime<M, C, P, R>) {
|
|
21
24
|
if (rt.scheduling) return
|
|
22
25
|
rt.scheduling = true
|
|
23
26
|
try {
|
|
@@ -36,11 +39,11 @@ export function scheduleWork<M extends IntentMap, C extends CursorMap<M>, P exte
|
|
|
36
39
|
* {@link StoreRuntime.inflightIntents}.
|
|
37
40
|
*/
|
|
38
41
|
export function scheduleIntentCreations<
|
|
39
|
-
M extends
|
|
40
|
-
C extends
|
|
42
|
+
M extends Contracts.Intent.Map,
|
|
43
|
+
C extends Contracts.Cursor.Map<M>,
|
|
41
44
|
P extends string,
|
|
42
|
-
R extends
|
|
43
|
-
>(rt:
|
|
45
|
+
R extends Contracts.Result.Base,
|
|
46
|
+
>(rt: Store.Runtime<M, C, P, R>) {
|
|
44
47
|
for (const item of rt.state.items.values()) {
|
|
45
48
|
if (item.phase !== 'creating_intent') continue
|
|
46
49
|
if (rt.inflightIntents.has(item.localId)) continue
|
|
@@ -54,11 +57,11 @@ export function scheduleIntentCreations<
|
|
|
54
57
|
* Finalization runs as an async effect and is guarded by {@link StoreRuntime.inflightCompletes}.
|
|
55
58
|
*/
|
|
56
59
|
export function scheduleCompletes<
|
|
57
|
-
M extends
|
|
58
|
-
C extends
|
|
60
|
+
M extends Contracts.Intent.Map,
|
|
61
|
+
C extends Contracts.Cursor.Map<M>,
|
|
59
62
|
P extends string,
|
|
60
|
-
R extends
|
|
61
|
-
>(rt:
|
|
63
|
+
R extends Contracts.Result.Base,
|
|
64
|
+
>(rt: Store.Runtime<M, C, P, R>) {
|
|
62
65
|
for (const item of rt.state.items.values()) {
|
|
63
66
|
if (item.phase !== 'completing') continue
|
|
64
67
|
if (rt.inflightCompletes.has(item.localId)) continue
|
|
@@ -71,11 +74,11 @@ export function scheduleCompletes<
|
|
|
71
74
|
* Respects `maxConcurrentUploads` from config.
|
|
72
75
|
*/
|
|
73
76
|
export function scheduleUploads<
|
|
74
|
-
M extends
|
|
75
|
-
C extends
|
|
77
|
+
M extends Contracts.Intent.Map,
|
|
78
|
+
C extends Contracts.Cursor.Map<M>,
|
|
76
79
|
P extends string,
|
|
77
|
-
R extends
|
|
78
|
-
>(rt:
|
|
80
|
+
R extends Contracts.Result.Base,
|
|
81
|
+
>(rt: Store.Runtime<M, C, P, R>) {
|
|
79
82
|
const maxConcurrent = Math.max(1, rt.opts.config.maxConcurrentUploads)
|
|
80
83
|
const active = rt.inflightUploads.size
|
|
81
84
|
|
|
@@ -109,8 +112,11 @@ export function scheduleUploads<
|
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
function isQueuedItem<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
function isQueuedItem<
|
|
116
|
+
M extends Contracts.Intent.Map,
|
|
117
|
+
C extends Contracts.Cursor.Map<M>,
|
|
118
|
+
P extends string,
|
|
119
|
+
R extends Contracts.Result.Base,
|
|
120
|
+
>(item: Engine.Item<M, C, P, R>): item is Extract<Engine.Item<M, C, P, R>, { phase: 'queued' }> {
|
|
115
121
|
return item.phase === 'queued'
|
|
116
122
|
}
|
|
@@ -1,250 +1,200 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
RejectReason,
|
|
7
|
-
StrategyRegistry,
|
|
8
|
-
UploadApi,
|
|
9
|
-
UploadError,
|
|
10
|
-
UploadResultBase,
|
|
11
|
-
UploadTransport,
|
|
12
|
-
} from '../../contracts'
|
|
13
|
-
import type { PersistedSnapshot, PersistenceAdapter } from '../../persistence'
|
|
14
|
-
import type { TypedEmitter } from '../../utils/emitter'
|
|
15
|
-
import type { UploadCommand } from '../commands.types'
|
|
16
|
-
import type { UploadEventMap } from '../event-map.types'
|
|
17
|
-
import type { InternalEvent } from '../internal-events.types'
|
|
18
|
-
import type { UploadOutcome } from '../outcome.types'
|
|
19
|
-
import type { createReducer, UploadState } from '../reducer'
|
|
1
|
+
import type { Contracts, Transport } from '../../contracts'
|
|
2
|
+
import type { UploadPersistence } from '../../persistence'
|
|
3
|
+
import type { Emitter } from '../../utils'
|
|
4
|
+
import type { Engine } from '../engine.types'
|
|
5
|
+
import type { createReducer } from '../reducer'
|
|
20
6
|
|
|
21
7
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* The store is intentionally minimal:
|
|
25
|
-
* - state is read through {@link UploadStore.getSnapshot}
|
|
26
|
-
* - updates are observed through {@link UploadStore.subscribe} and {@link UploadStore.on}
|
|
27
|
-
* - actions are performed through {@link UploadStore.dispatch}
|
|
28
|
-
*
|
|
29
|
-
* @template M - Intent map type (keyed by strategy id)
|
|
30
|
-
* @template C - Cursor map type (keyed by strategy id)
|
|
31
|
-
* @template P - Purpose string union type
|
|
8
|
+
* Store-level namespaces and contracts.
|
|
9
|
+
* @author wildduck2 <https://github.com/wildduck2>
|
|
32
10
|
*/
|
|
33
|
-
export
|
|
34
|
-
M extends IntentMap,
|
|
35
|
-
C extends CursorMap<M>,
|
|
36
|
-
P extends string,
|
|
37
|
-
R extends UploadResultBase = UploadResultBase,
|
|
38
|
-
> {
|
|
11
|
+
export namespace Store {
|
|
39
12
|
/**
|
|
40
|
-
*
|
|
41
|
-
* Useful for React loops or debugging.
|
|
42
|
-
*/
|
|
43
|
-
getSnapshot(): UploadState<M, C, P, R>
|
|
44
|
-
/**
|
|
45
|
-
* Subscribes to ANY state change.
|
|
46
|
-
* The listener is called after the reducer has run and effects have been scheduled.
|
|
47
|
-
* @param listener - Listener function to call on state change
|
|
48
|
-
*/
|
|
49
|
-
subscribe(listener: () => void): () => void
|
|
50
|
-
/**
|
|
51
|
-
* Main entry point for user actions.
|
|
52
|
-
* Converts external commands into internal state transitions and side effects.
|
|
13
|
+
* Public upload store interface used by UI adapters and application code.
|
|
53
14
|
*
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*
|
|
59
|
-
*
|
|
15
|
+
* Coordinates commands, subscriptions, event bindings, and lifecycle states.
|
|
16
|
+
*
|
|
17
|
+
* @template M - Intent map type (keyed by strategy id)
|
|
18
|
+
* @template C - Cursor map type (keyed by strategy id)
|
|
19
|
+
* @template P - Purpose string union type
|
|
20
|
+
* @template R - Backend result shape
|
|
60
21
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
22
|
+
export type UploadStore<
|
|
23
|
+
M extends Contracts.Intent.Map,
|
|
24
|
+
C extends Contracts.Cursor.Map<M>,
|
|
25
|
+
P extends string,
|
|
26
|
+
R extends Contracts.Result.Base = Contracts.Result.Base,
|
|
27
|
+
> = {
|
|
28
|
+
/**
|
|
29
|
+
* Returns the current immutable state snapshot.
|
|
30
|
+
* Use this in custom React lifecycle bindings or debug inspectors.
|
|
31
|
+
*/
|
|
32
|
+
getSnapshot(): Engine.State<M, C, P, R>
|
|
65
33
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Subscribes to any internal state change.
|
|
36
|
+
* Triggered after reducers process events and side effects are queued.
|
|
37
|
+
*
|
|
38
|
+
* @param listener - Callback invoked on state updates.
|
|
39
|
+
* @returns An unsubscribe function.
|
|
40
|
+
*/
|
|
41
|
+
subscribe(listener: () => void): () => void
|
|
74
42
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Dispatches a command to initiate state transitions (e.g. adding files, pausing, canceling).
|
|
45
|
+
*
|
|
46
|
+
* @param cmd - Public command payload.
|
|
47
|
+
*/
|
|
48
|
+
dispatch(cmd: Engine.Command<P>): void
|
|
80
49
|
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*
|
|
93
|
-
* @template M - Intent map type (keyed by strategy id)
|
|
94
|
-
* @template C - Cursor map type (keyed by strategy id)
|
|
95
|
-
* @template P - Purpose string union type
|
|
96
|
-
*/
|
|
97
|
-
export interface StoreOptions<
|
|
98
|
-
M extends IntentMap,
|
|
99
|
-
C extends CursorMap<M>,
|
|
100
|
-
P extends string,
|
|
101
|
-
R extends UploadResultBase = UploadResultBase,
|
|
102
|
-
> {
|
|
103
|
-
/** Initial state to hydrate from (e.g. from localStorage) */
|
|
104
|
-
initialState?: UploadState<M, C, P, R>
|
|
105
|
-
/** Static configuration rules (defaults are applied). */
|
|
106
|
-
config?: UploadConfigInput<P>
|
|
107
|
-
/** Persistence adapter and options */
|
|
108
|
-
persistence?: PersistenceOptions<M, C, P, R>
|
|
109
|
-
/** Backend API adapter for creating intents and finalizing uploads */
|
|
110
|
-
api: UploadApi<M, P, R>
|
|
111
|
-
/** Network transport (XHR/Fetch) for the actual file data transfer */
|
|
112
|
-
transport?: UploadTransport
|
|
113
|
-
/** Registry of available upload strategies (multipart, simple, etc.) */
|
|
114
|
-
strategies: StrategyRegistry<M, C, P, R>
|
|
115
|
-
/** Optional plugins to extend functionality (e.g. debugging, metrics) */
|
|
116
|
-
plugins?: Array<UploadPlugin<M, C, P, R>>
|
|
117
|
-
/** Lifecycle hooks for observing internal events */
|
|
118
|
-
hooks?: UploadHooks<M, C, P, R>
|
|
119
|
-
/**
|
|
120
|
-
* Optional custom fingerprinting (e.g. include sha256).
|
|
121
|
-
* Must be synchronous to keep addFiles fast.
|
|
122
|
-
*/
|
|
123
|
-
fingerprint?: (file: File) => FileFingerprint
|
|
124
|
-
/**
|
|
125
|
-
* If returns non-null, it rejects the file.
|
|
126
|
-
* Runs after built-in config validation.
|
|
127
|
-
*/
|
|
128
|
-
validateFile?: (file: File, purpose: P) => RejectReason | null
|
|
129
|
-
/**
|
|
130
|
-
* Optional custom error normalizer.
|
|
131
|
-
* Convert raw errors (from fetch, XHR, etc.) into your custom UploadError shape.
|
|
132
|
-
*/
|
|
133
|
-
errorNormalizer?: (err: unknown) => UploadError
|
|
134
|
-
}
|
|
50
|
+
/**
|
|
51
|
+
* Binds a listener to a specific typed event (e.g. `'upload.progress'`).
|
|
52
|
+
*
|
|
53
|
+
* @param type - Event name key.
|
|
54
|
+
* @param cb - Event listener callback.
|
|
55
|
+
* @returns An unsubscribe function.
|
|
56
|
+
*/
|
|
57
|
+
on: <K extends keyof Engine.EventMap<M, C, P, R> & string>(
|
|
58
|
+
type: K,
|
|
59
|
+
cb: (payload: Engine.EventMap<M, C, P, R>[K]) => void,
|
|
60
|
+
) => () => void
|
|
135
61
|
|
|
136
|
-
/**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Removes an event listener.
|
|
64
|
+
*
|
|
65
|
+
* @param type - Event name key.
|
|
66
|
+
* @param cb - Event listener callback.
|
|
67
|
+
*/
|
|
68
|
+
off: <K extends keyof Engine.EventMap<M, C, P, R> & string>(
|
|
69
|
+
type: K,
|
|
70
|
+
cb: (payload: Engine.EventMap<M, C, P, R>[K]) => void,
|
|
71
|
+
) => void
|
|
146
72
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
/** Persistence schema version (not app version) */
|
|
156
|
-
version: number
|
|
157
|
-
/** Debounce delay in milliseconds (default: 200) */
|
|
158
|
-
debounceMs?: number
|
|
159
|
-
/** Persistence adapter */
|
|
160
|
-
adapter: PersistenceAdapter
|
|
73
|
+
/**
|
|
74
|
+
* Resolves when the specified items reach a terminal state
|
|
75
|
+
* (completed, failed, canceled, or missing).
|
|
76
|
+
*
|
|
77
|
+
* @param localIds - Array of registered client file IDs.
|
|
78
|
+
*/
|
|
79
|
+
waitFor(localIds: string[]): Promise<Array<Engine.Outcome<R>>>
|
|
80
|
+
}
|
|
161
81
|
|
|
162
82
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
*
|
|
169
|
-
* Convert a JSON-safe snapshot into the upload state.
|
|
83
|
+
* Configuration options for constructing an upload store.
|
|
84
|
+
*
|
|
85
|
+
* @template M - Intent map type (keyed by strategy id)
|
|
86
|
+
* @template C - Cursor map type (keyed by strategy id)
|
|
87
|
+
* @template P - Purpose string union type
|
|
88
|
+
* @template R - Backend result shape
|
|
170
89
|
*/
|
|
171
|
-
|
|
90
|
+
export type Options<
|
|
91
|
+
M extends Contracts.Intent.Map,
|
|
92
|
+
C extends Contracts.Cursor.Map<M>,
|
|
93
|
+
P extends string,
|
|
94
|
+
R extends Contracts.Result.Base = Contracts.Result.Base,
|
|
95
|
+
> = {
|
|
96
|
+
/** Initial state to hydrate from (e.g. recovered from a persistence snapshot). */
|
|
97
|
+
initialState?: Engine.State<M, C, P, R>
|
|
98
|
+
|
|
99
|
+
/** Engine configuration (defaults applied for unspecified settings). */
|
|
100
|
+
config?: Partial<Engine.Config<P>>
|
|
101
|
+
|
|
102
|
+
/** Persistence adapter configuration (key prefix, adapters, storage handlers). */
|
|
103
|
+
persistence?: UploadPersistence.Options<M, C, P, R>
|
|
104
|
+
|
|
105
|
+
/** Your backend API implementation. */
|
|
106
|
+
api: Contracts.Api.Me<M, P, R>
|
|
107
|
+
|
|
108
|
+
/** Transport layer options for HTTP byte transfers. */
|
|
109
|
+
transport?: Transport.Options
|
|
110
|
+
|
|
111
|
+
/** Registry of available upload strategies (multipart, POST forms, simple PUTs, TUS). */
|
|
112
|
+
strategies: Contracts.Strategy.Registry<M, C, P, R>
|
|
113
|
+
|
|
114
|
+
/** Plugin modules to extend behavior (metrics, logging, analytics). */
|
|
115
|
+
plugins?: Array<Engine.Plugin<M, C, P, R>>
|
|
116
|
+
|
|
117
|
+
/** Lifecycle hooks for observing internal events. */
|
|
118
|
+
hooks?: Engine.Hooks<M, C, P, R>
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Override to use a custom fingerprinting implementation (e.g. a custom hash).
|
|
122
|
+
*/
|
|
123
|
+
fingerprint?: (file: File) => Contracts.FingerprintFile
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Custom per-file validator called after built-in config constraints pass.
|
|
127
|
+
* Return a rejection to block the file; return null to allow it.
|
|
128
|
+
*/
|
|
129
|
+
validateFile?: (file: File, purpose: P) => Contracts.Validation.Rejection | null
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Custom error normalizer.
|
|
133
|
+
* Converts raw thrown values into a standardized Contracts.Errors.Error.
|
|
134
|
+
*/
|
|
135
|
+
errorNormalizer?: (err: unknown) => Contracts.Errors.Error
|
|
136
|
+
}
|
|
172
137
|
|
|
173
138
|
/**
|
|
174
|
-
*
|
|
139
|
+
* Tracks an in-flight upload operation.
|
|
175
140
|
*/
|
|
176
|
-
|
|
141
|
+
export type InflightUpload = {
|
|
142
|
+
/** AbortController for this upload. */
|
|
143
|
+
controller: AbortController
|
|
144
|
+
/** Whether the upload is running normally, being paused, or being canceled. */
|
|
145
|
+
mode: 'normal' | 'pause' | 'cancel'
|
|
146
|
+
/** True once the strategy's start() has been called. */
|
|
147
|
+
started: boolean
|
|
148
|
+
}
|
|
177
149
|
|
|
178
150
|
/**
|
|
179
|
-
*
|
|
151
|
+
* Internal execution context shared across store utilities, handlers, and schedulers.
|
|
180
152
|
*/
|
|
181
|
-
|
|
182
|
-
|
|
153
|
+
export type Runtime<
|
|
154
|
+
M extends Contracts.Intent.Map,
|
|
155
|
+
C extends Contracts.Cursor.Map<M>,
|
|
156
|
+
P extends string,
|
|
157
|
+
R extends Contracts.Result.Base = Contracts.Result.Base,
|
|
158
|
+
> = {
|
|
159
|
+
/** Resolved store options with all defaults applied. */
|
|
160
|
+
opts: Options<M, C, P, R> & { config: Engine.Config<P>; transport: Transport.Options }
|
|
161
|
+
/** Current engine state. */
|
|
162
|
+
state: Engine.State<M, C, P, R>
|
|
183
163
|
|
|
184
|
-
/**
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
*/
|
|
191
|
-
export type InflightUpload = {
|
|
192
|
-
controller: AbortController
|
|
193
|
-
mode: 'normal' | 'pause' | 'cancel'
|
|
194
|
-
started: boolean
|
|
195
|
-
}
|
|
164
|
+
/** Active state change subscribers. */
|
|
165
|
+
listeners: Set<() => void>
|
|
166
|
+
/** Typed event emitter. */
|
|
167
|
+
emitter: Emitter.TypedEmitter<Engine.EventMap<M, C, P, R>>
|
|
168
|
+
/** Reducer function. */
|
|
169
|
+
reduce: ReturnType<typeof createReducer<M, C, P, R>>
|
|
196
170
|
|
|
197
|
-
/**
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
/** In-flight completes */
|
|
227
|
-
inflightCompletes: Map<string, AbortController>
|
|
228
|
-
|
|
229
|
-
/** Effect queue */
|
|
230
|
-
effectQueue: Array<() => Promise<void>>
|
|
231
|
-
/** Effect queue processing */
|
|
232
|
-
processingEffects: boolean
|
|
233
|
-
/** Effect queue scheduling */
|
|
234
|
-
scheduling: boolean
|
|
235
|
-
|
|
236
|
-
/** Notifies listeners */
|
|
237
|
-
notify: () => void
|
|
238
|
-
/** Schedules work */
|
|
239
|
-
scheduleWork: () => void
|
|
240
|
-
/** Applies an internal event */
|
|
241
|
-
applyInternal: (event: InternalEvent<M, C, P, R>) => void
|
|
242
|
-
/** Applies a command */
|
|
243
|
-
applyCommand: (cmd: UploadCommand<P>) => void
|
|
244
|
-
/** Enqueues an effect */
|
|
245
|
-
enqueueEffect: (effect: () => Promise<void>) => void
|
|
246
|
-
/** Processes effects */
|
|
247
|
-
processEffects: () => Promise<void>
|
|
248
|
-
/** Set by createUploadStore after construction; used by handlers for retries etc. */
|
|
249
|
-
dispatch: (cmd: UploadCommand<P>) => void
|
|
171
|
+
/** Inflight upload operations keyed by localId. */
|
|
172
|
+
inflightUploads: Map<string, InflightUpload>
|
|
173
|
+
/** Inflight intent creation requests keyed by localId. */
|
|
174
|
+
inflightIntents: Map<string, AbortController>
|
|
175
|
+
/** Inflight complete() calls keyed by localId. */
|
|
176
|
+
inflightCompletes: Map<string, AbortController>
|
|
177
|
+
|
|
178
|
+
/** Queued async side effects. */
|
|
179
|
+
effectQueue: Array<() => Promise<void>>
|
|
180
|
+
/** True while the effect queue is being drained. */
|
|
181
|
+
processingEffects: boolean
|
|
182
|
+
/** True while scheduleWork is running, preventing re-entrant calls. */
|
|
183
|
+
scheduling: boolean
|
|
184
|
+
|
|
185
|
+
/** Notifies all state change subscribers. */
|
|
186
|
+
notify: () => void
|
|
187
|
+
/** Starts queued uploads when concurrency slots are available. */
|
|
188
|
+
scheduleWork: () => void
|
|
189
|
+
/** Applies an internal event through the reducer and notifies subscribers. */
|
|
190
|
+
applyInternal: (event: Engine.Event<M, C, P, R>) => void
|
|
191
|
+
/** Runs a command through the reducer. */
|
|
192
|
+
applyCommand: (cmd: Engine.Command<P>) => void
|
|
193
|
+
/** Adds an async side effect to the execution queue. */
|
|
194
|
+
enqueueEffect: (effect: () => Promise<void>) => void
|
|
195
|
+
/** Drains the effect queue, running each side effect in order. */
|
|
196
|
+
processEffects: () => Promise<void>
|
|
197
|
+
/** Dispatches a public command. */
|
|
198
|
+
dispatch: (cmd: Engine.Command<P>) => void
|
|
199
|
+
}
|
|
250
200
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { validateIntent } from '../intent'
|
|
3
|
+
|
|
4
|
+
describe('validateIntent - base shape', () => {
|
|
5
|
+
test('rejects non-object', () => {
|
|
6
|
+
expect(validateIntent(null, 'post')?.message).toMatch(/must be an object/)
|
|
7
|
+
expect(validateIntent('str', 'post')?.message).toMatch(/must be an object/)
|
|
8
|
+
})
|
|
9
|
+
test('rejects missing strategy', () => {
|
|
10
|
+
expect(validateIntent({}, 'post')?.message).toMatch(/missing or invalid strategy/)
|
|
11
|
+
})
|
|
12
|
+
test('rejects strategy mismatch', () => {
|
|
13
|
+
expect(validateIntent({ strategy: 'multipart' }, 'post')?.message).toMatch(/strategy mismatch/)
|
|
14
|
+
})
|
|
15
|
+
test('rejects missing fileId', () => {
|
|
16
|
+
expect(validateIntent({ strategy: 'post' }, 'post')?.message).toMatch(/missing or invalid fileId/)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('validateIntent - post', () => {
|
|
21
|
+
const base = { strategy: 'post', fileId: 'f1', fields: {} }
|
|
22
|
+
test('rejects file://', () => {
|
|
23
|
+
expect(validateIntent({ ...base, url: 'file:///x' }, 'post')?.message).toMatch(/url must use http/)
|
|
24
|
+
})
|
|
25
|
+
test('rejects malformed url', () => {
|
|
26
|
+
expect(validateIntent({ ...base, url: 'not a url' }, 'post')?.message).toMatch(/not a valid URL/)
|
|
27
|
+
})
|
|
28
|
+
test('accepts well-formed', () => {
|
|
29
|
+
expect(validateIntent({ ...base, url: 'https://x.example/up' }, 'post')).toBeNull()
|
|
30
|
+
})
|
|
31
|
+
test('rejects non-object fields', () => {
|
|
32
|
+
expect(validateIntent({ ...base, url: 'https://x.example/up', fields: 'bad' }, 'post')?.message).toMatch(/fields/)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('validateIntent - multipart NaN-bypass defense', () => {
|
|
37
|
+
const base = { strategy: 'multipart', fileId: 'f1', uploadId: 'u1' }
|
|
38
|
+
|
|
39
|
+
test('rejects NaN partSize', () => {
|
|
40
|
+
expect(validateIntent({ ...base, partSize: Number.NaN }, 'multipart')?.message).toMatch(/partSize/)
|
|
41
|
+
})
|
|
42
|
+
test('rejects Infinity partSize', () => {
|
|
43
|
+
expect(validateIntent({ ...base, partSize: Number.POSITIVE_INFINITY }, 'multipart')?.message).toMatch(/partSize/)
|
|
44
|
+
})
|
|
45
|
+
test('rejects zero partSize', () => {
|
|
46
|
+
expect(validateIntent({ ...base, partSize: 0 }, 'multipart')?.message).toMatch(/partSize/)
|
|
47
|
+
})
|
|
48
|
+
test('rejects negative partSize', () => {
|
|
49
|
+
expect(validateIntent({ ...base, partSize: -1 }, 'multipart')?.message).toMatch(/partSize/)
|
|
50
|
+
})
|
|
51
|
+
test('rejects string partSize', () => {
|
|
52
|
+
expect(validateIntent({ ...base, partSize: '5' }, 'multipart')?.message).toMatch(/partSize/)
|
|
53
|
+
})
|
|
54
|
+
test('accepts finite positive partSize', () => {
|
|
55
|
+
expect(validateIntent({ ...base, partSize: 5_242_880 }, 'multipart')).toBeNull()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('validateIntent - multipart parts array', () => {
|
|
60
|
+
const base = { strategy: 'multipart', fileId: 'f1', uploadId: 'u1', partSize: 5_242_880 }
|
|
61
|
+
test('omits parts -> accepted', () => {
|
|
62
|
+
expect(validateIntent(base, 'multipart')).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
test('parts as object -> rejected', () => {
|
|
65
|
+
expect(validateIntent({ ...base, parts: { 1: 'x' } }, 'multipart')?.message).toMatch(/parts/)
|
|
66
|
+
})
|
|
67
|
+
test('parts as array -> accepted', () => {
|
|
68
|
+
expect(validateIntent({ ...base, parts: [] }, 'multipart')).toBeNull()
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { Contracts } from '../../contracts'
|
|
2
|
+
import type { Engine } from '../engine.types'
|
|
3
3
|
|
|
4
|
-
export function validateFile<P extends string>(
|
|
4
|
+
export function validateFile<P extends string>(
|
|
5
|
+
file: File,
|
|
6
|
+
purpose: P,
|
|
7
|
+
config: Engine.Config<P>,
|
|
8
|
+
): Contracts.Validation.Rejection | null {
|
|
5
9
|
// Get rules for this purpose, skip validation if no rules defined
|
|
6
10
|
const rules = config.validation?.[purpose]
|
|
7
11
|
if (!rules) return null
|
|
@@ -43,7 +47,7 @@ export function validateFile<P extends string>(file: File, purpose: P, config: U
|
|
|
43
47
|
})
|
|
44
48
|
|
|
45
49
|
const extMatches =
|
|
46
|
-
|
|
50
|
+
fileExt && extensions.length > 0 ? extensions.some((ext) => ext.toLowerCase() === fileExt) : false
|
|
47
51
|
|
|
48
52
|
// Determine if file passes type/extension check
|
|
49
53
|
const hasTypeRules = allowed.length > 0
|