enlace 0.0.1-beta.12 → 0.0.1-beta.14
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 +121 -30
- package/dist/hook/index.d.mts +40 -14
- package/dist/hook/index.d.ts +40 -14
- package/dist/hook/index.js +76 -30
- package/dist/hook/index.mjs +77 -31
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ npm install enlace
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import {
|
|
14
|
+
import { enlaceHookReact } from "enlace/hook";
|
|
15
15
|
import { Endpoint } from "enlace";
|
|
16
16
|
|
|
17
17
|
// Define your API error type
|
|
@@ -30,7 +30,7 @@ type ApiSchema = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
// Pass global error type as second generic
|
|
33
|
-
const useAPI =
|
|
33
|
+
const useAPI = enlaceHookReact<ApiSchema, ApiError>(
|
|
34
34
|
"https://api.example.com"
|
|
35
35
|
);
|
|
36
36
|
```
|
|
@@ -41,13 +41,13 @@ Defining a schema is **recommended** for full type safety, but **optional**. You
|
|
|
41
41
|
|
|
42
42
|
```typescript
|
|
43
43
|
// Without schema (untyped, but still works!)
|
|
44
|
-
const useAPI =
|
|
44
|
+
const useAPI = enlaceHookReact("https://api.example.com");
|
|
45
45
|
const { data } = useAPI((api) => api.any.path.you.want.get());
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
```typescript
|
|
49
49
|
// With schema (recommended for type safety)
|
|
50
|
-
const useAPI =
|
|
50
|
+
const useAPI = enlaceHookReact<ApiSchema>("https://api.example.com");
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
### Schema Structure
|
|
@@ -77,7 +77,7 @@ type ApiSchema = {
|
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
// Pass global error type - applies to all endpoints
|
|
80
|
-
const api =
|
|
80
|
+
const api = enlace<ApiSchema, ApiError>("https://api.example.com");
|
|
81
81
|
|
|
82
82
|
// Usage
|
|
83
83
|
api.users.get(); // GET /users
|
|
@@ -197,9 +197,9 @@ type ApiSchema = {
|
|
|
197
197
|
type ApiError = { message: string; code: number };
|
|
198
198
|
|
|
199
199
|
// Second generic sets default error type for all endpoints
|
|
200
|
-
const api =
|
|
201
|
-
// const useAPI =
|
|
202
|
-
// const useAPI =
|
|
200
|
+
const api = enlace<ApiSchema, ApiError>("https://api.example.com");
|
|
201
|
+
// const useAPI = enlaceHookReact<ApiSchema, ApiError>("...");
|
|
202
|
+
// const useAPI = enlaceHookNext<ApiSchema, ApiError>("...");
|
|
203
203
|
```
|
|
204
204
|
|
|
205
205
|
## React Hooks
|
|
@@ -269,6 +269,74 @@ function Post({ id }: { id: number }) {
|
|
|
269
269
|
}
|
|
270
270
|
```
|
|
271
271
|
|
|
272
|
+
### Polling
|
|
273
|
+
|
|
274
|
+
Automatically refetch data at intervals using the `pollingInterval` option. Polling uses sequential timing — the interval starts counting **after** the previous request completes, preventing request pile-up:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
function Notifications() {
|
|
278
|
+
const { data } = useAPI(
|
|
279
|
+
(api) => api.notifications.get(),
|
|
280
|
+
{ pollingInterval: 5000 } // Refetch every 5 seconds after previous request completes
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
return <NotificationList notifications={data} />;
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Behavior:**
|
|
288
|
+
|
|
289
|
+
- Polling starts after the initial fetch completes
|
|
290
|
+
- Next poll is scheduled only after the current request finishes (success or error)
|
|
291
|
+
- Continues polling even on errors (retry behavior)
|
|
292
|
+
- Stops when component unmounts or `enabled` becomes `false`
|
|
293
|
+
- Resets when component remounts
|
|
294
|
+
|
|
295
|
+
**Dynamic polling with function:**
|
|
296
|
+
|
|
297
|
+
Use a function to conditionally poll based on the response data or error:
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
function OrderStatus({ orderId }: { orderId: string }) {
|
|
301
|
+
const { data } = useAPI(
|
|
302
|
+
(api) => api.orders[orderId].get(),
|
|
303
|
+
{
|
|
304
|
+
// Poll every 2s while pending, stop when completed
|
|
305
|
+
pollingInterval: (order) => order?.status === "pending" ? 2000 : false,
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
return <div>Status: {data?.status}</div>;
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
The function receives `(data, error)` and should return:
|
|
314
|
+
- `number`: Interval in milliseconds
|
|
315
|
+
- `false`: Stop polling
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// Poll faster when there's an error (retry), slower otherwise
|
|
319
|
+
{ pollingInterval: (data, error) => error ? 1000 : 10000 }
|
|
320
|
+
|
|
321
|
+
// Stop polling once data meets a condition
|
|
322
|
+
{ pollingInterval: (order) => order?.status === "completed" ? false : 3000 }
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Combined with conditional fetching:**
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
function OrderStatus({ orderId }: { orderId: string | undefined }) {
|
|
329
|
+
const { data } = useAPI(
|
|
330
|
+
(api) => api.orders[orderId!].get(),
|
|
331
|
+
{
|
|
332
|
+
enabled: !!orderId,
|
|
333
|
+
pollingInterval: 10000, // Poll every 10 seconds
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
// Polling only runs when orderId is defined
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
272
340
|
### Request Deduplication
|
|
273
341
|
|
|
274
342
|
Multiple components requesting the same data will share a single network request:
|
|
@@ -428,7 +496,7 @@ trigger({ body: { title: "New" } });
|
|
|
428
496
|
Control how long cached data is considered fresh:
|
|
429
497
|
|
|
430
498
|
```typescript
|
|
431
|
-
const useAPI =
|
|
499
|
+
const useAPI = enlaceHookReact<ApiSchema>(
|
|
432
500
|
"https://api.example.com",
|
|
433
501
|
{},
|
|
434
502
|
{
|
|
@@ -459,7 +527,7 @@ trigger({
|
|
|
459
527
|
### Disable Auto-Revalidation
|
|
460
528
|
|
|
461
529
|
```typescript
|
|
462
|
-
const useAPI =
|
|
530
|
+
const useAPI = enlaceHookReact<ApiSchema>(
|
|
463
531
|
"https://api.example.com",
|
|
464
532
|
{},
|
|
465
533
|
{
|
|
@@ -472,7 +540,7 @@ const useAPI = createEnlaceHookReact<ApiSchema>(
|
|
|
472
540
|
## Hook Options
|
|
473
541
|
|
|
474
542
|
```typescript
|
|
475
|
-
const useAPI =
|
|
543
|
+
const useAPI = enlaceHookReact<ApiSchema>(
|
|
476
544
|
"https://api.example.com",
|
|
477
545
|
{
|
|
478
546
|
// Default fetch options
|
|
@@ -493,17 +561,17 @@ Headers can be provided as a static value, sync function, or async function. Thi
|
|
|
493
561
|
|
|
494
562
|
```typescript
|
|
495
563
|
// Static headers
|
|
496
|
-
const useAPI =
|
|
564
|
+
const useAPI = enlaceHookReact<ApiSchema>("https://api.example.com", {
|
|
497
565
|
headers: { Authorization: "Bearer token" },
|
|
498
566
|
});
|
|
499
567
|
|
|
500
568
|
// Sync function
|
|
501
|
-
const useAPI =
|
|
569
|
+
const useAPI = enlaceHookReact<ApiSchema>("https://api.example.com", {
|
|
502
570
|
headers: () => ({ Authorization: `Bearer ${getToken()}` }),
|
|
503
571
|
});
|
|
504
572
|
|
|
505
573
|
// Async function
|
|
506
|
-
const useAPI =
|
|
574
|
+
const useAPI = enlaceHookReact<ApiSchema>("https://api.example.com", {
|
|
507
575
|
headers: async () => {
|
|
508
576
|
const token = await getTokenFromStorage();
|
|
509
577
|
return { Authorization: `Bearer ${token}` };
|
|
@@ -529,7 +597,7 @@ const { data } = useAPI((api) =>
|
|
|
529
597
|
You can set up global `onSuccess` and `onError` callbacks that are called for every request:
|
|
530
598
|
|
|
531
599
|
```typescript
|
|
532
|
-
const useAPI =
|
|
600
|
+
const useAPI = enlaceHookReact<ApiSchema>(
|
|
533
601
|
"https://api.example.com",
|
|
534
602
|
{
|
|
535
603
|
headers: { Authorization: "Bearer token" },
|
|
@@ -585,7 +653,18 @@ const result = useAPI((api) => api.posts.get());
|
|
|
585
653
|
// With options
|
|
586
654
|
const result = useAPI(
|
|
587
655
|
(api) => api.posts.get(),
|
|
588
|
-
{
|
|
656
|
+
{
|
|
657
|
+
enabled: true, // Skip fetching when false
|
|
658
|
+
pollingInterval: 5000 // Refetch every 5s after previous request completes
|
|
659
|
+
}
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
// With dynamic polling
|
|
663
|
+
const result = useAPI(
|
|
664
|
+
(api) => api.orders[id].get(),
|
|
665
|
+
{
|
|
666
|
+
pollingInterval: (order) => order?.status === "pending" ? 2000 : false
|
|
667
|
+
}
|
|
589
668
|
);
|
|
590
669
|
|
|
591
670
|
type UseEnlaceQueryResult<TData, TError> = {
|
|
@@ -608,6 +687,18 @@ type UseEnlaceSelectorResult<TMethod> = {
|
|
|
608
687
|
};
|
|
609
688
|
```
|
|
610
689
|
|
|
690
|
+
### Query Options
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
type UseEnlaceQueryOptions<TData, TError> = {
|
|
694
|
+
enabled?: boolean; // Skip fetching when false (default: true)
|
|
695
|
+
pollingInterval?: // Refetch interval after request completes
|
|
696
|
+
| number // Fixed interval in ms
|
|
697
|
+
| false // Disable polling
|
|
698
|
+
| ((data: TData | undefined, error: TError | undefined) => number | false); // Dynamic
|
|
699
|
+
};
|
|
700
|
+
```
|
|
701
|
+
|
|
611
702
|
### Request Options
|
|
612
703
|
|
|
613
704
|
```typescript
|
|
@@ -628,14 +719,14 @@ type RequestOptions = {
|
|
|
628
719
|
|
|
629
720
|
### Server Components
|
|
630
721
|
|
|
631
|
-
Use `
|
|
722
|
+
Use `enlaceNext` from `enlace` for server components:
|
|
632
723
|
|
|
633
724
|
```typescript
|
|
634
|
-
import {
|
|
725
|
+
import { enlaceNext } from "enlace";
|
|
635
726
|
|
|
636
727
|
type ApiError = { message: string };
|
|
637
728
|
|
|
638
|
-
const api =
|
|
729
|
+
const api = enlaceNext<ApiSchema, ApiError>("https://api.example.com", {}, {
|
|
639
730
|
autoGenerateTags: true,
|
|
640
731
|
});
|
|
641
732
|
|
|
@@ -650,16 +741,16 @@ export default async function Page() {
|
|
|
650
741
|
|
|
651
742
|
### Client Components
|
|
652
743
|
|
|
653
|
-
Use `
|
|
744
|
+
Use `enlaceHookNext` from `enlace/hook` for client components:
|
|
654
745
|
|
|
655
746
|
```typescript
|
|
656
747
|
"use client";
|
|
657
748
|
|
|
658
|
-
import {
|
|
749
|
+
import { enlaceHookNext } from "enlace/hook";
|
|
659
750
|
|
|
660
751
|
type ApiError = { message: string };
|
|
661
752
|
|
|
662
|
-
const useAPI =
|
|
753
|
+
const useAPI = enlaceHookNext<ApiSchema, ApiError>(
|
|
663
754
|
"https://api.example.com"
|
|
664
755
|
);
|
|
665
756
|
```
|
|
@@ -686,12 +777,12 @@ export async function revalidateAction(tags: string[], paths: string[]) {
|
|
|
686
777
|
|
|
687
778
|
```typescript
|
|
688
779
|
// useAPI.ts
|
|
689
|
-
import {
|
|
780
|
+
import { enlaceHookNext } from "enlace/hook";
|
|
690
781
|
import { revalidateAction } from "./actions";
|
|
691
782
|
|
|
692
783
|
type ApiError = { message: string };
|
|
693
784
|
|
|
694
|
-
const useAPI =
|
|
785
|
+
const useAPI = enlaceHookNext<ApiSchema, ApiError>(
|
|
695
786
|
"/api",
|
|
696
787
|
{},
|
|
697
788
|
{
|
|
@@ -721,7 +812,7 @@ function CreatePost() {
|
|
|
721
812
|
For projects that primarily use client-side rendering with minimal SSR, you can disable server-side revalidation by default:
|
|
722
813
|
|
|
723
814
|
```typescript
|
|
724
|
-
const useAPI =
|
|
815
|
+
const useAPI = enlaceHookNext<ApiSchema, ApiError>(
|
|
725
816
|
"/api",
|
|
726
817
|
{},
|
|
727
818
|
{
|
|
@@ -767,26 +858,26 @@ Works with Next.js API routes:
|
|
|
767
858
|
|
|
768
859
|
```typescript
|
|
769
860
|
// Client component calling /api/posts
|
|
770
|
-
const useAPI =
|
|
861
|
+
const useAPI = enlaceHookNext<ApiSchema, ApiError>("/api");
|
|
771
862
|
```
|
|
772
863
|
|
|
773
864
|
---
|
|
774
865
|
|
|
775
866
|
## API Reference
|
|
776
867
|
|
|
777
|
-
### `
|
|
868
|
+
### `enlaceHookReact<TSchema, TDefaultError>(baseUrl, options?, hookOptions?)`
|
|
778
869
|
|
|
779
870
|
Creates a React hook for making API calls.
|
|
780
871
|
|
|
781
|
-
### `
|
|
872
|
+
### `enlaceHookNext<TSchema, TDefaultError>(baseUrl, options?, hookOptions?)`
|
|
782
873
|
|
|
783
874
|
Creates a Next.js hook with server revalidation support.
|
|
784
875
|
|
|
785
|
-
### `
|
|
876
|
+
### `enlace<TSchema, TDefaultError>(baseUrl, options?, callbacks?)`
|
|
786
877
|
|
|
787
878
|
Creates a typed API client (non-hook, for direct calls or server components).
|
|
788
879
|
|
|
789
|
-
### `
|
|
880
|
+
### `enlaceNext<TSchema, TDefaultError>(baseUrl, options?, nextOptions?)`
|
|
790
881
|
|
|
791
882
|
Creates a Next.js typed API client with caching support.
|
|
792
883
|
|
package/dist/hook/index.d.mts
CHANGED
|
@@ -19,14 +19,40 @@ type ReactRequestOptionsBase = {
|
|
|
19
19
|
*/
|
|
20
20
|
params?: Record<string, string | number>;
|
|
21
21
|
};
|
|
22
|
+
/** Polling interval value: milliseconds to wait, or false to stop polling */
|
|
23
|
+
type PollingIntervalValue = number | false;
|
|
24
|
+
/** Function that determines polling interval based on current data/error state */
|
|
25
|
+
type PollingIntervalFn<TData, TError> = (data: TData | undefined, error: TError | undefined) => PollingIntervalValue;
|
|
26
|
+
/** Polling interval option: static value or dynamic function */
|
|
27
|
+
type PollingInterval<TData = unknown, TError = unknown> = PollingIntervalValue | PollingIntervalFn<TData, TError>;
|
|
22
28
|
/** Options for query mode hooks */
|
|
23
|
-
type UseEnlaceQueryOptions = {
|
|
29
|
+
type UseEnlaceQueryOptions<TData = unknown, TError = unknown> = {
|
|
24
30
|
/**
|
|
25
31
|
* Whether the query should execute.
|
|
26
32
|
* Set to false to skip fetching (useful when ID is "new" or undefined).
|
|
27
33
|
* @default true
|
|
28
34
|
*/
|
|
29
35
|
enabled?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Polling interval in milliseconds, or a function that returns the interval.
|
|
38
|
+
* When set, the query will refetch after this interval AFTER the previous request completes.
|
|
39
|
+
* Uses sequential polling (setTimeout after fetch completes), not interval-based.
|
|
40
|
+
*
|
|
41
|
+
* Can be:
|
|
42
|
+
* - `number`: Fixed interval in milliseconds
|
|
43
|
+
* - `false`: Disable polling
|
|
44
|
+
* - `(data, error) => number | false`: Dynamic interval based on response
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Fixed interval
|
|
48
|
+
* { pollingInterval: 5000 }
|
|
49
|
+
*
|
|
50
|
+
* // Conditional polling based on data
|
|
51
|
+
* { pollingInterval: (order) => order?.status === 'pending' ? 2000 : false }
|
|
52
|
+
*
|
|
53
|
+
* @default undefined (no polling)
|
|
54
|
+
*/
|
|
55
|
+
pollingInterval?: PollingInterval<TData, TError>;
|
|
30
56
|
};
|
|
31
57
|
type ApiClient<TSchema, TDefaultError = unknown, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TDefaultError, TOptions>;
|
|
32
58
|
type QueryFn<TSchema, TData, TError, TDefaultError = unknown, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TDefaultError, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
|
|
@@ -64,7 +90,7 @@ type UseEnlaceSelectorResult<TMethod> = {
|
|
|
64
90
|
loading: boolean;
|
|
65
91
|
fetching: boolean;
|
|
66
92
|
} & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
|
|
67
|
-
/** Options for
|
|
93
|
+
/** Options for enlaceHookReact factory */
|
|
68
94
|
type EnlaceHookOptions = {
|
|
69
95
|
/**
|
|
70
96
|
* Auto-generate cache tags from URL path for GET requests.
|
|
@@ -81,10 +107,10 @@ type EnlaceHookOptions = {
|
|
|
81
107
|
/** Callback called on error responses (HTTP errors or network failures) */
|
|
82
108
|
onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
|
|
83
109
|
};
|
|
84
|
-
/** Hook type returned by
|
|
110
|
+
/** Hook type returned by enlaceHookReact */
|
|
85
111
|
type EnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
86
112
|
<TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: SelectorFn<TSchema, TMethod, TDefaultError>): UseEnlaceSelectorResult<TMethod>;
|
|
87
|
-
<TData, TError>(queryFn: QueryFn<TSchema, TData, TError, TDefaultError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
|
|
113
|
+
<TData, TError>(queryFn: QueryFn<TSchema, TData, TError, TDefaultError>, options?: UseEnlaceQueryOptions<TData, TError>): UseEnlaceQueryResult<TData, TError>;
|
|
88
114
|
};
|
|
89
115
|
|
|
90
116
|
/**
|
|
@@ -92,7 +118,7 @@ type EnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
|
92
118
|
* Called at module level to create a reusable hook.
|
|
93
119
|
*
|
|
94
120
|
* @example
|
|
95
|
-
* const useAPI =
|
|
121
|
+
* const useAPI = enlaceHookReact<ApiSchema>('https://api.com');
|
|
96
122
|
*
|
|
97
123
|
* // Query mode - auto-fetch (auto-tracks changes, no deps array needed)
|
|
98
124
|
* const { loading, data, error } = useAPI((api) => api.posts.get({ query: { userId } }));
|
|
@@ -101,7 +127,7 @@ type EnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
|
101
127
|
* const { trigger, loading, data, error } = useAPI((api) => api.posts.delete);
|
|
102
128
|
* onClick={() => trigger({ body: { id: 1 } })}
|
|
103
129
|
*/
|
|
104
|
-
declare function
|
|
130
|
+
declare function enlaceHookReact<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions, hookOptions?: EnlaceHookOptions): EnlaceHook<TSchema, TDefaultError>;
|
|
105
131
|
|
|
106
132
|
/**
|
|
107
133
|
* Handler function called after successful mutations to trigger server-side revalidation.
|
|
@@ -109,14 +135,14 @@ declare function createEnlaceHookReact<TSchema = unknown, TDefaultError = unknow
|
|
|
109
135
|
* @param paths - URL paths to revalidate
|
|
110
136
|
*/
|
|
111
137
|
type ServerRevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
|
|
112
|
-
/** Next.js-specific options (third argument for
|
|
138
|
+
/** Next.js-specific options (third argument for enlaceNext) */
|
|
113
139
|
type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
|
|
114
140
|
/**
|
|
115
141
|
* Handler called after successful mutations to trigger server-side revalidation.
|
|
116
142
|
* Receives auto-generated or manually specified tags and paths.
|
|
117
143
|
* @example
|
|
118
144
|
* ```ts
|
|
119
|
-
*
|
|
145
|
+
* enlaceNext("http://localhost:3000/api/", {}, {
|
|
120
146
|
* serverRevalidator: (tags, paths) => revalidateServerAction(tags, paths)
|
|
121
147
|
* });
|
|
122
148
|
* ```
|
|
@@ -130,7 +156,7 @@ type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateT
|
|
|
130
156
|
*/
|
|
131
157
|
skipServerRevalidation?: boolean;
|
|
132
158
|
};
|
|
133
|
-
/** Next.js hook options (third argument for
|
|
159
|
+
/** Next.js hook options (third argument for enlaceHookNext) - extends React's EnlaceHookOptions */
|
|
134
160
|
type NextHookOptions = EnlaceHookOptions & Pick<NextOptions, "serverRevalidator" | "skipServerRevalidation">;
|
|
135
161
|
/** Per-request options for Next.js fetch - extends React's base options */
|
|
136
162
|
type NextRequestOptionsBase = ReactRequestOptionsBase & {
|
|
@@ -151,10 +177,10 @@ type NextRequestOptionsBase = ReactRequestOptionsBase & {
|
|
|
151
177
|
};
|
|
152
178
|
type NextQueryFn<TSchema, TData, TError, TDefaultError = unknown> = QueryFn<TSchema, TData, TError, TDefaultError, NextRequestOptionsBase>;
|
|
153
179
|
type NextSelectorFn<TSchema, TMethod, TDefaultError = unknown> = SelectorFn<TSchema, TMethod, TDefaultError, NextRequestOptionsBase>;
|
|
154
|
-
/** Hook type returned by
|
|
180
|
+
/** Hook type returned by enlaceHookNext */
|
|
155
181
|
type NextEnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
156
182
|
<TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: NextSelectorFn<TSchema, TMethod, TDefaultError>): UseEnlaceSelectorResult<TMethod>;
|
|
157
|
-
<TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError, TDefaultError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
|
|
183
|
+
<TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError, TDefaultError>, options?: UseEnlaceQueryOptions<TData, TError>): UseEnlaceQueryResult<TData, TError>;
|
|
158
184
|
};
|
|
159
185
|
|
|
160
186
|
/**
|
|
@@ -162,7 +188,7 @@ type NextEnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
|
162
188
|
* Uses Next.js-specific features like serverRevalidator for server-side cache invalidation.
|
|
163
189
|
*
|
|
164
190
|
* @example
|
|
165
|
-
* const useAPI =
|
|
191
|
+
* const useAPI = enlaceHookNext<ApiSchema>('https://api.com', {}, {
|
|
166
192
|
* serverRevalidator: (tags) => revalidateTagsAction(tags),
|
|
167
193
|
* staleTime: 5000,
|
|
168
194
|
* });
|
|
@@ -173,6 +199,6 @@ type NextEnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
|
173
199
|
* // Selector mode - trigger for mutations
|
|
174
200
|
* const { trigger } = useAPI((api) => api.posts.delete);
|
|
175
201
|
*/
|
|
176
|
-
declare function
|
|
202
|
+
declare function enlaceHookNext<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions, hookOptions?: NextHookOptions): NextEnlaceHook<TSchema, TDefaultError>;
|
|
177
203
|
|
|
178
|
-
export { type ApiClient, type EnlaceHook, type EnlaceHookOptions, HTTP_METHODS, type HookState, type NextEnlaceHook, type NextHookOptions, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, type UseEnlaceQueryOptions, type UseEnlaceQueryResult, type UseEnlaceSelectorResult,
|
|
204
|
+
export { type ApiClient, type EnlaceHook, type EnlaceHookOptions, HTTP_METHODS, type HookState, type NextEnlaceHook, type NextHookOptions, type PollingInterval, type PollingIntervalFn, type PollingIntervalValue, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, type UseEnlaceQueryOptions, type UseEnlaceQueryResult, type UseEnlaceSelectorResult, enlaceHookNext, enlaceHookReact };
|
package/dist/hook/index.d.ts
CHANGED
|
@@ -19,14 +19,40 @@ type ReactRequestOptionsBase = {
|
|
|
19
19
|
*/
|
|
20
20
|
params?: Record<string, string | number>;
|
|
21
21
|
};
|
|
22
|
+
/** Polling interval value: milliseconds to wait, or false to stop polling */
|
|
23
|
+
type PollingIntervalValue = number | false;
|
|
24
|
+
/** Function that determines polling interval based on current data/error state */
|
|
25
|
+
type PollingIntervalFn<TData, TError> = (data: TData | undefined, error: TError | undefined) => PollingIntervalValue;
|
|
26
|
+
/** Polling interval option: static value or dynamic function */
|
|
27
|
+
type PollingInterval<TData = unknown, TError = unknown> = PollingIntervalValue | PollingIntervalFn<TData, TError>;
|
|
22
28
|
/** Options for query mode hooks */
|
|
23
|
-
type UseEnlaceQueryOptions = {
|
|
29
|
+
type UseEnlaceQueryOptions<TData = unknown, TError = unknown> = {
|
|
24
30
|
/**
|
|
25
31
|
* Whether the query should execute.
|
|
26
32
|
* Set to false to skip fetching (useful when ID is "new" or undefined).
|
|
27
33
|
* @default true
|
|
28
34
|
*/
|
|
29
35
|
enabled?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Polling interval in milliseconds, or a function that returns the interval.
|
|
38
|
+
* When set, the query will refetch after this interval AFTER the previous request completes.
|
|
39
|
+
* Uses sequential polling (setTimeout after fetch completes), not interval-based.
|
|
40
|
+
*
|
|
41
|
+
* Can be:
|
|
42
|
+
* - `number`: Fixed interval in milliseconds
|
|
43
|
+
* - `false`: Disable polling
|
|
44
|
+
* - `(data, error) => number | false`: Dynamic interval based on response
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Fixed interval
|
|
48
|
+
* { pollingInterval: 5000 }
|
|
49
|
+
*
|
|
50
|
+
* // Conditional polling based on data
|
|
51
|
+
* { pollingInterval: (order) => order?.status === 'pending' ? 2000 : false }
|
|
52
|
+
*
|
|
53
|
+
* @default undefined (no polling)
|
|
54
|
+
*/
|
|
55
|
+
pollingInterval?: PollingInterval<TData, TError>;
|
|
30
56
|
};
|
|
31
57
|
type ApiClient<TSchema, TDefaultError = unknown, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TDefaultError, TOptions>;
|
|
32
58
|
type QueryFn<TSchema, TData, TError, TDefaultError = unknown, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TDefaultError, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
|
|
@@ -64,7 +90,7 @@ type UseEnlaceSelectorResult<TMethod> = {
|
|
|
64
90
|
loading: boolean;
|
|
65
91
|
fetching: boolean;
|
|
66
92
|
} & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
|
|
67
|
-
/** Options for
|
|
93
|
+
/** Options for enlaceHookReact factory */
|
|
68
94
|
type EnlaceHookOptions = {
|
|
69
95
|
/**
|
|
70
96
|
* Auto-generate cache tags from URL path for GET requests.
|
|
@@ -81,10 +107,10 @@ type EnlaceHookOptions = {
|
|
|
81
107
|
/** Callback called on error responses (HTTP errors or network failures) */
|
|
82
108
|
onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
|
|
83
109
|
};
|
|
84
|
-
/** Hook type returned by
|
|
110
|
+
/** Hook type returned by enlaceHookReact */
|
|
85
111
|
type EnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
86
112
|
<TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: SelectorFn<TSchema, TMethod, TDefaultError>): UseEnlaceSelectorResult<TMethod>;
|
|
87
|
-
<TData, TError>(queryFn: QueryFn<TSchema, TData, TError, TDefaultError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
|
|
113
|
+
<TData, TError>(queryFn: QueryFn<TSchema, TData, TError, TDefaultError>, options?: UseEnlaceQueryOptions<TData, TError>): UseEnlaceQueryResult<TData, TError>;
|
|
88
114
|
};
|
|
89
115
|
|
|
90
116
|
/**
|
|
@@ -92,7 +118,7 @@ type EnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
|
92
118
|
* Called at module level to create a reusable hook.
|
|
93
119
|
*
|
|
94
120
|
* @example
|
|
95
|
-
* const useAPI =
|
|
121
|
+
* const useAPI = enlaceHookReact<ApiSchema>('https://api.com');
|
|
96
122
|
*
|
|
97
123
|
* // Query mode - auto-fetch (auto-tracks changes, no deps array needed)
|
|
98
124
|
* const { loading, data, error } = useAPI((api) => api.posts.get({ query: { userId } }));
|
|
@@ -101,7 +127,7 @@ type EnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
|
101
127
|
* const { trigger, loading, data, error } = useAPI((api) => api.posts.delete);
|
|
102
128
|
* onClick={() => trigger({ body: { id: 1 } })}
|
|
103
129
|
*/
|
|
104
|
-
declare function
|
|
130
|
+
declare function enlaceHookReact<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions, hookOptions?: EnlaceHookOptions): EnlaceHook<TSchema, TDefaultError>;
|
|
105
131
|
|
|
106
132
|
/**
|
|
107
133
|
* Handler function called after successful mutations to trigger server-side revalidation.
|
|
@@ -109,14 +135,14 @@ declare function createEnlaceHookReact<TSchema = unknown, TDefaultError = unknow
|
|
|
109
135
|
* @param paths - URL paths to revalidate
|
|
110
136
|
*/
|
|
111
137
|
type ServerRevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
|
|
112
|
-
/** Next.js-specific options (third argument for
|
|
138
|
+
/** Next.js-specific options (third argument for enlaceNext) */
|
|
113
139
|
type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
|
|
114
140
|
/**
|
|
115
141
|
* Handler called after successful mutations to trigger server-side revalidation.
|
|
116
142
|
* Receives auto-generated or manually specified tags and paths.
|
|
117
143
|
* @example
|
|
118
144
|
* ```ts
|
|
119
|
-
*
|
|
145
|
+
* enlaceNext("http://localhost:3000/api/", {}, {
|
|
120
146
|
* serverRevalidator: (tags, paths) => revalidateServerAction(tags, paths)
|
|
121
147
|
* });
|
|
122
148
|
* ```
|
|
@@ -130,7 +156,7 @@ type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateT
|
|
|
130
156
|
*/
|
|
131
157
|
skipServerRevalidation?: boolean;
|
|
132
158
|
};
|
|
133
|
-
/** Next.js hook options (third argument for
|
|
159
|
+
/** Next.js hook options (third argument for enlaceHookNext) - extends React's EnlaceHookOptions */
|
|
134
160
|
type NextHookOptions = EnlaceHookOptions & Pick<NextOptions, "serverRevalidator" | "skipServerRevalidation">;
|
|
135
161
|
/** Per-request options for Next.js fetch - extends React's base options */
|
|
136
162
|
type NextRequestOptionsBase = ReactRequestOptionsBase & {
|
|
@@ -151,10 +177,10 @@ type NextRequestOptionsBase = ReactRequestOptionsBase & {
|
|
|
151
177
|
};
|
|
152
178
|
type NextQueryFn<TSchema, TData, TError, TDefaultError = unknown> = QueryFn<TSchema, TData, TError, TDefaultError, NextRequestOptionsBase>;
|
|
153
179
|
type NextSelectorFn<TSchema, TMethod, TDefaultError = unknown> = SelectorFn<TSchema, TMethod, TDefaultError, NextRequestOptionsBase>;
|
|
154
|
-
/** Hook type returned by
|
|
180
|
+
/** Hook type returned by enlaceHookNext */
|
|
155
181
|
type NextEnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
156
182
|
<TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: NextSelectorFn<TSchema, TMethod, TDefaultError>): UseEnlaceSelectorResult<TMethod>;
|
|
157
|
-
<TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError, TDefaultError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
|
|
183
|
+
<TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError, TDefaultError>, options?: UseEnlaceQueryOptions<TData, TError>): UseEnlaceQueryResult<TData, TError>;
|
|
158
184
|
};
|
|
159
185
|
|
|
160
186
|
/**
|
|
@@ -162,7 +188,7 @@ type NextEnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
|
162
188
|
* Uses Next.js-specific features like serverRevalidator for server-side cache invalidation.
|
|
163
189
|
*
|
|
164
190
|
* @example
|
|
165
|
-
* const useAPI =
|
|
191
|
+
* const useAPI = enlaceHookNext<ApiSchema>('https://api.com', {}, {
|
|
166
192
|
* serverRevalidator: (tags) => revalidateTagsAction(tags),
|
|
167
193
|
* staleTime: 5000,
|
|
168
194
|
* });
|
|
@@ -173,6 +199,6 @@ type NextEnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
|
173
199
|
* // Selector mode - trigger for mutations
|
|
174
200
|
* const { trigger } = useAPI((api) => api.posts.delete);
|
|
175
201
|
*/
|
|
176
|
-
declare function
|
|
202
|
+
declare function enlaceHookNext<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions, hookOptions?: NextHookOptions): NextEnlaceHook<TSchema, TDefaultError>;
|
|
177
203
|
|
|
178
|
-
export { type ApiClient, type EnlaceHook, type EnlaceHookOptions, HTTP_METHODS, type HookState, type NextEnlaceHook, type NextHookOptions, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, type UseEnlaceQueryOptions, type UseEnlaceQueryResult, type UseEnlaceSelectorResult,
|
|
204
|
+
export { type ApiClient, type EnlaceHook, type EnlaceHookOptions, HTTP_METHODS, type HookState, type NextEnlaceHook, type NextHookOptions, type PollingInterval, type PollingIntervalFn, type PollingIntervalValue, type QueryFn, type ReactRequestOptionsBase, type SelectorFn, type TrackedCall, type UseEnlaceQueryOptions, type UseEnlaceQueryResult, type UseEnlaceSelectorResult, enlaceHookNext, enlaceHookReact };
|
package/dist/hook/index.js
CHANGED
|
@@ -23,12 +23,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
23
23
|
var hook_exports = {};
|
|
24
24
|
__export(hook_exports, {
|
|
25
25
|
HTTP_METHODS: () => HTTP_METHODS,
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
enlaceHookNext: () => enlaceHookNext,
|
|
27
|
+
enlaceHookReact: () => enlaceHookReact
|
|
28
28
|
});
|
|
29
29
|
module.exports = __toCommonJS(hook_exports);
|
|
30
30
|
|
|
31
|
-
// src/react/
|
|
31
|
+
// src/react/enlaceHookReact.ts
|
|
32
32
|
var import_enlace_core = require("enlace-core");
|
|
33
33
|
|
|
34
34
|
// src/react/useQueryMode.ts
|
|
@@ -52,6 +52,13 @@ function hookReducer(state, action) {
|
|
|
52
52
|
fetching: true,
|
|
53
53
|
error: void 0
|
|
54
54
|
};
|
|
55
|
+
case "MUTATION_START":
|
|
56
|
+
return {
|
|
57
|
+
...state,
|
|
58
|
+
loading: true,
|
|
59
|
+
fetching: true,
|
|
60
|
+
error: void 0
|
|
61
|
+
};
|
|
55
62
|
case "FETCH_SUCCESS":
|
|
56
63
|
return {
|
|
57
64
|
loading: false,
|
|
@@ -193,7 +200,7 @@ function resolvePath(path, params) {
|
|
|
193
200
|
});
|
|
194
201
|
}
|
|
195
202
|
function useQueryMode(api, trackedCall, options) {
|
|
196
|
-
const { autoGenerateTags, staleTime, enabled } = options;
|
|
203
|
+
const { autoGenerateTags, staleTime, enabled, pollingInterval } = options;
|
|
197
204
|
const queryKey = createQueryKey(trackedCall);
|
|
198
205
|
const requestOptions = trackedCall.options;
|
|
199
206
|
const resolvedPath = resolvePath(trackedCall.path, requestOptions?.params);
|
|
@@ -218,15 +225,41 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
218
225
|
);
|
|
219
226
|
const mountedRef = (0, import_react.useRef)(true);
|
|
220
227
|
const fetchRef = (0, import_react.useRef)(null);
|
|
228
|
+
const pollingTimeoutRef = (0, import_react.useRef)(null);
|
|
229
|
+
const pollingIntervalRef = (0, import_react.useRef)(pollingInterval);
|
|
230
|
+
pollingIntervalRef.current = pollingInterval;
|
|
221
231
|
(0, import_react.useEffect)(() => {
|
|
222
232
|
mountedRef.current = true;
|
|
223
233
|
if (!enabled) {
|
|
224
234
|
dispatch({ type: "RESET" });
|
|
235
|
+
if (pollingTimeoutRef.current) {
|
|
236
|
+
clearTimeout(pollingTimeoutRef.current);
|
|
237
|
+
pollingTimeoutRef.current = null;
|
|
238
|
+
}
|
|
225
239
|
return () => {
|
|
226
240
|
mountedRef.current = false;
|
|
227
241
|
};
|
|
228
242
|
}
|
|
229
243
|
dispatch({ type: "RESET", state: getCacheState(true) });
|
|
244
|
+
const scheduleNextPoll = () => {
|
|
245
|
+
const currentPollingInterval = pollingIntervalRef.current;
|
|
246
|
+
if (!mountedRef.current || !enabled || currentPollingInterval === void 0) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const cached2 = getCache(queryKey);
|
|
250
|
+
const interval = typeof currentPollingInterval === "function" ? currentPollingInterval(cached2?.data, cached2?.error) : currentPollingInterval;
|
|
251
|
+
if (interval === false || interval <= 0) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (pollingTimeoutRef.current) {
|
|
255
|
+
clearTimeout(pollingTimeoutRef.current);
|
|
256
|
+
}
|
|
257
|
+
pollingTimeoutRef.current = setTimeout(() => {
|
|
258
|
+
if (mountedRef.current && enabled && fetchRef.current) {
|
|
259
|
+
fetchRef.current();
|
|
260
|
+
}
|
|
261
|
+
}, interval);
|
|
262
|
+
};
|
|
230
263
|
const doFetch = () => {
|
|
231
264
|
const cached2 = getCache(queryKey);
|
|
232
265
|
if (cached2?.promise) {
|
|
@@ -239,23 +272,21 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
239
272
|
}
|
|
240
273
|
const method = current[trackedCall.method];
|
|
241
274
|
const fetchPromise = method(trackedCall.options).then((res) => {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
});
|
|
249
|
-
}
|
|
275
|
+
setCache(queryKey, {
|
|
276
|
+
data: res.error ? void 0 : res.data,
|
|
277
|
+
error: res.error,
|
|
278
|
+
timestamp: Date.now(),
|
|
279
|
+
tags: queryTags
|
|
280
|
+
});
|
|
250
281
|
}).catch((err) => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
282
|
+
setCache(queryKey, {
|
|
283
|
+
data: void 0,
|
|
284
|
+
error: err,
|
|
285
|
+
timestamp: Date.now(),
|
|
286
|
+
tags: queryTags
|
|
287
|
+
});
|
|
288
|
+
}).finally(() => {
|
|
289
|
+
scheduleNextPoll();
|
|
259
290
|
});
|
|
260
291
|
setCache(queryKey, {
|
|
261
292
|
promise: fetchPromise,
|
|
@@ -266,6 +297,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
266
297
|
const cached = getCache(queryKey);
|
|
267
298
|
if (cached?.data !== void 0 && !isStale(queryKey, staleTime)) {
|
|
268
299
|
dispatch({ type: "SYNC_CACHE", state: getCacheState() });
|
|
300
|
+
scheduleNextPoll();
|
|
269
301
|
} else {
|
|
270
302
|
doFetch();
|
|
271
303
|
}
|
|
@@ -277,6 +309,10 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
277
309
|
return () => {
|
|
278
310
|
mountedRef.current = false;
|
|
279
311
|
fetchRef.current = null;
|
|
312
|
+
if (pollingTimeoutRef.current) {
|
|
313
|
+
clearTimeout(pollingTimeoutRef.current);
|
|
314
|
+
pollingTimeoutRef.current = null;
|
|
315
|
+
}
|
|
280
316
|
unsubscribe();
|
|
281
317
|
};
|
|
282
318
|
}, [queryKey, enabled]);
|
|
@@ -359,7 +395,7 @@ function useSelectorMode(config) {
|
|
|
359
395
|
autoRevalidateRef.current = autoRevalidateTags;
|
|
360
396
|
if (!triggerRef.current) {
|
|
361
397
|
triggerRef.current = (async (...args) => {
|
|
362
|
-
dispatch({ type: "
|
|
398
|
+
dispatch({ type: "MUTATION_START" });
|
|
363
399
|
const options = args[0];
|
|
364
400
|
const resolvedPath = resolvePath2(pathRef.current, options?.params);
|
|
365
401
|
let res;
|
|
@@ -391,8 +427,8 @@ function useSelectorMode(config) {
|
|
|
391
427
|
};
|
|
392
428
|
}
|
|
393
429
|
|
|
394
|
-
// src/react/
|
|
395
|
-
function
|
|
430
|
+
// src/react/enlaceHookReact.ts
|
|
431
|
+
function enlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
396
432
|
const {
|
|
397
433
|
autoGenerateTags = true,
|
|
398
434
|
autoRevalidateTags = true,
|
|
@@ -400,7 +436,7 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
400
436
|
onSuccess,
|
|
401
437
|
onError
|
|
402
438
|
} = hookOptions;
|
|
403
|
-
const api = (0, import_enlace_core.
|
|
439
|
+
const api = (0, import_enlace_core.enlace)(baseUrl, defaultOptions, {
|
|
404
440
|
onSuccess,
|
|
405
441
|
onError
|
|
406
442
|
});
|
|
@@ -432,7 +468,12 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
432
468
|
return useQueryMode(
|
|
433
469
|
api,
|
|
434
470
|
trackingResult.trackedCall,
|
|
435
|
-
{
|
|
471
|
+
{
|
|
472
|
+
autoGenerateTags,
|
|
473
|
+
staleTime,
|
|
474
|
+
enabled: queryOptions?.enabled ?? true,
|
|
475
|
+
pollingInterval: queryOptions?.pollingInterval
|
|
476
|
+
}
|
|
436
477
|
);
|
|
437
478
|
}
|
|
438
479
|
return useEnlaceHook;
|
|
@@ -489,7 +530,7 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
489
530
|
}
|
|
490
531
|
|
|
491
532
|
// src/next/index.ts
|
|
492
|
-
function
|
|
533
|
+
function enlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
|
|
493
534
|
const combinedOptions = { ...defaultOptions, ...nextOptions };
|
|
494
535
|
return (0, import_enlace_core3.createProxyHandler)(
|
|
495
536
|
baseUrl,
|
|
@@ -499,15 +540,15 @@ function createEnlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
|
|
|
499
540
|
);
|
|
500
541
|
}
|
|
501
542
|
|
|
502
|
-
// src/next/
|
|
503
|
-
function
|
|
543
|
+
// src/next/enlaceHookNext.ts
|
|
544
|
+
function enlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
504
545
|
const {
|
|
505
546
|
autoGenerateTags = true,
|
|
506
547
|
autoRevalidateTags = true,
|
|
507
548
|
staleTime = 0,
|
|
508
549
|
...nextOptions
|
|
509
550
|
} = hookOptions;
|
|
510
|
-
const api =
|
|
551
|
+
const api = enlaceNext(
|
|
511
552
|
baseUrl,
|
|
512
553
|
defaultOptions,
|
|
513
554
|
{
|
|
@@ -544,7 +585,12 @@ function createEnlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
544
585
|
return useQueryMode(
|
|
545
586
|
api,
|
|
546
587
|
trackedCall,
|
|
547
|
-
{
|
|
588
|
+
{
|
|
589
|
+
autoGenerateTags,
|
|
590
|
+
staleTime,
|
|
591
|
+
enabled: queryOptions?.enabled ?? true,
|
|
592
|
+
pollingInterval: queryOptions?.pollingInterval
|
|
593
|
+
}
|
|
548
594
|
);
|
|
549
595
|
}
|
|
550
596
|
return useEnlaceHook;
|
package/dist/hook/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
// src/react/
|
|
4
|
+
// src/react/enlaceHookReact.ts
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
enlace
|
|
7
7
|
} from "enlace-core";
|
|
8
8
|
|
|
9
9
|
// src/react/useQueryMode.ts
|
|
@@ -27,6 +27,13 @@ function hookReducer(state, action) {
|
|
|
27
27
|
fetching: true,
|
|
28
28
|
error: void 0
|
|
29
29
|
};
|
|
30
|
+
case "MUTATION_START":
|
|
31
|
+
return {
|
|
32
|
+
...state,
|
|
33
|
+
loading: true,
|
|
34
|
+
fetching: true,
|
|
35
|
+
error: void 0
|
|
36
|
+
};
|
|
30
37
|
case "FETCH_SUCCESS":
|
|
31
38
|
return {
|
|
32
39
|
loading: false,
|
|
@@ -168,7 +175,7 @@ function resolvePath(path, params) {
|
|
|
168
175
|
});
|
|
169
176
|
}
|
|
170
177
|
function useQueryMode(api, trackedCall, options) {
|
|
171
|
-
const { autoGenerateTags, staleTime, enabled } = options;
|
|
178
|
+
const { autoGenerateTags, staleTime, enabled, pollingInterval } = options;
|
|
172
179
|
const queryKey = createQueryKey(trackedCall);
|
|
173
180
|
const requestOptions = trackedCall.options;
|
|
174
181
|
const resolvedPath = resolvePath(trackedCall.path, requestOptions?.params);
|
|
@@ -193,15 +200,41 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
193
200
|
);
|
|
194
201
|
const mountedRef = useRef(true);
|
|
195
202
|
const fetchRef = useRef(null);
|
|
203
|
+
const pollingTimeoutRef = useRef(null);
|
|
204
|
+
const pollingIntervalRef = useRef(pollingInterval);
|
|
205
|
+
pollingIntervalRef.current = pollingInterval;
|
|
196
206
|
useEffect(() => {
|
|
197
207
|
mountedRef.current = true;
|
|
198
208
|
if (!enabled) {
|
|
199
209
|
dispatch({ type: "RESET" });
|
|
210
|
+
if (pollingTimeoutRef.current) {
|
|
211
|
+
clearTimeout(pollingTimeoutRef.current);
|
|
212
|
+
pollingTimeoutRef.current = null;
|
|
213
|
+
}
|
|
200
214
|
return () => {
|
|
201
215
|
mountedRef.current = false;
|
|
202
216
|
};
|
|
203
217
|
}
|
|
204
218
|
dispatch({ type: "RESET", state: getCacheState(true) });
|
|
219
|
+
const scheduleNextPoll = () => {
|
|
220
|
+
const currentPollingInterval = pollingIntervalRef.current;
|
|
221
|
+
if (!mountedRef.current || !enabled || currentPollingInterval === void 0) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const cached2 = getCache(queryKey);
|
|
225
|
+
const interval = typeof currentPollingInterval === "function" ? currentPollingInterval(cached2?.data, cached2?.error) : currentPollingInterval;
|
|
226
|
+
if (interval === false || interval <= 0) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (pollingTimeoutRef.current) {
|
|
230
|
+
clearTimeout(pollingTimeoutRef.current);
|
|
231
|
+
}
|
|
232
|
+
pollingTimeoutRef.current = setTimeout(() => {
|
|
233
|
+
if (mountedRef.current && enabled && fetchRef.current) {
|
|
234
|
+
fetchRef.current();
|
|
235
|
+
}
|
|
236
|
+
}, interval);
|
|
237
|
+
};
|
|
205
238
|
const doFetch = () => {
|
|
206
239
|
const cached2 = getCache(queryKey);
|
|
207
240
|
if (cached2?.promise) {
|
|
@@ -214,23 +247,21 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
214
247
|
}
|
|
215
248
|
const method = current[trackedCall.method];
|
|
216
249
|
const fetchPromise = method(trackedCall.options).then((res) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
});
|
|
224
|
-
}
|
|
250
|
+
setCache(queryKey, {
|
|
251
|
+
data: res.error ? void 0 : res.data,
|
|
252
|
+
error: res.error,
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
tags: queryTags
|
|
255
|
+
});
|
|
225
256
|
}).catch((err) => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
257
|
+
setCache(queryKey, {
|
|
258
|
+
data: void 0,
|
|
259
|
+
error: err,
|
|
260
|
+
timestamp: Date.now(),
|
|
261
|
+
tags: queryTags
|
|
262
|
+
});
|
|
263
|
+
}).finally(() => {
|
|
264
|
+
scheduleNextPoll();
|
|
234
265
|
});
|
|
235
266
|
setCache(queryKey, {
|
|
236
267
|
promise: fetchPromise,
|
|
@@ -241,6 +272,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
241
272
|
const cached = getCache(queryKey);
|
|
242
273
|
if (cached?.data !== void 0 && !isStale(queryKey, staleTime)) {
|
|
243
274
|
dispatch({ type: "SYNC_CACHE", state: getCacheState() });
|
|
275
|
+
scheduleNextPoll();
|
|
244
276
|
} else {
|
|
245
277
|
doFetch();
|
|
246
278
|
}
|
|
@@ -252,6 +284,10 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
252
284
|
return () => {
|
|
253
285
|
mountedRef.current = false;
|
|
254
286
|
fetchRef.current = null;
|
|
287
|
+
if (pollingTimeoutRef.current) {
|
|
288
|
+
clearTimeout(pollingTimeoutRef.current);
|
|
289
|
+
pollingTimeoutRef.current = null;
|
|
290
|
+
}
|
|
255
291
|
unsubscribe();
|
|
256
292
|
};
|
|
257
293
|
}, [queryKey, enabled]);
|
|
@@ -334,7 +370,7 @@ function useSelectorMode(config) {
|
|
|
334
370
|
autoRevalidateRef.current = autoRevalidateTags;
|
|
335
371
|
if (!triggerRef.current) {
|
|
336
372
|
triggerRef.current = (async (...args) => {
|
|
337
|
-
dispatch({ type: "
|
|
373
|
+
dispatch({ type: "MUTATION_START" });
|
|
338
374
|
const options = args[0];
|
|
339
375
|
const resolvedPath = resolvePath2(pathRef.current, options?.params);
|
|
340
376
|
let res;
|
|
@@ -366,8 +402,8 @@ function useSelectorMode(config) {
|
|
|
366
402
|
};
|
|
367
403
|
}
|
|
368
404
|
|
|
369
|
-
// src/react/
|
|
370
|
-
function
|
|
405
|
+
// src/react/enlaceHookReact.ts
|
|
406
|
+
function enlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
371
407
|
const {
|
|
372
408
|
autoGenerateTags = true,
|
|
373
409
|
autoRevalidateTags = true,
|
|
@@ -375,7 +411,7 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
375
411
|
onSuccess,
|
|
376
412
|
onError
|
|
377
413
|
} = hookOptions;
|
|
378
|
-
const api =
|
|
414
|
+
const api = enlace(baseUrl, defaultOptions, {
|
|
379
415
|
onSuccess,
|
|
380
416
|
onError
|
|
381
417
|
});
|
|
@@ -407,7 +443,12 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
407
443
|
return useQueryMode(
|
|
408
444
|
api,
|
|
409
445
|
trackingResult.trackedCall,
|
|
410
|
-
{
|
|
446
|
+
{
|
|
447
|
+
autoGenerateTags,
|
|
448
|
+
staleTime,
|
|
449
|
+
enabled: queryOptions?.enabled ?? true,
|
|
450
|
+
pollingInterval: queryOptions?.pollingInterval
|
|
451
|
+
}
|
|
411
452
|
);
|
|
412
453
|
}
|
|
413
454
|
return useEnlaceHook;
|
|
@@ -468,7 +509,7 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
468
509
|
}
|
|
469
510
|
|
|
470
511
|
// src/next/index.ts
|
|
471
|
-
function
|
|
512
|
+
function enlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
|
|
472
513
|
const combinedOptions = { ...defaultOptions, ...nextOptions };
|
|
473
514
|
return createProxyHandler(
|
|
474
515
|
baseUrl,
|
|
@@ -478,15 +519,15 @@ function createEnlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
|
|
|
478
519
|
);
|
|
479
520
|
}
|
|
480
521
|
|
|
481
|
-
// src/next/
|
|
482
|
-
function
|
|
522
|
+
// src/next/enlaceHookNext.ts
|
|
523
|
+
function enlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
483
524
|
const {
|
|
484
525
|
autoGenerateTags = true,
|
|
485
526
|
autoRevalidateTags = true,
|
|
486
527
|
staleTime = 0,
|
|
487
528
|
...nextOptions
|
|
488
529
|
} = hookOptions;
|
|
489
|
-
const api =
|
|
530
|
+
const api = enlaceNext(
|
|
490
531
|
baseUrl,
|
|
491
532
|
defaultOptions,
|
|
492
533
|
{
|
|
@@ -523,13 +564,18 @@ function createEnlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
523
564
|
return useQueryMode(
|
|
524
565
|
api,
|
|
525
566
|
trackedCall,
|
|
526
|
-
{
|
|
567
|
+
{
|
|
568
|
+
autoGenerateTags,
|
|
569
|
+
staleTime,
|
|
570
|
+
enabled: queryOptions?.enabled ?? true,
|
|
571
|
+
pollingInterval: queryOptions?.pollingInterval
|
|
572
|
+
}
|
|
527
573
|
);
|
|
528
574
|
}
|
|
529
575
|
return useEnlaceHook;
|
|
530
576
|
}
|
|
531
577
|
export {
|
|
532
578
|
HTTP_METHODS,
|
|
533
|
-
|
|
534
|
-
|
|
579
|
+
enlaceHookNext,
|
|
580
|
+
enlaceHookReact
|
|
535
581
|
};
|
package/dist/index.d.mts
CHANGED
|
@@ -20,7 +20,7 @@ type ReactRequestOptionsBase = {
|
|
|
20
20
|
*/
|
|
21
21
|
params?: Record<string, string | number>;
|
|
22
22
|
};
|
|
23
|
-
/** Options for
|
|
23
|
+
/** Options for enlaceHookReact factory */
|
|
24
24
|
type EnlaceHookOptions = {
|
|
25
25
|
/**
|
|
26
26
|
* Auto-generate cache tags from URL path for GET requests.
|
|
@@ -44,14 +44,14 @@ type EnlaceHookOptions = {
|
|
|
44
44
|
* @param paths - URL paths to revalidate
|
|
45
45
|
*/
|
|
46
46
|
type ServerRevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
|
|
47
|
-
/** Next.js-specific options (third argument for
|
|
47
|
+
/** Next.js-specific options (third argument for enlaceNext) */
|
|
48
48
|
type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
|
|
49
49
|
/**
|
|
50
50
|
* Handler called after successful mutations to trigger server-side revalidation.
|
|
51
51
|
* Receives auto-generated or manually specified tags and paths.
|
|
52
52
|
* @example
|
|
53
53
|
* ```ts
|
|
54
|
-
*
|
|
54
|
+
* enlaceNext("http://localhost:3000/api/", {}, {
|
|
55
55
|
* serverRevalidator: (tags, paths) => revalidateServerAction(tags, paths)
|
|
56
56
|
* });
|
|
57
57
|
* ```
|
|
@@ -83,6 +83,6 @@ type NextRequestOptionsBase = ReactRequestOptionsBase & {
|
|
|
83
83
|
serverRevalidate?: boolean;
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
declare function
|
|
86
|
+
declare function enlaceNext<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions | null, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, TDefaultError, NextRequestOptionsBase>;
|
|
87
87
|
|
|
88
|
-
export { type NextOptions, type NextRequestOptionsBase,
|
|
88
|
+
export { type NextOptions, type NextRequestOptionsBase, enlaceNext };
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ type ReactRequestOptionsBase = {
|
|
|
20
20
|
*/
|
|
21
21
|
params?: Record<string, string | number>;
|
|
22
22
|
};
|
|
23
|
-
/** Options for
|
|
23
|
+
/** Options for enlaceHookReact factory */
|
|
24
24
|
type EnlaceHookOptions = {
|
|
25
25
|
/**
|
|
26
26
|
* Auto-generate cache tags from URL path for GET requests.
|
|
@@ -44,14 +44,14 @@ type EnlaceHookOptions = {
|
|
|
44
44
|
* @param paths - URL paths to revalidate
|
|
45
45
|
*/
|
|
46
46
|
type ServerRevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
|
|
47
|
-
/** Next.js-specific options (third argument for
|
|
47
|
+
/** Next.js-specific options (third argument for enlaceNext) */
|
|
48
48
|
type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
|
|
49
49
|
/**
|
|
50
50
|
* Handler called after successful mutations to trigger server-side revalidation.
|
|
51
51
|
* Receives auto-generated or manually specified tags and paths.
|
|
52
52
|
* @example
|
|
53
53
|
* ```ts
|
|
54
|
-
*
|
|
54
|
+
* enlaceNext("http://localhost:3000/api/", {}, {
|
|
55
55
|
* serverRevalidator: (tags, paths) => revalidateServerAction(tags, paths)
|
|
56
56
|
* });
|
|
57
57
|
* ```
|
|
@@ -83,6 +83,6 @@ type NextRequestOptionsBase = ReactRequestOptionsBase & {
|
|
|
83
83
|
serverRevalidate?: boolean;
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
declare function
|
|
86
|
+
declare function enlaceNext<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions | null, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, TDefaultError, NextRequestOptionsBase>;
|
|
87
87
|
|
|
88
|
-
export { type NextOptions, type NextRequestOptionsBase,
|
|
88
|
+
export { type NextOptions, type NextRequestOptionsBase, enlaceNext };
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
// src/index.ts
|
|
22
22
|
var src_exports = {};
|
|
23
23
|
__export(src_exports, {
|
|
24
|
-
|
|
24
|
+
enlaceNext: () => enlaceNext
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(src_exports);
|
|
27
27
|
__reExport(src_exports, require("enlace-core"), module.exports);
|
|
@@ -84,7 +84,7 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// src/next/index.ts
|
|
87
|
-
function
|
|
87
|
+
function enlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
|
|
88
88
|
const combinedOptions = { ...defaultOptions, ...nextOptions };
|
|
89
89
|
return (0, import_enlace_core2.createProxyHandler)(
|
|
90
90
|
baseUrl,
|
package/dist/index.mjs
CHANGED
|
@@ -63,7 +63,7 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// src/next/index.ts
|
|
66
|
-
function
|
|
66
|
+
function enlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
|
|
67
67
|
const combinedOptions = { ...defaultOptions, ...nextOptions };
|
|
68
68
|
return createProxyHandler(
|
|
69
69
|
baseUrl,
|
|
@@ -73,5 +73,5 @@ function createEnlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
|
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
export {
|
|
76
|
-
|
|
76
|
+
enlaceNext
|
|
77
77
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enlace",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"enlace-core": "0.0.1-beta.
|
|
21
|
+
"enlace-core": "0.0.1-beta.9"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"react": "^19"
|