enlace 0.0.1-beta.13 → 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 +56 -13
- package/dist/hook/index.mjs +57 -14
- 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
|
|
@@ -200,7 +200,7 @@ function resolvePath(path, params) {
|
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
202
|
function useQueryMode(api, trackedCall, options) {
|
|
203
|
-
const { autoGenerateTags, staleTime, enabled } = options;
|
|
203
|
+
const { autoGenerateTags, staleTime, enabled, pollingInterval } = options;
|
|
204
204
|
const queryKey = createQueryKey(trackedCall);
|
|
205
205
|
const requestOptions = trackedCall.options;
|
|
206
206
|
const resolvedPath = resolvePath(trackedCall.path, requestOptions?.params);
|
|
@@ -225,15 +225,41 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
225
225
|
);
|
|
226
226
|
const mountedRef = (0, import_react.useRef)(true);
|
|
227
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;
|
|
228
231
|
(0, import_react.useEffect)(() => {
|
|
229
232
|
mountedRef.current = true;
|
|
230
233
|
if (!enabled) {
|
|
231
234
|
dispatch({ type: "RESET" });
|
|
235
|
+
if (pollingTimeoutRef.current) {
|
|
236
|
+
clearTimeout(pollingTimeoutRef.current);
|
|
237
|
+
pollingTimeoutRef.current = null;
|
|
238
|
+
}
|
|
232
239
|
return () => {
|
|
233
240
|
mountedRef.current = false;
|
|
234
241
|
};
|
|
235
242
|
}
|
|
236
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
|
+
};
|
|
237
263
|
const doFetch = () => {
|
|
238
264
|
const cached2 = getCache(queryKey);
|
|
239
265
|
if (cached2?.promise) {
|
|
@@ -259,6 +285,8 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
259
285
|
timestamp: Date.now(),
|
|
260
286
|
tags: queryTags
|
|
261
287
|
});
|
|
288
|
+
}).finally(() => {
|
|
289
|
+
scheduleNextPoll();
|
|
262
290
|
});
|
|
263
291
|
setCache(queryKey, {
|
|
264
292
|
promise: fetchPromise,
|
|
@@ -269,6 +297,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
269
297
|
const cached = getCache(queryKey);
|
|
270
298
|
if (cached?.data !== void 0 && !isStale(queryKey, staleTime)) {
|
|
271
299
|
dispatch({ type: "SYNC_CACHE", state: getCacheState() });
|
|
300
|
+
scheduleNextPoll();
|
|
272
301
|
} else {
|
|
273
302
|
doFetch();
|
|
274
303
|
}
|
|
@@ -280,6 +309,10 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
280
309
|
return () => {
|
|
281
310
|
mountedRef.current = false;
|
|
282
311
|
fetchRef.current = null;
|
|
312
|
+
if (pollingTimeoutRef.current) {
|
|
313
|
+
clearTimeout(pollingTimeoutRef.current);
|
|
314
|
+
pollingTimeoutRef.current = null;
|
|
315
|
+
}
|
|
283
316
|
unsubscribe();
|
|
284
317
|
};
|
|
285
318
|
}, [queryKey, enabled]);
|
|
@@ -394,8 +427,8 @@ function useSelectorMode(config) {
|
|
|
394
427
|
};
|
|
395
428
|
}
|
|
396
429
|
|
|
397
|
-
// src/react/
|
|
398
|
-
function
|
|
430
|
+
// src/react/enlaceHookReact.ts
|
|
431
|
+
function enlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
399
432
|
const {
|
|
400
433
|
autoGenerateTags = true,
|
|
401
434
|
autoRevalidateTags = true,
|
|
@@ -403,7 +436,7 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
403
436
|
onSuccess,
|
|
404
437
|
onError
|
|
405
438
|
} = hookOptions;
|
|
406
|
-
const api = (0, import_enlace_core.
|
|
439
|
+
const api = (0, import_enlace_core.enlace)(baseUrl, defaultOptions, {
|
|
407
440
|
onSuccess,
|
|
408
441
|
onError
|
|
409
442
|
});
|
|
@@ -435,7 +468,12 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
435
468
|
return useQueryMode(
|
|
436
469
|
api,
|
|
437
470
|
trackingResult.trackedCall,
|
|
438
|
-
{
|
|
471
|
+
{
|
|
472
|
+
autoGenerateTags,
|
|
473
|
+
staleTime,
|
|
474
|
+
enabled: queryOptions?.enabled ?? true,
|
|
475
|
+
pollingInterval: queryOptions?.pollingInterval
|
|
476
|
+
}
|
|
439
477
|
);
|
|
440
478
|
}
|
|
441
479
|
return useEnlaceHook;
|
|
@@ -492,7 +530,7 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
492
530
|
}
|
|
493
531
|
|
|
494
532
|
// src/next/index.ts
|
|
495
|
-
function
|
|
533
|
+
function enlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
|
|
496
534
|
const combinedOptions = { ...defaultOptions, ...nextOptions };
|
|
497
535
|
return (0, import_enlace_core3.createProxyHandler)(
|
|
498
536
|
baseUrl,
|
|
@@ -502,15 +540,15 @@ function createEnlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
|
|
|
502
540
|
);
|
|
503
541
|
}
|
|
504
542
|
|
|
505
|
-
// src/next/
|
|
506
|
-
function
|
|
543
|
+
// src/next/enlaceHookNext.ts
|
|
544
|
+
function enlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
507
545
|
const {
|
|
508
546
|
autoGenerateTags = true,
|
|
509
547
|
autoRevalidateTags = true,
|
|
510
548
|
staleTime = 0,
|
|
511
549
|
...nextOptions
|
|
512
550
|
} = hookOptions;
|
|
513
|
-
const api =
|
|
551
|
+
const api = enlaceNext(
|
|
514
552
|
baseUrl,
|
|
515
553
|
defaultOptions,
|
|
516
554
|
{
|
|
@@ -547,7 +585,12 @@ function createEnlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
547
585
|
return useQueryMode(
|
|
548
586
|
api,
|
|
549
587
|
trackedCall,
|
|
550
|
-
{
|
|
588
|
+
{
|
|
589
|
+
autoGenerateTags,
|
|
590
|
+
staleTime,
|
|
591
|
+
enabled: queryOptions?.enabled ?? true,
|
|
592
|
+
pollingInterval: queryOptions?.pollingInterval
|
|
593
|
+
}
|
|
551
594
|
);
|
|
552
595
|
}
|
|
553
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
|
|
@@ -175,7 +175,7 @@ function resolvePath(path, params) {
|
|
|
175
175
|
});
|
|
176
176
|
}
|
|
177
177
|
function useQueryMode(api, trackedCall, options) {
|
|
178
|
-
const { autoGenerateTags, staleTime, enabled } = options;
|
|
178
|
+
const { autoGenerateTags, staleTime, enabled, pollingInterval } = options;
|
|
179
179
|
const queryKey = createQueryKey(trackedCall);
|
|
180
180
|
const requestOptions = trackedCall.options;
|
|
181
181
|
const resolvedPath = resolvePath(trackedCall.path, requestOptions?.params);
|
|
@@ -200,15 +200,41 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
200
200
|
);
|
|
201
201
|
const mountedRef = useRef(true);
|
|
202
202
|
const fetchRef = useRef(null);
|
|
203
|
+
const pollingTimeoutRef = useRef(null);
|
|
204
|
+
const pollingIntervalRef = useRef(pollingInterval);
|
|
205
|
+
pollingIntervalRef.current = pollingInterval;
|
|
203
206
|
useEffect(() => {
|
|
204
207
|
mountedRef.current = true;
|
|
205
208
|
if (!enabled) {
|
|
206
209
|
dispatch({ type: "RESET" });
|
|
210
|
+
if (pollingTimeoutRef.current) {
|
|
211
|
+
clearTimeout(pollingTimeoutRef.current);
|
|
212
|
+
pollingTimeoutRef.current = null;
|
|
213
|
+
}
|
|
207
214
|
return () => {
|
|
208
215
|
mountedRef.current = false;
|
|
209
216
|
};
|
|
210
217
|
}
|
|
211
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
|
+
};
|
|
212
238
|
const doFetch = () => {
|
|
213
239
|
const cached2 = getCache(queryKey);
|
|
214
240
|
if (cached2?.promise) {
|
|
@@ -234,6 +260,8 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
234
260
|
timestamp: Date.now(),
|
|
235
261
|
tags: queryTags
|
|
236
262
|
});
|
|
263
|
+
}).finally(() => {
|
|
264
|
+
scheduleNextPoll();
|
|
237
265
|
});
|
|
238
266
|
setCache(queryKey, {
|
|
239
267
|
promise: fetchPromise,
|
|
@@ -244,6 +272,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
244
272
|
const cached = getCache(queryKey);
|
|
245
273
|
if (cached?.data !== void 0 && !isStale(queryKey, staleTime)) {
|
|
246
274
|
dispatch({ type: "SYNC_CACHE", state: getCacheState() });
|
|
275
|
+
scheduleNextPoll();
|
|
247
276
|
} else {
|
|
248
277
|
doFetch();
|
|
249
278
|
}
|
|
@@ -255,6 +284,10 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
255
284
|
return () => {
|
|
256
285
|
mountedRef.current = false;
|
|
257
286
|
fetchRef.current = null;
|
|
287
|
+
if (pollingTimeoutRef.current) {
|
|
288
|
+
clearTimeout(pollingTimeoutRef.current);
|
|
289
|
+
pollingTimeoutRef.current = null;
|
|
290
|
+
}
|
|
258
291
|
unsubscribe();
|
|
259
292
|
};
|
|
260
293
|
}, [queryKey, enabled]);
|
|
@@ -369,8 +402,8 @@ function useSelectorMode(config) {
|
|
|
369
402
|
};
|
|
370
403
|
}
|
|
371
404
|
|
|
372
|
-
// src/react/
|
|
373
|
-
function
|
|
405
|
+
// src/react/enlaceHookReact.ts
|
|
406
|
+
function enlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
374
407
|
const {
|
|
375
408
|
autoGenerateTags = true,
|
|
376
409
|
autoRevalidateTags = true,
|
|
@@ -378,7 +411,7 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
378
411
|
onSuccess,
|
|
379
412
|
onError
|
|
380
413
|
} = hookOptions;
|
|
381
|
-
const api =
|
|
414
|
+
const api = enlace(baseUrl, defaultOptions, {
|
|
382
415
|
onSuccess,
|
|
383
416
|
onError
|
|
384
417
|
});
|
|
@@ -410,7 +443,12 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
410
443
|
return useQueryMode(
|
|
411
444
|
api,
|
|
412
445
|
trackingResult.trackedCall,
|
|
413
|
-
{
|
|
446
|
+
{
|
|
447
|
+
autoGenerateTags,
|
|
448
|
+
staleTime,
|
|
449
|
+
enabled: queryOptions?.enabled ?? true,
|
|
450
|
+
pollingInterval: queryOptions?.pollingInterval
|
|
451
|
+
}
|
|
414
452
|
);
|
|
415
453
|
}
|
|
416
454
|
return useEnlaceHook;
|
|
@@ -471,7 +509,7 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
471
509
|
}
|
|
472
510
|
|
|
473
511
|
// src/next/index.ts
|
|
474
|
-
function
|
|
512
|
+
function enlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
|
|
475
513
|
const combinedOptions = { ...defaultOptions, ...nextOptions };
|
|
476
514
|
return createProxyHandler(
|
|
477
515
|
baseUrl,
|
|
@@ -481,15 +519,15 @@ function createEnlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
|
|
|
481
519
|
);
|
|
482
520
|
}
|
|
483
521
|
|
|
484
|
-
// src/next/
|
|
485
|
-
function
|
|
522
|
+
// src/next/enlaceHookNext.ts
|
|
523
|
+
function enlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
486
524
|
const {
|
|
487
525
|
autoGenerateTags = true,
|
|
488
526
|
autoRevalidateTags = true,
|
|
489
527
|
staleTime = 0,
|
|
490
528
|
...nextOptions
|
|
491
529
|
} = hookOptions;
|
|
492
|
-
const api =
|
|
530
|
+
const api = enlaceNext(
|
|
493
531
|
baseUrl,
|
|
494
532
|
defaultOptions,
|
|
495
533
|
{
|
|
@@ -526,13 +564,18 @@ function createEnlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
526
564
|
return useQueryMode(
|
|
527
565
|
api,
|
|
528
566
|
trackedCall,
|
|
529
|
-
{
|
|
567
|
+
{
|
|
568
|
+
autoGenerateTags,
|
|
569
|
+
staleTime,
|
|
570
|
+
enabled: queryOptions?.enabled ?? true,
|
|
571
|
+
pollingInterval: queryOptions?.pollingInterval
|
|
572
|
+
}
|
|
530
573
|
);
|
|
531
574
|
}
|
|
532
575
|
return useEnlaceHook;
|
|
533
576
|
}
|
|
534
577
|
export {
|
|
535
578
|
HTTP_METHODS,
|
|
536
|
-
|
|
537
|
-
|
|
579
|
+
enlaceHookNext,
|
|
580
|
+
enlaceHookReact
|
|
538
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"
|