@spoosh/react 0.10.0 → 0.11.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/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { ReadClient, TagMode, SpooshResponse, SpooshPlugin, PluginTypeConfig, MergePluginResults, WriteSelectorClient, SpooshBody, StateManager, EventEmitter, PluginExecutor, PluginArray, ResolveTypes, MergePluginOptions, ResolverContext, ResolveResultTypes, MergePluginInstanceApi, SpooshOptions } from '@spoosh/core';
1
+ import { ReadClient, TagMode, SpooshResponse, SpooshPlugin, PluginTypeConfig, MergePluginResults, WriteSelectorClient, SpooshBody, QueueSelectorClient, QueueItem, QueueStats, StateManager, EventEmitter, PluginExecutor, PluginArray, ResolveTypes, MergePluginOptions, ResolverContext, ResolveResultTypes, MergePluginInstanceApi, SpooshOptions } from '@spoosh/core';
2
2
 
3
3
  type SuccessResponse<T> = Extract<T, {
4
4
  data: unknown;
@@ -81,28 +81,28 @@ type ResponseInputFields<TQuery, TBody, TParamNames extends string> = [
81
81
  ] extends [never, never, never] ? object : {
82
82
  input: ReadInputFields<TQuery, TBody, TParamNames>;
83
83
  };
84
- type TriggerAwaitedReturn$1<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
85
- type ExtractInputFromResponse$1<T> = T extends {
84
+ type TriggerAwaitedReturn$2<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
85
+ type ExtractInputFromResponse$2<T> = T extends {
86
86
  input: infer I;
87
87
  } ? I : never;
88
- type ExtractTriggerQuery$1<I> = I extends {
88
+ type ExtractTriggerQuery$2<I> = I extends {
89
89
  query: infer Q;
90
90
  } ? {
91
91
  query?: Q;
92
92
  } : unknown;
93
- type ExtractTriggerBody$1<I> = I extends {
93
+ type ExtractTriggerBody$2<I> = I extends {
94
94
  body: infer B;
95
95
  } ? {
96
96
  body?: B;
97
97
  } : unknown;
98
- type ExtractTriggerParams$1<I> = I extends {
98
+ type ExtractTriggerParams$2<I> = I extends {
99
99
  params: infer P;
100
100
  } ? {
101
101
  params?: P;
102
102
  } : unknown;
103
- type TriggerOptions<T> = ExtractInputFromResponse$1<TriggerAwaitedReturn$1<T>> extends infer I ? [I] extends [never] ? {
103
+ type TriggerOptions<T> = ExtractInputFromResponse$2<TriggerAwaitedReturn$2<T>> extends infer I ? [I] extends [never] ? {
104
104
  force?: boolean;
105
- } : ExtractTriggerQuery$1<I> & ExtractTriggerBody$1<I> & ExtractTriggerParams$1<I> & {
105
+ } : ExtractTriggerQuery$2<I> & ExtractTriggerBody$2<I> & ExtractTriggerParams$2<I> & {
106
106
  /** Force refetch even if data is cached */
107
107
  force?: boolean;
108
108
  } : {
@@ -160,30 +160,30 @@ type InputFields<TQuery, TBody, TParamNames extends string> = OptionalQueryField
160
160
  type WriteResponseInputFields<TQuery, TBody, TParamNames extends string> = [TQuery, TBody, TParamNames] extends [never, never, never] ? object : {
161
161
  input: InputFields<TQuery, TBody, TParamNames> | undefined;
162
162
  };
163
- type TriggerAwaitedReturn<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
164
- type ExtractInputFromResponse<T> = T extends {
163
+ type TriggerAwaitedReturn$1<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
164
+ type ExtractInputFromResponse$1<T> = T extends {
165
165
  input: infer I;
166
166
  } ? I : never;
167
- type ExtractTriggerQuery<I> = I extends {
167
+ type ExtractTriggerQuery$1<I> = I extends {
168
168
  query: infer Q;
169
169
  } ? undefined extends Q ? {
170
170
  query?: Exclude<Q, undefined>;
171
171
  } : {
172
172
  query: Q;
173
173
  } : unknown;
174
- type ExtractTriggerBody<I> = I extends {
174
+ type ExtractTriggerBody$1<I> = I extends {
175
175
  body: infer B;
176
176
  } ? undefined extends B ? {
177
177
  body?: Exclude<B, undefined> | SpooshBody<Exclude<B, undefined>>;
178
178
  } : {
179
179
  body: B | SpooshBody<B>;
180
180
  } : unknown;
181
- type ExtractTriggerParams<I> = I extends {
181
+ type ExtractTriggerParams$1<I> = I extends {
182
182
  params: infer P;
183
183
  } ? {
184
184
  params: P;
185
185
  } : unknown;
186
- type WriteTriggerInput<T> = ExtractInputFromResponse<TriggerAwaitedReturn<T>> extends infer I ? [I] extends [never] ? object : ExtractTriggerQuery<I> & ExtractTriggerBody<I> & ExtractTriggerParams<I> : object;
186
+ type WriteTriggerInput<T> = ExtractInputFromResponse$1<TriggerAwaitedReturn$1<T>> extends infer I ? [I] extends [never] ? object : ExtractTriggerQuery$1<I> & ExtractTriggerBody$1<I> & ExtractTriggerParams$1<I> : object;
187
187
  /**
188
188
  * Result returned by `useWrite` hook.
189
189
  *
@@ -209,6 +209,73 @@ type BaseWriteResult<TData, TError, TOptions, TMeta = Record<string, unknown>> =
209
209
  type UseWriteResult<TData, TError, TOptions, TMeta, TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = BaseWriteResult<TData, TError, TOptions, TMeta> & MergePluginResults<TPlugins>["write"];
210
210
  type WriteApiClient<TSchema, TDefaultError> = WriteSelectorClient<TSchema, TDefaultError>;
211
211
 
212
+ type TriggerAwaitedReturn<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
213
+ type ExtractInputFromResponse<T> = T extends {
214
+ input: infer I;
215
+ } ? I : never;
216
+ type ExtractTriggerQuery<I> = I extends {
217
+ query: infer Q;
218
+ } ? undefined extends Q ? {
219
+ query?: Exclude<Q, undefined>;
220
+ } : {
221
+ query: Q;
222
+ } : unknown;
223
+ type ExtractTriggerBody<I> = I extends {
224
+ body: infer B;
225
+ } ? undefined extends B ? {
226
+ body?: Exclude<B, undefined> | SpooshBody<Exclude<B, undefined>>;
227
+ } : {
228
+ body: B | SpooshBody<B>;
229
+ } : unknown;
230
+ type ExtractTriggerParams<I> = I extends {
231
+ params: infer P;
232
+ } ? {
233
+ params: P;
234
+ } : unknown;
235
+ type QueueTriggerBase = {
236
+ /** Custom ID for this queue item. If not provided, one will be auto-generated. */
237
+ id?: string;
238
+ };
239
+ type QueueTriggerInput<T> = ExtractInputFromResponse<TriggerAwaitedReturn<T>> extends infer I ? [I] extends [never] ? QueueTriggerBase : QueueTriggerBase & ExtractTriggerQuery<I> & ExtractTriggerBody<I> & ExtractTriggerParams<I> : QueueTriggerBase;
240
+ /**
241
+ * Options for useQueue hook.
242
+ */
243
+ interface UseQueueOptions {
244
+ /** Maximum concurrent operations. Defaults to 3. */
245
+ concurrency?: number;
246
+ }
247
+ /**
248
+ * Result returned by useQueue hook.
249
+ *
250
+ * @template TData - The response data type
251
+ * @template TError - The error type
252
+ * @template TTriggerInput - The trigger input type
253
+ * @template TMeta - Plugin-contributed metadata on queue items
254
+ */
255
+ type UseQueueResult<TData, TError, TTriggerInput, TMeta = object> = {
256
+ /** Add item to queue and execute. Returns promise for this item. */
257
+ trigger: (input?: TTriggerInput) => Promise<SpooshResponse<TData, TError>>;
258
+ /** All tasks in queue with their current status */
259
+ tasks: QueueItem<TData, TError, TMeta>[];
260
+ /** Queue statistics (pending/loading/settled/success/failed/total/percentage) */
261
+ stats: QueueStats;
262
+ /** Abort task by ID, or all tasks if no ID */
263
+ abort: (id?: string) => void;
264
+ /** Retry failed task by ID, or all failed if no ID */
265
+ retry: (id?: string) => Promise<void>;
266
+ /** Remove specific task by ID (aborts if active) */
267
+ remove: (id: string) => void;
268
+ /** Remove all settled tasks (success, error, aborted). Keeps pending/running. */
269
+ removeSettled: () => void;
270
+ /** Abort all and clear queue */
271
+ clear: () => void;
272
+ };
273
+ /**
274
+ * API client type for queue selector.
275
+ * Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE).
276
+ */
277
+ type QueueApiClient<TSchema, TDefaultError> = QueueSelectorClient<TSchema, TDefaultError>;
278
+
212
279
  type TagModeInArray = "all" | "self";
213
280
  type AnyInfiniteRequestOptions = {
214
281
  query?: Record<string, unknown>;
@@ -311,6 +378,9 @@ type InferError<T, TDefaultError> = [T] extends [unknown] ? TDefaultError : T;
311
378
  type WriteResolverContext<TSchema, TMethod, TDefaultError> = ResolverContext<TSchema, ExtractMethodData<TMethod>, InferError<ExtractMethodError<TMethod>, TDefaultError>, ExtractMethodQuery<TMethod>, ExtractMethodBody<TMethod>, ExtractResponseParamNames<TMethod> extends never ? never : Record<ExtractResponseParamNames<TMethod>, string | number>>;
312
379
  type ResolvedWriteOptions<TSchema, TPlugins extends PluginArray, TMethod, TDefaultError> = ResolveTypes<MergePluginOptions<TPlugins>["write"], WriteResolverContext<TSchema, TMethod, TDefaultError>>;
313
380
  type ResolvedWriteTriggerOptions<TSchema, TPlugins extends PluginArray, TMethod, TDefaultError> = ResolveTypes<MergePluginOptions<TPlugins>["writeTrigger"], WriteResolverContext<TSchema, TMethod, TDefaultError>>;
381
+ type QueueResolverContext<TSchema, TMethod, TDefaultError> = ResolverContext<TSchema, ExtractMethodData<TMethod>, InferError<ExtractMethodError<TMethod>, TDefaultError>, ExtractMethodQuery<TMethod>, ExtractMethodBody<TMethod>, ExtractResponseParamNames<TMethod> extends never ? never : Record<ExtractResponseParamNames<TMethod>, string | number>>;
382
+ type ResolvedQueueOptions<TSchema, TPlugins extends PluginArray, TMethod, TDefaultError> = ResolveTypes<MergePluginOptions<TPlugins>["queue"], QueueResolverContext<TSchema, TMethod, TDefaultError>>;
383
+ type ResolvedQueueTriggerOptions<TSchema, TPlugins extends PluginArray, TMethod, TDefaultError> = ResolveTypes<MergePluginOptions<TPlugins>["queueTrigger"], QueueResolverContext<TSchema, TMethod, TDefaultError>>;
314
384
  type UseReadFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
315
385
  <TReadFn extends (api: ReadApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<unknown, unknown>>, TReadOpts>(readFn: TReadFn, readOptions: TReadOpts & BaseReadOptions & ResolveTypes<MergePluginOptions<TPlugins>["read"], ResolverContext<TSchema, ExtractMethodData<TReadFn>, InferError<ExtractMethodError<TReadFn>, TDefaultError>, ExtractResponseQuery<TReadFn>, ExtractResponseBody<TReadFn>, ExtractResponseParamNames<TReadFn> extends never ? never : Record<ExtractResponseParamNames<TReadFn>, string | number>>>): BaseReadResult<ExtractMethodData<TReadFn>, InferError<ExtractMethodError<TReadFn>, TDefaultError>, ResolveResultTypes<MergePluginResults<TPlugins>["read"], TReadOpts>, TriggerOptions<TReadFn>> & ResponseInputFields<ExtractResponseQuery<TReadFn>, ExtractResponseBody<TReadFn>, ExtractResponseParamNames<TReadFn>>;
316
386
  <TReadFn extends (api: ReadApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<unknown, unknown>>>(readFn: TReadFn): BaseReadResult<ExtractMethodData<TReadFn>, InferError<ExtractMethodError<TReadFn>, TDefaultError>, MergePluginResults<TPlugins>["read"], TriggerOptions<TReadFn>> & ResponseInputFields<ExtractResponseQuery<TReadFn>, ExtractResponseBody<TReadFn>, ExtractResponseParamNames<TReadFn>>;
@@ -318,6 +388,9 @@ type UseReadFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
318
388
  type UseWriteFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
319
389
  <TWriteFn extends (api: WriteApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<unknown, unknown>>, TWriteOpts extends ResolvedWriteOptions<TSchema, TPlugins, TWriteFn, TDefaultError> = ResolvedWriteOptions<TSchema, TPlugins, TWriteFn, TDefaultError>>(writeFn: TWriteFn, writeOptions?: TWriteOpts): BaseWriteResult<ExtractMethodData<TWriteFn>, InferError<ExtractMethodError<TWriteFn>, TDefaultError>, WriteTriggerInput<TWriteFn> & ResolvedWriteTriggerOptions<TSchema, TPlugins, TWriteFn, TDefaultError>, ResolveResultTypes<MergePluginResults<TPlugins>["write"], TWriteOpts>> & WriteResponseInputFields<ExtractMethodQuery<TWriteFn>, ExtractMethodBody<TWriteFn>, ExtractResponseParamNames<TWriteFn>>;
320
390
  };
391
+ type UseQueueFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
392
+ <TQueueFn extends (api: QueueApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<unknown, unknown>>>(queueFn: TQueueFn, queueOptions?: ResolvedQueueOptions<TSchema, TPlugins, TQueueFn, TDefaultError> & UseQueueOptions): UseQueueResult<ExtractMethodData<TQueueFn>, InferError<ExtractMethodError<TQueueFn>, TDefaultError>, QueueTriggerInput<TQueueFn> & ResolvedQueueTriggerOptions<TSchema, TPlugins, TQueueFn, TDefaultError>, ResolveResultTypes<MergePluginResults<TPlugins>["queue"], ResolvedQueueOptions<TSchema, TPlugins, TQueueFn, TDefaultError> & UseQueueOptions>>;
393
+ };
321
394
  type InfiniteReadResolverContext<TSchema, TData, TError, TRequest> = ResolverContext<TSchema, TData, TError, TRequest extends {
322
395
  query: infer Q;
323
396
  } ? Q : never, TRequest extends {
@@ -390,6 +463,26 @@ type SpooshReactHooks<TDefaultError, TSchema, TPlugins extends PluginArray> = {
390
463
  * ```
391
464
  */
392
465
  useInfiniteRead: UseInfiniteReadFn<TDefaultError, TSchema, TPlugins>;
466
+ /**
467
+ * React hook for queued operations with concurrency control.
468
+ *
469
+ * @param queueFn - Function that selects the API endpoint
470
+ * @param queueOptions - Optional configuration including `concurrency`
471
+ * @returns Object containing `trigger`, `queue`, `progress`, `abort`, `retry`, `remove`, `clear`
472
+ *
473
+ * @example
474
+ * ```tsx
475
+ * const { trigger, queue, progress } = useQueue(
476
+ * (api) => api("uploads").POST(),
477
+ * { concurrency: 2 }
478
+ * );
479
+ *
480
+ * for (const file of files) {
481
+ * trigger({ body: form({ file }) });
482
+ * }
483
+ * ```
484
+ */
485
+ useQueue: UseQueueFn<TDefaultError, TSchema, TPlugins>;
393
486
  } & MergePluginInstanceApi<TPlugins, TSchema>;
394
487
  /**
395
488
  * Shape of a Spoosh instance required for creating React hooks.
@@ -439,4 +532,4 @@ type PluginHooksConfig<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[
439
532
  plugins: TPlugins;
440
533
  };
441
534
 
442
- export { type AnyInfiniteRequestOptions, type BaseInfiniteReadOptions, type BaseInfiniteReadResult, type BaseReadOptions, type BaseReadResult, type BaseWriteResult, type ExtractCoreMethodOptions, type ExtractMethodBody, type ExtractMethodData, type ExtractMethodError, type ExtractMethodOptions, type ExtractMethodQuery, type ExtractResponseBody, type ExtractResponseParamNames, type ExtractResponseQuery, type ExtractResponseRequestOptions, type InfiniteNextContext, type InfinitePrevContext, type InfiniteReadApiClient, type PluginHooksConfig, type ReadApiClient, type ResponseInputFields, type SpooshReactHooks, type TriggerOptions, type UseInfiniteReadResult, type UseReadResult, type UseWriteResult, type WriteApiClient, type WriteResponseInputFields, create };
535
+ export { type AnyInfiniteRequestOptions, type BaseInfiniteReadOptions, type BaseInfiniteReadResult, type BaseReadOptions, type BaseReadResult, type BaseWriteResult, type ExtractCoreMethodOptions, type ExtractMethodBody, type ExtractMethodData, type ExtractMethodError, type ExtractMethodOptions, type ExtractMethodQuery, type ExtractResponseBody, type ExtractResponseParamNames, type ExtractResponseQuery, type ExtractResponseRequestOptions, type InfiniteNextContext, type InfinitePrevContext, type InfiniteReadApiClient, type PluginHooksConfig, type QueueApiClient, type QueueTriggerInput, type ReadApiClient, type ResponseInputFields, type SpooshReactHooks, type TriggerOptions, type UseInfiniteReadResult, type UseQueueOptions, type UseQueueResult, type UseReadResult, type UseWriteResult, type WriteApiClient, type WriteResponseInputFields, create };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ReadClient, TagMode, SpooshResponse, SpooshPlugin, PluginTypeConfig, MergePluginResults, WriteSelectorClient, SpooshBody, StateManager, EventEmitter, PluginExecutor, PluginArray, ResolveTypes, MergePluginOptions, ResolverContext, ResolveResultTypes, MergePluginInstanceApi, SpooshOptions } from '@spoosh/core';
1
+ import { ReadClient, TagMode, SpooshResponse, SpooshPlugin, PluginTypeConfig, MergePluginResults, WriteSelectorClient, SpooshBody, QueueSelectorClient, QueueItem, QueueStats, StateManager, EventEmitter, PluginExecutor, PluginArray, ResolveTypes, MergePluginOptions, ResolverContext, ResolveResultTypes, MergePluginInstanceApi, SpooshOptions } from '@spoosh/core';
2
2
 
3
3
  type SuccessResponse<T> = Extract<T, {
4
4
  data: unknown;
@@ -81,28 +81,28 @@ type ResponseInputFields<TQuery, TBody, TParamNames extends string> = [
81
81
  ] extends [never, never, never] ? object : {
82
82
  input: ReadInputFields<TQuery, TBody, TParamNames>;
83
83
  };
84
- type TriggerAwaitedReturn$1<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
85
- type ExtractInputFromResponse$1<T> = T extends {
84
+ type TriggerAwaitedReturn$2<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
85
+ type ExtractInputFromResponse$2<T> = T extends {
86
86
  input: infer I;
87
87
  } ? I : never;
88
- type ExtractTriggerQuery$1<I> = I extends {
88
+ type ExtractTriggerQuery$2<I> = I extends {
89
89
  query: infer Q;
90
90
  } ? {
91
91
  query?: Q;
92
92
  } : unknown;
93
- type ExtractTriggerBody$1<I> = I extends {
93
+ type ExtractTriggerBody$2<I> = I extends {
94
94
  body: infer B;
95
95
  } ? {
96
96
  body?: B;
97
97
  } : unknown;
98
- type ExtractTriggerParams$1<I> = I extends {
98
+ type ExtractTriggerParams$2<I> = I extends {
99
99
  params: infer P;
100
100
  } ? {
101
101
  params?: P;
102
102
  } : unknown;
103
- type TriggerOptions<T> = ExtractInputFromResponse$1<TriggerAwaitedReturn$1<T>> extends infer I ? [I] extends [never] ? {
103
+ type TriggerOptions<T> = ExtractInputFromResponse$2<TriggerAwaitedReturn$2<T>> extends infer I ? [I] extends [never] ? {
104
104
  force?: boolean;
105
- } : ExtractTriggerQuery$1<I> & ExtractTriggerBody$1<I> & ExtractTriggerParams$1<I> & {
105
+ } : ExtractTriggerQuery$2<I> & ExtractTriggerBody$2<I> & ExtractTriggerParams$2<I> & {
106
106
  /** Force refetch even if data is cached */
107
107
  force?: boolean;
108
108
  } : {
@@ -160,30 +160,30 @@ type InputFields<TQuery, TBody, TParamNames extends string> = OptionalQueryField
160
160
  type WriteResponseInputFields<TQuery, TBody, TParamNames extends string> = [TQuery, TBody, TParamNames] extends [never, never, never] ? object : {
161
161
  input: InputFields<TQuery, TBody, TParamNames> | undefined;
162
162
  };
163
- type TriggerAwaitedReturn<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
164
- type ExtractInputFromResponse<T> = T extends {
163
+ type TriggerAwaitedReturn$1<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
164
+ type ExtractInputFromResponse$1<T> = T extends {
165
165
  input: infer I;
166
166
  } ? I : never;
167
- type ExtractTriggerQuery<I> = I extends {
167
+ type ExtractTriggerQuery$1<I> = I extends {
168
168
  query: infer Q;
169
169
  } ? undefined extends Q ? {
170
170
  query?: Exclude<Q, undefined>;
171
171
  } : {
172
172
  query: Q;
173
173
  } : unknown;
174
- type ExtractTriggerBody<I> = I extends {
174
+ type ExtractTriggerBody$1<I> = I extends {
175
175
  body: infer B;
176
176
  } ? undefined extends B ? {
177
177
  body?: Exclude<B, undefined> | SpooshBody<Exclude<B, undefined>>;
178
178
  } : {
179
179
  body: B | SpooshBody<B>;
180
180
  } : unknown;
181
- type ExtractTriggerParams<I> = I extends {
181
+ type ExtractTriggerParams$1<I> = I extends {
182
182
  params: infer P;
183
183
  } ? {
184
184
  params: P;
185
185
  } : unknown;
186
- type WriteTriggerInput<T> = ExtractInputFromResponse<TriggerAwaitedReturn<T>> extends infer I ? [I] extends [never] ? object : ExtractTriggerQuery<I> & ExtractTriggerBody<I> & ExtractTriggerParams<I> : object;
186
+ type WriteTriggerInput<T> = ExtractInputFromResponse$1<TriggerAwaitedReturn$1<T>> extends infer I ? [I] extends [never] ? object : ExtractTriggerQuery$1<I> & ExtractTriggerBody$1<I> & ExtractTriggerParams$1<I> : object;
187
187
  /**
188
188
  * Result returned by `useWrite` hook.
189
189
  *
@@ -209,6 +209,73 @@ type BaseWriteResult<TData, TError, TOptions, TMeta = Record<string, unknown>> =
209
209
  type UseWriteResult<TData, TError, TOptions, TMeta, TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = BaseWriteResult<TData, TError, TOptions, TMeta> & MergePluginResults<TPlugins>["write"];
210
210
  type WriteApiClient<TSchema, TDefaultError> = WriteSelectorClient<TSchema, TDefaultError>;
211
211
 
212
+ type TriggerAwaitedReturn<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
213
+ type ExtractInputFromResponse<T> = T extends {
214
+ input: infer I;
215
+ } ? I : never;
216
+ type ExtractTriggerQuery<I> = I extends {
217
+ query: infer Q;
218
+ } ? undefined extends Q ? {
219
+ query?: Exclude<Q, undefined>;
220
+ } : {
221
+ query: Q;
222
+ } : unknown;
223
+ type ExtractTriggerBody<I> = I extends {
224
+ body: infer B;
225
+ } ? undefined extends B ? {
226
+ body?: Exclude<B, undefined> | SpooshBody<Exclude<B, undefined>>;
227
+ } : {
228
+ body: B | SpooshBody<B>;
229
+ } : unknown;
230
+ type ExtractTriggerParams<I> = I extends {
231
+ params: infer P;
232
+ } ? {
233
+ params: P;
234
+ } : unknown;
235
+ type QueueTriggerBase = {
236
+ /** Custom ID for this queue item. If not provided, one will be auto-generated. */
237
+ id?: string;
238
+ };
239
+ type QueueTriggerInput<T> = ExtractInputFromResponse<TriggerAwaitedReturn<T>> extends infer I ? [I] extends [never] ? QueueTriggerBase : QueueTriggerBase & ExtractTriggerQuery<I> & ExtractTriggerBody<I> & ExtractTriggerParams<I> : QueueTriggerBase;
240
+ /**
241
+ * Options for useQueue hook.
242
+ */
243
+ interface UseQueueOptions {
244
+ /** Maximum concurrent operations. Defaults to 3. */
245
+ concurrency?: number;
246
+ }
247
+ /**
248
+ * Result returned by useQueue hook.
249
+ *
250
+ * @template TData - The response data type
251
+ * @template TError - The error type
252
+ * @template TTriggerInput - The trigger input type
253
+ * @template TMeta - Plugin-contributed metadata on queue items
254
+ */
255
+ type UseQueueResult<TData, TError, TTriggerInput, TMeta = object> = {
256
+ /** Add item to queue and execute. Returns promise for this item. */
257
+ trigger: (input?: TTriggerInput) => Promise<SpooshResponse<TData, TError>>;
258
+ /** All tasks in queue with their current status */
259
+ tasks: QueueItem<TData, TError, TMeta>[];
260
+ /** Queue statistics (pending/loading/settled/success/failed/total/percentage) */
261
+ stats: QueueStats;
262
+ /** Abort task by ID, or all tasks if no ID */
263
+ abort: (id?: string) => void;
264
+ /** Retry failed task by ID, or all failed if no ID */
265
+ retry: (id?: string) => Promise<void>;
266
+ /** Remove specific task by ID (aborts if active) */
267
+ remove: (id: string) => void;
268
+ /** Remove all settled tasks (success, error, aborted). Keeps pending/running. */
269
+ removeSettled: () => void;
270
+ /** Abort all and clear queue */
271
+ clear: () => void;
272
+ };
273
+ /**
274
+ * API client type for queue selector.
275
+ * Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE).
276
+ */
277
+ type QueueApiClient<TSchema, TDefaultError> = QueueSelectorClient<TSchema, TDefaultError>;
278
+
212
279
  type TagModeInArray = "all" | "self";
213
280
  type AnyInfiniteRequestOptions = {
214
281
  query?: Record<string, unknown>;
@@ -311,6 +378,9 @@ type InferError<T, TDefaultError> = [T] extends [unknown] ? TDefaultError : T;
311
378
  type WriteResolverContext<TSchema, TMethod, TDefaultError> = ResolverContext<TSchema, ExtractMethodData<TMethod>, InferError<ExtractMethodError<TMethod>, TDefaultError>, ExtractMethodQuery<TMethod>, ExtractMethodBody<TMethod>, ExtractResponseParamNames<TMethod> extends never ? never : Record<ExtractResponseParamNames<TMethod>, string | number>>;
312
379
  type ResolvedWriteOptions<TSchema, TPlugins extends PluginArray, TMethod, TDefaultError> = ResolveTypes<MergePluginOptions<TPlugins>["write"], WriteResolverContext<TSchema, TMethod, TDefaultError>>;
313
380
  type ResolvedWriteTriggerOptions<TSchema, TPlugins extends PluginArray, TMethod, TDefaultError> = ResolveTypes<MergePluginOptions<TPlugins>["writeTrigger"], WriteResolverContext<TSchema, TMethod, TDefaultError>>;
381
+ type QueueResolverContext<TSchema, TMethod, TDefaultError> = ResolverContext<TSchema, ExtractMethodData<TMethod>, InferError<ExtractMethodError<TMethod>, TDefaultError>, ExtractMethodQuery<TMethod>, ExtractMethodBody<TMethod>, ExtractResponseParamNames<TMethod> extends never ? never : Record<ExtractResponseParamNames<TMethod>, string | number>>;
382
+ type ResolvedQueueOptions<TSchema, TPlugins extends PluginArray, TMethod, TDefaultError> = ResolveTypes<MergePluginOptions<TPlugins>["queue"], QueueResolverContext<TSchema, TMethod, TDefaultError>>;
383
+ type ResolvedQueueTriggerOptions<TSchema, TPlugins extends PluginArray, TMethod, TDefaultError> = ResolveTypes<MergePluginOptions<TPlugins>["queueTrigger"], QueueResolverContext<TSchema, TMethod, TDefaultError>>;
314
384
  type UseReadFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
315
385
  <TReadFn extends (api: ReadApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<unknown, unknown>>, TReadOpts>(readFn: TReadFn, readOptions: TReadOpts & BaseReadOptions & ResolveTypes<MergePluginOptions<TPlugins>["read"], ResolverContext<TSchema, ExtractMethodData<TReadFn>, InferError<ExtractMethodError<TReadFn>, TDefaultError>, ExtractResponseQuery<TReadFn>, ExtractResponseBody<TReadFn>, ExtractResponseParamNames<TReadFn> extends never ? never : Record<ExtractResponseParamNames<TReadFn>, string | number>>>): BaseReadResult<ExtractMethodData<TReadFn>, InferError<ExtractMethodError<TReadFn>, TDefaultError>, ResolveResultTypes<MergePluginResults<TPlugins>["read"], TReadOpts>, TriggerOptions<TReadFn>> & ResponseInputFields<ExtractResponseQuery<TReadFn>, ExtractResponseBody<TReadFn>, ExtractResponseParamNames<TReadFn>>;
316
386
  <TReadFn extends (api: ReadApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<unknown, unknown>>>(readFn: TReadFn): BaseReadResult<ExtractMethodData<TReadFn>, InferError<ExtractMethodError<TReadFn>, TDefaultError>, MergePluginResults<TPlugins>["read"], TriggerOptions<TReadFn>> & ResponseInputFields<ExtractResponseQuery<TReadFn>, ExtractResponseBody<TReadFn>, ExtractResponseParamNames<TReadFn>>;
@@ -318,6 +388,9 @@ type UseReadFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
318
388
  type UseWriteFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
319
389
  <TWriteFn extends (api: WriteApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<unknown, unknown>>, TWriteOpts extends ResolvedWriteOptions<TSchema, TPlugins, TWriteFn, TDefaultError> = ResolvedWriteOptions<TSchema, TPlugins, TWriteFn, TDefaultError>>(writeFn: TWriteFn, writeOptions?: TWriteOpts): BaseWriteResult<ExtractMethodData<TWriteFn>, InferError<ExtractMethodError<TWriteFn>, TDefaultError>, WriteTriggerInput<TWriteFn> & ResolvedWriteTriggerOptions<TSchema, TPlugins, TWriteFn, TDefaultError>, ResolveResultTypes<MergePluginResults<TPlugins>["write"], TWriteOpts>> & WriteResponseInputFields<ExtractMethodQuery<TWriteFn>, ExtractMethodBody<TWriteFn>, ExtractResponseParamNames<TWriteFn>>;
320
390
  };
391
+ type UseQueueFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
392
+ <TQueueFn extends (api: QueueApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<unknown, unknown>>>(queueFn: TQueueFn, queueOptions?: ResolvedQueueOptions<TSchema, TPlugins, TQueueFn, TDefaultError> & UseQueueOptions): UseQueueResult<ExtractMethodData<TQueueFn>, InferError<ExtractMethodError<TQueueFn>, TDefaultError>, QueueTriggerInput<TQueueFn> & ResolvedQueueTriggerOptions<TSchema, TPlugins, TQueueFn, TDefaultError>, ResolveResultTypes<MergePluginResults<TPlugins>["queue"], ResolvedQueueOptions<TSchema, TPlugins, TQueueFn, TDefaultError> & UseQueueOptions>>;
393
+ };
321
394
  type InfiniteReadResolverContext<TSchema, TData, TError, TRequest> = ResolverContext<TSchema, TData, TError, TRequest extends {
322
395
  query: infer Q;
323
396
  } ? Q : never, TRequest extends {
@@ -390,6 +463,26 @@ type SpooshReactHooks<TDefaultError, TSchema, TPlugins extends PluginArray> = {
390
463
  * ```
391
464
  */
392
465
  useInfiniteRead: UseInfiniteReadFn<TDefaultError, TSchema, TPlugins>;
466
+ /**
467
+ * React hook for queued operations with concurrency control.
468
+ *
469
+ * @param queueFn - Function that selects the API endpoint
470
+ * @param queueOptions - Optional configuration including `concurrency`
471
+ * @returns Object containing `trigger`, `queue`, `progress`, `abort`, `retry`, `remove`, `clear`
472
+ *
473
+ * @example
474
+ * ```tsx
475
+ * const { trigger, queue, progress } = useQueue(
476
+ * (api) => api("uploads").POST(),
477
+ * { concurrency: 2 }
478
+ * );
479
+ *
480
+ * for (const file of files) {
481
+ * trigger({ body: form({ file }) });
482
+ * }
483
+ * ```
484
+ */
485
+ useQueue: UseQueueFn<TDefaultError, TSchema, TPlugins>;
393
486
  } & MergePluginInstanceApi<TPlugins, TSchema>;
394
487
  /**
395
488
  * Shape of a Spoosh instance required for creating React hooks.
@@ -439,4 +532,4 @@ type PluginHooksConfig<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[
439
532
  plugins: TPlugins;
440
533
  };
441
534
 
442
- export { type AnyInfiniteRequestOptions, type BaseInfiniteReadOptions, type BaseInfiniteReadResult, type BaseReadOptions, type BaseReadResult, type BaseWriteResult, type ExtractCoreMethodOptions, type ExtractMethodBody, type ExtractMethodData, type ExtractMethodError, type ExtractMethodOptions, type ExtractMethodQuery, type ExtractResponseBody, type ExtractResponseParamNames, type ExtractResponseQuery, type ExtractResponseRequestOptions, type InfiniteNextContext, type InfinitePrevContext, type InfiniteReadApiClient, type PluginHooksConfig, type ReadApiClient, type ResponseInputFields, type SpooshReactHooks, type TriggerOptions, type UseInfiniteReadResult, type UseReadResult, type UseWriteResult, type WriteApiClient, type WriteResponseInputFields, create };
535
+ export { type AnyInfiniteRequestOptions, type BaseInfiniteReadOptions, type BaseInfiniteReadResult, type BaseReadOptions, type BaseReadResult, type BaseWriteResult, type ExtractCoreMethodOptions, type ExtractMethodBody, type ExtractMethodData, type ExtractMethodError, type ExtractMethodOptions, type ExtractMethodQuery, type ExtractResponseBody, type ExtractResponseParamNames, type ExtractResponseQuery, type ExtractResponseRequestOptions, type InfiniteNextContext, type InfinitePrevContext, type InfiniteReadApiClient, type PluginHooksConfig, type QueueApiClient, type QueueTriggerInput, type ReadApiClient, type ResponseInputFields, type SpooshReactHooks, type TriggerOptions, type UseInfiniteReadResult, type UseQueueOptions, type UseQueueResult, type UseReadResult, type UseWriteResult, type WriteApiClient, type WriteResponseInputFields, create };
package/dist/index.js CHANGED
@@ -112,7 +112,7 @@ function createUseRead(options) {
112
112
  const tagsKey = JSON.stringify(tags);
113
113
  const executeWithTracking = (0, import_react.useCallback)(
114
114
  async (force = false, overrideOptions) => {
115
- setRequestState((prev) => ({ ...prev, isPending: true }));
115
+ setRequestState({ isPending: true, error: void 0 });
116
116
  try {
117
117
  const execOptions = overrideOptions ? { ...capturedCall.options ?? {}, ...overrideOptions } : void 0;
118
118
  const response = await controller.execute(execOptions, { force });
@@ -226,7 +226,7 @@ function createUseRead(options) {
226
226
  };
227
227
  forceUpdate((n) => n + 1);
228
228
  newController.mount();
229
- setRequestState((prev) => ({ ...prev, isPending: true }));
229
+ setRequestState({ isPending: true, error: void 0 });
230
230
  try {
231
231
  const response = await newController.execute(mergedOptions, {
232
232
  force
@@ -351,7 +351,7 @@ function createUseWrite(options) {
351
351
  const trigger = (0, import_react2.useCallback)(
352
352
  async (triggerOptions) => {
353
353
  setLastTriggerOptions(triggerOptions);
354
- setRequestState((prev) => ({ ...prev, isPending: true }));
354
+ setRequestState({ isPending: true, error: void 0 });
355
355
  const params = triggerOptions?.params;
356
356
  const resolvedPath = (0, import_core2.resolvePath)(pathSegments, params);
357
357
  const tags = (0, import_core2.resolveTags)(triggerOptions, resolvedPath);
@@ -588,6 +588,72 @@ function createUseInfiniteRead(options) {
588
588
  };
589
589
  }
590
590
 
591
+ // src/useQueue/index.ts
592
+ var import_react4 = require("react");
593
+ var import_core4 = require("@spoosh/core");
594
+ function createUseQueue(options) {
595
+ const { api, stateManager, pluginExecutor, eventEmitter } = options;
596
+ function useQueue(queueFn, queueOptions) {
597
+ (0, import_react4.useId)();
598
+ const selectorResultRef = (0, import_react4.useRef)({
599
+ call: null,
600
+ selector: null
601
+ });
602
+ const selectorProxy = (0, import_core4.createSelectorProxy)((result) => {
603
+ selectorResultRef.current = result;
604
+ });
605
+ queueFn(selectorProxy);
606
+ const capturedCall = selectorResultRef.current.call;
607
+ const capturedSelector = selectorResultRef.current.selector;
608
+ const captured = capturedCall ?? capturedSelector;
609
+ if (!captured) {
610
+ throw new Error(
611
+ 'useQueue requires selecting an HTTP method. Example: useQueue((api) => api("uploads").POST())'
612
+ );
613
+ }
614
+ const { concurrency, ...hookOptions } = queueOptions ?? {};
615
+ const controllerRef = (0, import_react4.useRef)(null);
616
+ if (!controllerRef.current) {
617
+ const config = {
618
+ path: captured.path,
619
+ method: captured.method,
620
+ concurrency,
621
+ operationType: "queue",
622
+ hookOptions
623
+ };
624
+ controllerRef.current = (0, import_core4.createQueueController)(config, {
625
+ api,
626
+ stateManager,
627
+ eventEmitter,
628
+ pluginExecutor
629
+ });
630
+ }
631
+ const controller = controllerRef.current;
632
+ (0, import_react4.useEffect)(() => {
633
+ if (concurrency !== void 0) {
634
+ controller.setConcurrency(concurrency);
635
+ }
636
+ }, [concurrency, controller]);
637
+ const tasks = (0, import_react4.useSyncExternalStore)(
638
+ controller.subscribe,
639
+ controller.getQueue,
640
+ controller.getQueue
641
+ );
642
+ return {
643
+ trigger: (input) => controller.trigger(input ?? {}),
644
+ tasks,
645
+ stats: controller.getStats(),
646
+ abort: controller.abort,
647
+ retry: controller.retry,
648
+ remove: controller.remove,
649
+ removeSettled: controller.removeSettled,
650
+ clear: controller.clear
651
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
652
+ };
653
+ }
654
+ return useQueue;
655
+ }
656
+
591
657
  // src/create/index.ts
592
658
  function create(instance) {
593
659
  const { api, stateManager, eventEmitter, pluginExecutor } = instance;
@@ -609,6 +675,12 @@ function create(instance) {
609
675
  eventEmitter,
610
676
  pluginExecutor
611
677
  });
678
+ const useQueue = createUseQueue({
679
+ api,
680
+ stateManager,
681
+ eventEmitter,
682
+ pluginExecutor
683
+ });
612
684
  const plugins = pluginExecutor.getPlugins();
613
685
  const setupContext = {
614
686
  stateManager,
@@ -637,6 +709,7 @@ function create(instance) {
637
709
  useRead,
638
710
  useWrite,
639
711
  useInfiniteRead,
712
+ useQueue,
640
713
  ...instanceApis
641
714
  };
642
715
  }
package/dist/index.mjs CHANGED
@@ -99,7 +99,7 @@ function createUseRead(options) {
99
99
  const tagsKey = JSON.stringify(tags);
100
100
  const executeWithTracking = useCallback(
101
101
  async (force = false, overrideOptions) => {
102
- setRequestState((prev) => ({ ...prev, isPending: true }));
102
+ setRequestState({ isPending: true, error: void 0 });
103
103
  try {
104
104
  const execOptions = overrideOptions ? { ...capturedCall.options ?? {}, ...overrideOptions } : void 0;
105
105
  const response = await controller.execute(execOptions, { force });
@@ -213,7 +213,7 @@ function createUseRead(options) {
213
213
  };
214
214
  forceUpdate((n) => n + 1);
215
215
  newController.mount();
216
- setRequestState((prev) => ({ ...prev, isPending: true }));
216
+ setRequestState({ isPending: true, error: void 0 });
217
217
  try {
218
218
  const response = await newController.execute(mergedOptions, {
219
219
  force
@@ -349,7 +349,7 @@ function createUseWrite(options) {
349
349
  const trigger = useCallback2(
350
350
  async (triggerOptions) => {
351
351
  setLastTriggerOptions(triggerOptions);
352
- setRequestState((prev) => ({ ...prev, isPending: true }));
352
+ setRequestState({ isPending: true, error: void 0 });
353
353
  const params = triggerOptions?.params;
354
354
  const resolvedPath = resolvePath2(pathSegments, params);
355
355
  const tags = resolveTags2(triggerOptions, resolvedPath);
@@ -597,6 +597,75 @@ function createUseInfiniteRead(options) {
597
597
  };
598
598
  }
599
599
 
600
+ // src/useQueue/index.ts
601
+ import { useSyncExternalStore as useSyncExternalStore4, useRef as useRef4, useId as useId4, useEffect as useEffect3 } from "react";
602
+ import {
603
+ createSelectorProxy as createSelectorProxy4,
604
+ createQueueController
605
+ } from "@spoosh/core";
606
+ function createUseQueue(options) {
607
+ const { api, stateManager, pluginExecutor, eventEmitter } = options;
608
+ function useQueue(queueFn, queueOptions) {
609
+ useId4();
610
+ const selectorResultRef = useRef4({
611
+ call: null,
612
+ selector: null
613
+ });
614
+ const selectorProxy = createSelectorProxy4((result) => {
615
+ selectorResultRef.current = result;
616
+ });
617
+ queueFn(selectorProxy);
618
+ const capturedCall = selectorResultRef.current.call;
619
+ const capturedSelector = selectorResultRef.current.selector;
620
+ const captured = capturedCall ?? capturedSelector;
621
+ if (!captured) {
622
+ throw new Error(
623
+ 'useQueue requires selecting an HTTP method. Example: useQueue((api) => api("uploads").POST())'
624
+ );
625
+ }
626
+ const { concurrency, ...hookOptions } = queueOptions ?? {};
627
+ const controllerRef = useRef4(null);
628
+ if (!controllerRef.current) {
629
+ const config = {
630
+ path: captured.path,
631
+ method: captured.method,
632
+ concurrency,
633
+ operationType: "queue",
634
+ hookOptions
635
+ };
636
+ controllerRef.current = createQueueController(config, {
637
+ api,
638
+ stateManager,
639
+ eventEmitter,
640
+ pluginExecutor
641
+ });
642
+ }
643
+ const controller = controllerRef.current;
644
+ useEffect3(() => {
645
+ if (concurrency !== void 0) {
646
+ controller.setConcurrency(concurrency);
647
+ }
648
+ }, [concurrency, controller]);
649
+ const tasks = useSyncExternalStore4(
650
+ controller.subscribe,
651
+ controller.getQueue,
652
+ controller.getQueue
653
+ );
654
+ return {
655
+ trigger: (input) => controller.trigger(input ?? {}),
656
+ tasks,
657
+ stats: controller.getStats(),
658
+ abort: controller.abort,
659
+ retry: controller.retry,
660
+ remove: controller.remove,
661
+ removeSettled: controller.removeSettled,
662
+ clear: controller.clear
663
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
664
+ };
665
+ }
666
+ return useQueue;
667
+ }
668
+
600
669
  // src/create/index.ts
601
670
  function create(instance) {
602
671
  const { api, stateManager, eventEmitter, pluginExecutor } = instance;
@@ -618,6 +687,12 @@ function create(instance) {
618
687
  eventEmitter,
619
688
  pluginExecutor
620
689
  });
690
+ const useQueue = createUseQueue({
691
+ api,
692
+ stateManager,
693
+ eventEmitter,
694
+ pluginExecutor
695
+ });
621
696
  const plugins = pluginExecutor.getPlugins();
622
697
  const setupContext = {
623
698
  stateManager,
@@ -646,6 +721,7 @@ function create(instance) {
646
721
  useRead,
647
722
  useWrite,
648
723
  useInfiniteRead,
724
+ useQueue,
649
725
  ...instanceApis
650
726
  };
651
727
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/react",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "description": "React hooks for Spoosh API toolkit",
6
6
  "keywords": [
@@ -34,13 +34,13 @@
34
34
  }
35
35
  },
36
36
  "peerDependencies": {
37
- "@spoosh/core": ">=0.13.1",
37
+ "@spoosh/core": ">=0.14.0",
38
38
  "react": "^18 || ^19"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@testing-library/react": "^16.0.0",
42
42
  "jsdom": "^26.0.0",
43
- "@spoosh/core": "0.13.1",
43
+ "@spoosh/core": "0.14.0",
44
44
  "@spoosh/test-utils": "0.2.0"
45
45
  },
46
46
  "scripts": {