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/README.md +188 -0
- package/dist/index.d.mts +27 -4
- package/dist/index.d.ts +27 -4
- package/dist/index.js +89 -24
- package/dist/index.mjs +90 -27
- package/dist/next/hook/index.d.mts +27 -5
- package/dist/next/hook/index.d.ts +27 -5
- package/dist/next/hook/index.js +113 -70
- package/dist/next/hook/index.mjs +114 -73
- package/dist/next/index.d.mts +65 -6
- package/dist/next/index.d.ts +65 -6
- package/dist/next/index.js +26 -48
- package/dist/next/index.mjs +27 -51
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -117,6 +117,34 @@ function Posts({ page, limit }: { page: number; limit: number }) {
|
|
|
117
117
|
- Returns cached data while revalidating
|
|
118
118
|
- **Request deduplication** — identical requests from multiple components trigger only one fetch
|
|
119
119
|
|
|
120
|
+
### Conditional Fetching
|
|
121
|
+
|
|
122
|
+
Skip fetching with the `enabled` option:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
function ProductForm({ id }: { id: string | "new" }) {
|
|
126
|
+
// Skip fetching when creating a new product
|
|
127
|
+
const { data, loading } = useAPI(
|
|
128
|
+
(api) => api.products[id].get(),
|
|
129
|
+
{ enabled: id !== "new" }
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (id === "new") return <CreateProductForm />;
|
|
133
|
+
if (loading) return <div>Loading...</div>;
|
|
134
|
+
return <EditProductForm product={data} />;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Also useful when waiting for a dependency
|
|
140
|
+
function UserPosts({ userId }: { userId: string | undefined }) {
|
|
141
|
+
const { data } = useAPI(
|
|
142
|
+
(api) => api.users[userId!].posts.get(),
|
|
143
|
+
{ enabled: userId !== undefined }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
120
148
|
```typescript
|
|
121
149
|
function Post({ id }: { id: number }) {
|
|
122
150
|
// Automatically re-fetches when `id` or query values change
|
|
@@ -186,6 +214,56 @@ function CreatePost() {
|
|
|
186
214
|
}
|
|
187
215
|
```
|
|
188
216
|
|
|
217
|
+
### Dynamic Path Parameters
|
|
218
|
+
|
|
219
|
+
Use `:paramName` syntax for dynamic IDs passed at trigger time:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
function PostList({ posts }: { posts: Post[] }) {
|
|
223
|
+
// Define once with :id placeholder
|
|
224
|
+
const { trigger, loading } = useAPI((api) => api.posts[":id"].delete);
|
|
225
|
+
|
|
226
|
+
const handleDelete = (postId: number) => {
|
|
227
|
+
// Pass the actual ID when triggering
|
|
228
|
+
trigger({ pathParams: { id: postId } });
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<ul>
|
|
233
|
+
{posts.map((post) => (
|
|
234
|
+
<li key={post.id}>
|
|
235
|
+
{post.title}
|
|
236
|
+
<button onClick={() => handleDelete(post.id)} disabled={loading}>
|
|
237
|
+
Delete
|
|
238
|
+
</button>
|
|
239
|
+
</li>
|
|
240
|
+
))}
|
|
241
|
+
</ul>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Multiple path parameters:**
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
const { trigger } = useAPI((api) => api.users[":userId"].posts[":postId"].delete);
|
|
250
|
+
|
|
251
|
+
trigger({ pathParams: { userId: "1", postId: "42" } });
|
|
252
|
+
// → DELETE /users/1/posts/42
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**With request body:**
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const { trigger } = useAPI((api) => api.products[":id"].patch);
|
|
259
|
+
|
|
260
|
+
trigger({
|
|
261
|
+
pathParams: { id: "123" },
|
|
262
|
+
body: { name: "Updated Product" },
|
|
263
|
+
});
|
|
264
|
+
// → PATCH /products/123 with body
|
|
265
|
+
```
|
|
266
|
+
|
|
189
267
|
## Caching & Auto-Revalidation
|
|
190
268
|
|
|
191
269
|
### Automatic Cache Tags (Zero Config)
|
|
@@ -283,11 +361,106 @@ const useAPI = createEnlaceHook<ApiSchema>(
|
|
|
283
361
|
);
|
|
284
362
|
```
|
|
285
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
|
+
|
|
286
450
|
## Return Types
|
|
287
451
|
|
|
288
452
|
### Query Mode
|
|
289
453
|
|
|
290
454
|
```typescript
|
|
455
|
+
// Basic usage
|
|
456
|
+
const result = useAPI((api) => api.posts.get());
|
|
457
|
+
|
|
458
|
+
// With options
|
|
459
|
+
const result = useAPI(
|
|
460
|
+
(api) => api.posts.get(),
|
|
461
|
+
{ enabled: true } // Skip fetching when false
|
|
462
|
+
);
|
|
463
|
+
|
|
291
464
|
type UseEnlaceQueryResult<TData, TError> = {
|
|
292
465
|
loading: boolean; // No cached data and fetching
|
|
293
466
|
fetching: boolean; // Request in progress
|
|
@@ -310,6 +483,19 @@ type UseEnlaceSelectorResult<TMethod> = {
|
|
|
310
483
|
};
|
|
311
484
|
```
|
|
312
485
|
|
|
486
|
+
### Request Options
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
type RequestOptions = {
|
|
490
|
+
query?: Record<string, unknown>; // Query parameters
|
|
491
|
+
body?: TBody; // Request body
|
|
492
|
+
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>); // Request headers
|
|
493
|
+
tags?: string[]; // Cache tags (GET only)
|
|
494
|
+
revalidateTags?: string[]; // Tags to invalidate after mutation
|
|
495
|
+
pathParams?: Record<string, string | number>; // Dynamic path parameters
|
|
496
|
+
};
|
|
497
|
+
```
|
|
498
|
+
|
|
313
499
|
---
|
|
314
500
|
|
|
315
501
|
## Next.js Integration
|
|
@@ -432,6 +618,8 @@ type EnlaceHookOptions = {
|
|
|
432
618
|
autoGenerateTags?: boolean; // default: true
|
|
433
619
|
autoRevalidateTags?: boolean; // default: true
|
|
434
620
|
staleTime?: number; // default: 0
|
|
621
|
+
onSuccess?: (payload: EnlaceCallbackPayload<unknown>) => void;
|
|
622
|
+
onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
|
|
435
623
|
};
|
|
436
624
|
```
|
|
437
625
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 */
|
|
@@ -11,6 +11,23 @@ type ReactRequestOptionsBase = {
|
|
|
11
11
|
tags?: string[];
|
|
12
12
|
/** Tags to invalidate after mutation (triggers refetch in matching queries) */
|
|
13
13
|
revalidateTags?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Path parameters for dynamic URL segments.
|
|
16
|
+
* Used to replace :paramName placeholders in the URL path.
|
|
17
|
+
* @example
|
|
18
|
+
* // With path api.products[':id'].delete
|
|
19
|
+
* trigger({ pathParams: { id: '123' } }) // → DELETE /products/123
|
|
20
|
+
*/
|
|
21
|
+
pathParams?: Record<string, string | number>;
|
|
22
|
+
};
|
|
23
|
+
/** Options for query mode hooks */
|
|
24
|
+
type UseEnlaceQueryOptions = {
|
|
25
|
+
/**
|
|
26
|
+
* Whether the query should execute.
|
|
27
|
+
* Set to false to skip fetching (useful when ID is "new" or undefined).
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
enabled?: boolean;
|
|
14
31
|
};
|
|
15
32
|
type ApiClient<TSchema, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TOptions>;
|
|
16
33
|
type QueryFn<TSchema, TData, TError, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
|
|
@@ -55,7 +72,7 @@ type UseEnlaceSelectorResult<TMethod> = {
|
|
|
55
72
|
loading: boolean;
|
|
56
73
|
fetching: boolean;
|
|
57
74
|
} & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
|
|
58
|
-
|
|
75
|
+
/** Options for createEnlaceHook factory */
|
|
59
76
|
type EnlaceHookOptions = {
|
|
60
77
|
/**
|
|
61
78
|
* Auto-generate cache tags from URL path for GET requests.
|
|
@@ -67,11 +84,17 @@ type EnlaceHookOptions = {
|
|
|
67
84
|
autoRevalidateTags?: boolean;
|
|
68
85
|
/** Time in ms before cached data is considered stale. @default 0 (always stale) */
|
|
69
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;
|
|
70
91
|
};
|
|
92
|
+
/** Hook type returned by createEnlaceHook */
|
|
71
93
|
type EnlaceHook<TSchema> = {
|
|
72
94
|
<TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: SelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
|
|
73
|
-
<TData, TError>(queryFn: QueryFn<TSchema, TData, TError
|
|
95
|
+
<TData, TError>(queryFn: QueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
|
|
74
96
|
};
|
|
97
|
+
|
|
75
98
|
/**
|
|
76
99
|
* Creates a React hook for making API calls.
|
|
77
100
|
* Called at module level to create a reusable hook.
|
|
@@ -94,4 +117,4 @@ declare function onRevalidate(callback: Listener): () => void;
|
|
|
94
117
|
|
|
95
118
|
declare function clearCache(key?: string): void;
|
|
96
119
|
|
|
97
|
-
export { type ApiClient, type EnlaceHookOptions, HTTP_METHODS, type HookState, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, 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 {
|
|
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 */
|
|
@@ -11,6 +11,23 @@ type ReactRequestOptionsBase = {
|
|
|
11
11
|
tags?: string[];
|
|
12
12
|
/** Tags to invalidate after mutation (triggers refetch in matching queries) */
|
|
13
13
|
revalidateTags?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Path parameters for dynamic URL segments.
|
|
16
|
+
* Used to replace :paramName placeholders in the URL path.
|
|
17
|
+
* @example
|
|
18
|
+
* // With path api.products[':id'].delete
|
|
19
|
+
* trigger({ pathParams: { id: '123' } }) // → DELETE /products/123
|
|
20
|
+
*/
|
|
21
|
+
pathParams?: Record<string, string | number>;
|
|
22
|
+
};
|
|
23
|
+
/** Options for query mode hooks */
|
|
24
|
+
type UseEnlaceQueryOptions = {
|
|
25
|
+
/**
|
|
26
|
+
* Whether the query should execute.
|
|
27
|
+
* Set to false to skip fetching (useful when ID is "new" or undefined).
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
enabled?: boolean;
|
|
14
31
|
};
|
|
15
32
|
type ApiClient<TSchema, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TOptions>;
|
|
16
33
|
type QueryFn<TSchema, TData, TError, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
|
|
@@ -55,7 +72,7 @@ type UseEnlaceSelectorResult<TMethod> = {
|
|
|
55
72
|
loading: boolean;
|
|
56
73
|
fetching: boolean;
|
|
57
74
|
} & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
|
|
58
|
-
|
|
75
|
+
/** Options for createEnlaceHook factory */
|
|
59
76
|
type EnlaceHookOptions = {
|
|
60
77
|
/**
|
|
61
78
|
* Auto-generate cache tags from URL path for GET requests.
|
|
@@ -67,11 +84,17 @@ type EnlaceHookOptions = {
|
|
|
67
84
|
autoRevalidateTags?: boolean;
|
|
68
85
|
/** Time in ms before cached data is considered stale. @default 0 (always stale) */
|
|
69
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;
|
|
70
91
|
};
|
|
92
|
+
/** Hook type returned by createEnlaceHook */
|
|
71
93
|
type EnlaceHook<TSchema> = {
|
|
72
94
|
<TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: SelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
|
|
73
|
-
<TData, TError>(queryFn: QueryFn<TSchema, TData, TError
|
|
95
|
+
<TData, TError>(queryFn: QueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
|
|
74
96
|
};
|
|
97
|
+
|
|
75
98
|
/**
|
|
76
99
|
* Creates a React hook for making API calls.
|
|
77
100
|
* Called at module level to create a reusable hook.
|
|
@@ -94,4 +117,4 @@ declare function onRevalidate(callback: Listener): () => void;
|
|
|
94
117
|
|
|
95
118
|
declare function clearCache(key?: string): void;
|
|
96
119
|
|
|
97
|
-
export { type ApiClient, type EnlaceHookOptions, HTTP_METHODS, type HookState, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, 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,
|
|
@@ -184,11 +184,29 @@ function onRevalidate(callback) {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
// src/react/useQueryMode.ts
|
|
187
|
+
function resolvePath(path, pathParams) {
|
|
188
|
+
if (!pathParams) return path;
|
|
189
|
+
return path.map((segment) => {
|
|
190
|
+
if (segment.startsWith(":")) {
|
|
191
|
+
const paramName = segment.slice(1);
|
|
192
|
+
const value = pathParams[paramName];
|
|
193
|
+
if (value === void 0) {
|
|
194
|
+
throw new Error(`Missing path parameter: ${paramName}`);
|
|
195
|
+
}
|
|
196
|
+
return String(value);
|
|
197
|
+
}
|
|
198
|
+
return segment;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
187
201
|
function useQueryMode(api, trackedCall, options) {
|
|
188
|
-
const { autoGenerateTags, staleTime } = options;
|
|
202
|
+
const { autoGenerateTags, staleTime, enabled } = options;
|
|
189
203
|
const queryKey = createQueryKey(trackedCall);
|
|
190
204
|
const requestOptions = trackedCall.options;
|
|
191
|
-
const
|
|
205
|
+
const resolvedPath = resolvePath(
|
|
206
|
+
trackedCall.path,
|
|
207
|
+
requestOptions?.pathParams
|
|
208
|
+
);
|
|
209
|
+
const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
|
|
192
210
|
const getCacheState = (includeNeedsFetch = false) => {
|
|
193
211
|
const cached = getCache(queryKey);
|
|
194
212
|
const hasCachedData = cached?.data !== void 0;
|
|
@@ -202,17 +220,22 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
202
220
|
error: cached?.error
|
|
203
221
|
};
|
|
204
222
|
};
|
|
205
|
-
const [state, dispatch] = (0, import_react.useReducer)(
|
|
223
|
+
const [state, dispatch] = (0, import_react.useReducer)(
|
|
224
|
+
hookReducer,
|
|
225
|
+
null,
|
|
226
|
+
() => getCacheState(true)
|
|
227
|
+
);
|
|
206
228
|
const mountedRef = (0, import_react.useRef)(true);
|
|
207
229
|
const fetchRef = (0, import_react.useRef)(null);
|
|
208
230
|
(0, import_react.useEffect)(() => {
|
|
209
231
|
mountedRef.current = true;
|
|
232
|
+
if (!enabled) {
|
|
233
|
+
dispatch({ type: "RESET" });
|
|
234
|
+
return () => {
|
|
235
|
+
mountedRef.current = false;
|
|
236
|
+
};
|
|
237
|
+
}
|
|
210
238
|
dispatch({ type: "RESET", state: getCacheState(true) });
|
|
211
|
-
const unsubscribe = subscribeCache(queryKey, () => {
|
|
212
|
-
if (mountedRef.current) {
|
|
213
|
-
dispatch({ type: "SYNC_CACHE", state: getCacheState() });
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
239
|
const doFetch = () => {
|
|
217
240
|
const cached2 = getCache(queryKey);
|
|
218
241
|
if (cached2?.promise) {
|
|
@@ -220,7 +243,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
220
243
|
}
|
|
221
244
|
dispatch({ type: "FETCH_START" });
|
|
222
245
|
let current = api;
|
|
223
|
-
for (const segment of
|
|
246
|
+
for (const segment of resolvedPath) {
|
|
224
247
|
current = current[segment];
|
|
225
248
|
}
|
|
226
249
|
const method = current[trackedCall.method];
|
|
@@ -228,7 +251,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
228
251
|
if (mountedRef.current) {
|
|
229
252
|
setCache(queryKey, {
|
|
230
253
|
data: res.ok ? res.data : void 0,
|
|
231
|
-
error: res.ok ? void 0 : res.error,
|
|
254
|
+
error: res.ok || res.status === 0 ? void 0 : res.error,
|
|
232
255
|
ok: res.ok,
|
|
233
256
|
timestamp: Date.now(),
|
|
234
257
|
tags: queryTags
|
|
@@ -247,12 +270,17 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
247
270
|
} else {
|
|
248
271
|
doFetch();
|
|
249
272
|
}
|
|
273
|
+
const unsubscribe = subscribeCache(queryKey, () => {
|
|
274
|
+
if (mountedRef.current) {
|
|
275
|
+
dispatch({ type: "SYNC_CACHE", state: getCacheState() });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
250
278
|
return () => {
|
|
251
279
|
mountedRef.current = false;
|
|
252
280
|
fetchRef.current = null;
|
|
253
281
|
unsubscribe();
|
|
254
282
|
};
|
|
255
|
-
}, [queryKey]);
|
|
283
|
+
}, [queryKey, enabled]);
|
|
256
284
|
(0, import_react.useEffect)(() => {
|
|
257
285
|
if (queryTags.length === 0) return;
|
|
258
286
|
return onRevalidate((invalidatedTags) => {
|
|
@@ -299,23 +327,56 @@ function createTrackingProxy(onTrack) {
|
|
|
299
327
|
|
|
300
328
|
// src/react/useSelectorMode.ts
|
|
301
329
|
var import_react2 = require("react");
|
|
302
|
-
function
|
|
330
|
+
function resolvePath2(path, pathParams) {
|
|
331
|
+
if (!pathParams) return path;
|
|
332
|
+
return path.map((segment) => {
|
|
333
|
+
if (segment.startsWith(":")) {
|
|
334
|
+
const paramName = segment.slice(1);
|
|
335
|
+
const value = pathParams[paramName];
|
|
336
|
+
if (value === void 0) {
|
|
337
|
+
throw new Error(`Missing path parameter: ${paramName}`);
|
|
338
|
+
}
|
|
339
|
+
return String(value);
|
|
340
|
+
}
|
|
341
|
+
return segment;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
function hasPathParams(path) {
|
|
345
|
+
return path.some((segment) => segment.startsWith(":"));
|
|
346
|
+
}
|
|
347
|
+
function useSelectorMode(config) {
|
|
348
|
+
const { method, api, path, methodName, autoRevalidateTags } = config;
|
|
303
349
|
const [state, dispatch] = (0, import_react2.useReducer)(hookReducer, initialState);
|
|
304
350
|
const methodRef = (0, import_react2.useRef)(method);
|
|
351
|
+
const apiRef = (0, import_react2.useRef)(api);
|
|
305
352
|
const triggerRef = (0, import_react2.useRef)(null);
|
|
306
353
|
const pathRef = (0, import_react2.useRef)(path);
|
|
354
|
+
const methodNameRef = (0, import_react2.useRef)(methodName);
|
|
307
355
|
const autoRevalidateRef = (0, import_react2.useRef)(autoRevalidateTags);
|
|
308
356
|
methodRef.current = method;
|
|
357
|
+
apiRef.current = api;
|
|
309
358
|
pathRef.current = path;
|
|
359
|
+
methodNameRef.current = methodName;
|
|
310
360
|
autoRevalidateRef.current = autoRevalidateTags;
|
|
311
361
|
if (!triggerRef.current) {
|
|
312
362
|
triggerRef.current = (async (...args) => {
|
|
313
363
|
dispatch({ type: "FETCH_START" });
|
|
314
|
-
const
|
|
364
|
+
const options = args[0];
|
|
365
|
+
const resolvedPath = resolvePath2(pathRef.current, options?.pathParams);
|
|
366
|
+
let res;
|
|
367
|
+
if (hasPathParams(pathRef.current)) {
|
|
368
|
+
let current = apiRef.current;
|
|
369
|
+
for (const segment of resolvedPath) {
|
|
370
|
+
current = current[segment];
|
|
371
|
+
}
|
|
372
|
+
const resolvedMethod = current[methodNameRef.current];
|
|
373
|
+
res = await resolvedMethod(...args);
|
|
374
|
+
} else {
|
|
375
|
+
res = await methodRef.current(...args);
|
|
376
|
+
}
|
|
315
377
|
if (res.ok) {
|
|
316
378
|
dispatch({ type: "FETCH_SUCCESS", data: res.data });
|
|
317
|
-
const
|
|
318
|
-
const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(pathRef.current) : []);
|
|
379
|
+
const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(resolvedPath) : []);
|
|
319
380
|
if (tagsToInvalidate.length > 0) {
|
|
320
381
|
invalidateTags(tagsToInvalidate);
|
|
321
382
|
}
|
|
@@ -333,13 +394,15 @@ function useSelectorMode(method, path, autoRevalidateTags) {
|
|
|
333
394
|
|
|
334
395
|
// src/react/createEnlaceHook.ts
|
|
335
396
|
function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
336
|
-
const api = (0, import_enlace_core.createEnlace)(baseUrl, defaultOptions);
|
|
337
397
|
const {
|
|
338
398
|
autoGenerateTags = true,
|
|
339
399
|
autoRevalidateTags = true,
|
|
340
|
-
staleTime = 0
|
|
400
|
+
staleTime = 0,
|
|
401
|
+
onSuccess,
|
|
402
|
+
onError
|
|
341
403
|
} = hookOptions;
|
|
342
|
-
|
|
404
|
+
const api = (0, import_enlace_core.createEnlace)(baseUrl, defaultOptions, { onSuccess, onError });
|
|
405
|
+
function useEnlaceHook(selectorOrQuery, queryOptions) {
|
|
343
406
|
let trackingResult = {
|
|
344
407
|
trackedCall: null,
|
|
345
408
|
selectorPath: null,
|
|
@@ -353,16 +416,18 @@ function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
353
416
|
);
|
|
354
417
|
if (typeof result === "function") {
|
|
355
418
|
const actualResult = selectorOrQuery(api);
|
|
356
|
-
return useSelectorMode(
|
|
357
|
-
actualResult,
|
|
358
|
-
|
|
419
|
+
return useSelectorMode({
|
|
420
|
+
method: actualResult,
|
|
421
|
+
api,
|
|
422
|
+
path: trackingResult.selectorPath ?? [],
|
|
423
|
+
methodName: trackingResult.selectorMethod ?? "",
|
|
359
424
|
autoRevalidateTags
|
|
360
|
-
);
|
|
425
|
+
});
|
|
361
426
|
}
|
|
362
427
|
return useQueryMode(
|
|
363
428
|
api,
|
|
364
429
|
trackingResult.trackedCall,
|
|
365
|
-
{ autoGenerateTags, staleTime }
|
|
430
|
+
{ autoGenerateTags, staleTime, enabled: queryOptions?.enabled ?? true }
|
|
366
431
|
);
|
|
367
432
|
}
|
|
368
433
|
return useEnlaceHook;
|