@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.
@@ -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.autoStart
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
  }
@@ -91,7 +91,9 @@ export function createStoreRuntime<
91
91
  scheduling: false,
92
92
 
93
93
  notify() {
94
- rt.listeners.forEach((l) => l())
94
+ rt.listeners.forEach((listener) => {
95
+ listener()
96
+ })
95
97
  },
96
98
 
97
99
  scheduleWork() {
@@ -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, C extends CursorMap<M>, P extends string> = {
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 (item.phase === 'completed' || item.phase === 'canceled' || item.phase === 'error') continue
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
- const set = listeners[type] ?? (listeners[type] = new Set<(payload: E[K]) => void>())
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 = useContext(UploadContext)
78
- if (!store || !isUploadStore<M, C, P, R>(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
- >(_store?: UploadStore<M, C, P, R> | undefined) {
67
- const store = _store ?? useUploadStore<M, C, P, R>()
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
  }