@spoosh/core 0.1.0-beta.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.
@@ -0,0 +1,1318 @@
1
+ type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
2
+ type SchemaMethod = "$get" | "$post" | "$put" | "$patch" | "$delete";
3
+
4
+ type MiddlewarePhase = "before" | "after";
5
+ type MiddlewareContext<TData = unknown, TError = unknown> = {
6
+ baseUrl: string;
7
+ path: string[];
8
+ method: HttpMethod;
9
+ defaultOptions: SpooshOptions & SpooshOptionsExtra;
10
+ requestOptions?: AnyRequestOptions;
11
+ fetchInit?: RequestInit;
12
+ response?: SpooshResponse<TData, TError>;
13
+ metadata: Record<string, unknown>;
14
+ };
15
+ type MiddlewareHandler<TData = unknown, TError = unknown> = (context: MiddlewareContext<TData, TError>) => MiddlewareContext<TData, TError> | Promise<MiddlewareContext<TData, TError>>;
16
+ type SpooshMiddleware<TData = unknown, TError = unknown> = {
17
+ name: string;
18
+ phase: MiddlewarePhase;
19
+ handler: MiddlewareHandler<TData, TError>;
20
+ };
21
+
22
+ type QueryField<TQuery> = [TQuery] extends [never] ? object : {
23
+ query: TQuery;
24
+ };
25
+ type BodyField<TBody> = [TBody] extends [never] ? object : {
26
+ body: TBody;
27
+ };
28
+ type FormDataField<TFormData> = [TFormData] extends [never] ? object : {
29
+ formData: TFormData;
30
+ };
31
+ type UrlEncodedField<TUrlEncoded> = [TUrlEncoded] extends [never] ? object : {
32
+ urlEncoded: TUrlEncoded;
33
+ };
34
+ type ParamsField<TParamNames extends string> = [TParamNames] extends [never] ? object : {
35
+ params: Record<TParamNames, string | number>;
36
+ };
37
+ type InputFields<TQuery, TBody, TFormData, TUrlEncoded, TParamNames extends string> = QueryField<TQuery> & BodyField<TBody> & FormDataField<TFormData> & UrlEncodedField<TUrlEncoded> & ParamsField<TParamNames>;
38
+ type InputFieldWrapper<TQuery, TBody, TFormData, TUrlEncoded, TParamNames extends string> = [TQuery, TBody, TFormData, TUrlEncoded, TParamNames] extends [
39
+ never,
40
+ never,
41
+ never,
42
+ never,
43
+ never
44
+ ] ? object : {
45
+ input: InputFields<TQuery, TBody, TFormData, TUrlEncoded, TParamNames>;
46
+ };
47
+ type SpooshResponse<TData, TError, TRequestOptions = unknown, TQuery = never, TBody = never, TFormData = never, TUrlEncoded = never, TParamNames extends string = never> = ({
48
+ status: number;
49
+ data: TData;
50
+ headers?: Headers;
51
+ error?: undefined;
52
+ aborted?: false;
53
+ readonly __requestOptions?: TRequestOptions;
54
+ } & InputFieldWrapper<TQuery, TBody, TFormData, TUrlEncoded, TParamNames>) | ({
55
+ status: number;
56
+ data?: undefined;
57
+ headers?: Headers;
58
+ error: TError;
59
+ aborted?: boolean;
60
+ readonly __requestOptions?: TRequestOptions;
61
+ } & InputFieldWrapper<TQuery, TBody, TFormData, TUrlEncoded, TParamNames>);
62
+ type SpooshOptionsExtra<TData = unknown, TError = unknown> = {
63
+ middlewares?: SpooshMiddleware<TData, TError>[];
64
+ };
65
+
66
+ type RetryConfig = {
67
+ retries?: number | false;
68
+ retryDelay?: number;
69
+ };
70
+ type HeadersInitOrGetter = HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
71
+ type SpooshOptions = Omit<RequestInit, "method" | "body" | "headers"> & {
72
+ headers?: HeadersInitOrGetter;
73
+ };
74
+ type BaseRequestOptions = {
75
+ headers?: HeadersInitOrGetter;
76
+ cache?: RequestCache;
77
+ signal?: AbortSignal;
78
+ };
79
+ type BodyOption<TBody> = [TBody] extends [never] ? object : {
80
+ body: TBody;
81
+ };
82
+ type QueryOption<TQuery> = [TQuery] extends [never] ? object : {
83
+ query: TQuery;
84
+ };
85
+ type FormDataOption<TFormData> = [TFormData] extends [never] ? object : {
86
+ formData: TFormData;
87
+ };
88
+ type UrlEncodedOption<TUrlEncoded> = [TUrlEncoded] extends [never] ? object : {
89
+ urlEncoded: TUrlEncoded;
90
+ };
91
+ type RequestOptions<TBody = never, TQuery = never, TFormData = never, TUrlEncoded = never> = BaseRequestOptions & BodyOption<TBody> & QueryOption<TQuery> & FormDataOption<TFormData> & UrlEncodedOption<TUrlEncoded>;
92
+ type AnyRequestOptions = BaseRequestOptions & {
93
+ body?: unknown;
94
+ query?: Record<string, string | number | boolean | undefined>;
95
+ formData?: Record<string, unknown>;
96
+ urlEncoded?: Record<string, unknown>;
97
+ params?: Record<string, string | number>;
98
+ signal?: AbortSignal;
99
+ } & Partial<RetryConfig>;
100
+ type DynamicParamsOption = {
101
+ params?: Record<string, string | number>;
102
+ };
103
+ type CoreRequestOptionsBase = {
104
+ __hasDynamicParams?: DynamicParamsOption;
105
+ };
106
+ type MethodOptionsMap<TQueryOptions = object, TMutationOptions = object> = {
107
+ $get: TQueryOptions;
108
+ $post: TMutationOptions;
109
+ $put: TMutationOptions;
110
+ $patch: TMutationOptions;
111
+ $delete: TMutationOptions;
112
+ };
113
+ type ExtractMethodOptions<TOptionsMap, TMethod extends SchemaMethod> = TOptionsMap extends MethodOptionsMap<infer TQuery, infer TMutation> ? TMethod extends "$get" ? TQuery : TMutation : TOptionsMap;
114
+ type FetchExecutor<TOptions = SpooshOptions, TRequestOptions = AnyRequestOptions> = <TData, TError>(baseUrl: string, path: string[], method: HttpMethod, defaultOptions: TOptions, requestOptions?: TRequestOptions, nextTags?: boolean) => Promise<SpooshResponse<TData, TError>>;
115
+ type TypedParamsOption<TParamNames extends string> = [TParamNames] extends [
116
+ never
117
+ ] ? object : {
118
+ params?: Record<TParamNames, string | number>;
119
+ };
120
+ type ComputeRequestOptions<TRequestOptionsBase, TParamNames extends string> = "__hasDynamicParams" extends keyof TRequestOptionsBase ? [TParamNames] extends [never] ? Omit<TRequestOptionsBase, "__hasDynamicParams"> : Omit<TRequestOptionsBase, "__hasDynamicParams"> & TypedParamsOption<TParamNames> : TRequestOptionsBase & TypedParamsOption<TParamNames>;
121
+
122
+ type EventCallback<T = unknown> = (payload: T) => void;
123
+ /**
124
+ * Built-in event map. Maps event names to their payload types.
125
+ *
126
+ * Third-party plugins can extend this via declaration merging:
127
+ * @example
128
+ * ```ts
129
+ * declare module '@spoosh/core' {
130
+ * interface BuiltInEvents {
131
+ * "my-custom-event": MyPayloadType;
132
+ * }
133
+ * }
134
+ * ```
135
+ */
136
+ interface BuiltInEvents {
137
+ refetch: RefetchEvent;
138
+ invalidate: string[];
139
+ }
140
+ /**
141
+ * Resolves event payload type. Built-in events get their specific type,
142
+ * custom events get `unknown` (or explicit type parameter).
143
+ */
144
+ type EventPayload<E extends string> = E extends keyof BuiltInEvents ? BuiltInEvents[E] : unknown;
145
+ type EventEmitter = {
146
+ /**
147
+ * Subscribe to an event. Built-in events have type-safe payloads.
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * // Built-in event - payload is typed as RefetchEvent
152
+ * eventEmitter.on("refetch", (event) => {
153
+ * console.log(event.queryKey, event.reason);
154
+ * });
155
+ *
156
+ * // Custom event - specify type explicitly
157
+ * eventEmitter.on<MyPayload>("my-event", (payload) => { ... });
158
+ * ```
159
+ */
160
+ on<E extends string>(event: E, callback: EventCallback<EventPayload<E>>): () => void;
161
+ /**
162
+ * Emit an event. Built-in events have type-safe payloads.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * // Built-in event - payload is type-checked
167
+ * eventEmitter.emit("refetch", { queryKey: "...", reason: "focus" });
168
+ *
169
+ * // Custom event
170
+ * eventEmitter.emit("my-event", myPayload);
171
+ * ```
172
+ */
173
+ emit<E extends string>(event: E, payload: EventPayload<E>): void;
174
+ off: (event: string, callback: EventCallback) => void;
175
+ clear: () => void;
176
+ };
177
+ declare function createEventEmitter(): EventEmitter;
178
+
179
+ type OperationType = "read" | "write" | "infiniteRead";
180
+ type LifecyclePhase = "onMount" | "onUnmount" | "onUpdate";
181
+ type OperationState<TData = unknown, TError = unknown> = {
182
+ data: TData | undefined;
183
+ error: TError | undefined;
184
+ timestamp: number;
185
+ };
186
+ type CacheEntry<TData = unknown, TError = unknown> = {
187
+ state: OperationState<TData, TError>;
188
+ tags: string[];
189
+ /** Plugin-contributed result data (e.g., isOptimistic, isStale). Merged into hook result. */
190
+ pluginResult: Map<string, unknown>;
191
+ /** The original path-derived tag (e.g., "posts/1/comments"). Used for exact matching in cache */
192
+ selfTag?: string;
193
+ previousData?: TData;
194
+ /** Cache was invalidated while no subscriber was listening. Triggers refetch on next mount. */
195
+ stale?: boolean;
196
+ };
197
+ type PluginContext<TData = unknown, TError = unknown> = {
198
+ readonly operationType: OperationType;
199
+ readonly path: string[];
200
+ readonly method: HttpMethod;
201
+ readonly queryKey: string;
202
+ readonly tags: string[];
203
+ /** Timestamp when this request was initiated. Useful for tracing and debugging. */
204
+ readonly requestTimestamp: number;
205
+ /** Unique identifier for the hook instance. Persists across queryKey changes within the same hook. */
206
+ readonly hookId?: string;
207
+ requestOptions: AnyRequestOptions;
208
+ state: OperationState<TData, TError>;
209
+ response?: SpooshResponse<TData, TError>;
210
+ metadata: Map<string, unknown>;
211
+ abort: () => void;
212
+ stateManager: StateManager;
213
+ eventEmitter: EventEmitter;
214
+ /** Resolved headers as a plain object. Modify via setHeaders(). */
215
+ headers: Record<string, string>;
216
+ /** Add/update headers. Merges with existing headers. */
217
+ setHeaders: (headers: Record<string, string>) => void;
218
+ /** Access other plugins' exported APIs */
219
+ plugins: PluginAccessor;
220
+ /** Plugin-specific options passed from hooks (useRead/useWrite/useInfiniteRead) */
221
+ pluginOptions?: unknown;
222
+ /** Force a network request even if cached data exists. Used by plugins to communicate intent. */
223
+ forceRefetch?: boolean;
224
+ };
225
+ /** Input type for creating PluginContext (without injected properties) */
226
+ type PluginContextInput<TData = unknown, TError = unknown> = Omit<PluginContext<TData, TError>, "plugins" | "setHeaders" | "headers">;
227
+ /**
228
+ * Middleware function that wraps the fetch flow.
229
+ * Plugins use this for full control over request/response handling.
230
+ *
231
+ * @param context - The plugin context with request info and utilities
232
+ * @param next - Call this to continue to the next middleware or actual fetch
233
+ * @returns The response (either from next() or early return)
234
+ *
235
+ * @example
236
+ * ```ts
237
+ * // Cache middleware - return cached data or continue
238
+ * middleware: async (context, next) => {
239
+ * const cached = context.stateManager.getCache(context.queryKey);
240
+ * if (cached?.state?.data && !isStale(cached)) {
241
+ * return { data: cached.state.data, status: 200 };
242
+ * }
243
+ * return next();
244
+ * }
245
+ *
246
+ * // Retry middleware - wrap and retry on error
247
+ * middleware: async (context, next) => {
248
+ * for (let i = 0; i < 3; i++) {
249
+ * const result = await next();
250
+ * if (!result.error) return result;
251
+ * }
252
+ * return next();
253
+ * }
254
+ * ```
255
+ */
256
+ type PluginMiddleware<TData = unknown, TError = unknown> = (context: PluginContext<TData, TError>, next: () => Promise<SpooshResponse<TData, TError>>) => Promise<SpooshResponse<TData, TError>>;
257
+ type PluginHandler<TData = unknown, TError = unknown> = (context: PluginContext<TData, TError>) => void | Promise<void>;
258
+ type PluginUpdateHandler<TData = unknown, TError = unknown> = (context: PluginContext<TData, TError>, previousContext: PluginContext<TData, TError>) => void | Promise<void>;
259
+ /**
260
+ * Handler called after every response, regardless of early returns from middleware.
261
+ * Use this for post-response logic like scheduling polls or emitting events.
262
+ */
263
+ type PluginResponseHandler<TData = unknown, TError = unknown> = (context: PluginContext<TData, TError>, response: SpooshResponse<TData, TError>) => void | Promise<void>;
264
+ type PluginLifecycle<TData = unknown, TError = unknown> = {
265
+ /** Called on component mount */
266
+ onMount?: PluginHandler<TData, TError>;
267
+ /** Called when options/query changes. Receives both new and previous context. */
268
+ onUpdate?: PluginUpdateHandler<TData, TError>;
269
+ /** Called on component unmount */
270
+ onUnmount?: PluginHandler<TData, TError>;
271
+ };
272
+ /**
273
+ * Configuration object for plugin type definitions.
274
+ * Use this to specify which options and results your plugin provides.
275
+ *
276
+ * @example
277
+ * ```ts
278
+ * // Plugin with read options only
279
+ * SpooshPlugin<{ readOptions: MyReadOptions }>
280
+ *
281
+ * // Plugin with read/write options and results
282
+ * SpooshPlugin<{
283
+ * readOptions: MyReadOptions;
284
+ * writeOptions: MyWriteOptions;
285
+ * readResult: MyReadResult;
286
+ * }>
287
+ *
288
+ * // Plugin with instance-level API
289
+ * SpooshPlugin<{
290
+ * instanceApi: { prefetch: (selector: Selector) => Promise<void> };
291
+ * }>
292
+ * ```
293
+ */
294
+ type PluginTypeConfig = {
295
+ readOptions?: object;
296
+ writeOptions?: object;
297
+ infiniteReadOptions?: object;
298
+ readResult?: object;
299
+ writeResult?: object;
300
+ instanceApi?: object;
301
+ };
302
+ /**
303
+ * Base interface for Spoosh plugins.
304
+ *
305
+ * Plugins can implement:
306
+ * - `middleware`: Wraps the fetch flow for full control (intercept, retry, transform)
307
+ * - `onResponse`: Called after every response, regardless of early returns
308
+ * - `lifecycle`: Component lifecycle hooks (onMount, onUpdate, onUnmount)
309
+ * - `exports`: Functions/variables accessible to other plugins
310
+ *
311
+ * @typeParam T - Plugin type configuration object. Specify only the types your plugin needs.
312
+ *
313
+ * @example
314
+ * ```ts
315
+ * function myPlugin(): SpooshPlugin<{
316
+ * readOptions: { cacheTime: number };
317
+ * readResult: { isFromCache: boolean };
318
+ * }> {
319
+ * return {
320
+ * name: "my-plugin",
321
+ * operations: ["read"],
322
+ * middleware: async (context, next) => {
323
+ * // Full control over fetch flow
324
+ * const result = await next();
325
+ * return result;
326
+ * },
327
+ * onResponse(context, response) {
328
+ * // Always runs after response
329
+ * },
330
+ * lifecycle: {
331
+ * onMount(context) { },
332
+ * onUpdate(context, previousContext) { },
333
+ * onUnmount(context) { },
334
+ * },
335
+ * };
336
+ * }
337
+ * ```
338
+ */
339
+ interface SpooshPlugin<T extends PluginTypeConfig = PluginTypeConfig> {
340
+ name: string;
341
+ operations: OperationType[];
342
+ /** Middleware for controlling the fetch flow. Called in plugin order, composing a chain. */
343
+ middleware?: PluginMiddleware;
344
+ /** Called after every response, regardless of early returns from middleware. */
345
+ onResponse?: PluginResponseHandler;
346
+ /** Component lifecycle hooks (setup, cleanup, option changes) */
347
+ lifecycle?: PluginLifecycle;
348
+ /** Expose functions/variables for other plugins to access via `context.plugins.get(name)` */
349
+ exports?: (context: PluginContext) => object;
350
+ /**
351
+ * Expose functions/properties on the framework adapter return value (e.g., createReactSpoosh).
352
+ * Unlike `exports`, these are accessible directly from the instance, not just within plugin context.
353
+ *
354
+ * @example
355
+ * ```ts
356
+ * instanceApi: ({ api, stateManager }) => ({
357
+ * prefetch: async (selector) => { ... },
358
+ * invalidateAll: () => { ... },
359
+ * })
360
+ * ```
361
+ */
362
+ instanceApi?: (context: InstanceApiContext) => T extends {
363
+ instanceApi: infer A;
364
+ } ? A : object;
365
+ /** Declare plugin dependencies. These plugins must be registered before this one. */
366
+ dependencies?: string[];
367
+ /** @internal Type carrier for inference - do not use directly */
368
+ readonly _types?: T;
369
+ }
370
+ /**
371
+ * Helper type for creating plugin factory functions.
372
+ *
373
+ * @typeParam TConfig - Configuration object type (use `void` for no config)
374
+ * @typeParam TTypes - Plugin type configuration object
375
+ *
376
+ * @example
377
+ * ```ts
378
+ * // Factory with no config
379
+ * const myPlugin: PluginFactory<void, { readOptions: MyOpts }> = () => ({ ... });
380
+ *
381
+ * // Factory with config
382
+ * const myPlugin: PluginFactory<MyConfig, { readOptions: MyOpts }> = (config) => ({ ... });
383
+ * ```
384
+ */
385
+ type PluginFactory<TConfig = void, TTypes extends PluginTypeConfig = PluginTypeConfig> = TConfig extends void ? () => SpooshPlugin<TTypes> : (config?: TConfig) => SpooshPlugin<TTypes>;
386
+ /**
387
+ * Marker type for callbacks that need TData/TError from useRead/useWrite.
388
+ * Third-party plugins should use this for data-aware callback options.
389
+ *
390
+ * @example
391
+ * ```ts
392
+ * interface MyPluginReadOptions {
393
+ * onDataChange?: DataAwareCallback<boolean>; // (data, error) => boolean
394
+ * transform?: DataAwareTransform; // (data, error) => data
395
+ * }
396
+ * ```
397
+ */
398
+ type DataAwareCallback<TReturn = void, TData = unknown, TError = unknown> = (data: TData | undefined, error: TError | undefined) => TReturn;
399
+ /**
400
+ * Marker type for transform functions that receive and return TData.
401
+ */
402
+ type DataAwareTransform<TData = unknown, TError = unknown> = (data: TData | undefined, error: TError | undefined) => TData | undefined;
403
+ /**
404
+ * Context object containing all type information available for resolution.
405
+ * 3rd party plugins can access any combination of these types.
406
+ */
407
+ type ResolverContext<TSchema = unknown, TData = unknown, TError = unknown, TQuery = unknown, TBody = unknown, TParams = unknown, TFormData = unknown, TUrlEncoded = unknown> = {
408
+ schema: TSchema;
409
+ data: TData;
410
+ error: TError;
411
+ input: {
412
+ query: TQuery;
413
+ body: TBody;
414
+ params: TParams;
415
+ formData: TFormData;
416
+ urlEncoded: TUrlEncoded;
417
+ };
418
+ };
419
+ /**
420
+ * Unified resolver registry for plugin type resolution.
421
+ *
422
+ * 3rd party plugins can extend this interface via TypeScript declaration
423
+ * merging to register their own type-aware options:
424
+ *
425
+ * @example
426
+ * ```ts
427
+ * // In your plugin's types file:
428
+ * declare module '@spoosh/core' {
429
+ * interface PluginResolvers<TContext> {
430
+ * // Access schema
431
+ * mySchemaCallback: MyFn<TContext['schema']> | undefined;
432
+ *
433
+ * // Access data/error
434
+ * myDataTransform: (data: TContext['data']) => TContext['data'];
435
+ *
436
+ * // Access request input
437
+ * myDebounce: (prev: { prevQuery: TContext['input']['query'] }) => number;
438
+ *
439
+ * // Access multiple contexts at once
440
+ * myComplexOption: ComplexFn<TContext['schema'], TContext['data']>;
441
+ * }
442
+ * }
443
+ * ```
444
+ */
445
+ interface PluginResolvers<TContext extends ResolverContext> {
446
+ }
447
+ /**
448
+ * Registry for plugin result type resolution based on options.
449
+ * Extend via declaration merging to provide type inference for hook results.
450
+ *
451
+ * Unlike PluginResolvers which receives the full context, this receives
452
+ * the OPTIONS type so plugins can infer result types from what the user passes.
453
+ *
454
+ * @example
455
+ * ```ts
456
+ * // In your plugin's types file:
457
+ * declare module '@spoosh/core' {
458
+ * interface PluginResultResolvers<TOptions> {
459
+ * transformedData: TOptions extends { transform: { response: (...args: never[]) => infer R } }
460
+ * ? Awaited<R> | undefined
461
+ * : never;
462
+ * }
463
+ * }
464
+ * ```
465
+ */
466
+ interface PluginResultResolvers<TOptions> {
467
+ }
468
+ /**
469
+ * Registry for plugin exports. Extend via declaration merging for type-safe access.
470
+ *
471
+ * Plugins can expose functions and variables that other plugins can access
472
+ * via `context.plugins.get("plugin-name")`.
473
+ *
474
+ * @example
475
+ * ```ts
476
+ * // In your plugin's types file:
477
+ * declare module '@spoosh/core' {
478
+ * interface PluginExportsRegistry {
479
+ * "my-plugin": { myMethod: () => void }
480
+ * }
481
+ * }
482
+ * ```
483
+ */
484
+ interface PluginExportsRegistry {
485
+ }
486
+ /**
487
+ * Registry for instance API type resolution. Extend via declaration merging.
488
+ *
489
+ * Plugins that expose schema-aware instance APIs should extend this interface
490
+ * to get proper type inference when the API is used.
491
+ *
492
+ * @example
493
+ * ```ts
494
+ * // In your plugin's types file:
495
+ * declare module '@spoosh/core' {
496
+ * interface InstanceApiResolvers<TSchema> {
497
+ * myFunction: MyFn<TSchema>;
498
+ * }
499
+ * }
500
+ * ```
501
+ */
502
+ interface InstanceApiResolvers<TSchema> {
503
+ }
504
+ /**
505
+ * Accessor for plugin exports with type-safe lookup.
506
+ */
507
+ type PluginAccessor = {
508
+ /** Get a plugin's exported API by name. Returns undefined if plugin not found. */
509
+ get<K extends keyof PluginExportsRegistry>(name: K): PluginExportsRegistry[K] | undefined;
510
+ get(name: string): unknown;
511
+ };
512
+ /**
513
+ * Event emitted by plugins to request a refetch.
514
+ * Hooks subscribe to this event and trigger controller.execute().
515
+ */
516
+ type RefetchEvent = {
517
+ queryKey: string;
518
+ reason: "focus" | "reconnect" | "polling" | "invalidate";
519
+ };
520
+ /**
521
+ * Minimal PluginExecutor interface for InstanceApiContext.
522
+ * Avoids circular dependency with executor.ts.
523
+ */
524
+ type InstancePluginExecutor = {
525
+ executeMiddleware: <TData, TError>(operationType: OperationType, context: PluginContext<TData, TError>, coreFetch: () => Promise<SpooshResponse<TData, TError>>) => Promise<SpooshResponse<TData, TError>>;
526
+ createContext: <TData, TError>(input: PluginContextInput<TData, TError>) => PluginContext<TData, TError>;
527
+ };
528
+ /**
529
+ * Context provided to plugin's instanceApi function.
530
+ * Used for creating framework-agnostic APIs exposed on the Spoosh instance.
531
+ */
532
+ type InstanceApiContext<TApi = unknown> = {
533
+ api: TApi;
534
+ stateManager: StateManager;
535
+ eventEmitter: EventEmitter;
536
+ pluginExecutor: InstancePluginExecutor;
537
+ };
538
+
539
+ type Subscriber = () => void;
540
+ type CacheEntryWithKey<TData = unknown, TError = unknown> = {
541
+ key: string;
542
+ entry: CacheEntry<TData, TError>;
543
+ };
544
+ declare function createInitialState<TData, TError>(): OperationState<TData, TError>;
545
+ type StateManager = {
546
+ createQueryKey: (params: {
547
+ path: string[];
548
+ method: string;
549
+ options?: unknown;
550
+ }) => string;
551
+ getCache: <TData, TError>(key: string) => CacheEntry<TData, TError> | undefined;
552
+ setCache: <TData, TError>(key: string, entry: Partial<CacheEntry<TData, TError>>) => void;
553
+ deleteCache: (key: string) => void;
554
+ subscribeCache: (key: string, callback: Subscriber) => () => void;
555
+ getCacheByTags: <TData>(tags: string[]) => CacheEntry<TData> | undefined;
556
+ getCacheEntriesByTags: <TData, TError>(tags: string[]) => CacheEntryWithKey<TData, TError>[];
557
+ getCacheEntriesBySelfTag: <TData, TError>(selfTag: string) => CacheEntryWithKey<TData, TError>[];
558
+ setPluginResult: (key: string, data: Record<string, unknown>) => void;
559
+ /** Mark all cache entries with matching tags as stale */
560
+ markStale: (tags: string[]) => void;
561
+ /** Get all cache entries */
562
+ getAllCacheEntries: <TData, TError>() => CacheEntryWithKey<TData, TError>[];
563
+ /** Get the number of cache entries */
564
+ getSize: () => number;
565
+ /** Set a pending promise for a query key (for deduplication) */
566
+ setPendingPromise: (key: string, promise: Promise<unknown> | undefined) => void;
567
+ /** Get a pending promise for a query key */
568
+ getPendingPromise: (key: string) => Promise<unknown> | undefined;
569
+ clear: () => void;
570
+ };
571
+ declare function createStateManager(): StateManager;
572
+
573
+ type PluginExecutor = {
574
+ /** Execute lifecycle hooks for onMount or onUnmount */
575
+ executeLifecycle: <TData, TError>(phase: "onMount" | "onUnmount", operationType: OperationType, context: PluginContext<TData, TError>) => Promise<void>;
576
+ /** Execute onUpdate lifecycle with previous context */
577
+ executeUpdateLifecycle: <TData, TError>(operationType: OperationType, context: PluginContext<TData, TError>, previousContext: PluginContext<TData, TError>) => Promise<void>;
578
+ /** Execute middleware chain with a core fetch function, then run onResponse handlers */
579
+ executeMiddleware: <TData, TError>(operationType: OperationType, context: PluginContext<TData, TError>, coreFetch: () => Promise<SpooshResponse<TData, TError>>) => Promise<SpooshResponse<TData, TError>>;
580
+ getPlugins: () => readonly SpooshPlugin[];
581
+ /** Creates a full PluginContext with plugins accessor injected */
582
+ createContext: <TData, TError>(input: PluginContextInput<TData, TError>) => PluginContext<TData, TError>;
583
+ };
584
+ declare function createPluginExecutor(initialPlugins?: SpooshPlugin[]): PluginExecutor;
585
+
586
+ declare const EndpointBrand: unique symbol;
587
+ /**
588
+ * Define an API endpoint with its data, request options, and error types.
589
+ *
590
+ * @example
591
+ * ```typescript
592
+ * // Simple GET endpoint
593
+ * $get: Endpoint<{ data: User[] }>
594
+ *
595
+ * // GET with query parameters
596
+ * $get: Endpoint<{ data: User[]; query: { page: number; limit: number } }>
597
+ *
598
+ * // POST with JSON body
599
+ * $post: Endpoint<{ data: User; body: CreateUserBody }>
600
+ *
601
+ * // POST with form data (file upload)
602
+ * $post: Endpoint<{ data: UploadResult; formData: { file: File; name: string } }>
603
+ *
604
+ * // POST with URL-encoded body (Stripe-style)
605
+ * $post: Endpoint<{ data: Payment; urlEncoded: { amount: number; currency: string } }>
606
+ *
607
+ * // With error type
608
+ * $get: Endpoint<{ data: User; error: ApiError }>
609
+ *
610
+ * // Complex: query + body + error
611
+ * $post: Endpoint<{ data: User; body: CreateUserBody; query: { notify?: boolean }; error: ApiError }>
612
+ * ```
613
+ */
614
+ type Endpoint<T extends {
615
+ data: unknown;
616
+ body?: unknown;
617
+ query?: unknown;
618
+ formData?: unknown;
619
+ urlEncoded?: unknown;
620
+ error?: unknown;
621
+ }> = {
622
+ [EndpointBrand]: true;
623
+ } & T;
624
+ type NormalizeEndpoint<T, TDefaultError> = T extends {
625
+ [EndpointBrand]: true;
626
+ } ? {
627
+ data: T extends {
628
+ data: infer D;
629
+ } ? D : never;
630
+ error: T extends {
631
+ error: infer E;
632
+ } ? E : TDefaultError;
633
+ body: T extends {
634
+ body: infer B;
635
+ } ? B : never;
636
+ query: T extends {
637
+ query: infer Q;
638
+ } ? Q : never;
639
+ formData: T extends {
640
+ formData: infer F;
641
+ } ? F : never;
642
+ urlEncoded: T extends {
643
+ urlEncoded: infer U;
644
+ } ? U : never;
645
+ } : {
646
+ data: T;
647
+ error: TDefaultError;
648
+ body: never;
649
+ query: never;
650
+ formData: never;
651
+ urlEncoded: never;
652
+ };
653
+ type ExtractMethodDef<TSchema, TMethod extends SchemaMethod, TDefaultError = unknown> = TSchema extends {
654
+ [K in TMethod]: infer M;
655
+ } ? NormalizeEndpoint<M, TDefaultError> : never;
656
+ type ExtractData<TSchema, TMethod extends SchemaMethod, TDefaultError = unknown> = ExtractMethodDef<TSchema, TMethod, TDefaultError> extends {
657
+ data: infer D;
658
+ } ? D : never;
659
+ type ExtractError<TSchema, TMethod extends SchemaMethod, TDefaultError = unknown> = ExtractMethodDef<TSchema, TMethod, TDefaultError> extends {
660
+ error: infer E;
661
+ } ? E : TDefaultError;
662
+ type ExtractBody<TSchema, TMethod extends SchemaMethod, TDefaultError = unknown> = ExtractMethodDef<TSchema, TMethod, TDefaultError> extends {
663
+ body: infer B;
664
+ } ? B : never;
665
+ type ExtractQuery<TSchema, TMethod extends SchemaMethod, TDefaultError = unknown> = ExtractMethodDef<TSchema, TMethod, TDefaultError> extends {
666
+ query: infer Q;
667
+ } ? Q : never;
668
+ type ExtractFormData<TSchema, TMethod extends SchemaMethod, TDefaultError = unknown> = ExtractMethodDef<TSchema, TMethod, TDefaultError> extends {
669
+ formData: infer F;
670
+ } ? F : never;
671
+ type ExtractUrlEncoded<TSchema, TMethod extends SchemaMethod, TDefaultError = unknown> = ExtractMethodDef<TSchema, TMethod, TDefaultError> extends {
672
+ urlEncoded: infer U;
673
+ } ? U : never;
674
+ type HasMethod<TSchema, TMethod extends SchemaMethod> = TSchema extends {
675
+ [K in TMethod]: unknown;
676
+ } ? true : false;
677
+ type HasRequiredOptions<TSchema, TMethod extends SchemaMethod, TDefaultError = unknown> = [ExtractBody<TSchema, TMethod, TDefaultError>] extends [never] ? [ExtractFormData<TSchema, TMethod, TDefaultError>] extends [never] ? [ExtractUrlEncoded<TSchema, TMethod, TDefaultError>] extends [never] ? false : true : true : true;
678
+
679
+ type ExtractParamName$1<S extends string> = S extends `:${infer P}` ? P : never;
680
+ type MethodRequestOptions<TSchema, TMethod extends SchemaMethod, TDefaultError, TOptionsMap, TParamNames extends string, TRequired extends boolean> = TRequired extends true ? RequestOptions<ExtractBody<TSchema, TMethod, TDefaultError>, ExtractQuery<TSchema, TMethod, TDefaultError>, ExtractFormData<TSchema, TMethod, TDefaultError>, ExtractUrlEncoded<TSchema, TMethod, TDefaultError>> & ComputeRequestOptions<ExtractMethodOptions<TOptionsMap, TMethod>, TParamNames> : RequestOptions<never, ExtractQuery<TSchema, TMethod, TDefaultError>, never, never> & ComputeRequestOptions<ExtractMethodOptions<TOptionsMap, TMethod>, TParamNames>;
681
+ type MethodFn<TSchema, TMethod extends SchemaMethod, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never> = HasMethod<TSchema, TMethod> extends true ? HasRequiredOptions<TSchema, TMethod, TDefaultError> extends true ? (options: MethodRequestOptions<TSchema, TMethod, TDefaultError, TOptionsMap, TParamNames, true>) => Promise<SpooshResponse<ExtractData<TSchema, TMethod, TDefaultError>, ExtractError<TSchema, TMethod, TDefaultError>, MethodRequestOptions<TSchema, TMethod, TDefaultError, TOptionsMap, TParamNames, true>, ExtractQuery<TSchema, TMethod, TDefaultError>, ExtractBody<TSchema, TMethod, TDefaultError>, ExtractFormData<TSchema, TMethod, TDefaultError>, ExtractUrlEncoded<TSchema, TMethod, TDefaultError>, TParamNames>> : (options?: MethodRequestOptions<TSchema, TMethod, TDefaultError, TOptionsMap, TParamNames, false>) => Promise<SpooshResponse<ExtractData<TSchema, TMethod, TDefaultError>, ExtractError<TSchema, TMethod, TDefaultError>, MethodRequestOptions<TSchema, TMethod, TDefaultError, TOptionsMap, TParamNames, false>, ExtractQuery<TSchema, TMethod, TDefaultError>, ExtractBody<TSchema, TMethod, TDefaultError>, ExtractFormData<TSchema, TMethod, TDefaultError>, ExtractUrlEncoded<TSchema, TMethod, TDefaultError>, TParamNames>> : never;
682
+ type IsSpecialKey<K> = K extends SchemaMethod | "_" ? true : false;
683
+ type StaticPathKeys<TSchema> = {
684
+ [K in keyof TSchema as IsSpecialKey<K> extends true ? never : K extends string ? K : never]: TSchema[K];
685
+ };
686
+ type ExtractDynamicSchema<TSchema> = TSchema extends {
687
+ _: infer D;
688
+ } ? D : never;
689
+ type HttpMethods<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never> = {
690
+ [K in SchemaMethod as K extends keyof TSchema ? K : never]: MethodFn<TSchema, K, TDefaultError, TOptionsMap, TParamNames>;
691
+ };
692
+ type DynamicAccess<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never, TRootSchema = TSchema> = ExtractDynamicSchema<TSchema> extends never ? object : {
693
+ [key: string]: SpooshClient<ExtractDynamicSchema<TSchema>, TDefaultError, TOptionsMap, TParamNames | string, TRootSchema>;
694
+ [key: number]: SpooshClient<ExtractDynamicSchema<TSchema>, TDefaultError, TOptionsMap, TParamNames | string, TRootSchema>;
695
+ /**
696
+ * Dynamic path segment with typed param name.
697
+ * Use `:paramName` format to get typed params in the response.
698
+ *
699
+ * @example
700
+ * ```ts
701
+ * // Typed params: { userId: string | number }
702
+ * const { data, params } = await api.users(':userId').$get({ params: { userId: 123 } })
703
+ * ```
704
+ */
705
+ <TKey extends string>(key: TKey): SpooshClient<ExtractDynamicSchema<TSchema>, TDefaultError, TOptionsMap, TParamNames | ExtractParamName$1<TKey>, TRootSchema>;
706
+ };
707
+ type DynamicKey<TSchema, TDefaultError, TOptionsMap, TParamNames extends string = never, TRootSchema = TSchema> = TSchema extends {
708
+ _: infer D;
709
+ } ? {
710
+ /**
711
+ * Dynamic path segment placeholder for routes like `/posts/:id`.
712
+ *
713
+ * @example
714
+ * ```ts
715
+ * // Direct client usage
716
+ * const { data } = await api.posts._.$get({ params: { id: 123 } })
717
+ * ```
718
+ */
719
+ _: SpooshClient<D, TDefaultError, TOptionsMap, TParamNames | string, TRootSchema>;
720
+ } : object;
721
+ type SpooshClient<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never, TRootSchema = TSchema> = HttpMethods<TSchema, TDefaultError, TOptionsMap, TParamNames> & DynamicAccess<TSchema, TDefaultError, TOptionsMap, TParamNames, TRootSchema> & DynamicKey<TSchema, TDefaultError, TOptionsMap, TParamNames, TRootSchema> & {
722
+ [K in keyof StaticPathKeys<TSchema> as K extends SchemaMethod ? never : K]: SpooshClient<TSchema[K], TDefaultError, TOptionsMap, TParamNames, TRootSchema>;
723
+ };
724
+
725
+ type PluginArray = readonly SpooshPlugin<PluginTypeConfig>[];
726
+ interface SpooshConfig<TPlugins extends PluginArray = PluginArray> {
727
+ baseUrl: string;
728
+ defaultOptions?: SpooshOptions;
729
+ plugins?: TPlugins;
730
+ }
731
+ type SpooshInstance<TSchema = unknown, TDefaultError = unknown, TPlugins extends PluginArray = PluginArray> = {
732
+ api: SpooshClient<TSchema, TDefaultError, CoreRequestOptionsBase>;
733
+ stateManager: StateManager;
734
+ eventEmitter: EventEmitter;
735
+ pluginExecutor: PluginExecutor;
736
+ config: {
737
+ baseUrl: string;
738
+ defaultOptions: SpooshOptions;
739
+ };
740
+ _types: {
741
+ schema: TSchema;
742
+ defaultError: TDefaultError;
743
+ plugins: TPlugins;
744
+ };
745
+ };
746
+
747
+ declare function createSpoosh<TSchema = unknown, TDefaultError = unknown, const TPlugins extends PluginArray = PluginArray>(config: SpooshConfig<TPlugins>): SpooshInstance<TSchema, TDefaultError, TPlugins>;
748
+
749
+ type SpooshClientConfig = {
750
+ baseUrl: string;
751
+ defaultOptions?: SpooshOptions;
752
+ middlewares?: SpooshMiddleware[];
753
+ };
754
+ /**
755
+ * Creates a lightweight type-safe API client for vanilla JavaScript/TypeScript usage.
756
+ *
757
+ * This is a simpler alternative to `createSpoosh` for users who don't need
758
+ * the full plugin system, state management, or React integration.
759
+ *
760
+ * @param config - Client configuration
761
+ * @returns Type-safe API client
762
+ *
763
+ * @example
764
+ * ```ts
765
+ * type ApiSchema = {
766
+ * posts: {
767
+ * $get: Endpoint<Post[]>;
768
+ * $post: Endpoint<Post, CreatePostBody>;
769
+ * _: {
770
+ * $get: Endpoint<Post>;
771
+ * $delete: Endpoint<void>;
772
+ * };
773
+ * };
774
+ * };
775
+ *
776
+ * type ApiError = {
777
+ * message: string;
778
+ * }
779
+ *
780
+ * const api = createClient<ApiSchema, ApiError>({
781
+ * baseUrl: "/api",
782
+ * });
783
+ *
784
+ * // Type-safe API calls
785
+ * const { data } = await api.posts.$get();
786
+ * const { data: post } = await api.posts[123].$get();
787
+ * ```
788
+ */
789
+ declare function createClient<TSchema, TDefaultError = unknown>(config: SpooshClientConfig): SpooshClient<TSchema, TDefaultError>;
790
+
791
+ type QueryMethod = "$get";
792
+ type MutationMethod = "$post" | "$put" | "$patch" | "$delete";
793
+ type HasQueryMethods<TSchema> = TSchema extends object ? "$get" extends keyof TSchema ? true : TSchema extends {
794
+ _: infer D;
795
+ } ? HasQueryMethods<D> : {
796
+ [K in keyof TSchema]: K extends SchemaMethod | "_" ? never : HasQueryMethods<TSchema[K]>;
797
+ }[keyof TSchema] extends never ? false : true extends {
798
+ [K in keyof TSchema]: K extends SchemaMethod | "_" ? never : HasQueryMethods<TSchema[K]>;
799
+ }[keyof TSchema] ? true : false : false;
800
+ type HasMutationMethods<TSchema> = TSchema extends object ? MutationMethod extends never ? false : Extract<keyof TSchema, MutationMethod> extends never ? TSchema extends {
801
+ _: infer D;
802
+ } ? HasMutationMethods<D> : {
803
+ [K in keyof TSchema]: K extends SchemaMethod | "_" ? never : HasMutationMethods<TSchema[K]>;
804
+ }[keyof TSchema] extends never ? false : true extends {
805
+ [K in keyof TSchema]: K extends SchemaMethod | "_" ? never : HasMutationMethods<TSchema[K]>;
806
+ }[keyof TSchema] ? true : false : true : false;
807
+ type QueryHttpMethods<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never> = {
808
+ [K in QueryMethod as K extends keyof TSchema ? K : never]: MethodFn<TSchema, K, TDefaultError, TOptionsMap, TParamNames>;
809
+ };
810
+ type ExtractParamName<S extends string> = S extends `:${infer P}` ? P : never;
811
+ type QueryDynamicAccess<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never, TRootSchema = TSchema> = TSchema extends {
812
+ _: infer D;
813
+ } ? HasQueryMethods<D> extends true ? {
814
+ [key: string]: QueryOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | string, TRootSchema>;
815
+ [key: number]: QueryOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | string, TRootSchema>;
816
+ <TKey extends string>(key: TKey): QueryOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | ExtractParamName<TKey>, TRootSchema>;
817
+ } : object : object;
818
+ type QueryDynamicKey<TSchema, TDefaultError, TOptionsMap, TParamNames extends string = never, TRootSchema = TSchema> = TSchema extends {
819
+ _: infer D;
820
+ } ? HasQueryMethods<D> extends true ? {
821
+ /**
822
+ * Dynamic path segment placeholder for routes like `/posts/:id`.
823
+ *
824
+ * @example
825
+ * ```ts
826
+ * useRead((api) => api.posts[123].$get())
827
+ * useRead((api) => api.posts(':id').$get({ params: { id: 123 } }))
828
+ * ```
829
+ */
830
+ _: QueryOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | string, TRootSchema>;
831
+ } : object : object;
832
+ type QueryOnlyClient<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never, TRootSchema = TSchema> = QueryHttpMethods<TSchema, TDefaultError, TOptionsMap, TParamNames> & QueryDynamicAccess<TSchema, TDefaultError, TOptionsMap, TParamNames, TRootSchema> & QueryDynamicKey<TSchema, TDefaultError, TOptionsMap, TParamNames, TRootSchema> & {
833
+ [K in keyof StaticPathKeys<TSchema> as K extends SchemaMethod ? never : HasQueryMethods<TSchema[K]> extends true ? K : never]: QueryOnlyClient<TSchema[K], TDefaultError, TOptionsMap, TParamNames, TRootSchema>;
834
+ };
835
+ type MutationHttpMethods<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never> = {
836
+ [K in MutationMethod as K extends keyof TSchema ? K : never]: MethodFn<TSchema, K, TDefaultError, TOptionsMap, TParamNames>;
837
+ };
838
+ type MutationDynamicAccess<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never> = TSchema extends {
839
+ _: infer D;
840
+ } ? HasMutationMethods<D> extends true ? {
841
+ [key: string]: MutationOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | string>;
842
+ [key: number]: MutationOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | string>;
843
+ <TKey extends string>(key: TKey): MutationOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | ExtractParamName<TKey>>;
844
+ } : object : object;
845
+ type MutationDynamicKey<TSchema, TDefaultError, TOptionsMap, TParamNames extends string = never> = TSchema extends {
846
+ _: infer D;
847
+ } ? HasMutationMethods<D> extends true ? {
848
+ /**
849
+ * Dynamic path segment placeholder for routes like `/posts/:id`.
850
+ *
851
+ * @example
852
+ * ```ts
853
+ * const { trigger } = useWrite((api) => api.posts(':id').$delete)
854
+ * trigger({ params: { id: 123 } })
855
+ * ```
856
+ */
857
+ _: MutationOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | string>;
858
+ } : object : object;
859
+ type MutationOnlyClient<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never> = MutationHttpMethods<TSchema, TDefaultError, TOptionsMap, TParamNames> & MutationDynamicAccess<TSchema, TDefaultError, TOptionsMap, TParamNames> & MutationDynamicKey<TSchema, TDefaultError, TOptionsMap, TParamNames> & {
860
+ [K in keyof StaticPathKeys<TSchema> as K extends SchemaMethod ? never : HasMutationMethods<TSchema[K]> extends true ? K : never]: MutationOnlyClient<TSchema[K], TDefaultError, TOptionsMap, TParamNames>;
861
+ };
862
+
863
+ declare function buildUrl(baseUrl: string, path: string[], query?: Record<string, string | number | boolean | undefined>): string;
864
+
865
+ /**
866
+ * Generate cache tags from URL path segments.
867
+ * e.g., ['posts', '1'] → ['posts', 'posts/1']
868
+ */
869
+ declare function generateTags(path: string[]): string[];
870
+
871
+ declare function isJsonBody(body: unknown): body is Record<string, unknown> | unknown[];
872
+
873
+ /**
874
+ * Resolves HeadersInitOrGetter to a plain Record<string, string>.
875
+ * Handles functions, Headers objects, arrays, and plain objects.
876
+ */
877
+ declare function resolveHeadersToRecord(headers?: HeadersInitOrGetter): Promise<Record<string, string>>;
878
+ declare function mergeHeaders(defaultHeaders?: HeadersInitOrGetter, requestHeaders?: HeadersInitOrGetter): Promise<HeadersInit | undefined>;
879
+ declare function setHeaders(requestOptions: {
880
+ headers?: HeadersInitOrGetter;
881
+ }, newHeaders: Record<string, string>): void;
882
+
883
+ declare function objectToFormData(obj: Record<string, unknown>): FormData;
884
+
885
+ declare function objectToUrlEncoded(obj: Record<string, unknown>): string;
886
+
887
+ declare function sortObjectKeys(obj: unknown, seen?: WeakSet<object>): unknown;
888
+
889
+ /**
890
+ * Common tag options used across plugins and operations.
891
+ */
892
+ type TagOptions = {
893
+ /** Custom tags to use instead of auto-generated path-based tags */
894
+ tags?: string[];
895
+ /** Additional tags to append to auto-generated or custom tags */
896
+ additionalTags?: string[];
897
+ };
898
+ declare function resolveTags(options: TagOptions | undefined, resolvedPath: string[]): string[];
899
+ declare function resolvePath(path: string[], params: Record<string, string | number> | undefined): string[];
900
+
901
+ type ProxyHandlerConfig<TOptions = SpooshOptions> = {
902
+ baseUrl: string;
903
+ defaultOptions: TOptions;
904
+ path?: string[];
905
+ fetchExecutor?: FetchExecutor<TOptions, AnyRequestOptions>;
906
+ nextTags?: boolean;
907
+ };
908
+ /**
909
+ * Creates the real API client proxy that executes actual HTTP requests.
910
+ *
911
+ * This proxy intercepts property access and function calls to build URL paths,
912
+ * then executes fetch requests when an HTTP method ($get, $post, etc.) is called.
913
+ *
914
+ * Used internally by `createClient` and `createSpoosh` to create typed API clients.
915
+ *
916
+ * @param config - Proxy handler configuration
917
+ *
918
+ * @returns A proxy object typed as TSchema that executes real HTTP requests
919
+ *
920
+ * @example
921
+ * ```ts
922
+ * const api = createProxyHandler<ApiSchema>({ baseUrl: '/api', defaultOptions: {} });
923
+ *
924
+ * // Accessing api.posts.$get() will:
925
+ * // 1. Build path: ['posts']
926
+ * // 2. Execute: GET /api/posts
927
+ * await api.posts.$get();
928
+ *
929
+ * // Dynamic segments via function call:
930
+ * // api.posts[123].$get() or api.posts('123').$get()
931
+ * // Executes: GET /api/posts/123
932
+ * await api.posts[123].$get();
933
+ * ```
934
+ */
935
+ declare function createProxyHandler<TSchema extends object, TOptions = SpooshOptions>(config: ProxyHandlerConfig<TOptions>): TSchema;
936
+
937
+ /** All supported HTTP method keys used in the API client */
938
+ declare const HTTP_METHODS: readonly ["$get", "$post", "$put", "$patch", "$delete"];
939
+ /** Union type of all HTTP method keys */
940
+ type HttpMethodKey = (typeof HTTP_METHODS)[number];
941
+ /**
942
+ * A function returned by `createSelectorProxy` that stores the selected path and method.
943
+ *
944
+ * Does not execute any request - only captures the API endpoint selection.
945
+ */
946
+ type SelectorFunction = (() => Promise<{
947
+ data: undefined;
948
+ }>) & {
949
+ /** The path segments leading to this endpoint (e.g., ['posts', 'comments']) */
950
+ __selectorPath?: string[];
951
+ /** The HTTP method selected (e.g., '$get', '$post') */
952
+ __selectorMethod?: string;
953
+ };
954
+ /**
955
+ * Represents a fully captured API call with path, method, and options.
956
+ *
957
+ * Captured when an HTTP method is invoked with options.
958
+ */
959
+ type CapturedCall = {
960
+ /** Path segments to the endpoint (e.g., ['posts', ':id']) */
961
+ path: string[];
962
+ /** HTTP method called (e.g., '$get', '$post') */
963
+ method: string;
964
+ /** Request options passed to the method (query, body, params, etc.) */
965
+ options: unknown;
966
+ };
967
+ /**
968
+ * Represents the selected endpoint (path and method) without options.
969
+ *
970
+ * Captured when an HTTP method is accessed but not yet called.
971
+ */
972
+ type SelectedEndpoint = {
973
+ /** Path segments to the endpoint */
974
+ path: string[];
975
+ /** HTTP method selected */
976
+ method: string;
977
+ };
978
+ /**
979
+ * Result from the selector proxy callback.
980
+ *
981
+ * Contains either:
982
+ * - `call`: Full call details when method is invoked with options (for useRead)
983
+ * - `selector`: Just path/method when method is accessed (for useWrite)
984
+ */
985
+ type SelectorResult = {
986
+ /** Full call details when method is invoked with options */
987
+ call: CapturedCall | null;
988
+ /** Endpoint selection when method is accessed but not called */
989
+ selector: SelectedEndpoint | null;
990
+ };
991
+ /**
992
+ * Creates a proxy for selecting API endpoints without executing requests.
993
+ *
994
+ * Used by plugins to let users specify which cache entries to target
995
+ * using a type-safe API selector syntax.
996
+ *
997
+ * @returns A proxy typed as TSchema for endpoint selection
998
+ *
999
+ * @example
1000
+ * ```ts
1001
+ * const proxy = createSelectorProxy<ApiSchema>();
1002
+ *
1003
+ * // Select an endpoint
1004
+ * const endpoint = proxy.posts.$get;
1005
+ *
1006
+ * // Extract path for cache operations
1007
+ * const path = extractPathFromSelector(endpoint); // ['posts']
1008
+ * const method = extractMethodFromSelector(endpoint); // '$get'
1009
+ * ```
1010
+ *
1011
+ * @internal onCapture - Used internally by framework adapters
1012
+ */
1013
+ declare function createSelectorProxy<TSchema>(onCapture?: (result: SelectorResult) => void): TSchema;
1014
+ /**
1015
+ * Extracts the path segments from a SelectorFunction.
1016
+ *
1017
+ * @param fn - A SelectorFunction returned from `createSelectorProxy`
1018
+ * @returns Array of path segments (e.g., ['posts', 'comments'])
1019
+ *
1020
+ * @example
1021
+ * ```ts
1022
+ * const proxy = createSelectorProxy<ApiSchema>();
1023
+ * const path = extractPathFromSelector(proxy.posts.comments.$get);
1024
+ * // path = ['posts', 'comments']
1025
+ * ```
1026
+ */
1027
+ declare function extractPathFromSelector(fn: unknown): string[];
1028
+ /**
1029
+ * Extracts the HTTP method from a SelectorFunction.
1030
+ *
1031
+ * @param fn - A SelectorFunction returned from `createSelectorProxy`
1032
+ * @returns The HTTP method string (e.g., '$get', '$post') or undefined
1033
+ *
1034
+ * @example
1035
+ * ```ts
1036
+ * const proxy = createSelectorProxy<ApiSchema>();
1037
+ * const method = extractMethodFromSelector(proxy.posts.$post);
1038
+ * // method = '$post'
1039
+ * ```
1040
+ */
1041
+ declare function extractMethodFromSelector(fn: unknown): string | undefined;
1042
+
1043
+ declare function executeFetch<TData, TError>(baseUrl: string, path: string[], method: HttpMethod, defaultOptions: SpooshOptions & SpooshOptionsExtra, requestOptions?: AnyRequestOptions, nextTags?: boolean): Promise<SpooshResponse<TData, TError>>;
1044
+
1045
+ declare function createMiddleware<TData = unknown, TError = unknown>(name: string, phase: MiddlewarePhase, handler: SpooshMiddleware<TData, TError>["handler"]): SpooshMiddleware<TData, TError>;
1046
+ declare function applyMiddlewares<TData = unknown, TError = unknown>(context: MiddlewareContext<TData, TError>, middlewares: SpooshMiddleware<TData, TError>[], phase: MiddlewarePhase): Promise<MiddlewareContext<TData, TError>>;
1047
+ declare function composeMiddlewares<TData = unknown, TError = unknown>(...middlewareLists: (SpooshMiddleware<TData, TError>[] | undefined)[]): SpooshMiddleware<TData, TError>[];
1048
+
1049
+ /**
1050
+ * Resolves plugin option types based on the full context.
1051
+ *
1052
+ * This is the single entry point for all type resolution. It receives
1053
+ * the full ResolverContext containing schema, data, error, and input types,
1054
+ * and resolves each option key accordingly.
1055
+ *
1056
+ * Plugins extend PluginResolvers via declaration merging to add their own
1057
+ * resolved option types.
1058
+ *
1059
+ * @example
1060
+ * ```ts
1061
+ * // In your plugin's types.ts:
1062
+ * declare module "@spoosh/core" {
1063
+ * interface PluginResolvers<TContext> {
1064
+ * myOption: MyResolvedType<TContext["data"]> | undefined;
1065
+ * }
1066
+ * }
1067
+ *
1068
+ * // Type resolution:
1069
+ * type ResolvedOptions = ResolveTypes<
1070
+ * MergePluginOptions<TPlugins>["read"],
1071
+ * {
1072
+ * schema: ApiSchema;
1073
+ * data: Post[];
1074
+ * error: Error;
1075
+ * input: { query: { page: number }; body: never; params: never; formData: never };
1076
+ * }
1077
+ * >;
1078
+ * ```
1079
+ */
1080
+ type ResolveTypes<TOptions, TContext extends ResolverContext> = {
1081
+ [K in keyof TOptions]: K extends keyof PluginResolvers<TContext> ? PluginResolvers<TContext>[K] : TOptions[K] extends DataAwareCallback<infer R, unknown, unknown> | undefined ? DataAwareCallback<R, TContext["data"], TContext["error"]> | undefined : TOptions[K] extends DataAwareTransform<unknown, unknown> | undefined ? DataAwareTransform<TContext["data"], TContext["error"]> | undefined : TOptions[K];
1082
+ };
1083
+ /**
1084
+ * Resolves schema-aware types in plugin options.
1085
+ * This is a simplified resolver for write operations that only need schema context.
1086
+ */
1087
+ type ResolveSchemaTypes<TOptions, TSchema> = ResolveTypes<TOptions, ResolverContext<TSchema>>;
1088
+ /**
1089
+ * Resolves plugin result types based on the options passed to the hook.
1090
+ *
1091
+ * This allows plugins to infer result types from the options. For example,
1092
+ * the transform plugin can infer `transformedData` type from the response
1093
+ * transformer's return type.
1094
+ *
1095
+ * @example
1096
+ * ```ts
1097
+ * // Usage in hooks:
1098
+ * type ResolvedResults = ResolveResultTypes<PluginResults["read"], TReadOpts>;
1099
+ * // If TReadOpts has { transform: { response: (d) => { count: number } } }
1100
+ * // Then transformedData will be { count: number } | undefined
1101
+ * ```
1102
+ */
1103
+ type ResolveResultTypes<TResults, TOptions> = TResults & PluginResultResolvers<TOptions>;
1104
+ /**
1105
+ * Resolves instance API types with schema awareness.
1106
+ * Maps each key in TInstanceApi to its resolved type from resolvers.
1107
+ *
1108
+ * Plugins extend InstanceApiResolvers via declaration merging to add their own
1109
+ * resolved instance API types.
1110
+ *
1111
+ * @example
1112
+ * ```ts
1113
+ * // In your plugin's types.ts:
1114
+ * declare module "@spoosh/core" {
1115
+ * interface InstanceApiResolvers<TSchema> {
1116
+ * prefetch: PrefetchFn<TSchema>;
1117
+ * }
1118
+ * }
1119
+ * ```
1120
+ */
1121
+ type ResolveInstanceApi<TInstanceApi, TSchema, TReadOptions = object> = {
1122
+ [K in keyof TInstanceApi]: K extends keyof InstanceApiResolvers<TSchema> ? InstanceApiResolvers<TSchema>[K] : TInstanceApi[K];
1123
+ };
1124
+
1125
+ type ExtractReadOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
1126
+ readOptions: infer R;
1127
+ } ? R : object : object;
1128
+ type ExtractWriteOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
1129
+ writeOptions: infer W;
1130
+ } ? W : object : object;
1131
+ type ExtractInfiniteReadOptions<T> = T extends SpooshPlugin<infer Types> ? Types extends {
1132
+ infiniteReadOptions: infer I;
1133
+ } ? I : object : object;
1134
+ type ExtractReadResult<T> = T extends SpooshPlugin<infer Types> ? Types extends {
1135
+ readResult: infer R;
1136
+ } ? R : object : object;
1137
+ type ExtractWriteResult<T> = T extends SpooshPlugin<infer Types> ? Types extends {
1138
+ writeResult: infer W;
1139
+ } ? W : object : object;
1140
+ type ExtractInstanceApi<T> = T extends SpooshPlugin<infer Types> ? Types extends {
1141
+ instanceApi: infer A;
1142
+ } ? A : object : object;
1143
+ type UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
1144
+ type MergePluginOptions<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = {
1145
+ read: UnionToIntersection<ExtractReadOptions<TPlugins[number]>>;
1146
+ write: UnionToIntersection<ExtractWriteOptions<TPlugins[number]>>;
1147
+ infiniteRead: UnionToIntersection<ExtractInfiniteReadOptions<TPlugins[number]>>;
1148
+ };
1149
+ type MergePluginResults<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[]> = {
1150
+ read: UnionToIntersection<ExtractReadResult<TPlugins[number]>>;
1151
+ write: UnionToIntersection<ExtractWriteResult<TPlugins[number]>>;
1152
+ };
1153
+ type MergePluginInstanceApi<TPlugins extends readonly SpooshPlugin<PluginTypeConfig>[], TSchema = unknown> = ResolveInstanceApi<UnionToIntersection<ExtractInstanceApi<TPlugins[number]>>, TSchema, MergePluginOptions<TPlugins>["read"]>;
1154
+ type PluginRegistry<TPlugins extends SpooshPlugin<PluginTypeConfig>[]> = {
1155
+ plugins: TPlugins;
1156
+ _options: MergePluginOptions<TPlugins>;
1157
+ };
1158
+ declare function createPluginRegistry<TPlugins extends SpooshPlugin<PluginTypeConfig>[]>(plugins: [...TPlugins]): PluginRegistry<TPlugins>;
1159
+
1160
+ type ExtractEndpointData<T> = T extends {
1161
+ data: infer D;
1162
+ } ? D : T extends void ? void : T;
1163
+ type ExtractEndpointRequestOptions<T> = {
1164
+ [K in Extract<keyof T, "query" | "body" | "params">]?: T[K];
1165
+ };
1166
+ type EndpointToMethod<T> = (options?: ExtractEndpointRequestOptions<T>) => Promise<SpooshResponse<ExtractEndpointData<T>, unknown, ExtractEndpointRequestOptions<T>>>;
1167
+ /**
1168
+ * Schema navigation helper for plugins that need type-safe API schema access.
1169
+ *
1170
+ * This type transforms the API schema into a navigable structure where:
1171
+ * - Static path segments become nested properties
1172
+ * - Dynamic segments (`_`) become index signatures
1173
+ * - `$get` endpoints become callable method types
1174
+ *
1175
+ * Use this in plugin option types that need to reference API endpoints:
1176
+ *
1177
+ * @example
1178
+ * ```ts
1179
+ * // Define your plugin's callback type
1180
+ * type MyCallbackFn<TSchema = unknown> = (
1181
+ * api: QuerySchemaHelper<TSchema>
1182
+ * ) => unknown;
1183
+ *
1184
+ * // Usage in plugin options
1185
+ * interface MyPluginWriteOptions {
1186
+ * myCallback?: MyCallbackFn<unknown>;
1187
+ * }
1188
+ *
1189
+ * // Register for schema resolution
1190
+ * declare module '@spoosh/core' {
1191
+ * interface SchemaResolvers<TSchema> {
1192
+ * myCallback: MyCallbackFn<TSchema> | undefined;
1193
+ * }
1194
+ * }
1195
+ * ```
1196
+ *
1197
+ * @example
1198
+ * ```ts
1199
+ * // User's code - paths are type-checked!
1200
+ * trigger({
1201
+ * myCallback: (api) => [
1202
+ * api.posts.$get, // ✓ Valid
1203
+ * api.users[1].$get, // ✓ Dynamic segment
1204
+ * api.nonexistent.$get, // ✗ Type error
1205
+ * ],
1206
+ * });
1207
+ * ```
1208
+ */
1209
+ type QuerySchemaHelper<TSchema> = {
1210
+ [K in keyof TSchema as K extends SchemaMethod | "_" ? never : HasQueryMethods<TSchema[K]> extends true ? K : never]: K extends keyof TSchema ? QuerySchemaHelper<TSchema[K]> : never;
1211
+ } & {
1212
+ [K in "$get" as K extends keyof TSchema ? K : never]: K extends keyof TSchema ? EndpointToMethod<TSchema[K]> : never;
1213
+ } & (TSchema extends {
1214
+ _: infer D;
1215
+ } ? HasQueryMethods<D> extends true ? {
1216
+ /**
1217
+ * Dynamic path segment placeholder for routes like `/posts/:id`.
1218
+ *
1219
+ * @example
1220
+ * ```ts
1221
+ * // In plugin callback - reference the endpoint
1222
+ * myCallback: (api) => api.posts._.$get
1223
+ * ```
1224
+ */
1225
+ _: QuerySchemaHelper<D>;
1226
+ [key: string]: QuerySchemaHelper<D>;
1227
+ [key: number]: QuerySchemaHelper<D>;
1228
+ } : object : object);
1229
+
1230
+ type ExecuteOptions = {
1231
+ force?: boolean;
1232
+ };
1233
+ type OperationController<TData = unknown, TError = unknown> = {
1234
+ execute: (options?: AnyRequestOptions, executeOptions?: ExecuteOptions) => Promise<SpooshResponse<TData, TError>>;
1235
+ getState: () => OperationState<TData, TError>;
1236
+ subscribe: (callback: () => void) => () => void;
1237
+ abort: () => void;
1238
+ refetch: () => Promise<SpooshResponse<TData, TError>>;
1239
+ /** Called once when hook first mounts */
1240
+ mount: () => void;
1241
+ /** Called once when hook finally unmounts */
1242
+ unmount: () => void;
1243
+ /** Called when options/query changes. Pass previous context for cleanup. */
1244
+ update: (previousContext: PluginContext<TData, TError>) => void;
1245
+ /** Get current context (for passing to update as previousContext) */
1246
+ getContext: () => PluginContext<TData, TError>;
1247
+ setPluginOptions: (options: unknown) => void;
1248
+ setMetadata: (key: string, value: unknown) => void;
1249
+ };
1250
+ type CreateOperationOptions<TData, TError> = {
1251
+ operationType: OperationType;
1252
+ path: string[];
1253
+ method: HttpMethod;
1254
+ tags: string[];
1255
+ requestOptions?: AnyRequestOptions;
1256
+ stateManager: StateManager;
1257
+ eventEmitter: EventEmitter;
1258
+ pluginExecutor: PluginExecutor;
1259
+ fetchFn: (options: AnyRequestOptions) => Promise<SpooshResponse<TData, TError>>;
1260
+ /** Unique identifier for the hook instance. Persists across queryKey changes. */
1261
+ hookId?: string;
1262
+ };
1263
+ declare function createOperationController<TData, TError>(options: CreateOperationOptions<TData, TError>): OperationController<TData, TError>;
1264
+
1265
+ type InfiniteRequestOptions = {
1266
+ query?: Record<string, unknown>;
1267
+ params?: Record<string, string | number>;
1268
+ body?: unknown;
1269
+ };
1270
+ type PageContext<TData, TRequest = InfiniteRequestOptions> = {
1271
+ response: TData | undefined;
1272
+ allResponses: TData[];
1273
+ request: TRequest;
1274
+ };
1275
+ type FetchDirection = "next" | "prev";
1276
+ type InfiniteReadState<TData, TItem, TError> = {
1277
+ data: TItem[] | undefined;
1278
+ allResponses: TData[] | undefined;
1279
+ allRequests: InfiniteRequestOptions[] | undefined;
1280
+ canFetchNext: boolean;
1281
+ canFetchPrev: boolean;
1282
+ error: TError | undefined;
1283
+ };
1284
+ type InfiniteReadController<TData, TItem, TError> = {
1285
+ getState: () => InfiniteReadState<TData, TItem, TError>;
1286
+ getFetchingDirection: () => FetchDirection | null;
1287
+ subscribe: (callback: () => void) => () => void;
1288
+ fetchNext: () => Promise<void>;
1289
+ fetchPrev: () => Promise<void>;
1290
+ refetch: () => Promise<void>;
1291
+ abort: () => void;
1292
+ mount: () => void;
1293
+ unmount: () => void;
1294
+ update: (previousContext: PluginContext<TData, TError>) => void;
1295
+ getContext: () => PluginContext<TData, TError>;
1296
+ setPluginOptions: (options: unknown) => void;
1297
+ };
1298
+ type CreateInfiniteReadOptions<TData, TItem, TError, TRequest> = {
1299
+ path: string[];
1300
+ method: HttpMethod;
1301
+ tags: string[];
1302
+ initialRequest: InfiniteRequestOptions;
1303
+ baseOptionsForKey: object;
1304
+ canFetchNext: (ctx: PageContext<TData, TRequest>) => boolean;
1305
+ canFetchPrev?: (ctx: PageContext<TData, TRequest>) => boolean;
1306
+ nextPageRequest: (ctx: PageContext<TData, TRequest>) => Partial<TRequest>;
1307
+ prevPageRequest?: (ctx: PageContext<TData, TRequest>) => Partial<TRequest>;
1308
+ merger: (responses: TData[]) => TItem[];
1309
+ stateManager: StateManager;
1310
+ eventEmitter: EventEmitter;
1311
+ pluginExecutor: PluginExecutor;
1312
+ fetchFn: (options: InfiniteRequestOptions, signal: AbortSignal) => Promise<SpooshResponse<TData, TError>>;
1313
+ /** Unique identifier for the hook instance. Persists across queryKey changes. */
1314
+ hookId?: string;
1315
+ };
1316
+ declare function createInfiniteReadController<TData, TItem, TError, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions>(options: CreateInfiniteReadOptions<TData, TItem, TError, TRequest>): InfiniteReadController<TData, TItem, TError>;
1317
+
1318
+ export { type AnyRequestOptions, type BuiltInEvents, type CacheEntry, type CacheEntryWithKey, type CapturedCall, type SpooshClientConfig as ClientConfig, type ComputeRequestOptions, type CoreRequestOptionsBase, type CreateInfiniteReadOptions, type CreateOperationOptions, type DataAwareCallback, type DataAwareTransform, type Endpoint, type EventEmitter, type ExtractBody, type ExtractData, type ExtractError, type ExtractFormData, type ExtractMethodDef, type ExtractMethodOptions, type ExtractQuery, type ExtractUrlEncoded, type FetchDirection, type FetchExecutor, HTTP_METHODS, type HasMethod, type HasQueryMethods, type HasRequiredOptions, type HeadersInitOrGetter, type HttpMethod, type HttpMethodKey, type InfiniteReadController, type InfiniteReadState, type InfiniteRequestOptions, type InstanceApiContext, type InstanceApiResolvers, type InstancePluginExecutor, type LifecyclePhase, type MergePluginInstanceApi, type MergePluginOptions, type MergePluginResults, type MethodFn, type MethodOptionsMap, type MiddlewareContext, type MiddlewareHandler, type MiddlewarePhase, type MutationOnlyClient, type NormalizeEndpoint, type OperationController, type OperationState, type OperationType, type PageContext, type PluginAccessor, type PluginArray, type PluginContext, type PluginContextInput, type PluginExecutor, type PluginExportsRegistry, type PluginFactory, type PluginHandler, type PluginLifecycle, type PluginMiddleware, type PluginRegistry, type PluginResolvers, type PluginResponseHandler, type PluginResultResolvers, type PluginTypeConfig, type PluginUpdateHandler, type QueryOnlyClient, type QuerySchemaHelper, type RefetchEvent, type RequestOptions, type ResolveInstanceApi, type ResolveResultTypes, type ResolveSchemaTypes, type ResolveTypes, type ResolverContext, type RetryConfig, type SchemaMethod, type SelectedEndpoint, type SelectorFunction, type SelectorResult, type SpooshClient, type SpooshConfig, type SpooshInstance, type SpooshMiddleware, type SpooshOptions, type SpooshOptionsExtra, type SpooshPlugin, type SpooshResponse, type StateManager, type StaticPathKeys, type TagOptions, applyMiddlewares, buildUrl, composeMiddlewares, createClient, createEventEmitter, createInfiniteReadController, createInitialState, createMiddleware, createOperationController, createPluginExecutor, createPluginRegistry, createProxyHandler, createSelectorProxy, createSpoosh, createStateManager, executeFetch, extractMethodFromSelector, extractPathFromSelector, generateTags, isJsonBody, mergeHeaders, objectToFormData, objectToUrlEncoded, resolveHeadersToRecord, resolvePath, resolveTags, setHeaders, sortObjectKeys };