enlace 0.0.1-beta.3 → 0.0.1-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2,9 +2,7 @@
2
2
  export * from "enlace-core";
3
3
 
4
4
  // src/react/createEnlaceHook.ts
5
- import {
6
- createEnlace
7
- } from "enlace-core";
5
+ import { createEnlace } from "enlace-core";
8
6
 
9
7
  // src/react/useQueryMode.ts
10
8
  import { useRef, useReducer, useEffect } from "react";
@@ -20,7 +18,7 @@ var initialState = {
20
18
  function hookReducer(state, action) {
21
19
  switch (action.type) {
22
20
  case "RESET":
23
- return action.state;
21
+ return action.state ?? initialState;
24
22
  case "FETCH_START":
25
23
  return {
26
24
  ...state,
@@ -157,11 +155,29 @@ function onRevalidate(callback) {
157
155
  }
158
156
 
159
157
  // src/react/useQueryMode.ts
158
+ function resolvePath(path, pathParams) {
159
+ if (!pathParams) return path;
160
+ return path.map((segment) => {
161
+ if (segment.startsWith(":")) {
162
+ const paramName = segment.slice(1);
163
+ const value = pathParams[paramName];
164
+ if (value === void 0) {
165
+ throw new Error(`Missing path parameter: ${paramName}`);
166
+ }
167
+ return String(value);
168
+ }
169
+ return segment;
170
+ });
171
+ }
160
172
  function useQueryMode(api, trackedCall, options) {
161
- const { autoGenerateTags, staleTime } = options;
173
+ const { autoGenerateTags, staleTime, enabled } = options;
162
174
  const queryKey = createQueryKey(trackedCall);
163
175
  const requestOptions = trackedCall.options;
164
- const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(trackedCall.path) : []);
176
+ const resolvedPath = resolvePath(
177
+ trackedCall.path,
178
+ requestOptions?.pathParams
179
+ );
180
+ const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
165
181
  const getCacheState = (includeNeedsFetch = false) => {
166
182
  const cached = getCache(queryKey);
167
183
  const hasCachedData = cached?.data !== void 0;
@@ -175,17 +191,22 @@ function useQueryMode(api, trackedCall, options) {
175
191
  error: cached?.error
176
192
  };
177
193
  };
178
- const [state, dispatch] = useReducer(hookReducer, null, () => getCacheState(true));
194
+ const [state, dispatch] = useReducer(
195
+ hookReducer,
196
+ null,
197
+ () => getCacheState(true)
198
+ );
179
199
  const mountedRef = useRef(true);
180
200
  const fetchRef = useRef(null);
181
201
  useEffect(() => {
182
202
  mountedRef.current = true;
203
+ if (!enabled) {
204
+ dispatch({ type: "RESET" });
205
+ return () => {
206
+ mountedRef.current = false;
207
+ };
208
+ }
183
209
  dispatch({ type: "RESET", state: getCacheState(true) });
184
- const unsubscribe = subscribeCache(queryKey, () => {
185
- if (mountedRef.current) {
186
- dispatch({ type: "SYNC_CACHE", state: getCacheState() });
187
- }
188
- });
189
210
  const doFetch = () => {
190
211
  const cached2 = getCache(queryKey);
191
212
  if (cached2?.promise) {
@@ -193,7 +214,7 @@ function useQueryMode(api, trackedCall, options) {
193
214
  }
194
215
  dispatch({ type: "FETCH_START" });
195
216
  let current = api;
196
- for (const segment of trackedCall.path) {
217
+ for (const segment of resolvedPath) {
197
218
  current = current[segment];
198
219
  }
199
220
  const method = current[trackedCall.method];
@@ -201,7 +222,7 @@ function useQueryMode(api, trackedCall, options) {
201
222
  if (mountedRef.current) {
202
223
  setCache(queryKey, {
203
224
  data: res.ok ? res.data : void 0,
204
- error: res.ok ? void 0 : res.error,
225
+ error: res.ok || res.status === 0 ? void 0 : res.error,
205
226
  ok: res.ok,
206
227
  timestamp: Date.now(),
207
228
  tags: queryTags
@@ -220,12 +241,17 @@ function useQueryMode(api, trackedCall, options) {
220
241
  } else {
221
242
  doFetch();
222
243
  }
244
+ const unsubscribe = subscribeCache(queryKey, () => {
245
+ if (mountedRef.current) {
246
+ dispatch({ type: "SYNC_CACHE", state: getCacheState() });
247
+ }
248
+ });
223
249
  return () => {
224
250
  mountedRef.current = false;
225
251
  fetchRef.current = null;
226
252
  unsubscribe();
227
253
  };
228
- }, [queryKey]);
254
+ }, [queryKey, enabled]);
229
255
  useEffect(() => {
230
256
  if (queryTags.length === 0) return;
231
257
  return onRevalidate((invalidatedTags) => {
@@ -272,23 +298,56 @@ function createTrackingProxy(onTrack) {
272
298
 
273
299
  // src/react/useSelectorMode.ts
274
300
  import { useRef as useRef2, useReducer as useReducer2 } from "react";
275
- function useSelectorMode(method, path, autoRevalidateTags) {
301
+ function resolvePath2(path, pathParams) {
302
+ if (!pathParams) return path;
303
+ return path.map((segment) => {
304
+ if (segment.startsWith(":")) {
305
+ const paramName = segment.slice(1);
306
+ const value = pathParams[paramName];
307
+ if (value === void 0) {
308
+ throw new Error(`Missing path parameter: ${paramName}`);
309
+ }
310
+ return String(value);
311
+ }
312
+ return segment;
313
+ });
314
+ }
315
+ function hasPathParams(path) {
316
+ return path.some((segment) => segment.startsWith(":"));
317
+ }
318
+ function useSelectorMode(config) {
319
+ const { method, api, path, methodName, autoRevalidateTags } = config;
276
320
  const [state, dispatch] = useReducer2(hookReducer, initialState);
277
321
  const methodRef = useRef2(method);
322
+ const apiRef = useRef2(api);
278
323
  const triggerRef = useRef2(null);
279
324
  const pathRef = useRef2(path);
325
+ const methodNameRef = useRef2(methodName);
280
326
  const autoRevalidateRef = useRef2(autoRevalidateTags);
281
327
  methodRef.current = method;
328
+ apiRef.current = api;
282
329
  pathRef.current = path;
330
+ methodNameRef.current = methodName;
283
331
  autoRevalidateRef.current = autoRevalidateTags;
284
332
  if (!triggerRef.current) {
285
333
  triggerRef.current = (async (...args) => {
286
334
  dispatch({ type: "FETCH_START" });
287
- const res = await methodRef.current(...args);
335
+ const options = args[0];
336
+ const resolvedPath = resolvePath2(pathRef.current, options?.pathParams);
337
+ let res;
338
+ if (hasPathParams(pathRef.current)) {
339
+ let current = apiRef.current;
340
+ for (const segment of resolvedPath) {
341
+ current = current[segment];
342
+ }
343
+ const resolvedMethod = current[methodNameRef.current];
344
+ res = await resolvedMethod(...args);
345
+ } else {
346
+ res = await methodRef.current(...args);
347
+ }
288
348
  if (res.ok) {
289
349
  dispatch({ type: "FETCH_SUCCESS", data: res.data });
290
- const options = args[0];
291
- const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(pathRef.current) : []);
350
+ const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(resolvedPath) : []);
292
351
  if (tagsToInvalidate.length > 0) {
293
352
  invalidateTags(tagsToInvalidate);
294
353
  }
@@ -306,13 +365,15 @@ function useSelectorMode(method, path, autoRevalidateTags) {
306
365
 
307
366
  // src/react/createEnlaceHook.ts
308
367
  function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
309
- const api = createEnlace(baseUrl, defaultOptions);
310
368
  const {
311
369
  autoGenerateTags = true,
312
370
  autoRevalidateTags = true,
313
- staleTime = 0
371
+ staleTime = 0,
372
+ onSuccess,
373
+ onError
314
374
  } = hookOptions;
315
- function useEnlaceHook(selectorOrQuery) {
375
+ const api = createEnlace(baseUrl, defaultOptions, { onSuccess, onError });
376
+ function useEnlaceHook(selectorOrQuery, queryOptions) {
316
377
  let trackingResult = {
317
378
  trackedCall: null,
318
379
  selectorPath: null,
@@ -326,16 +387,18 @@ function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
326
387
  );
327
388
  if (typeof result === "function") {
328
389
  const actualResult = selectorOrQuery(api);
329
- return useSelectorMode(
330
- actualResult,
331
- trackingResult.selectorPath ?? [],
390
+ return useSelectorMode({
391
+ method: actualResult,
392
+ api,
393
+ path: trackingResult.selectorPath ?? [],
394
+ methodName: trackingResult.selectorMethod ?? "",
332
395
  autoRevalidateTags
333
- );
396
+ });
334
397
  }
335
398
  return useQueryMode(
336
399
  api,
337
400
  trackingResult.trackedCall,
338
- { autoGenerateTags, staleTime }
401
+ { autoGenerateTags, staleTime, enabled: queryOptions?.enabled ?? true }
339
402
  );
340
403
  }
341
404
  return useEnlaceHook;
@@ -1,4 +1,4 @@
1
- import { WildcardClient, EnlaceClient, EnlaceResponse, EnlaceOptions } from 'enlace-core';
1
+ import { EnlaceCallbackPayload, EnlaceErrorCallbackPayload, WildcardClient, EnlaceClient, EnlaceResponse, EnlaceCallbacks, EnlaceOptions } from 'enlace-core';
2
2
 
3
3
  /** Per-request options for React hooks */
4
4
  type ReactRequestOptionsBase = {
@@ -10,6 +10,23 @@ type ReactRequestOptionsBase = {
10
10
  tags?: string[];
11
11
  /** Tags to invalidate after mutation (triggers refetch in matching queries) */
12
12
  revalidateTags?: string[];
13
+ /**
14
+ * Path parameters for dynamic URL segments.
15
+ * Used to replace :paramName placeholders in the URL path.
16
+ * @example
17
+ * // With path api.products[':id'].delete
18
+ * trigger({ pathParams: { id: '123' } }) // → DELETE /products/123
19
+ */
20
+ pathParams?: Record<string, string | number>;
21
+ };
22
+ /** Options for query mode hooks */
23
+ type UseEnlaceQueryOptions = {
24
+ /**
25
+ * Whether the query should execute.
26
+ * Set to false to skip fetching (useful when ID is "new" or undefined).
27
+ * @default true
28
+ */
29
+ enabled?: boolean;
13
30
  };
14
31
  type ApiClient<TSchema, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TOptions>;
15
32
  type QueryFn<TSchema, TData, TError, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
@@ -41,7 +58,7 @@ type UseEnlaceSelectorResult<TMethod> = {
41
58
  loading: boolean;
42
59
  fetching: boolean;
43
60
  } & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
44
-
61
+ /** Options for createEnlaceHook factory */
45
62
  type EnlaceHookOptions = {
46
63
  /**
47
64
  * Auto-generate cache tags from URL path for GET requests.
@@ -53,6 +70,10 @@ type EnlaceHookOptions = {
53
70
  autoRevalidateTags?: boolean;
54
71
  /** Time in ms before cached data is considered stale. @default 0 (always stale) */
55
72
  staleTime?: number;
73
+ /** Callback called on successful API responses */
74
+ onSuccess?: (payload: EnlaceCallbackPayload<unknown>) => void;
75
+ /** Callback called on error responses (HTTP errors or network failures) */
76
+ onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
56
77
  };
57
78
 
58
79
  /**
@@ -62,7 +83,7 @@ type EnlaceHookOptions = {
62
83
  */
63
84
  type RevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
64
85
  /** Next.js-specific options (third argument for createEnlace) */
65
- type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & {
86
+ type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
66
87
  /**
67
88
  * Handler called after successful mutations to trigger server-side revalidation.
68
89
  * Receives auto-generated or manually specified tags and paths.
@@ -96,13 +117,14 @@ type NextRequestOptionsBase = ReactRequestOptionsBase & {
96
117
  */
97
118
  skipRevalidator?: boolean;
98
119
  };
99
-
100
120
  type NextQueryFn<TSchema, TData, TError> = QueryFn<TSchema, TData, TError, NextRequestOptionsBase>;
101
121
  type NextSelectorFn<TSchema, TMethod> = SelectorFn<TSchema, TMethod, NextRequestOptionsBase>;
122
+ /** Hook type returned by Next.js createEnlaceHook */
102
123
  type NextEnlaceHook<TSchema> = {
103
124
  <TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: NextSelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
104
- <TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError>): UseEnlaceQueryResult<TData, TError>;
125
+ <TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
105
126
  };
127
+
106
128
  /**
107
129
  * Creates a React hook for making API calls in Next.js Client Components.
108
130
  * Uses Next.js-specific features like revalidator for server-side cache invalidation.
@@ -1,4 +1,4 @@
1
- import { WildcardClient, EnlaceClient, EnlaceResponse, EnlaceOptions } from 'enlace-core';
1
+ import { EnlaceCallbackPayload, EnlaceErrorCallbackPayload, WildcardClient, EnlaceClient, EnlaceResponse, EnlaceCallbacks, EnlaceOptions } from 'enlace-core';
2
2
 
3
3
  /** Per-request options for React hooks */
4
4
  type ReactRequestOptionsBase = {
@@ -10,6 +10,23 @@ type ReactRequestOptionsBase = {
10
10
  tags?: string[];
11
11
  /** Tags to invalidate after mutation (triggers refetch in matching queries) */
12
12
  revalidateTags?: string[];
13
+ /**
14
+ * Path parameters for dynamic URL segments.
15
+ * Used to replace :paramName placeholders in the URL path.
16
+ * @example
17
+ * // With path api.products[':id'].delete
18
+ * trigger({ pathParams: { id: '123' } }) // → DELETE /products/123
19
+ */
20
+ pathParams?: Record<string, string | number>;
21
+ };
22
+ /** Options for query mode hooks */
23
+ type UseEnlaceQueryOptions = {
24
+ /**
25
+ * Whether the query should execute.
26
+ * Set to false to skip fetching (useful when ID is "new" or undefined).
27
+ * @default true
28
+ */
29
+ enabled?: boolean;
13
30
  };
14
31
  type ApiClient<TSchema, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TOptions>;
15
32
  type QueryFn<TSchema, TData, TError, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
@@ -41,7 +58,7 @@ type UseEnlaceSelectorResult<TMethod> = {
41
58
  loading: boolean;
42
59
  fetching: boolean;
43
60
  } & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
44
-
61
+ /** Options for createEnlaceHook factory */
45
62
  type EnlaceHookOptions = {
46
63
  /**
47
64
  * Auto-generate cache tags from URL path for GET requests.
@@ -53,6 +70,10 @@ type EnlaceHookOptions = {
53
70
  autoRevalidateTags?: boolean;
54
71
  /** Time in ms before cached data is considered stale. @default 0 (always stale) */
55
72
  staleTime?: number;
73
+ /** Callback called on successful API responses */
74
+ onSuccess?: (payload: EnlaceCallbackPayload<unknown>) => void;
75
+ /** Callback called on error responses (HTTP errors or network failures) */
76
+ onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
56
77
  };
57
78
 
58
79
  /**
@@ -62,7 +83,7 @@ type EnlaceHookOptions = {
62
83
  */
63
84
  type RevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
64
85
  /** Next.js-specific options (third argument for createEnlace) */
65
- type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & {
86
+ type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
66
87
  /**
67
88
  * Handler called after successful mutations to trigger server-side revalidation.
68
89
  * Receives auto-generated or manually specified tags and paths.
@@ -96,13 +117,14 @@ type NextRequestOptionsBase = ReactRequestOptionsBase & {
96
117
  */
97
118
  skipRevalidator?: boolean;
98
119
  };
99
-
100
120
  type NextQueryFn<TSchema, TData, TError> = QueryFn<TSchema, TData, TError, NextRequestOptionsBase>;
101
121
  type NextSelectorFn<TSchema, TMethod> = SelectorFn<TSchema, TMethod, NextRequestOptionsBase>;
122
+ /** Hook type returned by Next.js createEnlaceHook */
102
123
  type NextEnlaceHook<TSchema> = {
103
124
  <TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: NextSelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
104
- <TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError>): UseEnlaceQueryResult<TData, TError>;
125
+ <TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
105
126
  };
127
+
106
128
  /**
107
129
  * Creates a React hook for making API calls in Next.js Client Components.
108
130
  * Uses Next.js-specific features like revalidator for server-side cache invalidation.
@@ -48,20 +48,22 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
48
48
  autoGenerateTags = true,
49
49
  autoRevalidateTags = true,
50
50
  revalidator,
51
- headers: defaultHeaders,
52
- ...restOptions
51
+ onSuccess,
52
+ ...coreOptions
53
53
  } = combinedOptions;
54
- const url = (0, import_enlace_core.buildUrl)(baseUrl, path, requestOptions?.query);
55
- let headers = (0, import_enlace_core.mergeHeaders)(defaultHeaders, requestOptions?.headers);
56
54
  const isGet = method === "GET";
57
55
  const autoTags = generateTags(path);
58
- const fetchOptions = {
59
- ...restOptions,
60
- method
56
+ const nextOnSuccess = (payload) => {
57
+ if (!isGet && !requestOptions?.skipRevalidator) {
58
+ const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
59
+ const revalidatePaths = requestOptions?.revalidatePaths ?? [];
60
+ if (revalidateTags.length || revalidatePaths.length) {
61
+ revalidator?.(revalidateTags, revalidatePaths);
62
+ }
63
+ }
64
+ onSuccess?.(payload);
61
65
  };
62
- if (requestOptions?.cache) {
63
- fetchOptions.cache = requestOptions.cache;
64
- }
66
+ const nextRequestOptions = { ...requestOptions };
65
67
  if (isGet) {
66
68
  const tags = requestOptions?.tags ?? (autoGenerateTags ? autoTags : void 0);
67
69
  const nextFetchOptions = {};
@@ -71,51 +73,27 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
71
73
  if (requestOptions?.revalidate !== void 0) {
72
74
  nextFetchOptions.revalidate = requestOptions.revalidate;
73
75
  }
74
- fetchOptions.next = nextFetchOptions;
75
- }
76
- if (headers) {
77
- fetchOptions.headers = headers;
78
- }
79
- if (requestOptions?.body !== void 0) {
80
- if ((0, import_enlace_core.isJsonBody)(requestOptions.body)) {
81
- fetchOptions.body = JSON.stringify(requestOptions.body);
82
- headers = (0, import_enlace_core.mergeHeaders)(headers, { "Content-Type": "application/json" });
83
- if (headers) {
84
- fetchOptions.headers = headers;
85
- }
86
- } else {
87
- fetchOptions.body = requestOptions.body;
88
- }
89
- }
90
- const response = await fetch(url, fetchOptions);
91
- const contentType = response.headers.get("content-type");
92
- const isJson = contentType?.includes("application/json");
93
- if (response.ok) {
94
- if (!isGet && !requestOptions?.skipRevalidator) {
95
- const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
96
- const revalidatePaths = requestOptions?.revalidatePaths ?? [];
97
- if (revalidateTags.length || revalidatePaths.length) {
98
- revalidator?.(revalidateTags, revalidatePaths);
99
- }
100
- }
101
- return {
102
- ok: true,
103
- status: response.status,
104
- data: isJson ? await response.json() : response
105
- };
76
+ nextRequestOptions.next = nextFetchOptions;
106
77
  }
107
- return {
108
- ok: false,
109
- status: response.status,
110
- error: isJson ? await response.json() : response
111
- };
78
+ return (0, import_enlace_core.executeFetch)(
79
+ baseUrl,
80
+ path,
81
+ method,
82
+ { ...coreOptions, onSuccess: nextOnSuccess },
83
+ nextRequestOptions
84
+ );
112
85
  }
113
86
 
114
87
  // src/next/index.ts
115
88
  __reExport(next_exports, require("enlace-core"));
116
89
  function createEnlace(baseUrl, defaultOptions = {}, nextOptions = {}) {
117
90
  const combinedOptions = { ...defaultOptions, ...nextOptions };
118
- return (0, import_enlace_core2.createProxyHandler)(baseUrl, combinedOptions, [], executeNextFetch);
91
+ return (0, import_enlace_core2.createProxyHandler)(
92
+ baseUrl,
93
+ combinedOptions,
94
+ [],
95
+ executeNextFetch
96
+ );
119
97
  }
120
98
 
121
99
  // src/react/useQueryMode.ts
@@ -132,7 +110,7 @@ var initialState = {
132
110
  function hookReducer(state, action) {
133
111
  switch (action.type) {
134
112
  case "RESET":
135
- return action.state;
113
+ return action.state ?? initialState;
136
114
  case "FETCH_START":
137
115
  return {
138
116
  ...state,
@@ -257,11 +235,29 @@ function onRevalidate(callback) {
257
235
  }
258
236
 
259
237
  // src/react/useQueryMode.ts
238
+ function resolvePath(path, pathParams) {
239
+ if (!pathParams) return path;
240
+ return path.map((segment) => {
241
+ if (segment.startsWith(":")) {
242
+ const paramName = segment.slice(1);
243
+ const value = pathParams[paramName];
244
+ if (value === void 0) {
245
+ throw new Error(`Missing path parameter: ${paramName}`);
246
+ }
247
+ return String(value);
248
+ }
249
+ return segment;
250
+ });
251
+ }
260
252
  function useQueryMode(api, trackedCall, options) {
261
- const { autoGenerateTags, staleTime } = options;
253
+ const { autoGenerateTags, staleTime, enabled } = options;
262
254
  const queryKey = createQueryKey(trackedCall);
263
255
  const requestOptions = trackedCall.options;
264
- const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(trackedCall.path) : []);
256
+ const resolvedPath = resolvePath(
257
+ trackedCall.path,
258
+ requestOptions?.pathParams
259
+ );
260
+ const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
265
261
  const getCacheState = (includeNeedsFetch = false) => {
266
262
  const cached = getCache(queryKey);
267
263
  const hasCachedData = cached?.data !== void 0;
@@ -275,17 +271,22 @@ function useQueryMode(api, trackedCall, options) {
275
271
  error: cached?.error
276
272
  };
277
273
  };
278
- const [state, dispatch] = (0, import_react.useReducer)(hookReducer, null, () => getCacheState(true));
274
+ const [state, dispatch] = (0, import_react.useReducer)(
275
+ hookReducer,
276
+ null,
277
+ () => getCacheState(true)
278
+ );
279
279
  const mountedRef = (0, import_react.useRef)(true);
280
280
  const fetchRef = (0, import_react.useRef)(null);
281
281
  (0, import_react.useEffect)(() => {
282
282
  mountedRef.current = true;
283
+ if (!enabled) {
284
+ dispatch({ type: "RESET" });
285
+ return () => {
286
+ mountedRef.current = false;
287
+ };
288
+ }
283
289
  dispatch({ type: "RESET", state: getCacheState(true) });
284
- const unsubscribe = subscribeCache(queryKey, () => {
285
- if (mountedRef.current) {
286
- dispatch({ type: "SYNC_CACHE", state: getCacheState() });
287
- }
288
- });
289
290
  const doFetch = () => {
290
291
  const cached2 = getCache(queryKey);
291
292
  if (cached2?.promise) {
@@ -293,7 +294,7 @@ function useQueryMode(api, trackedCall, options) {
293
294
  }
294
295
  dispatch({ type: "FETCH_START" });
295
296
  let current = api;
296
- for (const segment of trackedCall.path) {
297
+ for (const segment of resolvedPath) {
297
298
  current = current[segment];
298
299
  }
299
300
  const method = current[trackedCall.method];
@@ -301,7 +302,7 @@ function useQueryMode(api, trackedCall, options) {
301
302
  if (mountedRef.current) {
302
303
  setCache(queryKey, {
303
304
  data: res.ok ? res.data : void 0,
304
- error: res.ok ? void 0 : res.error,
305
+ error: res.ok || res.status === 0 ? void 0 : res.error,
305
306
  ok: res.ok,
306
307
  timestamp: Date.now(),
307
308
  tags: queryTags
@@ -320,12 +321,17 @@ function useQueryMode(api, trackedCall, options) {
320
321
  } else {
321
322
  doFetch();
322
323
  }
324
+ const unsubscribe = subscribeCache(queryKey, () => {
325
+ if (mountedRef.current) {
326
+ dispatch({ type: "SYNC_CACHE", state: getCacheState() });
327
+ }
328
+ });
323
329
  return () => {
324
330
  mountedRef.current = false;
325
331
  fetchRef.current = null;
326
332
  unsubscribe();
327
333
  };
328
- }, [queryKey]);
334
+ }, [queryKey, enabled]);
329
335
  (0, import_react.useEffect)(() => {
330
336
  if (queryTags.length === 0) return;
331
337
  return onRevalidate((invalidatedTags) => {
@@ -340,23 +346,56 @@ function useQueryMode(api, trackedCall, options) {
340
346
 
341
347
  // src/react/useSelectorMode.ts
342
348
  var import_react2 = require("react");
343
- function useSelectorMode(method, path, autoRevalidateTags) {
349
+ function resolvePath2(path, pathParams) {
350
+ if (!pathParams) return path;
351
+ return path.map((segment) => {
352
+ if (segment.startsWith(":")) {
353
+ const paramName = segment.slice(1);
354
+ const value = pathParams[paramName];
355
+ if (value === void 0) {
356
+ throw new Error(`Missing path parameter: ${paramName}`);
357
+ }
358
+ return String(value);
359
+ }
360
+ return segment;
361
+ });
362
+ }
363
+ function hasPathParams(path) {
364
+ return path.some((segment) => segment.startsWith(":"));
365
+ }
366
+ function useSelectorMode(config) {
367
+ const { method, api, path, methodName, autoRevalidateTags } = config;
344
368
  const [state, dispatch] = (0, import_react2.useReducer)(hookReducer, initialState);
345
369
  const methodRef = (0, import_react2.useRef)(method);
370
+ const apiRef = (0, import_react2.useRef)(api);
346
371
  const triggerRef = (0, import_react2.useRef)(null);
347
372
  const pathRef = (0, import_react2.useRef)(path);
373
+ const methodNameRef = (0, import_react2.useRef)(methodName);
348
374
  const autoRevalidateRef = (0, import_react2.useRef)(autoRevalidateTags);
349
375
  methodRef.current = method;
376
+ apiRef.current = api;
350
377
  pathRef.current = path;
378
+ methodNameRef.current = methodName;
351
379
  autoRevalidateRef.current = autoRevalidateTags;
352
380
  if (!triggerRef.current) {
353
381
  triggerRef.current = (async (...args) => {
354
382
  dispatch({ type: "FETCH_START" });
355
- const res = await methodRef.current(...args);
383
+ const options = args[0];
384
+ const resolvedPath = resolvePath2(pathRef.current, options?.pathParams);
385
+ let res;
386
+ if (hasPathParams(pathRef.current)) {
387
+ let current = apiRef.current;
388
+ for (const segment of resolvedPath) {
389
+ current = current[segment];
390
+ }
391
+ const resolvedMethod = current[methodNameRef.current];
392
+ res = await resolvedMethod(...args);
393
+ } else {
394
+ res = await methodRef.current(...args);
395
+ }
356
396
  if (res.ok) {
357
397
  dispatch({ type: "FETCH_SUCCESS", data: res.data });
358
- const options = args[0];
359
- const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(pathRef.current) : []);
398
+ const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(resolvedPath) : []);
360
399
  if (tagsToInvalidate.length > 0) {
361
400
  invalidateTags(tagsToInvalidate);
362
401
  }
@@ -417,26 +456,30 @@ function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
417
456
  autoRevalidateTags,
418
457
  ...nextOptions
419
458
  });
420
- function useEnlaceHook(selectorOrQuery) {
459
+ function useEnlaceHook(selectorOrQuery, queryOptions) {
421
460
  let trackedCall = null;
422
461
  let selectorPath = null;
462
+ let selectorMethod = null;
423
463
  const trackingProxy = createTrackingProxy((result2) => {
424
464
  trackedCall = result2.trackedCall;
425
465
  selectorPath = result2.selectorPath;
466
+ selectorMethod = result2.selectorMethod;
426
467
  });
427
468
  const result = selectorOrQuery(trackingProxy);
428
469
  if (typeof result === "function") {
429
470
  const actualResult = selectorOrQuery(api);
430
- return useSelectorMode(
431
- actualResult,
432
- selectorPath ?? [],
471
+ return useSelectorMode({
472
+ method: actualResult,
473
+ api,
474
+ path: selectorPath ?? [],
475
+ methodName: selectorMethod ?? "",
433
476
  autoRevalidateTags
434
- );
477
+ });
435
478
  }
436
479
  return useQueryMode(
437
480
  api,
438
481
  trackedCall,
439
- { autoGenerateTags, staleTime }
482
+ { autoGenerateTags, staleTime, enabled: queryOptions?.enabled ?? true }
440
483
  );
441
484
  }
442
485
  return useEnlaceHook;