enlace 0.0.1-beta.4 → 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/README.md CHANGED
@@ -361,6 +361,92 @@ const useAPI = createEnlaceHook<ApiSchema>(
361
361
  );
362
362
  ```
363
363
 
364
+ ### Async Headers
365
+
366
+ Headers can be provided as a static value, sync function, or async function. This is useful when you need to fetch headers dynamically (e.g., auth tokens from async storage):
367
+
368
+ ```typescript
369
+ // Static headers
370
+ const useAPI = createEnlaceHook<ApiSchema>("https://api.example.com", {
371
+ headers: { Authorization: "Bearer token" },
372
+ });
373
+
374
+ // Sync function
375
+ const useAPI = createEnlaceHook<ApiSchema>("https://api.example.com", {
376
+ headers: () => ({ Authorization: `Bearer ${getToken()}` }),
377
+ });
378
+
379
+ // Async function
380
+ const useAPI = createEnlaceHook<ApiSchema>("https://api.example.com", {
381
+ headers: async () => {
382
+ const token = await getTokenFromStorage();
383
+ return { Authorization: `Bearer ${token}` };
384
+ },
385
+ });
386
+ ```
387
+
388
+ This also works for per-request headers:
389
+
390
+ ```typescript
391
+ const { data } = useAPI((api) =>
392
+ api.posts.get({
393
+ headers: async () => {
394
+ const token = await refreshToken();
395
+ return { Authorization: `Bearer ${token}` };
396
+ },
397
+ })
398
+ );
399
+ ```
400
+
401
+ ### Global Callbacks
402
+
403
+ You can set up global `onSuccess` and `onError` callbacks that are called for every request:
404
+
405
+ ```typescript
406
+ const useAPI = createEnlaceHook<ApiSchema>(
407
+ "https://api.example.com",
408
+ {
409
+ headers: { Authorization: "Bearer token" },
410
+ },
411
+ {
412
+ onSuccess: (payload) => {
413
+ console.log("Request succeeded:", payload.status, payload.data);
414
+ },
415
+ onError: (payload) => {
416
+ if (payload.status === 0) {
417
+ // Network error
418
+ console.error("Network error:", payload.error.message);
419
+ } else {
420
+ // HTTP error (4xx, 5xx)
421
+ console.error("HTTP error:", payload.status, payload.error);
422
+ }
423
+ },
424
+ }
425
+ );
426
+ ```
427
+
428
+ **Callback Payloads:**
429
+
430
+ ```typescript
431
+ // onSuccess payload
432
+ type EnlaceCallbackPayload<T> = {
433
+ status: number;
434
+ data: T;
435
+ headers: Headers;
436
+ };
437
+
438
+ // onError payload (HTTP error or network error)
439
+ type EnlaceErrorCallbackPayload<T> =
440
+ | { status: number; error: T; headers: Headers } // HTTP error
441
+ | { status: 0; error: Error; headers: null }; // Network error
442
+ ```
443
+
444
+ **Use cases:**
445
+ - Global error logging/reporting
446
+ - Toast notifications for all API errors
447
+ - Authentication refresh on 401 errors
448
+ - Analytics tracking
449
+
364
450
  ## Return Types
365
451
 
366
452
  ### Query Mode
@@ -403,6 +489,7 @@ type UseEnlaceSelectorResult<TMethod> = {
403
489
  type RequestOptions = {
404
490
  query?: Record<string, unknown>; // Query parameters
405
491
  body?: TBody; // Request body
492
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>); // Request headers
406
493
  tags?: string[]; // Cache tags (GET only)
407
494
  revalidateTags?: string[]; // Tags to invalidate after mutation
408
495
  pathParams?: Record<string, string | number>; // Dynamic path parameters
@@ -531,6 +618,8 @@ type EnlaceHookOptions = {
531
618
  autoGenerateTags?: boolean; // default: true
532
619
  autoRevalidateTags?: boolean; // default: true
533
620
  staleTime?: number; // default: 0
621
+ onSuccess?: (payload: EnlaceCallbackPayload<unknown>) => void;
622
+ onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
534
623
  };
535
624
  ```
536
625
 
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { WildcardClient, EnlaceClient, EnlaceResponse, EnlaceOptions } from 'enlace-core';
1
+ import { EnlaceCallbackPayload, EnlaceErrorCallbackPayload, EnlaceResponse, WildcardClient, EnlaceClient, EnlaceOptions } from 'enlace-core';
2
2
  export * from 'enlace-core';
3
3
 
4
4
  /** Per-request options for React hooks */
@@ -72,7 +72,7 @@ type UseEnlaceSelectorResult<TMethod> = {
72
72
  loading: boolean;
73
73
  fetching: boolean;
74
74
  } & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
75
-
75
+ /** Options for createEnlaceHook factory */
76
76
  type EnlaceHookOptions = {
77
77
  /**
78
78
  * Auto-generate cache tags from URL path for GET requests.
@@ -84,11 +84,17 @@ type EnlaceHookOptions = {
84
84
  autoRevalidateTags?: boolean;
85
85
  /** Time in ms before cached data is considered stale. @default 0 (always stale) */
86
86
  staleTime?: number;
87
+ /** Callback called on successful API responses */
88
+ onSuccess?: (payload: EnlaceCallbackPayload<unknown>) => void;
89
+ /** Callback called on error responses (HTTP errors or network failures) */
90
+ onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
87
91
  };
92
+ /** Hook type returned by createEnlaceHook */
88
93
  type EnlaceHook<TSchema> = {
89
94
  <TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: SelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
90
95
  <TData, TError>(queryFn: QueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
91
96
  };
97
+
92
98
  /**
93
99
  * Creates a React hook for making API calls.
94
100
  * Called at module level to create a reusable hook.
@@ -111,4 +117,4 @@ declare function onRevalidate(callback: Listener): () => void;
111
117
 
112
118
  declare function clearCache(key?: string): void;
113
119
 
114
- export { type ApiClient, type EnlaceHookOptions, HTTP_METHODS, type HookState, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, type UseEnlaceQueryOptions, type UseEnlaceQueryResult, type UseEnlaceSelectorResult, clearCache, createEnlaceHook, invalidateTags, onRevalidate };
120
+ export { type ApiClient, type EnlaceHook, type EnlaceHookOptions, HTTP_METHODS, type HookState, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, type UseEnlaceQueryOptions, type UseEnlaceQueryResult, type UseEnlaceSelectorResult, clearCache, createEnlaceHook, invalidateTags, onRevalidate };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { WildcardClient, EnlaceClient, EnlaceResponse, EnlaceOptions } from 'enlace-core';
1
+ import { EnlaceCallbackPayload, EnlaceErrorCallbackPayload, EnlaceResponse, WildcardClient, EnlaceClient, EnlaceOptions } from 'enlace-core';
2
2
  export * from 'enlace-core';
3
3
 
4
4
  /** Per-request options for React hooks */
@@ -72,7 +72,7 @@ type UseEnlaceSelectorResult<TMethod> = {
72
72
  loading: boolean;
73
73
  fetching: boolean;
74
74
  } & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
75
-
75
+ /** Options for createEnlaceHook factory */
76
76
  type EnlaceHookOptions = {
77
77
  /**
78
78
  * Auto-generate cache tags from URL path for GET requests.
@@ -84,11 +84,17 @@ type EnlaceHookOptions = {
84
84
  autoRevalidateTags?: boolean;
85
85
  /** Time in ms before cached data is considered stale. @default 0 (always stale) */
86
86
  staleTime?: number;
87
+ /** Callback called on successful API responses */
88
+ onSuccess?: (payload: EnlaceCallbackPayload<unknown>) => void;
89
+ /** Callback called on error responses (HTTP errors or network failures) */
90
+ onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
87
91
  };
92
+ /** Hook type returned by createEnlaceHook */
88
93
  type EnlaceHook<TSchema> = {
89
94
  <TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: SelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
90
95
  <TData, TError>(queryFn: QueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
91
96
  };
97
+
92
98
  /**
93
99
  * Creates a React hook for making API calls.
94
100
  * Called at module level to create a reusable hook.
@@ -111,4 +117,4 @@ declare function onRevalidate(callback: Listener): () => void;
111
117
 
112
118
  declare function clearCache(key?: string): void;
113
119
 
114
- export { type ApiClient, type EnlaceHookOptions, HTTP_METHODS, type HookState, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, type UseEnlaceQueryOptions, type UseEnlaceQueryResult, type UseEnlaceSelectorResult, clearCache, createEnlaceHook, invalidateTags, onRevalidate };
120
+ export { type ApiClient, type EnlaceHook, type EnlaceHookOptions, HTTP_METHODS, type HookState, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, type UseEnlaceQueryOptions, type UseEnlaceQueryResult, type UseEnlaceSelectorResult, clearCache, createEnlaceHook, invalidateTags, onRevalidate };
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ var initialState = {
47
47
  function hookReducer(state, action) {
48
48
  switch (action.type) {
49
49
  case "RESET":
50
- return action.state;
50
+ return action.state ?? initialState;
51
51
  case "FETCH_START":
52
52
  return {
53
53
  ...state,
@@ -202,7 +202,10 @@ function useQueryMode(api, trackedCall, options) {
202
202
  const { autoGenerateTags, staleTime, enabled } = options;
203
203
  const queryKey = createQueryKey(trackedCall);
204
204
  const requestOptions = trackedCall.options;
205
- const resolvedPath = resolvePath(trackedCall.path, requestOptions?.pathParams);
205
+ const resolvedPath = resolvePath(
206
+ trackedCall.path,
207
+ requestOptions?.pathParams
208
+ );
206
209
  const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
207
210
  const getCacheState = (includeNeedsFetch = false) => {
208
211
  const cached = getCache(queryKey);
@@ -217,26 +220,22 @@ function useQueryMode(api, trackedCall, options) {
217
220
  error: cached?.error
218
221
  };
219
222
  };
220
- const [state, dispatch] = (0, import_react.useReducer)(hookReducer, null, () => getCacheState(true));
223
+ const [state, dispatch] = (0, import_react.useReducer)(
224
+ hookReducer,
225
+ null,
226
+ () => getCacheState(true)
227
+ );
221
228
  const mountedRef = (0, import_react.useRef)(true);
222
229
  const fetchRef = (0, import_react.useRef)(null);
223
230
  (0, import_react.useEffect)(() => {
224
231
  mountedRef.current = true;
225
232
  if (!enabled) {
226
- dispatch({
227
- type: "RESET",
228
- state: { loading: false, fetching: false, ok: void 0, data: void 0, error: void 0 }
229
- });
233
+ dispatch({ type: "RESET" });
230
234
  return () => {
231
235
  mountedRef.current = false;
232
236
  };
233
237
  }
234
238
  dispatch({ type: "RESET", state: getCacheState(true) });
235
- const unsubscribe = subscribeCache(queryKey, () => {
236
- if (mountedRef.current) {
237
- dispatch({ type: "SYNC_CACHE", state: getCacheState() });
238
- }
239
- });
240
239
  const doFetch = () => {
241
240
  const cached2 = getCache(queryKey);
242
241
  if (cached2?.promise) {
@@ -252,7 +251,7 @@ function useQueryMode(api, trackedCall, options) {
252
251
  if (mountedRef.current) {
253
252
  setCache(queryKey, {
254
253
  data: res.ok ? res.data : void 0,
255
- error: res.ok ? void 0 : res.error,
254
+ error: res.ok || res.status === 0 ? void 0 : res.error,
256
255
  ok: res.ok,
257
256
  timestamp: Date.now(),
258
257
  tags: queryTags
@@ -271,6 +270,11 @@ function useQueryMode(api, trackedCall, options) {
271
270
  } else {
272
271
  doFetch();
273
272
  }
273
+ const unsubscribe = subscribeCache(queryKey, () => {
274
+ if (mountedRef.current) {
275
+ dispatch({ type: "SYNC_CACHE", state: getCacheState() });
276
+ }
277
+ });
274
278
  return () => {
275
279
  mountedRef.current = false;
276
280
  fetchRef.current = null;
@@ -390,12 +394,14 @@ function useSelectorMode(config) {
390
394
 
391
395
  // src/react/createEnlaceHook.ts
392
396
  function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
393
- const api = (0, import_enlace_core.createEnlace)(baseUrl, defaultOptions);
394
397
  const {
395
398
  autoGenerateTags = true,
396
399
  autoRevalidateTags = true,
397
- staleTime = 0
400
+ staleTime = 0,
401
+ onSuccess,
402
+ onError
398
403
  } = hookOptions;
404
+ const api = (0, import_enlace_core.createEnlace)(baseUrl, defaultOptions, { onSuccess, onError });
399
405
  function useEnlaceHook(selectorOrQuery, queryOptions) {
400
406
  let trackingResult = {
401
407
  trackedCall: null,
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,
@@ -175,7 +173,10 @@ function useQueryMode(api, trackedCall, options) {
175
173
  const { autoGenerateTags, staleTime, enabled } = options;
176
174
  const queryKey = createQueryKey(trackedCall);
177
175
  const requestOptions = trackedCall.options;
178
- const resolvedPath = resolvePath(trackedCall.path, requestOptions?.pathParams);
176
+ const resolvedPath = resolvePath(
177
+ trackedCall.path,
178
+ requestOptions?.pathParams
179
+ );
179
180
  const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
180
181
  const getCacheState = (includeNeedsFetch = false) => {
181
182
  const cached = getCache(queryKey);
@@ -190,26 +191,22 @@ function useQueryMode(api, trackedCall, options) {
190
191
  error: cached?.error
191
192
  };
192
193
  };
193
- const [state, dispatch] = useReducer(hookReducer, null, () => getCacheState(true));
194
+ const [state, dispatch] = useReducer(
195
+ hookReducer,
196
+ null,
197
+ () => getCacheState(true)
198
+ );
194
199
  const mountedRef = useRef(true);
195
200
  const fetchRef = useRef(null);
196
201
  useEffect(() => {
197
202
  mountedRef.current = true;
198
203
  if (!enabled) {
199
- dispatch({
200
- type: "RESET",
201
- state: { loading: false, fetching: false, ok: void 0, data: void 0, error: void 0 }
202
- });
204
+ dispatch({ type: "RESET" });
203
205
  return () => {
204
206
  mountedRef.current = false;
205
207
  };
206
208
  }
207
209
  dispatch({ type: "RESET", state: getCacheState(true) });
208
- const unsubscribe = subscribeCache(queryKey, () => {
209
- if (mountedRef.current) {
210
- dispatch({ type: "SYNC_CACHE", state: getCacheState() });
211
- }
212
- });
213
210
  const doFetch = () => {
214
211
  const cached2 = getCache(queryKey);
215
212
  if (cached2?.promise) {
@@ -225,7 +222,7 @@ function useQueryMode(api, trackedCall, options) {
225
222
  if (mountedRef.current) {
226
223
  setCache(queryKey, {
227
224
  data: res.ok ? res.data : void 0,
228
- error: res.ok ? void 0 : res.error,
225
+ error: res.ok || res.status === 0 ? void 0 : res.error,
229
226
  ok: res.ok,
230
227
  timestamp: Date.now(),
231
228
  tags: queryTags
@@ -244,6 +241,11 @@ function useQueryMode(api, trackedCall, options) {
244
241
  } else {
245
242
  doFetch();
246
243
  }
244
+ const unsubscribe = subscribeCache(queryKey, () => {
245
+ if (mountedRef.current) {
246
+ dispatch({ type: "SYNC_CACHE", state: getCacheState() });
247
+ }
248
+ });
247
249
  return () => {
248
250
  mountedRef.current = false;
249
251
  fetchRef.current = null;
@@ -363,12 +365,14 @@ function useSelectorMode(config) {
363
365
 
364
366
  // src/react/createEnlaceHook.ts
365
367
  function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
366
- const api = createEnlace(baseUrl, defaultOptions);
367
368
  const {
368
369
  autoGenerateTags = true,
369
370
  autoRevalidateTags = true,
370
- staleTime = 0
371
+ staleTime = 0,
372
+ onSuccess,
373
+ onError
371
374
  } = hookOptions;
375
+ const api = createEnlace(baseUrl, defaultOptions, { onSuccess, onError });
372
376
  function useEnlaceHook(selectorOrQuery, queryOptions) {
373
377
  let trackingResult = {
374
378
  trackedCall: null,
@@ -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 = {
@@ -58,7 +58,7 @@ type UseEnlaceSelectorResult<TMethod> = {
58
58
  loading: boolean;
59
59
  fetching: boolean;
60
60
  } & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
61
-
61
+ /** Options for createEnlaceHook factory */
62
62
  type EnlaceHookOptions = {
63
63
  /**
64
64
  * Auto-generate cache tags from URL path for GET requests.
@@ -70,6 +70,10 @@ type EnlaceHookOptions = {
70
70
  autoRevalidateTags?: boolean;
71
71
  /** Time in ms before cached data is considered stale. @default 0 (always stale) */
72
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;
73
77
  };
74
78
 
75
79
  /**
@@ -79,7 +83,7 @@ type EnlaceHookOptions = {
79
83
  */
80
84
  type RevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
81
85
  /** Next.js-specific options (third argument for createEnlace) */
82
- type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & {
86
+ type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
83
87
  /**
84
88
  * Handler called after successful mutations to trigger server-side revalidation.
85
89
  * Receives auto-generated or manually specified tags and paths.
@@ -113,13 +117,14 @@ type NextRequestOptionsBase = ReactRequestOptionsBase & {
113
117
  */
114
118
  skipRevalidator?: boolean;
115
119
  };
116
-
117
120
  type NextQueryFn<TSchema, TData, TError> = QueryFn<TSchema, TData, TError, NextRequestOptionsBase>;
118
121
  type NextSelectorFn<TSchema, TMethod> = SelectorFn<TSchema, TMethod, NextRequestOptionsBase>;
122
+ /** Hook type returned by Next.js createEnlaceHook */
119
123
  type NextEnlaceHook<TSchema> = {
120
124
  <TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: NextSelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
121
125
  <TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
122
126
  };
127
+
123
128
  /**
124
129
  * Creates a React hook for making API calls in Next.js Client Components.
125
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 = {
@@ -58,7 +58,7 @@ type UseEnlaceSelectorResult<TMethod> = {
58
58
  loading: boolean;
59
59
  fetching: boolean;
60
60
  } & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
61
-
61
+ /** Options for createEnlaceHook factory */
62
62
  type EnlaceHookOptions = {
63
63
  /**
64
64
  * Auto-generate cache tags from URL path for GET requests.
@@ -70,6 +70,10 @@ type EnlaceHookOptions = {
70
70
  autoRevalidateTags?: boolean;
71
71
  /** Time in ms before cached data is considered stale. @default 0 (always stale) */
72
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;
73
77
  };
74
78
 
75
79
  /**
@@ -79,7 +83,7 @@ type EnlaceHookOptions = {
79
83
  */
80
84
  type RevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
81
85
  /** Next.js-specific options (third argument for createEnlace) */
82
- type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & {
86
+ type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
83
87
  /**
84
88
  * Handler called after successful mutations to trigger server-side revalidation.
85
89
  * Receives auto-generated or manually specified tags and paths.
@@ -113,13 +117,14 @@ type NextRequestOptionsBase = ReactRequestOptionsBase & {
113
117
  */
114
118
  skipRevalidator?: boolean;
115
119
  };
116
-
117
120
  type NextQueryFn<TSchema, TData, TError> = QueryFn<TSchema, TData, TError, NextRequestOptionsBase>;
118
121
  type NextSelectorFn<TSchema, TMethod> = SelectorFn<TSchema, TMethod, NextRequestOptionsBase>;
122
+ /** Hook type returned by Next.js createEnlaceHook */
119
123
  type NextEnlaceHook<TSchema> = {
120
124
  <TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: NextSelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
121
125
  <TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
122
126
  };
127
+
123
128
  /**
124
129
  * Creates a React hook for making API calls in Next.js Client Components.
125
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;
76
+ nextRequestOptions.next = nextFetchOptions;
78
77
  }
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
- };
106
- }
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,
@@ -275,7 +253,10 @@ function useQueryMode(api, trackedCall, options) {
275
253
  const { autoGenerateTags, staleTime, enabled } = options;
276
254
  const queryKey = createQueryKey(trackedCall);
277
255
  const requestOptions = trackedCall.options;
278
- const resolvedPath = resolvePath(trackedCall.path, requestOptions?.pathParams);
256
+ const resolvedPath = resolvePath(
257
+ trackedCall.path,
258
+ requestOptions?.pathParams
259
+ );
279
260
  const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
280
261
  const getCacheState = (includeNeedsFetch = false) => {
281
262
  const cached = getCache(queryKey);
@@ -290,26 +271,22 @@ function useQueryMode(api, trackedCall, options) {
290
271
  error: cached?.error
291
272
  };
292
273
  };
293
- 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
+ );
294
279
  const mountedRef = (0, import_react.useRef)(true);
295
280
  const fetchRef = (0, import_react.useRef)(null);
296
281
  (0, import_react.useEffect)(() => {
297
282
  mountedRef.current = true;
298
283
  if (!enabled) {
299
- dispatch({
300
- type: "RESET",
301
- state: { loading: false, fetching: false, ok: void 0, data: void 0, error: void 0 }
302
- });
284
+ dispatch({ type: "RESET" });
303
285
  return () => {
304
286
  mountedRef.current = false;
305
287
  };
306
288
  }
307
289
  dispatch({ type: "RESET", state: getCacheState(true) });
308
- const unsubscribe = subscribeCache(queryKey, () => {
309
- if (mountedRef.current) {
310
- dispatch({ type: "SYNC_CACHE", state: getCacheState() });
311
- }
312
- });
313
290
  const doFetch = () => {
314
291
  const cached2 = getCache(queryKey);
315
292
  if (cached2?.promise) {
@@ -325,7 +302,7 @@ function useQueryMode(api, trackedCall, options) {
325
302
  if (mountedRef.current) {
326
303
  setCache(queryKey, {
327
304
  data: res.ok ? res.data : void 0,
328
- error: res.ok ? void 0 : res.error,
305
+ error: res.ok || res.status === 0 ? void 0 : res.error,
329
306
  ok: res.ok,
330
307
  timestamp: Date.now(),
331
308
  tags: queryTags
@@ -344,6 +321,11 @@ function useQueryMode(api, trackedCall, options) {
344
321
  } else {
345
322
  doFetch();
346
323
  }
324
+ const unsubscribe = subscribeCache(queryKey, () => {
325
+ if (mountedRef.current) {
326
+ dispatch({ type: "SYNC_CACHE", state: getCacheState() });
327
+ }
328
+ });
347
329
  return () => {
348
330
  mountedRef.current = false;
349
331
  fetchRef.current = null;
@@ -29,9 +29,7 @@ import {
29
29
 
30
30
  // src/next/fetch.ts
31
31
  import {
32
- buildUrl,
33
- isJsonBody,
34
- mergeHeaders
32
+ executeFetch
35
33
  } from "enlace-core";
36
34
 
37
35
  // src/utils/generateTags.ts
@@ -45,20 +43,22 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
45
43
  autoGenerateTags = true,
46
44
  autoRevalidateTags = true,
47
45
  revalidator,
48
- headers: defaultHeaders,
49
- ...restOptions
46
+ onSuccess,
47
+ ...coreOptions
50
48
  } = combinedOptions;
51
- const url = buildUrl(baseUrl, path, requestOptions?.query);
52
- let headers = mergeHeaders(defaultHeaders, requestOptions?.headers);
53
49
  const isGet = method === "GET";
54
50
  const autoTags = generateTags(path);
55
- const fetchOptions = {
56
- ...restOptions,
57
- method
51
+ const nextOnSuccess = (payload) => {
52
+ if (!isGet && !requestOptions?.skipRevalidator) {
53
+ const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
54
+ const revalidatePaths = requestOptions?.revalidatePaths ?? [];
55
+ if (revalidateTags.length || revalidatePaths.length) {
56
+ revalidator?.(revalidateTags, revalidatePaths);
57
+ }
58
+ }
59
+ onSuccess?.(payload);
58
60
  };
59
- if (requestOptions?.cache) {
60
- fetchOptions.cache = requestOptions.cache;
61
- }
61
+ const nextRequestOptions = { ...requestOptions };
62
62
  if (isGet) {
63
63
  const tags = requestOptions?.tags ?? (autoGenerateTags ? autoTags : void 0);
64
64
  const nextFetchOptions = {};
@@ -68,44 +68,15 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
68
68
  if (requestOptions?.revalidate !== void 0) {
69
69
  nextFetchOptions.revalidate = requestOptions.revalidate;
70
70
  }
71
- fetchOptions.next = nextFetchOptions;
72
- }
73
- if (headers) {
74
- fetchOptions.headers = headers;
71
+ nextRequestOptions.next = nextFetchOptions;
75
72
  }
76
- if (requestOptions?.body !== void 0) {
77
- if (isJsonBody(requestOptions.body)) {
78
- fetchOptions.body = JSON.stringify(requestOptions.body);
79
- headers = mergeHeaders(headers, { "Content-Type": "application/json" });
80
- if (headers) {
81
- fetchOptions.headers = headers;
82
- }
83
- } else {
84
- fetchOptions.body = requestOptions.body;
85
- }
86
- }
87
- const response = await fetch(url, fetchOptions);
88
- const contentType = response.headers.get("content-type");
89
- const isJson = contentType?.includes("application/json");
90
- if (response.ok) {
91
- if (!isGet && !requestOptions?.skipRevalidator) {
92
- const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
93
- const revalidatePaths = requestOptions?.revalidatePaths ?? [];
94
- if (revalidateTags.length || revalidatePaths.length) {
95
- revalidator?.(revalidateTags, revalidatePaths);
96
- }
97
- }
98
- return {
99
- ok: true,
100
- status: response.status,
101
- data: isJson ? await response.json() : response
102
- };
103
- }
104
- return {
105
- ok: false,
106
- status: response.status,
107
- error: isJson ? await response.json() : response
108
- };
73
+ return executeFetch(
74
+ baseUrl,
75
+ path,
76
+ method,
77
+ { ...coreOptions, onSuccess: nextOnSuccess },
78
+ nextRequestOptions
79
+ );
109
80
  }
110
81
 
111
82
  // src/next/index.ts
@@ -113,7 +84,12 @@ __reExport(next_exports, enlace_core_star);
113
84
  import * as enlace_core_star from "enlace-core";
114
85
  function createEnlace(baseUrl, defaultOptions = {}, nextOptions = {}) {
115
86
  const combinedOptions = { ...defaultOptions, ...nextOptions };
116
- return createProxyHandler(baseUrl, combinedOptions, [], executeNextFetch);
87
+ return createProxyHandler(
88
+ baseUrl,
89
+ combinedOptions,
90
+ [],
91
+ executeNextFetch
92
+ );
117
93
  }
118
94
 
119
95
  // src/react/useQueryMode.ts
@@ -130,7 +106,7 @@ var initialState = {
130
106
  function hookReducer(state, action) {
131
107
  switch (action.type) {
132
108
  case "RESET":
133
- return action.state;
109
+ return action.state ?? initialState;
134
110
  case "FETCH_START":
135
111
  return {
136
112
  ...state,
@@ -273,7 +249,10 @@ function useQueryMode(api, trackedCall, options) {
273
249
  const { autoGenerateTags, staleTime, enabled } = options;
274
250
  const queryKey = createQueryKey(trackedCall);
275
251
  const requestOptions = trackedCall.options;
276
- const resolvedPath = resolvePath(trackedCall.path, requestOptions?.pathParams);
252
+ const resolvedPath = resolvePath(
253
+ trackedCall.path,
254
+ requestOptions?.pathParams
255
+ );
277
256
  const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
278
257
  const getCacheState = (includeNeedsFetch = false) => {
279
258
  const cached = getCache(queryKey);
@@ -288,26 +267,22 @@ function useQueryMode(api, trackedCall, options) {
288
267
  error: cached?.error
289
268
  };
290
269
  };
291
- const [state, dispatch] = useReducer(hookReducer, null, () => getCacheState(true));
270
+ const [state, dispatch] = useReducer(
271
+ hookReducer,
272
+ null,
273
+ () => getCacheState(true)
274
+ );
292
275
  const mountedRef = useRef(true);
293
276
  const fetchRef = useRef(null);
294
277
  useEffect(() => {
295
278
  mountedRef.current = true;
296
279
  if (!enabled) {
297
- dispatch({
298
- type: "RESET",
299
- state: { loading: false, fetching: false, ok: void 0, data: void 0, error: void 0 }
300
- });
280
+ dispatch({ type: "RESET" });
301
281
  return () => {
302
282
  mountedRef.current = false;
303
283
  };
304
284
  }
305
285
  dispatch({ type: "RESET", state: getCacheState(true) });
306
- const unsubscribe = subscribeCache(queryKey, () => {
307
- if (mountedRef.current) {
308
- dispatch({ type: "SYNC_CACHE", state: getCacheState() });
309
- }
310
- });
311
286
  const doFetch = () => {
312
287
  const cached2 = getCache(queryKey);
313
288
  if (cached2?.promise) {
@@ -323,7 +298,7 @@ function useQueryMode(api, trackedCall, options) {
323
298
  if (mountedRef.current) {
324
299
  setCache(queryKey, {
325
300
  data: res.ok ? res.data : void 0,
326
- error: res.ok ? void 0 : res.error,
301
+ error: res.ok || res.status === 0 ? void 0 : res.error,
327
302
  ok: res.ok,
328
303
  timestamp: Date.now(),
329
304
  tags: queryTags
@@ -342,6 +317,11 @@ function useQueryMode(api, trackedCall, options) {
342
317
  } else {
343
318
  doFetch();
344
319
  }
320
+ const unsubscribe = subscribeCache(queryKey, () => {
321
+ if (mountedRef.current) {
322
+ dispatch({ type: "SYNC_CACHE", state: getCacheState() });
323
+ }
324
+ });
345
325
  return () => {
346
326
  mountedRef.current = false;
347
327
  fetchRef.current = null;
@@ -1,6 +1,6 @@
1
- import { EnlaceOptions, WildcardClient, EnlaceClient } from 'enlace-core';
1
+ import { EnlaceCallbackPayload, EnlaceErrorCallbackPayload, WildcardClient, EnlaceClient, EnlaceResponse, EnlaceCallbacks, EnlaceOptions } from 'enlace-core';
2
2
  export * from 'enlace-core';
3
- export { EnlaceOptions } from 'enlace-core';
3
+ export { EnlaceCallbacks, EnlaceOptions } from 'enlace-core';
4
4
 
5
5
  /** Per-request options for React hooks */
6
6
  type ReactRequestOptionsBase = {
@@ -21,7 +21,46 @@ type ReactRequestOptionsBase = {
21
21
  */
22
22
  pathParams?: Record<string, string | number>;
23
23
  };
24
-
24
+ /** Options for query mode hooks */
25
+ type UseEnlaceQueryOptions = {
26
+ /**
27
+ * Whether the query should execute.
28
+ * Set to false to skip fetching (useful when ID is "new" or undefined).
29
+ * @default true
30
+ */
31
+ enabled?: boolean;
32
+ };
33
+ type ApiClient<TSchema, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TOptions>;
34
+ type QueryFn<TSchema, TData, TError, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
35
+ type SelectorFn<TSchema, TMethod, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => TMethod;
36
+ type ExtractData<T> = T extends (...args: any[]) => Promise<EnlaceResponse<infer D, unknown>> ? D : never;
37
+ type ExtractError<T> = T extends (...args: any[]) => Promise<EnlaceResponse<unknown, infer E>> ? E : never;
38
+ /** Discriminated union for hook response state - enables type narrowing on ok check */
39
+ type HookResponseState<TData, TError> = {
40
+ ok: undefined;
41
+ data: undefined;
42
+ error: undefined;
43
+ } | {
44
+ ok: true;
45
+ data: TData;
46
+ error: undefined;
47
+ } | {
48
+ ok: false;
49
+ data: undefined;
50
+ error: TError;
51
+ };
52
+ /** Result when hook is called with query function (auto-fetch mode) */
53
+ type UseEnlaceQueryResult<TData, TError> = {
54
+ loading: boolean;
55
+ fetching: boolean;
56
+ } & HookResponseState<TData, TError>;
57
+ /** Result when hook is called with method selector (trigger mode) */
58
+ type UseEnlaceSelectorResult<TMethod> = {
59
+ trigger: TMethod;
60
+ loading: boolean;
61
+ fetching: boolean;
62
+ } & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
63
+ /** Options for createEnlaceHook factory */
25
64
  type EnlaceHookOptions = {
26
65
  /**
27
66
  * Auto-generate cache tags from URL path for GET requests.
@@ -33,6 +72,10 @@ type EnlaceHookOptions = {
33
72
  autoRevalidateTags?: boolean;
34
73
  /** Time in ms before cached data is considered stale. @default 0 (always stale) */
35
74
  staleTime?: number;
75
+ /** Callback called on successful API responses */
76
+ onSuccess?: (payload: EnlaceCallbackPayload<unknown>) => void;
77
+ /** Callback called on error responses (HTTP errors or network failures) */
78
+ onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
36
79
  };
37
80
 
38
81
  /**
@@ -42,7 +85,7 @@ type EnlaceHookOptions = {
42
85
  */
43
86
  type RevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
44
87
  /** Next.js-specific options (third argument for createEnlace) */
45
- type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & {
88
+ type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
46
89
  /**
47
90
  * Handler called after successful mutations to trigger server-side revalidation.
48
91
  * Receives auto-generated or manually specified tags and paths.
@@ -76,7 +119,15 @@ type NextRequestOptionsBase = ReactRequestOptionsBase & {
76
119
  */
77
120
  skipRevalidator?: boolean;
78
121
  };
122
+ type NextApiClient<TSchema> = ApiClient<TSchema, NextRequestOptionsBase>;
123
+ type NextQueryFn<TSchema, TData, TError> = QueryFn<TSchema, TData, TError, NextRequestOptionsBase>;
124
+ type NextSelectorFn<TSchema, TMethod> = SelectorFn<TSchema, TMethod, NextRequestOptionsBase>;
125
+ /** Hook type returned by Next.js createEnlaceHook */
126
+ type NextEnlaceHook<TSchema> = {
127
+ <TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: NextSelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
128
+ <TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
129
+ };
79
130
 
80
- declare function createEnlace<TSchema = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, NextRequestOptionsBase>;
131
+ declare function createEnlace<TSchema = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions | null, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, NextRequestOptionsBase>;
81
132
 
82
- export { type NextHookOptions, type NextOptions, type NextRequestOptionsBase, type RevalidateHandler, createEnlace };
133
+ export { type NextApiClient, type NextEnlaceHook, type NextHookOptions, type NextOptions, type NextQueryFn, type NextRequestOptionsBase, type NextSelectorFn, type RevalidateHandler, createEnlace };
@@ -1,6 +1,6 @@
1
- import { EnlaceOptions, WildcardClient, EnlaceClient } from 'enlace-core';
1
+ import { EnlaceCallbackPayload, EnlaceErrorCallbackPayload, WildcardClient, EnlaceClient, EnlaceResponse, EnlaceCallbacks, EnlaceOptions } from 'enlace-core';
2
2
  export * from 'enlace-core';
3
- export { EnlaceOptions } from 'enlace-core';
3
+ export { EnlaceCallbacks, EnlaceOptions } from 'enlace-core';
4
4
 
5
5
  /** Per-request options for React hooks */
6
6
  type ReactRequestOptionsBase = {
@@ -21,7 +21,46 @@ type ReactRequestOptionsBase = {
21
21
  */
22
22
  pathParams?: Record<string, string | number>;
23
23
  };
24
-
24
+ /** Options for query mode hooks */
25
+ type UseEnlaceQueryOptions = {
26
+ /**
27
+ * Whether the query should execute.
28
+ * Set to false to skip fetching (useful when ID is "new" or undefined).
29
+ * @default true
30
+ */
31
+ enabled?: boolean;
32
+ };
33
+ type ApiClient<TSchema, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TOptions>;
34
+ type QueryFn<TSchema, TData, TError, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
35
+ type SelectorFn<TSchema, TMethod, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => TMethod;
36
+ type ExtractData<T> = T extends (...args: any[]) => Promise<EnlaceResponse<infer D, unknown>> ? D : never;
37
+ type ExtractError<T> = T extends (...args: any[]) => Promise<EnlaceResponse<unknown, infer E>> ? E : never;
38
+ /** Discriminated union for hook response state - enables type narrowing on ok check */
39
+ type HookResponseState<TData, TError> = {
40
+ ok: undefined;
41
+ data: undefined;
42
+ error: undefined;
43
+ } | {
44
+ ok: true;
45
+ data: TData;
46
+ error: undefined;
47
+ } | {
48
+ ok: false;
49
+ data: undefined;
50
+ error: TError;
51
+ };
52
+ /** Result when hook is called with query function (auto-fetch mode) */
53
+ type UseEnlaceQueryResult<TData, TError> = {
54
+ loading: boolean;
55
+ fetching: boolean;
56
+ } & HookResponseState<TData, TError>;
57
+ /** Result when hook is called with method selector (trigger mode) */
58
+ type UseEnlaceSelectorResult<TMethod> = {
59
+ trigger: TMethod;
60
+ loading: boolean;
61
+ fetching: boolean;
62
+ } & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
63
+ /** Options for createEnlaceHook factory */
25
64
  type EnlaceHookOptions = {
26
65
  /**
27
66
  * Auto-generate cache tags from URL path for GET requests.
@@ -33,6 +72,10 @@ type EnlaceHookOptions = {
33
72
  autoRevalidateTags?: boolean;
34
73
  /** Time in ms before cached data is considered stale. @default 0 (always stale) */
35
74
  staleTime?: number;
75
+ /** Callback called on successful API responses */
76
+ onSuccess?: (payload: EnlaceCallbackPayload<unknown>) => void;
77
+ /** Callback called on error responses (HTTP errors or network failures) */
78
+ onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
36
79
  };
37
80
 
38
81
  /**
@@ -42,7 +85,7 @@ type EnlaceHookOptions = {
42
85
  */
43
86
  type RevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
44
87
  /** Next.js-specific options (third argument for createEnlace) */
45
- type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & {
88
+ type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
46
89
  /**
47
90
  * Handler called after successful mutations to trigger server-side revalidation.
48
91
  * Receives auto-generated or manually specified tags and paths.
@@ -76,7 +119,15 @@ type NextRequestOptionsBase = ReactRequestOptionsBase & {
76
119
  */
77
120
  skipRevalidator?: boolean;
78
121
  };
122
+ type NextApiClient<TSchema> = ApiClient<TSchema, NextRequestOptionsBase>;
123
+ type NextQueryFn<TSchema, TData, TError> = QueryFn<TSchema, TData, TError, NextRequestOptionsBase>;
124
+ type NextSelectorFn<TSchema, TMethod> = SelectorFn<TSchema, TMethod, NextRequestOptionsBase>;
125
+ /** Hook type returned by Next.js createEnlaceHook */
126
+ type NextEnlaceHook<TSchema> = {
127
+ <TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: NextSelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
128
+ <TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
129
+ };
79
130
 
80
- declare function createEnlace<TSchema = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, NextRequestOptionsBase>;
131
+ declare function createEnlace<TSchema = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions | null, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, NextRequestOptionsBase>;
81
132
 
82
- export { type NextHookOptions, type NextOptions, type NextRequestOptionsBase, type RevalidateHandler, createEnlace };
133
+ export { type NextApiClient, type NextEnlaceHook, type NextHookOptions, type NextOptions, type NextQueryFn, type NextRequestOptionsBase, type NextSelectorFn, type RevalidateHandler, createEnlace };
@@ -40,20 +40,22 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
40
40
  autoGenerateTags = true,
41
41
  autoRevalidateTags = true,
42
42
  revalidator,
43
- headers: defaultHeaders,
44
- ...restOptions
43
+ onSuccess,
44
+ ...coreOptions
45
45
  } = combinedOptions;
46
- const url = (0, import_enlace_core.buildUrl)(baseUrl, path, requestOptions?.query);
47
- let headers = (0, import_enlace_core.mergeHeaders)(defaultHeaders, requestOptions?.headers);
48
46
  const isGet = method === "GET";
49
47
  const autoTags = generateTags(path);
50
- const fetchOptions = {
51
- ...restOptions,
52
- method
48
+ const nextOnSuccess = (payload) => {
49
+ if (!isGet && !requestOptions?.skipRevalidator) {
50
+ const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
51
+ const revalidatePaths = requestOptions?.revalidatePaths ?? [];
52
+ if (revalidateTags.length || revalidatePaths.length) {
53
+ revalidator?.(revalidateTags, revalidatePaths);
54
+ }
55
+ }
56
+ onSuccess?.(payload);
53
57
  };
54
- if (requestOptions?.cache) {
55
- fetchOptions.cache = requestOptions.cache;
56
- }
58
+ const nextRequestOptions = { ...requestOptions };
57
59
  if (isGet) {
58
60
  const tags = requestOptions?.tags ?? (autoGenerateTags ? autoTags : void 0);
59
61
  const nextFetchOptions = {};
@@ -63,49 +65,25 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
63
65
  if (requestOptions?.revalidate !== void 0) {
64
66
  nextFetchOptions.revalidate = requestOptions.revalidate;
65
67
  }
66
- fetchOptions.next = nextFetchOptions;
67
- }
68
- if (headers) {
69
- fetchOptions.headers = headers;
68
+ nextRequestOptions.next = nextFetchOptions;
70
69
  }
71
- if (requestOptions?.body !== void 0) {
72
- if ((0, import_enlace_core.isJsonBody)(requestOptions.body)) {
73
- fetchOptions.body = JSON.stringify(requestOptions.body);
74
- headers = (0, import_enlace_core.mergeHeaders)(headers, { "Content-Type": "application/json" });
75
- if (headers) {
76
- fetchOptions.headers = headers;
77
- }
78
- } else {
79
- fetchOptions.body = requestOptions.body;
80
- }
81
- }
82
- const response = await fetch(url, fetchOptions);
83
- const contentType = response.headers.get("content-type");
84
- const isJson = contentType?.includes("application/json");
85
- if (response.ok) {
86
- if (!isGet && !requestOptions?.skipRevalidator) {
87
- const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
88
- const revalidatePaths = requestOptions?.revalidatePaths ?? [];
89
- if (revalidateTags.length || revalidatePaths.length) {
90
- revalidator?.(revalidateTags, revalidatePaths);
91
- }
92
- }
93
- return {
94
- ok: true,
95
- status: response.status,
96
- data: isJson ? await response.json() : response
97
- };
98
- }
99
- return {
100
- ok: false,
101
- status: response.status,
102
- error: isJson ? await response.json() : response
103
- };
70
+ return (0, import_enlace_core.executeFetch)(
71
+ baseUrl,
72
+ path,
73
+ method,
74
+ { ...coreOptions, onSuccess: nextOnSuccess },
75
+ nextRequestOptions
76
+ );
104
77
  }
105
78
 
106
79
  // src/next/index.ts
107
80
  __reExport(next_exports, require("enlace-core"), module.exports);
108
81
  function createEnlace(baseUrl, defaultOptions = {}, nextOptions = {}) {
109
82
  const combinedOptions = { ...defaultOptions, ...nextOptions };
110
- return (0, import_enlace_core2.createProxyHandler)(baseUrl, combinedOptions, [], executeNextFetch);
83
+ return (0, import_enlace_core2.createProxyHandler)(
84
+ baseUrl,
85
+ combinedOptions,
86
+ [],
87
+ executeNextFetch
88
+ );
111
89
  }
@@ -5,9 +5,7 @@ import {
5
5
 
6
6
  // src/next/fetch.ts
7
7
  import {
8
- buildUrl,
9
- isJsonBody,
10
- mergeHeaders
8
+ executeFetch
11
9
  } from "enlace-core";
12
10
 
13
11
  // src/utils/generateTags.ts
@@ -21,20 +19,22 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
21
19
  autoGenerateTags = true,
22
20
  autoRevalidateTags = true,
23
21
  revalidator,
24
- headers: defaultHeaders,
25
- ...restOptions
22
+ onSuccess,
23
+ ...coreOptions
26
24
  } = combinedOptions;
27
- const url = buildUrl(baseUrl, path, requestOptions?.query);
28
- let headers = mergeHeaders(defaultHeaders, requestOptions?.headers);
29
25
  const isGet = method === "GET";
30
26
  const autoTags = generateTags(path);
31
- const fetchOptions = {
32
- ...restOptions,
33
- method
27
+ const nextOnSuccess = (payload) => {
28
+ if (!isGet && !requestOptions?.skipRevalidator) {
29
+ const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
30
+ const revalidatePaths = requestOptions?.revalidatePaths ?? [];
31
+ if (revalidateTags.length || revalidatePaths.length) {
32
+ revalidator?.(revalidateTags, revalidatePaths);
33
+ }
34
+ }
35
+ onSuccess?.(payload);
34
36
  };
35
- if (requestOptions?.cache) {
36
- fetchOptions.cache = requestOptions.cache;
37
- }
37
+ const nextRequestOptions = { ...requestOptions };
38
38
  if (isGet) {
39
39
  const tags = requestOptions?.tags ?? (autoGenerateTags ? autoTags : void 0);
40
40
  const nextFetchOptions = {};
@@ -44,51 +44,27 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
44
44
  if (requestOptions?.revalidate !== void 0) {
45
45
  nextFetchOptions.revalidate = requestOptions.revalidate;
46
46
  }
47
- fetchOptions.next = nextFetchOptions;
48
- }
49
- if (headers) {
50
- fetchOptions.headers = headers;
47
+ nextRequestOptions.next = nextFetchOptions;
51
48
  }
52
- if (requestOptions?.body !== void 0) {
53
- if (isJsonBody(requestOptions.body)) {
54
- fetchOptions.body = JSON.stringify(requestOptions.body);
55
- headers = mergeHeaders(headers, { "Content-Type": "application/json" });
56
- if (headers) {
57
- fetchOptions.headers = headers;
58
- }
59
- } else {
60
- fetchOptions.body = requestOptions.body;
61
- }
62
- }
63
- const response = await fetch(url, fetchOptions);
64
- const contentType = response.headers.get("content-type");
65
- const isJson = contentType?.includes("application/json");
66
- if (response.ok) {
67
- if (!isGet && !requestOptions?.skipRevalidator) {
68
- const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
69
- const revalidatePaths = requestOptions?.revalidatePaths ?? [];
70
- if (revalidateTags.length || revalidatePaths.length) {
71
- revalidator?.(revalidateTags, revalidatePaths);
72
- }
73
- }
74
- return {
75
- ok: true,
76
- status: response.status,
77
- data: isJson ? await response.json() : response
78
- };
79
- }
80
- return {
81
- ok: false,
82
- status: response.status,
83
- error: isJson ? await response.json() : response
84
- };
49
+ return executeFetch(
50
+ baseUrl,
51
+ path,
52
+ method,
53
+ { ...coreOptions, onSuccess: nextOnSuccess },
54
+ nextRequestOptions
55
+ );
85
56
  }
86
57
 
87
58
  // src/next/index.ts
88
59
  export * from "enlace-core";
89
60
  function createEnlace(baseUrl, defaultOptions = {}, nextOptions = {}) {
90
61
  const combinedOptions = { ...defaultOptions, ...nextOptions };
91
- return createProxyHandler(baseUrl, combinedOptions, [], executeNextFetch);
62
+ return createProxyHandler(
63
+ baseUrl,
64
+ combinedOptions,
65
+ [],
66
+ executeNextFetch
67
+ );
92
68
  }
93
69
  export {
94
70
  createEnlace
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enlace",
3
- "version": "0.0.1-beta.4",
3
+ "version": "0.0.1-beta.5",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"
@@ -23,7 +23,7 @@
23
23
  }
24
24
  },
25
25
  "dependencies": {
26
- "enlace-core": "0.0.1-beta.2"
26
+ "enlace-core": "0.0.1-beta.3"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": "^19"