@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.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/index.d.mts +1318 -0
- package/dist/index.d.ts +1318 -0
- package/dist/index.js +1441 -0
- package/dist/index.mjs +1418 -0
- package/package.json +44 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|