@spoosh/core 0.9.2 → 0.10.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Core client and plugin system for Spoosh - a type-safe API client framework.
4
4
 
5
- **[Documentation](https://spoosh.dev/docs)** · **Requirements:** TypeScript >= 5.0
5
+ **[Documentation](https://spoosh.dev/docs/react)** · **Requirements:** TypeScript >= 5.0
6
6
 
7
7
  ## Installation
8
8
 
package/dist/index.d.mts CHANGED
@@ -28,40 +28,32 @@ type QueryField<TQuery> = [TQuery] extends [never] ? object : {
28
28
  type BodyField<TBody> = [TBody] extends [never] ? object : {
29
29
  body: TBody;
30
30
  };
31
- type FormDataField<TFormData> = [TFormData] extends [never] ? object : {
32
- formData: TFormData;
33
- };
34
- type UrlEncodedField<TUrlEncoded> = [TUrlEncoded] extends [never] ? object : {
35
- urlEncoded: TUrlEncoded;
36
- };
37
31
  type ParamsField<TParamNames extends string> = [TParamNames] extends [never] ? object : {
38
32
  params: Record<TParamNames, string | number>;
39
33
  };
40
- type InputFields<TQuery, TBody, TFormData, TUrlEncoded, TParamNames extends string> = QueryField<TQuery> & BodyField<TBody> & FormDataField<TFormData> & UrlEncodedField<TUrlEncoded> & ParamsField<TParamNames>;
41
- type InputFieldWrapper<TQuery, TBody, TFormData, TUrlEncoded, TParamNames extends string> = [TQuery, TBody, TFormData, TUrlEncoded, TParamNames] extends [
42
- never,
43
- never,
44
- never,
45
- never,
46
- never
47
- ] ? object : {
48
- input: InputFields<TQuery, TBody, TFormData, TUrlEncoded, TParamNames>;
34
+ type InputFields<TQuery, TBody, TParamNames extends string> = QueryField<TQuery> & BodyField<TBody> & ParamsField<TParamNames>;
35
+ type InputFieldWrapper<TQuery, TBody, TParamNames extends string> = [
36
+ TQuery,
37
+ TBody,
38
+ TParamNames
39
+ ] extends [never, never, never] ? object : {
40
+ input: InputFields<TQuery, TBody, TParamNames>;
49
41
  };
50
- type SpooshResponse<TData, TError, TRequestOptions = unknown, TQuery = never, TBody = never, TFormData = never, TUrlEncoded = never, TParamNames extends string = never> = ({
42
+ type SpooshResponse<TData, TError, TRequestOptions = unknown, TQuery = never, TBody = never, TParamNames extends string = never> = ({
51
43
  status: number;
52
44
  data: TData;
53
45
  headers?: Headers;
54
46
  error?: undefined;
55
47
  aborted?: false;
56
48
  readonly __requestOptions?: TRequestOptions;
57
- } & InputFieldWrapper<TQuery, TBody, TFormData, TUrlEncoded, TParamNames>) | ({
49
+ } & InputFieldWrapper<TQuery, TBody, TParamNames>) | ({
58
50
  status: number;
59
51
  data?: undefined;
60
52
  headers?: Headers;
61
53
  error: TError;
62
54
  aborted?: boolean;
63
55
  readonly __requestOptions?: TRequestOptions;
64
- } & InputFieldWrapper<TQuery, TBody, TFormData, TUrlEncoded, TParamNames>);
56
+ } & InputFieldWrapper<TQuery, TBody, TParamNames>);
65
57
  type SpooshOptionsExtra<TData = unknown, TError = unknown> = {
66
58
  middlewares?: SpooshMiddleware<TData, TError>[];
67
59
  };
@@ -80,6 +72,24 @@ declare function resolveRequestBody(rawBody: unknown): {
80
72
  headers?: Record<string, string>;
81
73
  } | undefined;
82
74
 
75
+ interface TransportResponse {
76
+ ok: boolean;
77
+ status: number;
78
+ headers: Headers;
79
+ data: unknown;
80
+ }
81
+ type Transport<TOptions = unknown> = (url: string, init: RequestInit, options?: TOptions) => Promise<TransportResponse>;
82
+ /**
83
+ * Transport layer used for requests.
84
+ *
85
+ * - `"fetch"` — Uses the Fetch API (default).
86
+ * - `"xhr"` — Uses XMLHttpRequest. Required for upload/download progress tracking.
87
+ */
88
+ type TransportOption = "fetch" | "xhr";
89
+ interface TransportOptionsMap {
90
+ fetch: never;
91
+ }
92
+
83
93
  type RetryConfig = {
84
94
  retries?: number | false;
85
95
  retryDelay?: number;
@@ -87,7 +97,26 @@ type RetryConfig = {
87
97
  type HeadersInitOrGetter = HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
88
98
  type SpooshOptions = Omit<RequestInit, "method" | "body" | "headers"> & {
89
99
  headers?: HeadersInitOrGetter;
100
+ /** Default transport for all requests. */
101
+ transport?: TransportOption;
102
+ };
103
+ type FetchOnlyInitKeys = "mode" | "cache" | "integrity" | "keepalive" | "next" | "priority" | "redirect" | "referrer" | "referrerPolicy" | "window";
104
+ type SharedSpooshOptions = Omit<RequestInit, "signal" | "method" | "body" | "headers" | FetchOnlyInitKeys> & {
105
+ headers?: HeadersInitOrGetter;
106
+ };
107
+ type SpooshFetchOptions = SharedSpooshOptions & Pick<RequestInit, Extract<keyof RequestInit, FetchOnlyInitKeys>> & {
108
+ transport?: "fetch";
90
109
  };
110
+ type SpooshXhrOptions = SharedSpooshOptions & {
111
+ transport: "xhr";
112
+ };
113
+ /**
114
+ * Constructor-level options with transport-aware type narrowing.
115
+ *
116
+ * When `transport` is `"xhr"`, fetch-only fields (e.g. `mode`, `cache`, `redirect`) are
117
+ * excluded from autocomplete since they have no effect on XMLHttpRequest.
118
+ */
119
+ type SpooshOptionsInput = SpooshFetchOptions | SpooshXhrOptions;
91
120
  type BaseRequestOptions$1 = {
92
121
  headers?: HeadersInitOrGetter;
93
122
  cache?: RequestCache;
@@ -109,6 +138,10 @@ type AnyRequestOptions = BaseRequestOptions$1 & {
109
138
  query?: Record<string, string | number | boolean | undefined>;
110
139
  params?: Record<string, string | number>;
111
140
  signal?: AbortSignal;
141
+ /** Per-request transport override. */
142
+ transport?: TransportOption;
143
+ /** Transport-specific options passed through to the transport function. */
144
+ transportOptions?: unknown;
112
145
  } & Partial<RetryConfig>;
113
146
  type DynamicParamsOption = {
114
147
  params?: Record<string, string | number>;
@@ -149,6 +182,7 @@ type EventCallback<T = unknown> = (payload: T) => void;
149
182
  interface BuiltInEvents {
150
183
  refetch: RefetchEvent;
151
184
  invalidate: string[];
185
+ refetchAll: void;
152
186
  }
153
187
  /**
154
188
  * Resolves event payload type. Built-in events get their specific type,
@@ -558,13 +592,14 @@ type PluginAccessor = {
558
592
  get<K extends keyof PluginExportsRegistry>(name: K): PluginExportsRegistry[K] | undefined;
559
593
  get(name: string): unknown;
560
594
  };
595
+ type RefetchEventReason = "focus" | "reconnect" | "polling" | "invalidate";
561
596
  /**
562
597
  * Event emitted by plugins to request a refetch.
563
598
  * Hooks subscribe to this event and trigger controller.execute().
564
599
  */
565
600
  type RefetchEvent = {
566
601
  queryKey: string;
567
- reason: "focus" | "reconnect" | "polling" | "invalidate";
602
+ reason: RefetchEventReason | Omit<string, RefetchEventReason>;
568
603
  };
569
604
  /**
570
605
  * Minimal PluginExecutor interface for InstanceApiContext.
@@ -734,7 +769,7 @@ declare function createPluginRegistry<TPlugins extends SpooshPlugin<PluginTypeCo
734
769
  type ApiSchema = {
735
770
  [path: string]: {
736
771
  [method in HttpMethod]?: {
737
- data: unknown;
772
+ data?: unknown;
738
773
  body?: unknown;
739
774
  query?: unknown;
740
775
  error?: unknown;
@@ -746,7 +781,7 @@ type ApiSchema = {
746
781
  */
747
782
  type ExtractData<T> = T extends {
748
783
  data: infer D;
749
- } ? D : never;
784
+ } ? D : void;
750
785
  /**
751
786
  * Extract body type from an endpoint.
752
787
  */
@@ -786,16 +821,7 @@ type ExtractError<T, TDefault = unknown> = T extends {
786
821
  * const api = createClient<ApiSchema>({ baseUrl: "/api" });
787
822
  * ```
788
823
  */
789
- type SpooshSchema<T extends {
790
- [path: string]: {
791
- [M in HttpMethod]?: {
792
- data: unknown;
793
- body?: unknown;
794
- query?: unknown;
795
- error?: unknown;
796
- };
797
- };
798
- }> = T;
824
+ type SpooshSchema<T extends ApiSchema> = T;
799
825
  /**
800
826
  * Convert a route pattern like "posts/:id" to a path matcher pattern like `posts/${string}`.
801
827
  * This enables TypeScript to match actual paths like "posts/123" to their schema definitions.
@@ -807,7 +833,7 @@ type SpooshSchema<T extends {
807
833
  * type C = RouteToPath<"posts">; // "posts"
808
834
  * ```
809
835
  */
810
- type RouteToPath<T extends string> = T extends `${infer Start}/:${string}/${infer Rest}` ? `${Start}/${string}/${RouteToPath<Rest>}` : T extends `${infer Start}/:${string}` ? `${Start}/${string}` : T;
836
+ type RouteToPath<T extends string> = T extends `/${infer Rest}` ? `/${RouteToPath<Rest>}` : T extends `${infer Segment}/${infer Rest}` ? Segment extends `:${string}` ? `${string}/${RouteToPath<Rest>}` : `${Segment}/${RouteToPath<Rest>}` : T extends `:${string}` ? string : T;
811
837
  /**
812
838
  * Find which schema key matches a given path.
813
839
  * First checks for exact match, then checks pattern matches.
@@ -875,12 +901,12 @@ type StripPrefixFromPath<TPath extends string, TPrefix extends string> = TPath e
875
901
  * "api": { GET: { data: string } };
876
902
  * "api/users": { GET: { data: User[] } };
877
903
  * "api/posts/:id": { GET: { data: Post } };
878
- * "health": { GET: { data: { status: string } } };
904
+ * "api/health": { GET: { data: { status: string } } };
879
905
  * };
880
906
  *
881
907
  * type ApiSchema = StripPrefix<FullSchema, "api">;
882
908
  * // {
883
- * // "": { GET: { data: string } };
909
+ * // "/": { GET: { data: string } };
884
910
  * // "users": { GET: { data: User[] } };
885
911
  * // "posts/:id": { GET: { data: Post } };
886
912
  * // "health": { GET: { data: { status: string } } };
@@ -1011,7 +1037,7 @@ type IsOptionsRequired<TMethodConfig, TUserPath extends string> = IsBodyRequired
1011
1037
  /**
1012
1038
  * Build response type for a method call.
1013
1039
  */
1014
- type MethodResponse<TMethodConfig, TDefaultError, TUserPath extends string> = SpooshResponse<ExtractData<TMethodConfig>, ExtractError<TMethodConfig, TDefaultError>, RequestOptions<TMethodConfig, TUserPath>, ExtractQuery<TMethodConfig>, ExtractBody<TMethodConfig>, never, never, ExtractParamNames<TUserPath>>;
1040
+ type MethodResponse<TMethodConfig, TDefaultError, TUserPath extends string> = SpooshResponse<ExtractData<TMethodConfig>, ExtractError<TMethodConfig, TDefaultError>, RequestOptions<TMethodConfig, TUserPath>, ExtractQuery<TMethodConfig>, ExtractBody<TMethodConfig>, ExtractParamNames<TUserPath>>;
1015
1041
  /**
1016
1042
  * Create a method function type.
1017
1043
  * Direct lookup: Schema[Path][Method] → method config → build function type
@@ -1151,7 +1177,7 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1151
1177
  * Creates a new Spoosh instance.
1152
1178
  *
1153
1179
  * @param baseUrl - The base URL for all API requests (e.g., '/api' or 'https://api.example.com')
1154
- * @param defaultOptions - Optional default options applied to all requests (headers, credentials, etc.)
1180
+ * @param defaultOptions - Optional default options applied to all requests (headers, credentials, transport, etc.)
1155
1181
  * @param plugins - Internal parameter used by the `.use()` method. Do not pass directly.
1156
1182
  *
1157
1183
  * @example
@@ -1163,9 +1189,15 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1163
1189
  * const client = new Spoosh<ApiSchema, Error>('/api', {
1164
1190
  * headers: { 'X-API-Key': 'secret' }
1165
1191
  * });
1192
+ *
1193
+ * // With XHR transport (narrows available options to XHR-compatible fields)
1194
+ * const client = new Spoosh<ApiSchema, Error>('/api', {
1195
+ * transport: 'xhr',
1196
+ * credentials: 'include',
1197
+ * });
1166
1198
  * ```
1167
1199
  */
1168
- constructor(baseUrl: string, defaultOptions?: SpooshOptions, plugins?: TPlugins);
1200
+ constructor(baseUrl: string, defaultOptions?: SpooshOptionsInput, plugins?: TPlugins);
1169
1201
  /**
1170
1202
  * Adds plugins to the Spoosh instance.
1171
1203
  *
@@ -1316,7 +1348,7 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1316
1348
 
1317
1349
  type SpooshClientConfig = {
1318
1350
  baseUrl: string;
1319
- defaultOptions?: SpooshOptions;
1351
+ defaultOptions?: SpooshOptionsInput;
1320
1352
  middlewares?: SpooshMiddleware[];
1321
1353
  };
1322
1354
  /**
@@ -1354,6 +1386,12 @@ type SpooshClientConfig = {
1354
1386
  * const { data } = await api("posts").GET();
1355
1387
  * const { data: post } = await api("posts/123").GET();
1356
1388
  * await api("posts/:id").GET({ params: { id: 123 } });
1389
+ *
1390
+ * // With XHR transport
1391
+ * const api = createClient<ApiSchema, ApiError>({
1392
+ * baseUrl: "/api",
1393
+ * defaultOptions: { transport: "xhr" },
1394
+ * });
1357
1395
  * ```
1358
1396
  */
1359
1397
  declare function createClient<TSchema, TDefaultError = unknown>(config: SpooshClientConfig): SpooshClient<TSchema, TDefaultError>;
@@ -1542,6 +1580,19 @@ declare function extractPathFromSelector(fn: unknown): string;
1542
1580
  */
1543
1581
  declare function extractMethodFromSelector(fn: unknown): string | undefined;
1544
1582
 
1583
+ declare const fetchTransport: Transport;
1584
+
1585
+ interface XhrTransportOptions {
1586
+ /** Called on upload and download progress events. */
1587
+ onProgress?: (event: ProgressEvent, xhr: XMLHttpRequest) => void;
1588
+ }
1589
+ declare module "./types" {
1590
+ interface TransportOptionsMap {
1591
+ xhr: XhrTransportOptions;
1592
+ }
1593
+ }
1594
+ declare const xhrTransport: Transport<XhrTransportOptions>;
1595
+
1545
1596
  declare function executeFetch<TData, TError>(baseUrl: string, path: string[], method: HttpMethod, defaultOptions: SpooshOptions & SpooshOptionsExtra, requestOptions?: AnyRequestOptions, nextTags?: boolean): Promise<SpooshResponse<TData, TError>>;
1546
1597
 
1547
1598
  declare function createMiddleware<TData = unknown, TError = unknown>(name: string, phase: MiddlewarePhase, handler: SpooshMiddleware<TData, TError>["handler"]): SpooshMiddleware<TData, TError>;
@@ -1636,4 +1687,4 @@ type CreateInfiniteReadOptions<TData, TItem, TError, TRequest> = {
1636
1687
  };
1637
1688
  declare function createInfiniteReadController<TData, TItem, TError, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions>(options: CreateInfiniteReadOptions<TData, TItem, TError, TRequest>): InfiniteReadController<TData, TItem, TError>;
1638
1689
 
1639
- export { type AnyRequestOptions, type ApiSchema, 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 EventEmitter, type ExtractBody$1 as ExtractBody, type ExtractData, type ExtractError, type ExtractMethodOptions, type ExtractParamNames, type ExtractQuery$1 as ExtractQuery, type FetchDirection, type FetchExecutor, type FindMatchingKey, HTTP_METHODS, type HasParams, type HasReadMethod, type HasWriteMethod, 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 MethodOptionsMap, type MiddlewareContext, type MiddlewareHandler, type MiddlewarePhase, 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 ReadClient, type ReadPaths, type ReadSchemaHelper, type RefetchEvent, type RequestOptions$1 as RequestOptions, type ResolveInstanceApi, type ResolveResultTypes, type ResolveSchemaTypes, type ResolveTypes, type ResolverContext, type RetryConfig, type RouteToPath, type SchemaPaths, type SelectedEndpoint, type SelectorFunction, type SelectorResult, type Simplify, Spoosh, type SpooshBody, type SpooshClient, type SpooshConfig, type SpooshInstance, type SpooshMiddleware, type SpooshOptions, type SpooshOptionsExtra, type SpooshPlugin, type SpooshResponse, type SpooshSchema, type StateManager, type StripPrefix, type TagMode, type TagOptions, type WriteClient, type WriteMethod, type WritePaths, type WriteSchemaHelper, __DEV__, applyMiddlewares, buildUrl, composeMiddlewares, containsFile, createClient, createEventEmitter, createInfiniteReadController, createInitialState, createMiddleware, createOperationController, createPluginExecutor, createPluginRegistry, createProxyHandler, createSelectorProxy, createStateManager, executeFetch, extractMethodFromSelector, extractPathFromSelector, form, generateTags, getContentType, isJsonBody, isSpooshBody, json, mergeHeaders, objectToFormData, objectToUrlEncoded, resolveHeadersToRecord, resolvePath, resolveRequestBody, resolveTags, setHeaders, sortObjectKeys, urlencoded };
1690
+ export { type AnyRequestOptions, type ApiSchema, 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 EventEmitter, type ExtractBody$1 as ExtractBody, type ExtractData, type ExtractError, type ExtractMethodOptions, type ExtractParamNames, type ExtractQuery$1 as ExtractQuery, type FetchDirection, type FetchExecutor, type FindMatchingKey, HTTP_METHODS, type HasParams, type HasReadMethod, type HasWriteMethod, 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 MethodOptionsMap, type MiddlewareContext, type MiddlewareHandler, type MiddlewarePhase, 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 ReadClient, type ReadPaths, type ReadSchemaHelper, type RefetchEvent, type RequestOptions$1 as RequestOptions, type ResolveInstanceApi, type ResolveResultTypes, type ResolveSchemaTypes, type ResolveTypes, type ResolverContext, type RetryConfig, type SchemaPaths, type SelectedEndpoint, type SelectorFunction, type SelectorResult, type Simplify, Spoosh, type SpooshBody, type SpooshClient, type SpooshConfig, type SpooshInstance, type SpooshMiddleware, type SpooshOptions, type SpooshOptionsExtra, type SpooshOptionsInput, type SpooshPlugin, type SpooshResponse, type SpooshSchema, type StateManager, type StripPrefix, type TagMode, type TagOptions, type Transport, type TransportOption, type TransportOptionsMap, type TransportResponse, type WriteClient, type WriteMethod, type WritePaths, type WriteSchemaHelper, __DEV__, applyMiddlewares, buildUrl, composeMiddlewares, containsFile, createClient, createEventEmitter, createInfiniteReadController, createInitialState, createMiddleware, createOperationController, createPluginExecutor, createPluginRegistry, createProxyHandler, createSelectorProxy, createStateManager, executeFetch, extractMethodFromSelector, extractPathFromSelector, fetchTransport, form, generateTags, getContentType, isJsonBody, isSpooshBody, json, mergeHeaders, objectToFormData, objectToUrlEncoded, resolveHeadersToRecord, resolvePath, resolveRequestBody, resolveTags, setHeaders, sortObjectKeys, urlencoded, xhrTransport };
package/dist/index.d.ts CHANGED
@@ -28,40 +28,32 @@ type QueryField<TQuery> = [TQuery] extends [never] ? object : {
28
28
  type BodyField<TBody> = [TBody] extends [never] ? object : {
29
29
  body: TBody;
30
30
  };
31
- type FormDataField<TFormData> = [TFormData] extends [never] ? object : {
32
- formData: TFormData;
33
- };
34
- type UrlEncodedField<TUrlEncoded> = [TUrlEncoded] extends [never] ? object : {
35
- urlEncoded: TUrlEncoded;
36
- };
37
31
  type ParamsField<TParamNames extends string> = [TParamNames] extends [never] ? object : {
38
32
  params: Record<TParamNames, string | number>;
39
33
  };
40
- type InputFields<TQuery, TBody, TFormData, TUrlEncoded, TParamNames extends string> = QueryField<TQuery> & BodyField<TBody> & FormDataField<TFormData> & UrlEncodedField<TUrlEncoded> & ParamsField<TParamNames>;
41
- type InputFieldWrapper<TQuery, TBody, TFormData, TUrlEncoded, TParamNames extends string> = [TQuery, TBody, TFormData, TUrlEncoded, TParamNames] extends [
42
- never,
43
- never,
44
- never,
45
- never,
46
- never
47
- ] ? object : {
48
- input: InputFields<TQuery, TBody, TFormData, TUrlEncoded, TParamNames>;
34
+ type InputFields<TQuery, TBody, TParamNames extends string> = QueryField<TQuery> & BodyField<TBody> & ParamsField<TParamNames>;
35
+ type InputFieldWrapper<TQuery, TBody, TParamNames extends string> = [
36
+ TQuery,
37
+ TBody,
38
+ TParamNames
39
+ ] extends [never, never, never] ? object : {
40
+ input: InputFields<TQuery, TBody, TParamNames>;
49
41
  };
50
- type SpooshResponse<TData, TError, TRequestOptions = unknown, TQuery = never, TBody = never, TFormData = never, TUrlEncoded = never, TParamNames extends string = never> = ({
42
+ type SpooshResponse<TData, TError, TRequestOptions = unknown, TQuery = never, TBody = never, TParamNames extends string = never> = ({
51
43
  status: number;
52
44
  data: TData;
53
45
  headers?: Headers;
54
46
  error?: undefined;
55
47
  aborted?: false;
56
48
  readonly __requestOptions?: TRequestOptions;
57
- } & InputFieldWrapper<TQuery, TBody, TFormData, TUrlEncoded, TParamNames>) | ({
49
+ } & InputFieldWrapper<TQuery, TBody, TParamNames>) | ({
58
50
  status: number;
59
51
  data?: undefined;
60
52
  headers?: Headers;
61
53
  error: TError;
62
54
  aborted?: boolean;
63
55
  readonly __requestOptions?: TRequestOptions;
64
- } & InputFieldWrapper<TQuery, TBody, TFormData, TUrlEncoded, TParamNames>);
56
+ } & InputFieldWrapper<TQuery, TBody, TParamNames>);
65
57
  type SpooshOptionsExtra<TData = unknown, TError = unknown> = {
66
58
  middlewares?: SpooshMiddleware<TData, TError>[];
67
59
  };
@@ -80,6 +72,24 @@ declare function resolveRequestBody(rawBody: unknown): {
80
72
  headers?: Record<string, string>;
81
73
  } | undefined;
82
74
 
75
+ interface TransportResponse {
76
+ ok: boolean;
77
+ status: number;
78
+ headers: Headers;
79
+ data: unknown;
80
+ }
81
+ type Transport<TOptions = unknown> = (url: string, init: RequestInit, options?: TOptions) => Promise<TransportResponse>;
82
+ /**
83
+ * Transport layer used for requests.
84
+ *
85
+ * - `"fetch"` — Uses the Fetch API (default).
86
+ * - `"xhr"` — Uses XMLHttpRequest. Required for upload/download progress tracking.
87
+ */
88
+ type TransportOption = "fetch" | "xhr";
89
+ interface TransportOptionsMap {
90
+ fetch: never;
91
+ }
92
+
83
93
  type RetryConfig = {
84
94
  retries?: number | false;
85
95
  retryDelay?: number;
@@ -87,7 +97,26 @@ type RetryConfig = {
87
97
  type HeadersInitOrGetter = HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
88
98
  type SpooshOptions = Omit<RequestInit, "method" | "body" | "headers"> & {
89
99
  headers?: HeadersInitOrGetter;
100
+ /** Default transport for all requests. */
101
+ transport?: TransportOption;
102
+ };
103
+ type FetchOnlyInitKeys = "mode" | "cache" | "integrity" | "keepalive" | "next" | "priority" | "redirect" | "referrer" | "referrerPolicy" | "window";
104
+ type SharedSpooshOptions = Omit<RequestInit, "signal" | "method" | "body" | "headers" | FetchOnlyInitKeys> & {
105
+ headers?: HeadersInitOrGetter;
106
+ };
107
+ type SpooshFetchOptions = SharedSpooshOptions & Pick<RequestInit, Extract<keyof RequestInit, FetchOnlyInitKeys>> & {
108
+ transport?: "fetch";
90
109
  };
110
+ type SpooshXhrOptions = SharedSpooshOptions & {
111
+ transport: "xhr";
112
+ };
113
+ /**
114
+ * Constructor-level options with transport-aware type narrowing.
115
+ *
116
+ * When `transport` is `"xhr"`, fetch-only fields (e.g. `mode`, `cache`, `redirect`) are
117
+ * excluded from autocomplete since they have no effect on XMLHttpRequest.
118
+ */
119
+ type SpooshOptionsInput = SpooshFetchOptions | SpooshXhrOptions;
91
120
  type BaseRequestOptions$1 = {
92
121
  headers?: HeadersInitOrGetter;
93
122
  cache?: RequestCache;
@@ -109,6 +138,10 @@ type AnyRequestOptions = BaseRequestOptions$1 & {
109
138
  query?: Record<string, string | number | boolean | undefined>;
110
139
  params?: Record<string, string | number>;
111
140
  signal?: AbortSignal;
141
+ /** Per-request transport override. */
142
+ transport?: TransportOption;
143
+ /** Transport-specific options passed through to the transport function. */
144
+ transportOptions?: unknown;
112
145
  } & Partial<RetryConfig>;
113
146
  type DynamicParamsOption = {
114
147
  params?: Record<string, string | number>;
@@ -149,6 +182,7 @@ type EventCallback<T = unknown> = (payload: T) => void;
149
182
  interface BuiltInEvents {
150
183
  refetch: RefetchEvent;
151
184
  invalidate: string[];
185
+ refetchAll: void;
152
186
  }
153
187
  /**
154
188
  * Resolves event payload type. Built-in events get their specific type,
@@ -558,13 +592,14 @@ type PluginAccessor = {
558
592
  get<K extends keyof PluginExportsRegistry>(name: K): PluginExportsRegistry[K] | undefined;
559
593
  get(name: string): unknown;
560
594
  };
595
+ type RefetchEventReason = "focus" | "reconnect" | "polling" | "invalidate";
561
596
  /**
562
597
  * Event emitted by plugins to request a refetch.
563
598
  * Hooks subscribe to this event and trigger controller.execute().
564
599
  */
565
600
  type RefetchEvent = {
566
601
  queryKey: string;
567
- reason: "focus" | "reconnect" | "polling" | "invalidate";
602
+ reason: RefetchEventReason | Omit<string, RefetchEventReason>;
568
603
  };
569
604
  /**
570
605
  * Minimal PluginExecutor interface for InstanceApiContext.
@@ -734,7 +769,7 @@ declare function createPluginRegistry<TPlugins extends SpooshPlugin<PluginTypeCo
734
769
  type ApiSchema = {
735
770
  [path: string]: {
736
771
  [method in HttpMethod]?: {
737
- data: unknown;
772
+ data?: unknown;
738
773
  body?: unknown;
739
774
  query?: unknown;
740
775
  error?: unknown;
@@ -746,7 +781,7 @@ type ApiSchema = {
746
781
  */
747
782
  type ExtractData<T> = T extends {
748
783
  data: infer D;
749
- } ? D : never;
784
+ } ? D : void;
750
785
  /**
751
786
  * Extract body type from an endpoint.
752
787
  */
@@ -786,16 +821,7 @@ type ExtractError<T, TDefault = unknown> = T extends {
786
821
  * const api = createClient<ApiSchema>({ baseUrl: "/api" });
787
822
  * ```
788
823
  */
789
- type SpooshSchema<T extends {
790
- [path: string]: {
791
- [M in HttpMethod]?: {
792
- data: unknown;
793
- body?: unknown;
794
- query?: unknown;
795
- error?: unknown;
796
- };
797
- };
798
- }> = T;
824
+ type SpooshSchema<T extends ApiSchema> = T;
799
825
  /**
800
826
  * Convert a route pattern like "posts/:id" to a path matcher pattern like `posts/${string}`.
801
827
  * This enables TypeScript to match actual paths like "posts/123" to their schema definitions.
@@ -807,7 +833,7 @@ type SpooshSchema<T extends {
807
833
  * type C = RouteToPath<"posts">; // "posts"
808
834
  * ```
809
835
  */
810
- type RouteToPath<T extends string> = T extends `${infer Start}/:${string}/${infer Rest}` ? `${Start}/${string}/${RouteToPath<Rest>}` : T extends `${infer Start}/:${string}` ? `${Start}/${string}` : T;
836
+ type RouteToPath<T extends string> = T extends `/${infer Rest}` ? `/${RouteToPath<Rest>}` : T extends `${infer Segment}/${infer Rest}` ? Segment extends `:${string}` ? `${string}/${RouteToPath<Rest>}` : `${Segment}/${RouteToPath<Rest>}` : T extends `:${string}` ? string : T;
811
837
  /**
812
838
  * Find which schema key matches a given path.
813
839
  * First checks for exact match, then checks pattern matches.
@@ -875,12 +901,12 @@ type StripPrefixFromPath<TPath extends string, TPrefix extends string> = TPath e
875
901
  * "api": { GET: { data: string } };
876
902
  * "api/users": { GET: { data: User[] } };
877
903
  * "api/posts/:id": { GET: { data: Post } };
878
- * "health": { GET: { data: { status: string } } };
904
+ * "api/health": { GET: { data: { status: string } } };
879
905
  * };
880
906
  *
881
907
  * type ApiSchema = StripPrefix<FullSchema, "api">;
882
908
  * // {
883
- * // "": { GET: { data: string } };
909
+ * // "/": { GET: { data: string } };
884
910
  * // "users": { GET: { data: User[] } };
885
911
  * // "posts/:id": { GET: { data: Post } };
886
912
  * // "health": { GET: { data: { status: string } } };
@@ -1011,7 +1037,7 @@ type IsOptionsRequired<TMethodConfig, TUserPath extends string> = IsBodyRequired
1011
1037
  /**
1012
1038
  * Build response type for a method call.
1013
1039
  */
1014
- type MethodResponse<TMethodConfig, TDefaultError, TUserPath extends string> = SpooshResponse<ExtractData<TMethodConfig>, ExtractError<TMethodConfig, TDefaultError>, RequestOptions<TMethodConfig, TUserPath>, ExtractQuery<TMethodConfig>, ExtractBody<TMethodConfig>, never, never, ExtractParamNames<TUserPath>>;
1040
+ type MethodResponse<TMethodConfig, TDefaultError, TUserPath extends string> = SpooshResponse<ExtractData<TMethodConfig>, ExtractError<TMethodConfig, TDefaultError>, RequestOptions<TMethodConfig, TUserPath>, ExtractQuery<TMethodConfig>, ExtractBody<TMethodConfig>, ExtractParamNames<TUserPath>>;
1015
1041
  /**
1016
1042
  * Create a method function type.
1017
1043
  * Direct lookup: Schema[Path][Method] → method config → build function type
@@ -1151,7 +1177,7 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1151
1177
  * Creates a new Spoosh instance.
1152
1178
  *
1153
1179
  * @param baseUrl - The base URL for all API requests (e.g., '/api' or 'https://api.example.com')
1154
- * @param defaultOptions - Optional default options applied to all requests (headers, credentials, etc.)
1180
+ * @param defaultOptions - Optional default options applied to all requests (headers, credentials, transport, etc.)
1155
1181
  * @param plugins - Internal parameter used by the `.use()` method. Do not pass directly.
1156
1182
  *
1157
1183
  * @example
@@ -1163,9 +1189,15 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1163
1189
  * const client = new Spoosh<ApiSchema, Error>('/api', {
1164
1190
  * headers: { 'X-API-Key': 'secret' }
1165
1191
  * });
1192
+ *
1193
+ * // With XHR transport (narrows available options to XHR-compatible fields)
1194
+ * const client = new Spoosh<ApiSchema, Error>('/api', {
1195
+ * transport: 'xhr',
1196
+ * credentials: 'include',
1197
+ * });
1166
1198
  * ```
1167
1199
  */
1168
- constructor(baseUrl: string, defaultOptions?: SpooshOptions, plugins?: TPlugins);
1200
+ constructor(baseUrl: string, defaultOptions?: SpooshOptionsInput, plugins?: TPlugins);
1169
1201
  /**
1170
1202
  * Adds plugins to the Spoosh instance.
1171
1203
  *
@@ -1316,7 +1348,7 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1316
1348
 
1317
1349
  type SpooshClientConfig = {
1318
1350
  baseUrl: string;
1319
- defaultOptions?: SpooshOptions;
1351
+ defaultOptions?: SpooshOptionsInput;
1320
1352
  middlewares?: SpooshMiddleware[];
1321
1353
  };
1322
1354
  /**
@@ -1354,6 +1386,12 @@ type SpooshClientConfig = {
1354
1386
  * const { data } = await api("posts").GET();
1355
1387
  * const { data: post } = await api("posts/123").GET();
1356
1388
  * await api("posts/:id").GET({ params: { id: 123 } });
1389
+ *
1390
+ * // With XHR transport
1391
+ * const api = createClient<ApiSchema, ApiError>({
1392
+ * baseUrl: "/api",
1393
+ * defaultOptions: { transport: "xhr" },
1394
+ * });
1357
1395
  * ```
1358
1396
  */
1359
1397
  declare function createClient<TSchema, TDefaultError = unknown>(config: SpooshClientConfig): SpooshClient<TSchema, TDefaultError>;
@@ -1542,6 +1580,19 @@ declare function extractPathFromSelector(fn: unknown): string;
1542
1580
  */
1543
1581
  declare function extractMethodFromSelector(fn: unknown): string | undefined;
1544
1582
 
1583
+ declare const fetchTransport: Transport;
1584
+
1585
+ interface XhrTransportOptions {
1586
+ /** Called on upload and download progress events. */
1587
+ onProgress?: (event: ProgressEvent, xhr: XMLHttpRequest) => void;
1588
+ }
1589
+ declare module "./types" {
1590
+ interface TransportOptionsMap {
1591
+ xhr: XhrTransportOptions;
1592
+ }
1593
+ }
1594
+ declare const xhrTransport: Transport<XhrTransportOptions>;
1595
+
1545
1596
  declare function executeFetch<TData, TError>(baseUrl: string, path: string[], method: HttpMethod, defaultOptions: SpooshOptions & SpooshOptionsExtra, requestOptions?: AnyRequestOptions, nextTags?: boolean): Promise<SpooshResponse<TData, TError>>;
1546
1597
 
1547
1598
  declare function createMiddleware<TData = unknown, TError = unknown>(name: string, phase: MiddlewarePhase, handler: SpooshMiddleware<TData, TError>["handler"]): SpooshMiddleware<TData, TError>;
@@ -1636,4 +1687,4 @@ type CreateInfiniteReadOptions<TData, TItem, TError, TRequest> = {
1636
1687
  };
1637
1688
  declare function createInfiniteReadController<TData, TItem, TError, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions>(options: CreateInfiniteReadOptions<TData, TItem, TError, TRequest>): InfiniteReadController<TData, TItem, TError>;
1638
1689
 
1639
- export { type AnyRequestOptions, type ApiSchema, 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 EventEmitter, type ExtractBody$1 as ExtractBody, type ExtractData, type ExtractError, type ExtractMethodOptions, type ExtractParamNames, type ExtractQuery$1 as ExtractQuery, type FetchDirection, type FetchExecutor, type FindMatchingKey, HTTP_METHODS, type HasParams, type HasReadMethod, type HasWriteMethod, 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 MethodOptionsMap, type MiddlewareContext, type MiddlewareHandler, type MiddlewarePhase, 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 ReadClient, type ReadPaths, type ReadSchemaHelper, type RefetchEvent, type RequestOptions$1 as RequestOptions, type ResolveInstanceApi, type ResolveResultTypes, type ResolveSchemaTypes, type ResolveTypes, type ResolverContext, type RetryConfig, type RouteToPath, type SchemaPaths, type SelectedEndpoint, type SelectorFunction, type SelectorResult, type Simplify, Spoosh, type SpooshBody, type SpooshClient, type SpooshConfig, type SpooshInstance, type SpooshMiddleware, type SpooshOptions, type SpooshOptionsExtra, type SpooshPlugin, type SpooshResponse, type SpooshSchema, type StateManager, type StripPrefix, type TagMode, type TagOptions, type WriteClient, type WriteMethod, type WritePaths, type WriteSchemaHelper, __DEV__, applyMiddlewares, buildUrl, composeMiddlewares, containsFile, createClient, createEventEmitter, createInfiniteReadController, createInitialState, createMiddleware, createOperationController, createPluginExecutor, createPluginRegistry, createProxyHandler, createSelectorProxy, createStateManager, executeFetch, extractMethodFromSelector, extractPathFromSelector, form, generateTags, getContentType, isJsonBody, isSpooshBody, json, mergeHeaders, objectToFormData, objectToUrlEncoded, resolveHeadersToRecord, resolvePath, resolveRequestBody, resolveTags, setHeaders, sortObjectKeys, urlencoded };
1690
+ export { type AnyRequestOptions, type ApiSchema, 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 EventEmitter, type ExtractBody$1 as ExtractBody, type ExtractData, type ExtractError, type ExtractMethodOptions, type ExtractParamNames, type ExtractQuery$1 as ExtractQuery, type FetchDirection, type FetchExecutor, type FindMatchingKey, HTTP_METHODS, type HasParams, type HasReadMethod, type HasWriteMethod, 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 MethodOptionsMap, type MiddlewareContext, type MiddlewareHandler, type MiddlewarePhase, 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 ReadClient, type ReadPaths, type ReadSchemaHelper, type RefetchEvent, type RequestOptions$1 as RequestOptions, type ResolveInstanceApi, type ResolveResultTypes, type ResolveSchemaTypes, type ResolveTypes, type ResolverContext, type RetryConfig, type SchemaPaths, type SelectedEndpoint, type SelectorFunction, type SelectorResult, type Simplify, Spoosh, type SpooshBody, type SpooshClient, type SpooshConfig, type SpooshInstance, type SpooshMiddleware, type SpooshOptions, type SpooshOptionsExtra, type SpooshOptionsInput, type SpooshPlugin, type SpooshResponse, type SpooshSchema, type StateManager, type StripPrefix, type TagMode, type TagOptions, type Transport, type TransportOption, type TransportOptionsMap, type TransportResponse, type WriteClient, type WriteMethod, type WritePaths, type WriteSchemaHelper, __DEV__, applyMiddlewares, buildUrl, composeMiddlewares, containsFile, createClient, createEventEmitter, createInfiniteReadController, createInitialState, createMiddleware, createOperationController, createPluginExecutor, createPluginRegistry, createProxyHandler, createSelectorProxy, createStateManager, executeFetch, extractMethodFromSelector, extractPathFromSelector, fetchTransport, form, generateTags, getContentType, isJsonBody, isSpooshBody, json, mergeHeaders, objectToFormData, objectToUrlEncoded, resolveHeadersToRecord, resolvePath, resolveRequestBody, resolveTags, setHeaders, sortObjectKeys, urlencoded, xhrTransport };
package/dist/index.js CHANGED
@@ -41,6 +41,7 @@ __export(src_exports, {
41
41
  executeFetch: () => executeFetch,
42
42
  extractMethodFromSelector: () => extractMethodFromSelector,
43
43
  extractPathFromSelector: () => extractPathFromSelector,
44
+ fetchTransport: () => fetchTransport,
44
45
  form: () => form,
45
46
  generateTags: () => generateTags,
46
47
  getContentType: () => getContentType,
@@ -56,7 +57,8 @@ __export(src_exports, {
56
57
  resolveTags: () => resolveTags,
57
58
  setHeaders: () => setHeaders,
58
59
  sortObjectKeys: () => sortObjectKeys,
59
- urlencoded: () => urlencoded
60
+ urlencoded: () => urlencoded,
61
+ xhrTransport: () => xhrTransport
60
62
  });
61
63
  module.exports = __toCommonJS(src_exports);
62
64
 
@@ -368,6 +370,87 @@ function resolvePath(path, params) {
368
370
  });
369
371
  }
370
372
 
373
+ // src/transport/fetch.ts
374
+ var fetchTransport = async (url, init) => {
375
+ const res = await fetch(url, init);
376
+ const contentType = res.headers.get("content-type");
377
+ const isJson = contentType?.includes("application/json");
378
+ const data = isJson ? await res.json() : res;
379
+ return { ok: res.ok, status: res.status, headers: res.headers, data };
380
+ };
381
+
382
+ // src/transport/xhr.ts
383
+ var xhrTransport = (url, init, options) => {
384
+ return new Promise((resolve, reject) => {
385
+ const xhr = new XMLHttpRequest();
386
+ xhr.open(init.method ?? "GET", url);
387
+ if (init.headers) {
388
+ const headers = init.headers instanceof Headers ? init.headers : new Headers(init.headers);
389
+ headers.forEach((value, key) => {
390
+ xhr.setRequestHeader(key, value);
391
+ });
392
+ }
393
+ if (init.credentials === "include") {
394
+ xhr.withCredentials = true;
395
+ }
396
+ const onAbort = () => xhr.abort();
397
+ if (init.signal) {
398
+ if (init.signal.aborted) {
399
+ xhr.abort();
400
+ return;
401
+ }
402
+ init.signal.addEventListener("abort", onAbort);
403
+ }
404
+ const cleanup = () => {
405
+ init.signal?.removeEventListener("abort", onAbort);
406
+ };
407
+ if (options?.onProgress) {
408
+ xhr.upload.addEventListener("progress", (event) => {
409
+ options.onProgress(event, xhr);
410
+ });
411
+ xhr.addEventListener("progress", (event) => {
412
+ options.onProgress(event, xhr);
413
+ });
414
+ }
415
+ xhr.addEventListener("load", () => {
416
+ cleanup();
417
+ const status = xhr.status;
418
+ const ok = status >= 200 && status < 300;
419
+ const responseHeaders = new Headers();
420
+ const rawHeaders = xhr.getAllResponseHeaders().trim();
421
+ if (rawHeaders) {
422
+ rawHeaders.split("\r\n").forEach((line) => {
423
+ const idx = line.indexOf(": ");
424
+ if (idx > 0) {
425
+ responseHeaders.append(
426
+ line.substring(0, idx),
427
+ line.substring(idx + 2)
428
+ );
429
+ }
430
+ });
431
+ }
432
+ const contentType = responseHeaders.get("content-type");
433
+ const isJson = contentType?.includes("application/json");
434
+ let data;
435
+ try {
436
+ data = isJson ? JSON.parse(xhr.responseText) : xhr.responseText;
437
+ } catch {
438
+ data = xhr.responseText;
439
+ }
440
+ resolve({ ok, status, headers: responseHeaders, data });
441
+ });
442
+ xhr.addEventListener("error", () => {
443
+ cleanup();
444
+ reject(new TypeError("Network request failed"));
445
+ });
446
+ xhr.addEventListener("abort", () => {
447
+ cleanup();
448
+ reject(new DOMException("Aborted", "AbortError"));
449
+ });
450
+ xhr.send(init.body);
451
+ });
452
+ };
453
+
371
454
  // src/fetch.ts
372
455
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
373
456
  var isNetworkError = (err) => err instanceof TypeError;
@@ -416,6 +499,12 @@ function buildInputFields(requestOptions) {
416
499
  }
417
500
  return { input: fields };
418
501
  }
502
+ function resolveTransport(option) {
503
+ if (option === "xhr" && typeof XMLHttpRequest !== "undefined") {
504
+ return xhrTransport;
505
+ }
506
+ return fetchTransport;
507
+ }
419
508
  async function executeCoreFetch(config) {
420
509
  const {
421
510
  baseUrl,
@@ -429,6 +518,7 @@ async function executeCoreFetch(config) {
429
518
  const {
430
519
  middlewares: _,
431
520
  headers: defaultHeaders,
521
+ transport: defaultTransport,
432
522
  ...fetchDefaults
433
523
  } = defaultOptions;
434
524
  void _;
@@ -473,28 +563,30 @@ async function executeCoreFetch(config) {
473
563
  }
474
564
  }
475
565
  }
566
+ const resolvedTransport = resolveTransport(
567
+ requestOptions?.transport ?? defaultTransport
568
+ );
476
569
  let lastError;
477
570
  for (let attempt = 0; attempt <= retryCount; attempt++) {
478
571
  try {
479
- const res = await fetch(url, fetchInit);
480
- const status = res.status;
481
- const resHeaders = res.headers;
482
- const contentType = resHeaders.get("content-type");
483
- const isJson = contentType?.includes("application/json");
484
- const body = isJson ? await res.json() : res;
485
- if (res.ok) {
572
+ const result = await resolvedTransport(
573
+ url,
574
+ fetchInit,
575
+ requestOptions?.transportOptions
576
+ );
577
+ if (result.ok) {
486
578
  return {
487
- status,
488
- data: body,
489
- headers: resHeaders,
579
+ status: result.status,
580
+ data: result.data,
581
+ headers: result.headers,
490
582
  error: void 0,
491
583
  ...inputFields
492
584
  };
493
585
  }
494
586
  return {
495
- status,
496
- error: body,
497
- headers: resHeaders,
587
+ status: result.status,
588
+ error: result.data,
589
+ headers: result.headers,
498
590
  data: void 0,
499
591
  ...inputFields
500
592
  };
@@ -738,6 +830,7 @@ function createStateManager() {
738
830
  for (const [name, value] of Object.entries(data)) {
739
831
  entry.meta.set(name, value);
740
832
  }
833
+ entry.state = { ...entry.state };
741
834
  notifySubscribers(key);
742
835
  },
743
836
  markStale(tags) {
@@ -949,7 +1042,7 @@ var Spoosh = class _Spoosh {
949
1042
  * Creates a new Spoosh instance.
950
1043
  *
951
1044
  * @param baseUrl - The base URL for all API requests (e.g., '/api' or 'https://api.example.com')
952
- * @param defaultOptions - Optional default options applied to all requests (headers, credentials, etc.)
1045
+ * @param defaultOptions - Optional default options applied to all requests (headers, credentials, transport, etc.)
953
1046
  * @param plugins - Internal parameter used by the `.use()` method. Do not pass directly.
954
1047
  *
955
1048
  * @example
@@ -961,6 +1054,12 @@ var Spoosh = class _Spoosh {
961
1054
  * const client = new Spoosh<ApiSchema, Error>('/api', {
962
1055
  * headers: { 'X-API-Key': 'secret' }
963
1056
  * });
1057
+ *
1058
+ * // With XHR transport (narrows available options to XHR-compatible fields)
1059
+ * const client = new Spoosh<ApiSchema, Error>('/api', {
1060
+ * transport: 'xhr',
1061
+ * credentials: 'include',
1062
+ * });
964
1063
  * ```
965
1064
  */
966
1065
  constructor(baseUrl, defaultOptions, plugins) {
@@ -1156,7 +1255,10 @@ var Spoosh = class _Spoosh {
1156
1255
  // src/createClient.ts
1157
1256
  function createClient(config) {
1158
1257
  const { baseUrl, defaultOptions = {}, middlewares = [] } = config;
1159
- const optionsWithMiddlewares = { ...defaultOptions, middlewares };
1258
+ const optionsWithMiddlewares = {
1259
+ ...defaultOptions,
1260
+ middlewares
1261
+ };
1160
1262
  return createProxyHandler({
1161
1263
  baseUrl,
1162
1264
  defaultOptions: optionsWithMiddlewares,
package/dist/index.mjs CHANGED
@@ -306,6 +306,87 @@ function resolvePath(path, params) {
306
306
  });
307
307
  }
308
308
 
309
+ // src/transport/fetch.ts
310
+ var fetchTransport = async (url, init) => {
311
+ const res = await fetch(url, init);
312
+ const contentType = res.headers.get("content-type");
313
+ const isJson = contentType?.includes("application/json");
314
+ const data = isJson ? await res.json() : res;
315
+ return { ok: res.ok, status: res.status, headers: res.headers, data };
316
+ };
317
+
318
+ // src/transport/xhr.ts
319
+ var xhrTransport = (url, init, options) => {
320
+ return new Promise((resolve, reject) => {
321
+ const xhr = new XMLHttpRequest();
322
+ xhr.open(init.method ?? "GET", url);
323
+ if (init.headers) {
324
+ const headers = init.headers instanceof Headers ? init.headers : new Headers(init.headers);
325
+ headers.forEach((value, key) => {
326
+ xhr.setRequestHeader(key, value);
327
+ });
328
+ }
329
+ if (init.credentials === "include") {
330
+ xhr.withCredentials = true;
331
+ }
332
+ const onAbort = () => xhr.abort();
333
+ if (init.signal) {
334
+ if (init.signal.aborted) {
335
+ xhr.abort();
336
+ return;
337
+ }
338
+ init.signal.addEventListener("abort", onAbort);
339
+ }
340
+ const cleanup = () => {
341
+ init.signal?.removeEventListener("abort", onAbort);
342
+ };
343
+ if (options?.onProgress) {
344
+ xhr.upload.addEventListener("progress", (event) => {
345
+ options.onProgress(event, xhr);
346
+ });
347
+ xhr.addEventListener("progress", (event) => {
348
+ options.onProgress(event, xhr);
349
+ });
350
+ }
351
+ xhr.addEventListener("load", () => {
352
+ cleanup();
353
+ const status = xhr.status;
354
+ const ok = status >= 200 && status < 300;
355
+ const responseHeaders = new Headers();
356
+ const rawHeaders = xhr.getAllResponseHeaders().trim();
357
+ if (rawHeaders) {
358
+ rawHeaders.split("\r\n").forEach((line) => {
359
+ const idx = line.indexOf(": ");
360
+ if (idx > 0) {
361
+ responseHeaders.append(
362
+ line.substring(0, idx),
363
+ line.substring(idx + 2)
364
+ );
365
+ }
366
+ });
367
+ }
368
+ const contentType = responseHeaders.get("content-type");
369
+ const isJson = contentType?.includes("application/json");
370
+ let data;
371
+ try {
372
+ data = isJson ? JSON.parse(xhr.responseText) : xhr.responseText;
373
+ } catch {
374
+ data = xhr.responseText;
375
+ }
376
+ resolve({ ok, status, headers: responseHeaders, data });
377
+ });
378
+ xhr.addEventListener("error", () => {
379
+ cleanup();
380
+ reject(new TypeError("Network request failed"));
381
+ });
382
+ xhr.addEventListener("abort", () => {
383
+ cleanup();
384
+ reject(new DOMException("Aborted", "AbortError"));
385
+ });
386
+ xhr.send(init.body);
387
+ });
388
+ };
389
+
309
390
  // src/fetch.ts
310
391
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
311
392
  var isNetworkError = (err) => err instanceof TypeError;
@@ -354,6 +435,12 @@ function buildInputFields(requestOptions) {
354
435
  }
355
436
  return { input: fields };
356
437
  }
438
+ function resolveTransport(option) {
439
+ if (option === "xhr" && typeof XMLHttpRequest !== "undefined") {
440
+ return xhrTransport;
441
+ }
442
+ return fetchTransport;
443
+ }
357
444
  async function executeCoreFetch(config) {
358
445
  const {
359
446
  baseUrl,
@@ -367,6 +454,7 @@ async function executeCoreFetch(config) {
367
454
  const {
368
455
  middlewares: _,
369
456
  headers: defaultHeaders,
457
+ transport: defaultTransport,
370
458
  ...fetchDefaults
371
459
  } = defaultOptions;
372
460
  void _;
@@ -411,28 +499,30 @@ async function executeCoreFetch(config) {
411
499
  }
412
500
  }
413
501
  }
502
+ const resolvedTransport = resolveTransport(
503
+ requestOptions?.transport ?? defaultTransport
504
+ );
414
505
  let lastError;
415
506
  for (let attempt = 0; attempt <= retryCount; attempt++) {
416
507
  try {
417
- const res = await fetch(url, fetchInit);
418
- const status = res.status;
419
- const resHeaders = res.headers;
420
- const contentType = resHeaders.get("content-type");
421
- const isJson = contentType?.includes("application/json");
422
- const body = isJson ? await res.json() : res;
423
- if (res.ok) {
508
+ const result = await resolvedTransport(
509
+ url,
510
+ fetchInit,
511
+ requestOptions?.transportOptions
512
+ );
513
+ if (result.ok) {
424
514
  return {
425
- status,
426
- data: body,
427
- headers: resHeaders,
515
+ status: result.status,
516
+ data: result.data,
517
+ headers: result.headers,
428
518
  error: void 0,
429
519
  ...inputFields
430
520
  };
431
521
  }
432
522
  return {
433
- status,
434
- error: body,
435
- headers: resHeaders,
523
+ status: result.status,
524
+ error: result.data,
525
+ headers: result.headers,
436
526
  data: void 0,
437
527
  ...inputFields
438
528
  };
@@ -676,6 +766,7 @@ function createStateManager() {
676
766
  for (const [name, value] of Object.entries(data)) {
677
767
  entry.meta.set(name, value);
678
768
  }
769
+ entry.state = { ...entry.state };
679
770
  notifySubscribers(key);
680
771
  },
681
772
  markStale(tags) {
@@ -887,7 +978,7 @@ var Spoosh = class _Spoosh {
887
978
  * Creates a new Spoosh instance.
888
979
  *
889
980
  * @param baseUrl - The base URL for all API requests (e.g., '/api' or 'https://api.example.com')
890
- * @param defaultOptions - Optional default options applied to all requests (headers, credentials, etc.)
981
+ * @param defaultOptions - Optional default options applied to all requests (headers, credentials, transport, etc.)
891
982
  * @param plugins - Internal parameter used by the `.use()` method. Do not pass directly.
892
983
  *
893
984
  * @example
@@ -899,6 +990,12 @@ var Spoosh = class _Spoosh {
899
990
  * const client = new Spoosh<ApiSchema, Error>('/api', {
900
991
  * headers: { 'X-API-Key': 'secret' }
901
992
  * });
993
+ *
994
+ * // With XHR transport (narrows available options to XHR-compatible fields)
995
+ * const client = new Spoosh<ApiSchema, Error>('/api', {
996
+ * transport: 'xhr',
997
+ * credentials: 'include',
998
+ * });
902
999
  * ```
903
1000
  */
904
1001
  constructor(baseUrl, defaultOptions, plugins) {
@@ -1094,7 +1191,10 @@ var Spoosh = class _Spoosh {
1094
1191
  // src/createClient.ts
1095
1192
  function createClient(config) {
1096
1193
  const { baseUrl, defaultOptions = {}, middlewares = [] } = config;
1097
- const optionsWithMiddlewares = { ...defaultOptions, middlewares };
1194
+ const optionsWithMiddlewares = {
1195
+ ...defaultOptions,
1196
+ middlewares
1197
+ };
1098
1198
  return createProxyHandler({
1099
1199
  baseUrl,
1100
1200
  defaultOptions: optionsWithMiddlewares,
@@ -1681,6 +1781,7 @@ export {
1681
1781
  executeFetch,
1682
1782
  extractMethodFromSelector,
1683
1783
  extractPathFromSelector,
1784
+ fetchTransport,
1684
1785
  form,
1685
1786
  generateTags,
1686
1787
  getContentType,
@@ -1696,5 +1797,6 @@ export {
1696
1797
  resolveTags,
1697
1798
  setHeaders,
1698
1799
  sortObjectKeys,
1699
- urlencoded
1800
+ urlencoded,
1801
+ xhrTransport
1700
1802
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/core",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "license": "MIT",
5
5
  "description": "Type-safe API client with plugin middleware system",
6
6
  "keywords": [
@@ -14,13 +14,13 @@
14
14
  ],
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "git+https://github.com/nxnom/spoosh.git",
17
+ "url": "git+https://github.com/spooshdev/spoosh.git",
18
18
  "directory": "packages/core"
19
19
  },
20
20
  "bugs": {
21
- "url": "https://github.com/nxnom/spoosh/issues"
21
+ "url": "https://github.com/spooshdev/spoosh/issues"
22
22
  },
23
- "homepage": "https://spoosh.dev/react/docs/core",
23
+ "homepage": "https://spoosh.dev/docs/react/core",
24
24
  "publishConfig": {
25
25
  "access": "public"
26
26
  },