@gentleduck/upload 0.2.1 → 0.2.4
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/dist/async-DxRWu4le.js +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +1 -0
- package/dist/core-BNxznRif.js +2 -0
- package/dist/index-BXg9WIce.d.ts +987 -0
- package/dist/index.d.ts +5 -1417
- package/dist/index.js +1 -6
- package/dist/react/index.d.ts +90 -0
- package/dist/react/index.js +1 -0
- package/dist/strategies/index.d.ts +56 -0
- package/dist/strategies/index.js +1 -0
- package/dist/strategy.types-gg9ZStRe.d.ts +225 -0
- package/package.json +29 -12
- package/src/core/contracts/transport/transport.ts +1 -1
- package/src/core/engine/store/handlers/clean-up.ts +3 -3
- package/src/core/engine/store/handlers/create-intent.ts +2 -2
- package/src/core/engine/store/store.dispatch.ts +9 -3
- package/src/core/engine/store/store.libs.ts +2 -2
- package/src/core/engine/store/store.runtime.ts +3 -1
- package/src/core/engine/store/store.types.ts +1 -1
- package/src/core/engine/validation/file.ts +1 -1
- package/src/core/persistence/persistence.ts +9 -4
- package/src/core/persistence/persistence.types.ts +1 -1
- package/src/core/utils/emitter/index.ts +7 -1
- package/src/react/upload-provider.tsx +27 -2
- package/src/react/use-uploader.tsx +25 -4
- package/src/strategies/multipart/index.ts +2 -2
|
@@ -32,7 +32,7 @@ export function isAutoStart<M extends IntentMap, C extends CursorMap<M>, P exten
|
|
|
32
32
|
opts: StoreOptions<M, C, P, R>,
|
|
33
33
|
purpose: P,
|
|
34
34
|
): boolean {
|
|
35
|
-
const v = opts.config
|
|
35
|
+
const v = opts.config?.autoStart
|
|
36
36
|
if (v === undefined) return false
|
|
37
37
|
if (Array.isArray(v)) return v.includes(purpose)
|
|
38
38
|
if (typeof v === 'function') return v(purpose)
|
|
@@ -73,7 +73,7 @@ export function normalizeError(err: unknown, customNormalizer?: (err: unknown) =
|
|
|
73
73
|
|
|
74
74
|
// HTTP errors
|
|
75
75
|
if (isRecord(err) && (typeof err.status === 'number' || typeof err.statusCode === 'number')) {
|
|
76
|
-
const status = typeof err.status === 'number' ? err.status : err.statusCode
|
|
76
|
+
const status = (typeof err.status === 'number' ? err.status : err.statusCode) as number
|
|
77
77
|
const retryable = status >= 500 || status === 429
|
|
78
78
|
return { code: 'http', status, message: String(msg), cause: err, retryable }
|
|
79
79
|
}
|
|
@@ -138,7 +138,7 @@ export interface StoreOptions<
|
|
|
138
138
|
*
|
|
139
139
|
* Used to configure the upload store persistence layer.
|
|
140
140
|
*/
|
|
141
|
-
export type DeserializeContext<M extends IntentMap,
|
|
141
|
+
export type DeserializeContext<M extends IntentMap, _C extends CursorMap<M>, P extends string> = {
|
|
142
142
|
isPurpose?: (value: string) => value is P
|
|
143
143
|
isIntent?: (value: unknown) => value is M[keyof M]
|
|
144
144
|
hasStrategy: (value: string) => boolean
|
|
@@ -36,7 +36,7 @@ export function validateFile<P extends string>(file: File, purpose: P, config: U
|
|
|
36
36
|
if (type.endsWith('/*')) {
|
|
37
37
|
// Wildcard match: 'image/*' matches 'image/jpeg', 'image/png', etc.
|
|
38
38
|
const prefix = type.slice(0, -2).toLowerCase()
|
|
39
|
-
return mimeType.startsWith(prefix
|
|
39
|
+
return mimeType.startsWith(`${prefix}/`)
|
|
40
40
|
}
|
|
41
41
|
// Exact match
|
|
42
42
|
return mimeType === type.toLowerCase()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyCursor, CursorMap, IntentMap, UploadResultBase } from '../contracts'
|
|
1
|
+
import type { AnyCursor, AnyIntent, CursorMap, IntentMap, UploadResultBase } from '../contracts'
|
|
2
2
|
import type { UploadItem } from '../engine/internal-events.types'
|
|
3
3
|
import type { UploadState } from '../engine/reducer'
|
|
4
4
|
import { hasCursor, hasIntent } from '../engine/store/store.libs'
|
|
@@ -22,7 +22,12 @@ export function serializeSnapshot<
|
|
|
22
22
|
if (!hasIntent(item)) continue
|
|
23
23
|
|
|
24
24
|
// Do not persist terminal items. Persistence is for resuming and recovery, not history.
|
|
25
|
-
if (
|
|
25
|
+
if (
|
|
26
|
+
(item.phase as string) === 'completed' ||
|
|
27
|
+
(item.phase as string) === 'canceled' ||
|
|
28
|
+
(item.phase as string) === 'error'
|
|
29
|
+
)
|
|
30
|
+
continue
|
|
26
31
|
|
|
27
32
|
const cursor = hasCursor(item) ? item.cursor : undefined
|
|
28
33
|
|
|
@@ -116,13 +121,13 @@ export function deserializeSnapshot<
|
|
|
116
121
|
lastModified: parsed.file.lastModified,
|
|
117
122
|
checksum: parsed.file.checksum,
|
|
118
123
|
},
|
|
119
|
-
intent: parsed.intent
|
|
124
|
+
intent: parsed.intent as AnyIntent<M>,
|
|
120
125
|
cursor: parsed.cursor,
|
|
121
126
|
progress: { uploadedBytes, totalBytes, pct },
|
|
122
127
|
pausedAt: Date.now(),
|
|
123
128
|
createdAt: raw.createdAt ?? Date.now(),
|
|
124
129
|
file: undefined,
|
|
125
|
-
})
|
|
130
|
+
} as UploadItem<M, C, P, R>)
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
return { items }
|
|
@@ -41,7 +41,7 @@ export type PersistedUploadItem<M, C, P extends string> = {
|
|
|
41
41
|
/** Backend-provided "intent" that describes how to upload (strategy, URLs, fields, etc). */
|
|
42
42
|
intent: M[keyof M]
|
|
43
43
|
/** Strategy cursor used to resume uploads that support continuation. */
|
|
44
|
-
cursor?: import('../contracts').AnyCursor<C
|
|
44
|
+
cursor?: import('../contracts').AnyCursor<C & Record<string, unknown>>
|
|
45
45
|
/** Last-known progress for UX continuity after restoring from persistence. */
|
|
46
46
|
progress?: { uploadedBytes: number; totalBytes: number; pct?: number }
|
|
47
47
|
/** The last known state-machine phase at the time of persistence. */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { TypedEmitter } from './emitter.types'
|
|
2
2
|
|
|
3
|
+
export type { TypedEmitter } from './emitter.types'
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Creates a lightweight, typed event emitter.
|
|
5
7
|
* We use this instead of `EventEmitter` or `EventTarget` to ensure:
|
|
@@ -28,7 +30,11 @@ export function createTypedEmitter<E extends Record<string, unknown>>(): TypedEm
|
|
|
28
30
|
return {
|
|
29
31
|
on<K extends keyof E & string>(type: K, cb: (payload: E[K]) => void): () => void {
|
|
30
32
|
// Create listener set for this event type if it doesn't exist
|
|
31
|
-
|
|
33
|
+
let set = listeners[type]
|
|
34
|
+
if (!set) {
|
|
35
|
+
set = new Set<(payload: E[K]) => void>()
|
|
36
|
+
listeners[type] = set
|
|
37
|
+
}
|
|
32
38
|
set.add(cb)
|
|
33
39
|
|
|
34
40
|
// Return unsubscribe function
|
|
@@ -52,6 +52,31 @@ export function UploadProvider<
|
|
|
52
52
|
return <UploadContext.Provider value={store}>{children}</UploadContext.Provider>
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Hook to access the upload store from React context when present.
|
|
57
|
+
*
|
|
58
|
+
* Some higher-level hooks support either an explicit store argument or context.
|
|
59
|
+
* They use this helper so the context read stays unconditional without throwing
|
|
60
|
+
* when a caller intentionally passes a store instance.
|
|
61
|
+
*/
|
|
62
|
+
export function useOptionalUploadStore<
|
|
63
|
+
M extends IntentMap,
|
|
64
|
+
C extends CursorMap<M>,
|
|
65
|
+
P extends string,
|
|
66
|
+
R extends UploadResultBase = UploadResultBase,
|
|
67
|
+
>(): UploadStore<M, C, P, R> | null {
|
|
68
|
+
const store = useContext(UploadContext)
|
|
69
|
+
if (store === null) {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!isUploadStore<M, C, P, R>(store)) {
|
|
74
|
+
throw new Error('UploadProvider received an invalid store value')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return store
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
/**
|
|
56
81
|
* Hook to access the upload store from React context.
|
|
57
82
|
*
|
|
@@ -74,8 +99,8 @@ export function useUploadStore<
|
|
|
74
99
|
P extends string,
|
|
75
100
|
R extends UploadResultBase = UploadResultBase,
|
|
76
101
|
>(): UploadStore<M, C, P, R> {
|
|
77
|
-
const store =
|
|
78
|
-
if (!store
|
|
102
|
+
const store = useOptionalUploadStore<M, C, P, R>()
|
|
103
|
+
if (!store) {
|
|
79
104
|
throw new Error('useUploadStore must be used within UploadProvider')
|
|
80
105
|
}
|
|
81
106
|
return store
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
UploadResultBase,
|
|
12
12
|
} from '../core'
|
|
13
13
|
import type { UploadStore } from '../core/engine/store'
|
|
14
|
-
import { useUploadStore } from './upload-provider'
|
|
14
|
+
import { useOptionalUploadStore, useUploadStore } from './upload-provider'
|
|
15
15
|
|
|
16
16
|
export function createUploadFactory<
|
|
17
17
|
M extends IntentMap,
|
|
@@ -46,6 +46,22 @@ type Uploader<M extends IntentMap, C extends CursorMap<M>, P extends string, R e
|
|
|
46
46
|
ready: UploadItem<M, C, P, R>[]
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Stable action surface for React consumers that need imperative store access.
|
|
51
|
+
*
|
|
52
|
+
* This is declared explicitly instead of relying on an inferred object return
|
|
53
|
+
* type so the generated package declarations do not leak the internal generic
|
|
54
|
+
* names created by `store.on.bind(store)`.
|
|
55
|
+
*/
|
|
56
|
+
export type UploaderActions<
|
|
57
|
+
M extends IntentMap,
|
|
58
|
+
C extends CursorMap<M>,
|
|
59
|
+
P extends string,
|
|
60
|
+
R extends UploadResultBase = UploadResultBase,
|
|
61
|
+
> = Pick<UploadStore<M, C, P, R>, 'dispatch' | 'on'> & {
|
|
62
|
+
store: UploadStore<M, C, P, R>
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
export function useUploader<
|
|
50
66
|
M extends IntentMap,
|
|
51
67
|
C extends CursorMap<M>,
|
|
@@ -63,8 +79,13 @@ export function useUploader<
|
|
|
63
79
|
C extends CursorMap<M>,
|
|
64
80
|
P extends string,
|
|
65
81
|
R extends UploadResultBase = UploadResultBase,
|
|
66
|
-
>(
|
|
67
|
-
const
|
|
82
|
+
>(providedStore?: UploadStore<M, C, P, R> | undefined) {
|
|
83
|
+
const contextStore = useOptionalUploadStore<M, C, P, R>()
|
|
84
|
+
const store = providedStore ?? contextStore
|
|
85
|
+
if (!store) {
|
|
86
|
+
throw new Error('useUploader must be used within UploadProvider when no store argument is provided')
|
|
87
|
+
}
|
|
88
|
+
|
|
68
89
|
const snapshot = React.useSyncExternalStore(
|
|
69
90
|
store.subscribe.bind(store),
|
|
70
91
|
store.getSnapshot.bind(store),
|
|
@@ -105,7 +126,7 @@ export function useUploaderActions<
|
|
|
105
126
|
C extends CursorMap<M>,
|
|
106
127
|
P extends string,
|
|
107
128
|
R extends UploadResultBase = UploadResultBase,
|
|
108
|
-
>() {
|
|
129
|
+
>(): UploaderActions<M, C, P, R> {
|
|
109
130
|
const store = useUploadStore<M, C, P, R>()
|
|
110
131
|
|
|
111
132
|
const dispatch = React.useMemo(() => store.dispatch.bind(store), [store])
|
|
@@ -160,7 +160,7 @@ export function multipartStrategy<
|
|
|
160
160
|
.map(([partNumber, v]) => ({ partNumber, etag: v.etag, size: v.size }))
|
|
161
161
|
.sort((a, b) => a.partNumber - b.partNumber),
|
|
162
162
|
}
|
|
163
|
-
ctx.persistCursor(snapshot)
|
|
163
|
+
ctx.persistCursor(snapshot as C['multipart'])
|
|
164
164
|
report()
|
|
165
165
|
} catch (err) {
|
|
166
166
|
inflightBytes.delete(p.partNumber)
|
|
@@ -236,7 +236,7 @@ export function multipartStrategy<
|
|
|
236
236
|
.sort((a, b) => a.partNumber - b.partNumber),
|
|
237
237
|
completed: true,
|
|
238
238
|
}
|
|
239
|
-
ctx.persistCursor(snapshot)
|
|
239
|
+
ctx.persistCursor(snapshot as C['multipart'])
|
|
240
240
|
}
|
|
241
241
|
},
|
|
242
242
|
}
|