@spoosh/react 0.11.0 → 0.13.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.ts CHANGED
@@ -1,4 +1,4 @@
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';
1
+ import { ReadClient, TagMode, SpooshResponse, ExtractTriggerQuery as ExtractTriggerQuery$3, ExtractTriggerBody as ExtractTriggerBody$3, ExtractTriggerParams as ExtractTriggerParams$3, SpooshPlugin, PluginTypeConfig, MergePluginResults, WriteSelectorClient, SpooshBody, QueueSelectorClient, QueueItem, QueueStats, InfiniteRequestOptions, InfiniteNextContext, InfinitePage, InfinitePrevContext, SubscriptionClient, SubscriptionAdapter, OperationType, StateManager, EventEmitter, PluginExecutor, SpooshTransport, PluginArray, ResolveTypes, MergePluginOptions, ResolverContext, ResolveResultTypes, MergePluginInstanceApi, SpooshOptions } from '@spoosh/core';
2
2
 
3
3
  type SuccessResponse<T> = Extract<T, {
4
4
  data: unknown;
@@ -43,6 +43,31 @@ type ExtractResponseParamNames<T> = SuccessReturnType<T> extends {
43
43
  params: Record<infer K, unknown>;
44
44
  };
45
45
  } ? K extends string ? K : never : never;
46
+ type SubscriptionReturnType<T> = T extends (...args: never[]) => infer R ? R : never;
47
+ type ExtractSubscriptionEvents<T> = SubscriptionReturnType<T> extends {
48
+ events: infer E;
49
+ requestedEvents: infer RequestedEvents;
50
+ } ? RequestedEvents extends readonly (keyof E)[] ? E extends Record<string, unknown> ? {
51
+ [K in RequestedEvents[number]]: E[K] extends {
52
+ data: infer EventData;
53
+ } ? EventData : unknown;
54
+ } : unknown : unknown : unknown;
55
+ type ExtractSubscriptionQuery<T> = SubscriptionReturnType<T> extends {
56
+ query: infer Q;
57
+ } ? Q : never;
58
+ type ExtractSubscriptionBody<T> = SubscriptionReturnType<T> extends {
59
+ body: infer B;
60
+ } ? B : never;
61
+ type ExtractAllSubscriptionEventKeys<T> = SubscriptionReturnType<T> extends {
62
+ events: infer E;
63
+ } ? E extends Record<string, unknown> ? keyof E : never : never;
64
+ type ExtractAllSubscriptionEvents<T> = SubscriptionReturnType<T> extends {
65
+ events: infer E;
66
+ } ? E extends Record<string, unknown> ? {
67
+ [K in keyof E]: E[K] extends {
68
+ data: infer EventData;
69
+ } ? EventData : unknown;
70
+ } : unknown : unknown;
46
71
 
47
72
  type TagModeInArray$1 = "all" | "self";
48
73
  /**
@@ -81,28 +106,13 @@ type ResponseInputFields<TQuery, TBody, TParamNames extends string> = [
81
106
  ] extends [never, never, never] ? object : {
82
107
  input: ReadInputFields<TQuery, TBody, TParamNames>;
83
108
  };
84
- type TriggerAwaitedReturn$2<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
85
- type ExtractInputFromResponse$2<T> = T extends {
109
+ type TriggerAwaitedReturn$3<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
110
+ type ExtractInputFromResponse$3<T> = T extends {
86
111
  input: infer I;
87
112
  } ? I : never;
88
- type ExtractTriggerQuery$2<I> = I extends {
89
- query: infer Q;
90
- } ? {
91
- query?: Q;
92
- } : unknown;
93
- type ExtractTriggerBody$2<I> = I extends {
94
- body: infer B;
95
- } ? {
96
- body?: B;
97
- } : unknown;
98
- type ExtractTriggerParams$2<I> = I extends {
99
- params: infer P;
100
- } ? {
101
- params?: P;
102
- } : unknown;
103
- type TriggerOptions<T> = ExtractInputFromResponse$2<TriggerAwaitedReturn$2<T>> extends infer I ? [I] extends [never] ? {
113
+ type TriggerOptions<T> = ExtractInputFromResponse$3<TriggerAwaitedReturn$3<T>> extends infer I ? [I] extends [never] ? {
104
114
  force?: boolean;
105
- } : ExtractTriggerQuery$2<I> & ExtractTriggerBody$2<I> & ExtractTriggerParams$2<I> & {
115
+ } : ExtractTriggerQuery$3<I> & ExtractTriggerBody$3<I> & ExtractTriggerParams$3<I> & {
106
116
  /** Force refetch even if data is cached */
107
117
  force?: boolean;
108
118
  } : {
@@ -160,30 +170,30 @@ type InputFields<TQuery, TBody, TParamNames extends string> = OptionalQueryField
160
170
  type WriteResponseInputFields<TQuery, TBody, TParamNames extends string> = [TQuery, TBody, TParamNames] extends [never, never, never] ? object : {
161
171
  input: InputFields<TQuery, TBody, TParamNames> | undefined;
162
172
  };
163
- type TriggerAwaitedReturn$1<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
164
- type ExtractInputFromResponse$1<T> = T extends {
173
+ type TriggerAwaitedReturn$2<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
174
+ type ExtractInputFromResponse$2<T> = T extends {
165
175
  input: infer I;
166
176
  } ? I : never;
167
- type ExtractTriggerQuery$1<I> = I extends {
177
+ type ExtractTriggerQuery$2<I> = I extends {
168
178
  query: infer Q;
169
179
  } ? undefined extends Q ? {
170
180
  query?: Exclude<Q, undefined>;
171
181
  } : {
172
182
  query: Q;
173
183
  } : unknown;
174
- type ExtractTriggerBody$1<I> = I extends {
184
+ type ExtractTriggerBody$2<I> = I extends {
175
185
  body: infer B;
176
186
  } ? undefined extends B ? {
177
187
  body?: Exclude<B, undefined> | SpooshBody<Exclude<B, undefined>>;
178
188
  } : {
179
189
  body: B | SpooshBody<B>;
180
190
  } : unknown;
181
- type ExtractTriggerParams$1<I> = I extends {
191
+ type ExtractTriggerParams$2<I> = I extends {
182
192
  params: infer P;
183
193
  } ? {
184
194
  params: P;
185
195
  } : unknown;
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;
196
+ type WriteTriggerInput<T> = ExtractInputFromResponse$2<TriggerAwaitedReturn$2<T>> extends infer I ? [I] extends [never] ? object : ExtractTriggerQuery$2<I> & ExtractTriggerBody$2<I> & ExtractTriggerParams$2<I> : object;
187
197
  /**
188
198
  * Result returned by `useWrite` hook.
189
199
  *
@@ -209,25 +219,25 @@ type BaseWriteResult<TData, TError, TOptions, TMeta = Record<string, unknown>> =
209
219
  type UseWriteResult<TData, TError, TOptions, TMeta, TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = BaseWriteResult<TData, TError, TOptions, TMeta> & MergePluginResults<TPlugins>["write"];
210
220
  type WriteApiClient<TSchema, TDefaultError> = WriteSelectorClient<TSchema, TDefaultError>;
211
221
 
212
- type TriggerAwaitedReturn<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
213
- type ExtractInputFromResponse<T> = T extends {
222
+ type TriggerAwaitedReturn$1<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
223
+ type ExtractInputFromResponse$1<T> = T extends {
214
224
  input: infer I;
215
225
  } ? I : never;
216
- type ExtractTriggerQuery<I> = I extends {
226
+ type ExtractTriggerQuery$1<I> = I extends {
217
227
  query: infer Q;
218
228
  } ? undefined extends Q ? {
219
229
  query?: Exclude<Q, undefined>;
220
230
  } : {
221
231
  query: Q;
222
232
  } : unknown;
223
- type ExtractTriggerBody<I> = I extends {
233
+ type ExtractTriggerBody$1<I> = I extends {
224
234
  body: infer B;
225
235
  } ? undefined extends B ? {
226
236
  body?: Exclude<B, undefined> | SpooshBody<Exclude<B, undefined>>;
227
237
  } : {
228
238
  body: B | SpooshBody<B>;
229
239
  } : unknown;
230
- type ExtractTriggerParams<I> = I extends {
240
+ type ExtractTriggerParams$1<I> = I extends {
231
241
  params: infer P;
232
242
  } ? {
233
243
  params: P;
@@ -236,7 +246,7 @@ type QueueTriggerBase = {
236
246
  /** Custom ID for this queue item. If not provided, one will be auto-generated. */
237
247
  id?: string;
238
248
  };
239
- type QueueTriggerInput<T> = ExtractInputFromResponse<TriggerAwaitedReturn<T>> extends infer I ? [I] extends [never] ? QueueTriggerBase : QueueTriggerBase & ExtractTriggerQuery<I> & ExtractTriggerBody<I> & ExtractTriggerParams<I> : QueueTriggerBase;
249
+ type QueueTriggerInput<T> = ExtractInputFromResponse$1<TriggerAwaitedReturn$1<T>> extends infer I ? [I] extends [never] ? QueueTriggerBase : QueueTriggerBase & ExtractTriggerQuery$1<I> & ExtractTriggerBody$1<I> & ExtractTriggerParams$1<I> : QueueTriggerBase;
240
250
  /**
241
251
  * Options for useQueue hook.
242
252
  */
@@ -277,41 +287,25 @@ type UseQueueResult<TData, TError, TTriggerInput, TMeta = object> = {
277
287
  type QueueApiClient<TSchema, TDefaultError> = QueueSelectorClient<TSchema, TDefaultError>;
278
288
 
279
289
  type TagModeInArray = "all" | "self";
280
- type AnyInfiniteRequestOptions = {
281
- query?: Record<string, unknown>;
282
- params?: Record<string, string | number>;
283
- body?: unknown;
284
- };
285
- /**
286
- * Context passed to `canFetchNext` and `nextPageRequest` callbacks.
287
- */
288
- type InfiniteNextContext<TData, TRequest> = {
289
- /** The latest fetched response data */
290
- response: TData | undefined;
291
- /** All responses fetched so far */
292
- allResponses: TData[];
293
- /** The current request options (query, params, body) */
294
- request: TRequest;
295
- };
296
- /**
297
- * Context passed to `canFetchPrev` and `prevPageRequest` callbacks.
298
- */
299
- type InfinitePrevContext<TData, TRequest> = {
300
- /** The latest fetched response data */
301
- response: TData | undefined;
302
- /** All responses fetched so far */
303
- allResponses: TData[];
304
- /** The current request options (query, params, body) */
305
- request: TRequest;
290
+ type TriggerAwaitedReturn<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
291
+ type ExtractInputFromResponse<T> = T extends {
292
+ input: infer I;
293
+ } ? I : never;
294
+ type BaseTriggerOptions = {
295
+ /** Bypass cache and force refetch. Default: true */
296
+ force?: boolean;
306
297
  };
298
+ type PagesTriggerOptions<TReadFn> = ExtractInputFromResponse<TriggerAwaitedReturn<TReadFn>> extends infer I ? [I] extends [never] ? BaseTriggerOptions : ExtractTriggerQuery$3<I> & ExtractTriggerBody$3<I> & ExtractTriggerParams$3<I> & BaseTriggerOptions : BaseTriggerOptions;
307
299
  /**
308
- * Options for `useInfiniteRead` hook.
300
+ * Options for `usePages` hook.
309
301
  *
310
302
  * @template TData - The response data type for each page
311
303
  * @template TItem - The item type after merging all responses
304
+ * @template TError - The error type
312
305
  * @template TRequest - The request options type (query, params, body)
306
+ * @template TMeta - Plugin metadata type
313
307
  */
314
- type BaseInfiniteReadOptions<TData, TItem, TRequest = AnyInfiniteRequestOptions> = {
308
+ type BasePagesOptions<TData, TItem, TError = unknown, TRequest = InfiniteRequestOptions, TMeta = Record<string, unknown>> = {
315
309
  /** Whether to fetch automatically on mount. Default: true */
316
310
  enabled?: boolean;
317
311
  /**
@@ -322,30 +316,66 @@ type BaseInfiniteReadOptions<TData, TItem, TRequest = AnyInfiniteRequestOptions>
322
316
  * - 'none' should only be used as string (use `tags: 'none'` not in array)
323
317
  */
324
318
  tags?: TagMode | (TagModeInArray | (string & {}))[];
325
- /** Callback to determine if there's a next page to fetch */
326
- canFetchNext: (ctx: InfiniteNextContext<TData, TRequest>) => boolean;
327
- /** Callback to build the request options for the next page */
328
- nextPageRequest: (ctx: InfiniteNextContext<TData, TRequest>) => Partial<TRequest>;
329
- /** Callback to merge all responses into a single array of items */
330
- merger: (allResponses: TData[]) => TItem[];
331
- /** Callback to determine if there's a previous page to fetch */
332
- canFetchPrev?: (ctx: InfinitePrevContext<TData, TRequest>) => boolean;
333
- /** Callback to build the request options for the previous page */
334
- prevPageRequest?: (ctx: InfinitePrevContext<TData, TRequest>) => Partial<TRequest>;
319
+ /**
320
+ * Callback to determine if there's a next page to fetch.
321
+ * Receives the last page to check for pagination indicators.
322
+ * Default: `() => false` (no next page fetching)
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * canFetchNext: ({ lastPage }) => lastPage?.data?.nextCursor != null
327
+ * ```
328
+ */
329
+ canFetchNext?: (ctx: InfiniteNextContext<TData, TError, TRequest, TMeta>) => boolean;
330
+ /**
331
+ * Callback to build the request options for the next page.
332
+ * Return only the fields that change - they will be **merged** with the initial request.
333
+ * Default: `() => ({})` (no changes to request)
334
+ *
335
+ * @example
336
+ * ```ts
337
+ * // Initial: { query: { cursor: 0, limit: 10 } }
338
+ * // Only return cursor - limit is preserved automatically
339
+ * nextPageRequest: ({ lastPage }) => ({
340
+ * query: { cursor: lastPage?.data?.nextCursor }
341
+ * })
342
+ * ```
343
+ */
344
+ nextPageRequest?: (ctx: InfiniteNextContext<TData, TError, TRequest, TMeta>) => Partial<TRequest>;
345
+ /**
346
+ * Callback to merge all pages into a single array of items.
347
+ *
348
+ * @example
349
+ * ```ts
350
+ * merger: (pages) => pages.flatMap(p => p.data?.items ?? [])
351
+ * ```
352
+ */
353
+ merger: (pages: InfinitePage<TData, TError, TMeta>[]) => TItem[];
354
+ /**
355
+ * Callback to determine if there's a previous page to fetch.
356
+ * Receives the first page to check for pagination indicators.
357
+ */
358
+ canFetchPrev?: (ctx: InfinitePrevContext<TData, TError, TRequest, TMeta>) => boolean;
359
+ /**
360
+ * Callback to build the request options for the previous page.
361
+ * Return only the fields that change - they will be **merged** with the initial request.
362
+ */
363
+ prevPageRequest?: (ctx: InfinitePrevContext<TData, TError, TRequest, TMeta>) => Partial<TRequest>;
335
364
  };
336
365
  /**
337
- * Result returned by `useInfiniteRead` hook.
366
+ * Result returned by `usePages` hook.
338
367
  *
339
368
  * @template TData - The response data type for each page
340
369
  * @template TError - The error type
341
370
  * @template TItem - The item type after merging all responses
342
371
  * @template TPluginResult - Plugin-provided result fields
372
+ * @template TTriggerOptions - Options that can be passed to trigger()
343
373
  */
344
- type BaseInfiniteReadResult<TData, TError, TItem, TPluginResult = Record<string, unknown>> = {
345
- /** Merged items from all fetched responses */
374
+ type BasePagesResult<TData, TError, TItem, TPluginResult = Record<string, unknown>, TTriggerOptions = object> = {
375
+ /** Merged items from all fetched pages */
346
376
  data: TItem[] | undefined;
347
- /** Array of all raw response data */
348
- allResponses: TData[] | undefined;
377
+ /** Array of all pages with status, data, error, and meta per page */
378
+ pages: InfinitePage<TData, TError, TPluginResult>[];
349
379
  /** True during the initial load (no data yet) */
350
380
  loading: boolean;
351
381
  /** True during any fetch operation */
@@ -358,23 +388,121 @@ type BaseInfiniteReadResult<TData, TError, TItem, TPluginResult = Record<string,
358
388
  canFetchNext: boolean;
359
389
  /** Whether there's a previous page available to fetch */
360
390
  canFetchPrev: boolean;
361
- /** Plugin-provided metadata */
362
- meta: TPluginResult;
363
391
  /** Fetch the next page */
364
392
  fetchNext: () => Promise<void>;
365
393
  /** Fetch the previous page */
366
394
  fetchPrev: () => Promise<void>;
367
- /** Trigger refetch of all pages from the beginning */
368
- trigger: () => Promise<void>;
395
+ /** Trigger refetch of all pages from the beginning, optionally with new request options */
396
+ trigger: (options?: TTriggerOptions) => Promise<void>;
369
397
  /** Abort the current fetch operation */
370
398
  abort: () => void;
371
399
  /** Error from the last failed request */
372
400
  error: TError | undefined;
373
401
  };
374
- type UseInfiniteReadResult<TData, TError, TItem, TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = BaseInfiniteReadResult<TData, TError, TItem> & MergePluginResults<TPlugins>["read"];
375
- type InfiniteReadApiClient<TSchema, TDefaultError> = ReadClient<TSchema, TDefaultError>;
402
+ type UsePagesResult<TData, TError, TItem, TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[], TTriggerOptions = object> = BasePagesResult<TData, TError, TItem, MergePluginResults<TPlugins>["read"], TTriggerOptions>;
403
+ type PagesApiClient<TSchema, TDefaultError> = ReadClient<TSchema, TDefaultError>;
376
404
 
377
- type InferError<T, TDefaultError> = [T] extends [unknown] ? TDefaultError : T;
405
+ interface BaseSubscriptionOptions {
406
+ enabled?: boolean;
407
+ /** Subscription adapter instance created by the transport-specific hook */
408
+ adapter: SubscriptionAdapter;
409
+ /** Operation type for the subscription (e.g., "sse", "websocket") */
410
+ operationType: OperationType;
411
+ }
412
+ type ExtractTriggerQuery<TQuery> = [TQuery] extends [never] ? unknown : undefined extends TQuery ? {
413
+ query?: Exclude<TQuery, undefined>;
414
+ } : {
415
+ query: TQuery;
416
+ };
417
+ type ExtractTriggerBody<TBody> = [TBody] extends [never] ? unknown : undefined extends TBody ? {
418
+ body?: Exclude<TBody, undefined> | SpooshBody<Exclude<TBody, undefined>>;
419
+ } : {
420
+ body: TBody | SpooshBody<TBody>;
421
+ };
422
+ type ExtractTriggerParams<TParams> = [TParams] extends [never] ? unknown : {
423
+ params: TParams;
424
+ };
425
+ type SubscriptionTriggerInput<TQuery, TBody, TParams> = [
426
+ TQuery,
427
+ TBody,
428
+ TParams
429
+ ] extends [never, never, never] ? object : ExtractTriggerQuery<TQuery> & ExtractTriggerBody<TBody> & ExtractTriggerParams<TParams>;
430
+ interface BaseSubscriptionResult<TData, TError, TPluginResult, TTriggerOptions> {
431
+ data: TData | undefined;
432
+ error: TError | undefined;
433
+ isConnected: boolean;
434
+ loading: boolean;
435
+ meta: TPluginResult;
436
+ /** @internal Query key for devtool integration */
437
+ _queryKey: string;
438
+ /** @internal Subscription version for tracking resubscriptions */
439
+ _subscriptionVersion: number;
440
+ trigger: (options?: TTriggerOptions) => Promise<void>;
441
+ disconnect: () => void;
442
+ }
443
+ type SubscriptionApiClient<TSchema, TDefaultError = unknown> = SubscriptionClient<TSchema, TDefaultError>;
444
+
445
+ type ParseStrategy = "auto" | "json" | "text" | "json-done";
446
+ type AccumulateStrategy = "replace" | "merge";
447
+ type AccumulatorFn<T = unknown> = (previous: T | undefined, current: T) => T;
448
+ type AccumulateFieldConfig<T> = T extends Record<string, unknown> ? {
449
+ [K in keyof T]?: AccumulateStrategy;
450
+ } : Record<string, AccumulateStrategy>;
451
+ interface UseSSEOptionsBase {
452
+ /** Whether the subscription is enabled. Defaults to true. */
453
+ enabled?: boolean;
454
+ /** Max retry attempts on connection failure */
455
+ maxRetries?: number;
456
+ /** Delay between retries in ms */
457
+ retryDelay?: number;
458
+ }
459
+ type ParseFn = (data: string) => unknown;
460
+ type TypedParseConfig<TEventKeys extends string> = ParseStrategy | ParseFn | Partial<Record<TEventKeys, ParseStrategy | ParseFn>>;
461
+ type TypedAccumulateConfig<TEvents extends Record<string, unknown>> = AccumulateStrategy | {
462
+ [K in keyof TEvents]?: AccumulateStrategy | AccumulatorFn<TEvents[K]> | AccumulateFieldConfig<TEvents[K]>;
463
+ };
464
+ interface TypedUseSSEOptions<TEventKeys extends string, TEvents extends Record<string, unknown>, TSelectedEvents extends readonly TEventKeys[] = readonly TEventKeys[]> extends UseSSEOptionsBase {
465
+ /** Event types to listen for. If not provided, listens to all events. */
466
+ events?: TSelectedEvents;
467
+ /** Parse strategy for SSE event data. Defaults to 'auto'. */
468
+ parse?: TypedParseConfig<TEventKeys>;
469
+ /** Accumulate strategy for combining events. Defaults to 'replace'. */
470
+ accumulate?: TypedAccumulateConfig<TEvents>;
471
+ }
472
+ interface UseSSEOptions extends UseSSEOptionsBase {
473
+ /** Event types to listen for. If not provided, listens to all events. */
474
+ events?: string[];
475
+ /** Parse strategy for SSE event data. Defaults to 'auto'. */
476
+ parse?: ParseStrategy | Record<string, ParseStrategy>;
477
+ /** Accumulate strategy for combining events. Defaults to 'replace'. */
478
+ accumulate?: AccumulateStrategy | Record<string, AccumulateStrategy | AccumulatorFn>;
479
+ }
480
+ interface UseSSEResult<TEvents, TError> {
481
+ /** Accumulated data keyed by event type */
482
+ data: Partial<TEvents> | undefined;
483
+ /** Connection or parse error */
484
+ error: TError | undefined;
485
+ /** Whether currently connected to the SSE endpoint */
486
+ isConnected: boolean;
487
+ /** Whether connection is in progress */
488
+ loading: boolean;
489
+ /** Plugin metadata */
490
+ meta: Record<string, never>;
491
+ /**
492
+ * Manually trigger connection with optional body/query overrides
493
+ * @param options - Optional body and query parameters
494
+ */
495
+ trigger: (options?: {
496
+ body?: unknown;
497
+ query?: unknown;
498
+ }) => Promise<void>;
499
+ /** Disconnect from the SSE endpoint */
500
+ disconnect: () => void;
501
+ /** Reset accumulated data */
502
+ reset: () => void;
503
+ }
504
+
505
+ type InferError<T, TDefaultError> = unknown extends T ? TDefaultError : T;
378
506
  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>>;
379
507
  type ResolvedWriteOptions<TSchema, TPlugins extends PluginArray, TMethod, TDefaultError> = ResolveTypes<MergePluginOptions<TPlugins>["write"], WriteResolverContext<TSchema, TMethod, TDefaultError>>;
380
508
  type ResolvedWriteTriggerOptions<TSchema, TPlugins extends PluginArray, TMethod, TDefaultError> = ResolveTypes<MergePluginOptions<TPlugins>["writeTrigger"], WriteResolverContext<TSchema, TMethod, TDefaultError>>;
@@ -391,23 +519,30 @@ type UseWriteFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
391
519
  type UseQueueFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
392
520
  <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
521
  };
394
- type InfiniteReadResolverContext<TSchema, TData, TError, TRequest> = ResolverContext<TSchema, TData, TError, TRequest extends {
395
- query: infer Q;
396
- } ? Q : never, TRequest extends {
397
- body: infer B;
398
- } ? B : never, TRequest extends {
399
- params: infer P;
400
- } ? P : never>;
401
- type ResolvedInfiniteReadOptions<TSchema, TPlugins extends PluginArray, TData, TError, TRequest> = ResolveTypes<MergePluginOptions<TPlugins>["infiniteRead"], InfiniteReadResolverContext<TSchema, TData, TError, TRequest>>;
402
- type UseInfiniteReadFn<TDefaultError, TSchema, TPlugins extends PluginArray> = <TData, TItem, TError = TDefaultError, TRequest extends AnyInfiniteRequestOptions = AnyInfiniteRequestOptions>(readFn: (api: InfiniteReadApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<TData, TError>>, readOptions: BaseInfiniteReadOptions<TData, TItem, TRequest> & ResolvedInfiniteReadOptions<TSchema, TPlugins, TData, TError, TRequest>) => BaseInfiniteReadResult<TData, TError, TItem, MergePluginResults<TPlugins>["read"]>;
522
+ type UsePagesFn<TDefaultError, TSchema, TPlugins extends PluginArray> = <TReadFn extends (api: PagesApiClient<TSchema, TDefaultError>) => Promise<SpooshResponse<unknown, unknown>>, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions, TItem = unknown>(readFn: TReadFn, readOptions: BasePagesOptions<ExtractMethodData<TReadFn>, TItem, InferError<ExtractMethodError<TReadFn>, TDefaultError>, TRequest, MergePluginResults<TPlugins>["read"]> & ResolveTypes<MergePluginOptions<TPlugins>["pages"], ResolverContext<TSchema, ExtractMethodData<TReadFn>, InferError<ExtractMethodError<TReadFn>, TDefaultError>>>) => BasePagesResult<ExtractMethodData<TReadFn>, InferError<ExtractMethodError<TReadFn>, TDefaultError>, TItem, MergePluginResults<TPlugins>["read"], PagesTriggerOptions<TReadFn>>;
523
+ type UseSubscriptionFn<TDefaultError, TSchema, TPlugins extends PluginArray> = <TSubFn extends (api: SubscriptionApiClient<TSchema, TDefaultError>) => unknown>(subFn: TSubFn, subOptions?: BaseSubscriptionOptions) => BaseSubscriptionResult<ExtractSubscriptionEvents<TSubFn>, InferError<ExtractMethodError<TSubFn>, TDefaultError>, MergePluginResults<TPlugins>["subscribe"], SubscriptionTriggerInput<ExtractSubscriptionQuery<TSubFn>, ExtractSubscriptionBody<TSubFn>, never>>;
524
+ type InferSSEEvents<T> = ExtractAllSubscriptionEvents<T> extends Record<string, unknown> ? ExtractAllSubscriptionEvents<T> : Record<string, unknown>;
525
+ type FilteredEvents<TAllEvents extends Record<string, unknown>, TSelectedEvents extends readonly string[]> = TSelectedEvents[number] extends keyof TAllEvents ? Pick<TAllEvents, TSelectedEvents[number]> : TAllEvents;
526
+ type UseSSEFn<TDefaultError, TSchema, TPlugins extends PluginArray> = {
527
+ <TSubFn extends (api: SubscriptionApiClient<TSchema, TDefaultError>) => {
528
+ _subscription: true;
529
+ events: Record<string, {
530
+ data: unknown;
531
+ }>;
532
+ }, const TSelectedEvents extends readonly Extract<ExtractAllSubscriptionEventKeys<TSubFn>, string>[]>(subFn: TSubFn, sseOptions: TypedUseSSEOptions<Extract<ExtractAllSubscriptionEventKeys<TSubFn>, string>, InferSSEEvents<TSubFn>, TSelectedEvents> & {
533
+ events: TSelectedEvents;
534
+ }): UseSSEResult<FilteredEvents<InferSSEEvents<TSubFn>, TSelectedEvents>, InferError<ExtractMethodError<TSubFn>, TDefaultError>>;
535
+ <TSubFn extends (api: SubscriptionApiClient<TSchema, TDefaultError>) => {
536
+ _subscription: true;
537
+ events: Record<string, {
538
+ data: unknown;
539
+ }>;
540
+ }>(subFn: TSubFn, sseOptions?: Omit<TypedUseSSEOptions<Extract<ExtractAllSubscriptionEventKeys<TSubFn>, string>, InferSSEEvents<TSubFn>>, "events">): UseSSEResult<InferSSEEvents<TSubFn>, InferError<ExtractMethodError<TSubFn>, TDefaultError>>;
541
+ };
403
542
  /**
404
- * Spoosh React hooks interface containing useRead, useWrite, and useInfiniteRead.
405
- *
406
- * @template TDefaultError - The default error type
407
- * @template TSchema - The API schema type
408
- * @template TPlugins - The plugins array type
543
+ * Base hooks that are always available.
409
544
  */
410
- type SpooshReactHooks<TDefaultError, TSchema, TPlugins extends PluginArray> = {
545
+ type BaseSpooshReactHooks<TDefaultError, TSchema, TPlugins extends PluginArray> = {
411
546
  /**
412
547
  * React hook for fetching data from an API endpoint with automatic caching and revalidation.
413
548
  *
@@ -448,21 +583,21 @@ type SpooshReactHooks<TDefaultError, TSchema, TPlugins extends PluginArray> = {
448
583
  *
449
584
  * @param readFn - Function that selects the API endpoint to call
450
585
  * @param readOptions - Configuration including `canFetchNext`, `nextPageRequest`, `merger`, and optional `canFetchPrev`/`prevPageRequest`
451
- * @returns Object containing `data`, `allResponses`, `fetchNext`, `fetchPrev`, `canFetchNext`, `canFetchPrev`, `loading`, `fetching`, and pagination states
586
+ * @returns Object containing `data`, `pages`, `fetchNext`, `fetchPrev`, `canFetchNext`, `canFetchPrev`, `loading`, `fetching`, and pagination states
452
587
  *
453
588
  * @example
454
589
  * ```tsx
455
- * const { data, fetchNext, canFetchNext, loading } = useInfiniteRead(
590
+ * const { data, fetchNext, canFetchNext, loading } = usePages(
456
591
  * (api) => api("posts").GET(),
457
592
  * {
458
- * canFetchNext: ({ response }) => !!response?.nextCursor,
459
- * nextPageRequest: ({ response }) => ({ query: { cursor: response?.nextCursor } }),
460
- * merger: (responses) => responses.flatMap(r => r.items)
593
+ * canFetchNext: ({ lastPage }) => !!lastPage?.data?.nextCursor,
594
+ * nextPageRequest: ({ lastPage }) => ({ query: { cursor: lastPage?.data?.nextCursor } }),
595
+ * merger: (pages) => pages.flatMap(p => p.data?.items ?? [])
461
596
  * }
462
597
  * );
463
598
  * ```
464
599
  */
465
- useInfiniteRead: UseInfiniteReadFn<TDefaultError, TSchema, TPlugins>;
600
+ usePages: UsePagesFn<TDefaultError, TSchema, TPlugins>;
466
601
  /**
467
602
  * React hook for queued operations with concurrency control.
468
603
  *
@@ -483,11 +618,62 @@ type SpooshReactHooks<TDefaultError, TSchema, TPlugins extends PluginArray> = {
483
618
  * ```
484
619
  */
485
620
  useQueue: UseQueueFn<TDefaultError, TSchema, TPlugins>;
621
+ /**
622
+ * React hook for subscribing to real-time data streams (SSE, WebSocket, etc.).
623
+ *
624
+ * @param subFn - Function that selects the subscription endpoint
625
+ * @param subOptions - Optional configuration including `enabled`, `tags`
626
+ * @returns Object containing `data`, `error`, `loading`, `isSubscribed`, `emit`, `unsubscribe`
627
+ *
628
+ * @example
629
+ * ```tsx
630
+ * const { data } = useSubscription((api) =>
631
+ * api("notifications").GET({ events: ["alert", "message"] })
632
+ * );
633
+ * ```
634
+ */
635
+ useSubscription: UseSubscriptionFn<TDefaultError, TSchema, TPlugins>;
486
636
  } & MergePluginInstanceApi<TPlugins, TSchema>;
637
+ /**
638
+ * SSE hooks available when SSE transport is registered.
639
+ */
640
+ type SSEHooks<TDefaultError, TSchema, TPlugins extends PluginArray> = {
641
+ /**
642
+ * React hook for SSE streams with per-hook parsing and accumulation.
643
+ *
644
+ * @param subFn - Function that selects the SSE endpoint
645
+ * @param sseOptions - Configuration including `events`, `parse`, `accumulate`, `maxRetries`, `retryDelay`
646
+ * @returns Object containing `data`, `error`, `loading`, `isConnected`, `trigger`, `disconnect`, `reset`
647
+ *
648
+ * @example
649
+ * ```tsx
650
+ * const { data, reset } = useSSE(
651
+ * (api) => api("chat").POST(),
652
+ * {
653
+ * events: ["chunk", "done"],
654
+ * parse: "json-done",
655
+ * accumulate: { chunk: "merge" },
656
+ * maxRetries: 5,
657
+ * }
658
+ * );
659
+ * ```
660
+ */
661
+ useSSE: UseSSEFn<TDefaultError, TSchema, TPlugins>;
662
+ };
663
+ /**
664
+ * Spoosh React hooks interface containing useRead, useWrite, and usePages.
665
+ * useSSE is only available when SSE transport is registered via withTransports([sse()]).
666
+ *
667
+ * @template TDefaultError - The default error type
668
+ * @template TSchema - The API schema type
669
+ * @template TPlugins - The plugins array type
670
+ * @template TTransports - The registered transport names
671
+ */
672
+ type SpooshReactHooks<TDefaultError, TSchema, TPlugins extends PluginArray, TTransports extends string = never> = BaseSpooshReactHooks<TDefaultError, TSchema, TPlugins> & ([TTransports] extends [never] ? object : "sse" extends TTransports ? SSEHooks<TDefaultError, TSchema, TPlugins> : object);
487
673
  /**
488
674
  * Shape of a Spoosh instance required for creating React hooks.
489
675
  */
490
- type SpooshInstanceShape<TApi, TSchema, TDefaultError, TPlugins> = {
676
+ type SpooshInstanceShape<TApi, TSchema, TDefaultError, TPlugins, TTransports extends string = never> = {
491
677
  /** The API instance */
492
678
  api: TApi;
493
679
  /** State manager for caching and state */
@@ -496,27 +682,38 @@ type SpooshInstanceShape<TApi, TSchema, TDefaultError, TPlugins> = {
496
682
  eventEmitter: EventEmitter;
497
683
  /** Plugin executor for running plugins */
498
684
  pluginExecutor: PluginExecutor;
685
+ /** Registered transports for subscriptions */
686
+ transports: Map<string, SpooshTransport>;
687
+ /** Config with baseUrl and default options */
688
+ config: {
689
+ baseUrl: string;
690
+ defaultOptions: {
691
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
692
+ [key: string]: unknown;
693
+ };
694
+ };
499
695
  /** Type information (not used at runtime) */
500
696
  _types: {
501
697
  schema: TSchema;
502
698
  defaultError: TDefaultError;
503
699
  plugins: TPlugins;
700
+ transports: TTransports;
504
701
  };
505
702
  };
506
703
 
507
704
  /**
508
- * Creates React hooks (useRead, useWrite, useInfiniteRead) from a Spoosh instance.
705
+ * Creates React hooks (useRead, useWrite, usePages) from a Spoosh instance.
509
706
  *
510
707
  * @template TSchema - The API schema type
511
708
  * @template TDefaultError - The default error type
512
709
  * @template TPlugins - The plugins array type
513
710
  * @template TApi - The API type
514
711
  * @param instance - The Spoosh instance containing api, stateManager, eventEmitter, and pluginExecutor
515
- * @returns An object containing useRead, useWrite, useInfiniteRead hooks and plugin instance APIs
712
+ * @returns An object containing useRead, useWrite, usePages hooks and plugin instance APIs
516
713
  *
517
714
  * @example
518
715
  * ```ts
519
- * const { useRead, useWrite, useInfiniteRead } = create(spooshInstance);
716
+ * const { useRead, useWrite, usePages } = create(spooshInstance);
520
717
  *
521
718
  * function MyComponent() {
522
719
  * const { data, loading } = useRead((api) => api("posts").GET());
@@ -524,7 +721,7 @@ type SpooshInstanceShape<TApi, TSchema, TDefaultError, TPlugins> = {
524
721
  * }
525
722
  * ```
526
723
  */
527
- declare function create<TSchema, TDefaultError, TPlugins extends PluginArray, TApi>(instance: SpooshInstanceShape<TApi, TSchema, TDefaultError, TPlugins>): SpooshReactHooks<TDefaultError, TSchema, TPlugins>;
724
+ declare function create<TSchema, TDefaultError, TPlugins extends PluginArray, TApi, TTransports extends string = never>(instance: SpooshInstanceShape<TApi, TSchema, TDefaultError, TPlugins, TTransports>): SpooshReactHooks<TDefaultError, TSchema, TPlugins, TTransports>;
528
725
 
529
726
  type PluginHooksConfig<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = {
530
727
  baseUrl: string;
@@ -532,4 +729,4 @@ type PluginHooksConfig<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[
532
729
  plugins: TPlugins;
533
730
  };
534
731
 
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 };
732
+ export { type BasePagesOptions, type BasePagesResult, 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 PagesApiClient, type PagesTriggerOptions, type PluginHooksConfig, type QueueApiClient, type QueueTriggerInput, type ReadApiClient, type ResponseInputFields, type SpooshReactHooks, type TriggerOptions, type TypedAccumulateConfig, type TypedParseConfig, type TypedUseSSEOptions, type UsePagesResult, type UseQueueOptions, type UseQueueResult, type UseReadResult, type UseSSEOptions, type UseSSEResult, type UseWriteResult, type WriteApiClient, type WriteResponseInputFields, create };