enlace 0.0.1-beta.13 → 0.0.1-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ npm install enlace
11
11
  ## Quick Start
12
12
 
13
13
  ```typescript
14
- import { createEnlaceHookReact } from "enlace/hook";
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 = createEnlaceHookReact<ApiSchema, ApiError>(
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 = createEnlaceHookReact("https://api.example.com");
45
- const { data } = useAPI((api) => api.any.path.you.want.get());
44
+ const useAPI = enlaceHookReact("https://api.example.com");
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 = createEnlaceHookReact<ApiSchema>("https://api.example.com");
50
+ const useAPI = enlaceHookReact<ApiSchema>("https://api.example.com");
51
51
  ```
52
52
 
53
53
  ### Schema Structure
@@ -77,12 +77,12 @@ type ApiSchema = {
77
77
  };
78
78
 
79
79
  // Pass global error type - applies to all endpoints
80
- const api = createEnlace<ApiSchema, ApiError>("https://api.example.com");
80
+ const api = enlace<ApiSchema, ApiError>("https://api.example.com");
81
81
 
82
82
  // Usage
83
- api.users.get(); // GET /users
84
- api.users[123].get(); // GET /users/123
85
- api.users[123].profile.get(); // GET /users/123/profile
83
+ api.users.$get(); // GET /users
84
+ api.users[123].$get(); // GET /users/123
85
+ api.users[123].profile.$get(); // GET /users/123/profile
86
86
  ```
87
87
 
88
88
  ### Endpoint Types
@@ -124,8 +124,8 @@ type ApiSchema = {
124
124
  };
125
125
 
126
126
  // Usage - query params are fully typed
127
- const { data } = useAPI((api) => api.users.get({ query: { page: 1, limit: 10 } }));
128
- // api.users.get({ query: { foo: "bar" } }); // ✗ Error: 'foo' does not exist
127
+ const { data } = useAPI((api) => api.users.$get({ query: { page: 1, limit: 10 } }));
128
+ // api.users.$get({ query: { foo: "bar" } }); // ✗ Error: 'foo' does not exist
129
129
  ```
130
130
 
131
131
  #### `EndpointWithFormData<TData, TFormData, TError?>`
@@ -145,7 +145,7 @@ type ApiSchema = {
145
145
  };
146
146
 
147
147
  // Usage - formData is automatically converted to FormData
148
- const { trigger } = useAPI((api) => api.uploads.post);
148
+ const { trigger } = useAPI((api) => api.uploads.$post);
149
149
  trigger({
150
150
  formData: {
151
151
  file: selectedFile, // File object
@@ -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 = createEnlace<ApiSchema, ApiError>("https://api.example.com");
201
- // const useAPI = createEnlaceHookReact<ApiSchema, ApiError>("...");
202
- // const useAPI = createEnlaceHookNext<ApiSchema, ApiError>("...");
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
@@ -211,7 +211,7 @@ For GET requests that fetch data automatically:
211
211
  ```typescript
212
212
  function Posts({ page, limit }: { page: number; limit: number }) {
213
213
  const { data, loading, error } = useAPI((api) =>
214
- api.posts.get({ query: { page, limit, published: true } })
214
+ api.posts.$get({ query: { page, limit, published: true } })
215
215
  );
216
216
 
217
217
  if (loading) return <div>Loading...</div>;
@@ -242,7 +242,7 @@ Skip fetching with the `enabled` option:
242
242
  function ProductForm({ id }: { id: string | "new" }) {
243
243
  // Skip fetching when creating a new product
244
244
  const { data, loading } = useAPI(
245
- (api) => api.products[id].get(),
245
+ (api) => api.products[id].$get(),
246
246
  { enabled: id !== "new" }
247
247
  );
248
248
 
@@ -255,7 +255,7 @@ function ProductForm({ id }: { id: string | "new" }) {
255
255
  ```typescript
256
256
  // Also useful when waiting for a dependency
257
257
  function UserPosts({ userId }: { userId: string | undefined }) {
258
- const { data } = useAPI((api) => api.users[userId!].posts.get(), {
258
+ const { data } = useAPI((api) => api.users[userId!].posts.$get(), {
259
259
  enabled: userId !== undefined,
260
260
  });
261
261
  }
@@ -264,11 +264,79 @@ function UserPosts({ userId }: { userId: string | undefined }) {
264
264
  ```typescript
265
265
  function Post({ id }: { id: number }) {
266
266
  // Automatically re-fetches when `id` or query values change
267
- const { data } = useAPI((api) => api.posts[id].get({ query: { include: "author" } }));
267
+ const { data } = useAPI((api) => api.posts[id].$get({ query: { include: "author" } }));
268
268
  return <div>{data?.title}</div>;
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:
@@ -276,12 +344,12 @@ Multiple components requesting the same data will share a single network request
276
344
  ```typescript
277
345
  // Both components render at the same time
278
346
  function PostTitle({ id }: { id: number }) {
279
- const { data } = useAPI((api) => api.posts[id].get());
347
+ const { data } = useAPI((api) => api.posts[id].$get());
280
348
  return <h1>{data?.title}</h1>;
281
349
  }
282
350
 
283
351
  function PostBody({ id }: { id: number }) {
284
- const { data } = useAPI((api) => api.posts[id].get());
352
+ const { data } = useAPI((api) => api.posts[id].$get());
285
353
  return <p>{data?.body}</p>;
286
354
  }
287
355
 
@@ -303,7 +371,7 @@ For mutations or lazy-loaded requests:
303
371
 
304
372
  ```typescript
305
373
  function DeleteButton({ id }: { id: number }) {
306
- const { trigger, loading } = useAPI((api) => api.posts[id].delete);
374
+ const { trigger, loading } = useAPI((api) => api.posts[id].$delete);
307
375
 
308
376
  return (
309
377
  <button onClick={() => trigger()} disabled={loading}>
@@ -317,7 +385,7 @@ function DeleteButton({ id }: { id: number }) {
317
385
 
318
386
  ```typescript
319
387
  function CreatePost() {
320
- const { trigger, loading, data } = useAPI((api) => api.posts.post);
388
+ const { trigger, loading, data } = useAPI((api) => api.posts.$post);
321
389
 
322
390
  const handleSubmit = async (title: string) => {
323
391
  const result = await trigger({ body: { title } });
@@ -337,7 +405,7 @@ Use `:paramName` syntax for dynamic IDs passed at trigger time:
337
405
  ```typescript
338
406
  function PostList({ posts }: { posts: Post[] }) {
339
407
  // Define once with :id placeholder
340
- const { trigger, loading } = useAPI((api) => api.posts[":id"].delete);
408
+ const { trigger, loading } = useAPI((api) => api.posts[":id"].$delete);
341
409
 
342
410
  const handleDelete = (postId: number) => {
343
411
  // Pass the actual ID when triggering
@@ -363,7 +431,7 @@ function PostList({ posts }: { posts: Post[] }) {
363
431
 
364
432
  ```typescript
365
433
  const { trigger } = useAPI(
366
- (api) => api.users[":userId"].posts[":postId"].delete
434
+ (api) => api.users[":userId"].posts[":postId"].$delete
367
435
  );
368
436
 
369
437
  trigger({ params: { userId: "1", postId: "42" } });
@@ -373,7 +441,7 @@ trigger({ params: { userId: "1", postId: "42" } });
373
441
  **With request body:**
374
442
 
375
443
  ```typescript
376
- const { trigger } = useAPI((api) => api.products[":id"].patch);
444
+ const { trigger } = useAPI((api) => api.products[":id"].$patch);
377
445
 
378
446
  trigger({
379
447
  params: { id: "123" },
@@ -397,7 +465,7 @@ trigger({
397
465
  **Mutations automatically revalidate matching tags:**
398
466
 
399
467
  ```typescript
400
- const { trigger } = useAPI((api) => api.posts.post);
468
+ const { trigger } = useAPI((api) => api.posts.$post);
401
469
 
402
470
  // POST /posts automatically revalidates 'posts' tag
403
471
  // All queries with 'posts' tag will refetch!
@@ -414,10 +482,10 @@ This means in most cases, **you don't need to specify any tags manually**. The c
414
482
 
415
483
  ```typescript
416
484
  // Component A: fetches posts (cached with tag 'posts')
417
- const { data } = useAPI((api) => api.posts.get());
485
+ const { data } = useAPI((api) => api.posts.$get());
418
486
 
419
487
  // Component B: creates a post
420
- const { trigger } = useAPI((api) => api.posts.post);
488
+ const { trigger } = useAPI((api) => api.posts.$post);
421
489
  trigger({ body: { title: "New" } });
422
490
  // → Automatically revalidates 'posts' tag
423
491
  // → Component A refetches automatically!
@@ -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 = createEnlaceHookReact<ApiSchema>(
499
+ const useAPI = enlaceHookReact<ApiSchema>(
432
500
  "https://api.example.com",
433
501
  {},
434
502
  {
@@ -447,7 +515,7 @@ Override auto-generated tags when needed:
447
515
 
448
516
  ```typescript
449
517
  // Custom cache tags
450
- const { data } = useAPI((api) => api.posts.get({ tags: ["my-custom-tag"] }));
518
+ const { data } = useAPI((api) => api.posts.$get({ tags: ["my-custom-tag"] }));
451
519
 
452
520
  // Custom revalidation tags
453
521
  trigger({
@@ -459,7 +527,7 @@ trigger({
459
527
  ### Disable Auto-Revalidation
460
528
 
461
529
  ```typescript
462
- const useAPI = createEnlaceHookReact<ApiSchema>(
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 = createEnlaceHookReact<ApiSchema>(
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 = createEnlaceHookReact<ApiSchema>("https://api.example.com", {
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 = createEnlaceHookReact<ApiSchema>("https://api.example.com", {
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 = createEnlaceHookReact<ApiSchema>("https://api.example.com", {
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}` };
@@ -515,7 +583,7 @@ This also works for per-request headers:
515
583
 
516
584
  ```typescript
517
585
  const { data } = useAPI((api) =>
518
- api.posts.get({
586
+ api.posts.$get({
519
587
  headers: async () => {
520
588
  const token = await refreshToken();
521
589
  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 = createEnlaceHookReact<ApiSchema>(
600
+ const useAPI = enlaceHookReact<ApiSchema>(
533
601
  "https://api.example.com",
534
602
  {
535
603
  headers: { Authorization: "Bearer token" },
@@ -580,12 +648,23 @@ type EnlaceErrorCallbackPayload<T> =
580
648
 
581
649
  ```typescript
582
650
  // Basic usage
583
- const result = useAPI((api) => api.posts.get());
651
+ const result = useAPI((api) => api.posts.$get());
584
652
 
585
653
  // With options
586
654
  const result = useAPI(
587
- (api) => api.posts.get(),
588
- { enabled: true } // Skip fetching when false
655
+ (api) => api.posts.$get(),
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,19 +719,19 @@ type RequestOptions = {
628
719
 
629
720
  ### Server Components
630
721
 
631
- Use `createEnlaceNext` from `enlace` for server components:
722
+ Use `enlaceNext` from `enlace` for server components:
632
723
 
633
724
  ```typescript
634
- import { createEnlaceNext } from "enlace";
725
+ import { enlaceNext } from "enlace";
635
726
 
636
727
  type ApiError = { message: string };
637
728
 
638
- const api = createEnlaceNext<ApiSchema, ApiError>("https://api.example.com", {}, {
729
+ const api = enlaceNext<ApiSchema, ApiError>("https://api.example.com", {}, {
639
730
  autoGenerateTags: true,
640
731
  });
641
732
 
642
733
  export default async function Page() {
643
- const { data } = await api.posts.get({
734
+ const { data } = await api.posts.$get({
644
735
  revalidate: 60, // ISR: revalidate every 60 seconds
645
736
  });
646
737
 
@@ -650,16 +741,16 @@ export default async function Page() {
650
741
 
651
742
  ### Client Components
652
743
 
653
- Use `createEnlaceHookNext` from `enlace/hook` for client components:
744
+ Use `enlaceHookNext` from `enlace/hook` for client components:
654
745
 
655
746
  ```typescript
656
747
  "use client";
657
748
 
658
- import { createEnlaceHookNext } from "enlace/hook";
749
+ import { enlaceHookNext } from "enlace/hook";
659
750
 
660
751
  type ApiError = { message: string };
661
752
 
662
- const useAPI = createEnlaceHookNext<ApiSchema, ApiError>(
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 { createEnlaceHookNext } from "enlace/hook";
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 = createEnlaceHookNext<ApiSchema, ApiError>(
785
+ const useAPI = enlaceHookNext<ApiSchema, ApiError>(
695
786
  "/api",
696
787
  {},
697
788
  {
@@ -704,7 +795,7 @@ const useAPI = createEnlaceHookNext<ApiSchema, ApiError>(
704
795
 
705
796
  ```typescript
706
797
  function CreatePost() {
707
- const { trigger } = useAPI((api) => api.posts.post);
798
+ const { trigger } = useAPI((api) => api.posts.$post);
708
799
 
709
800
  const handleCreate = () => {
710
801
  trigger({
@@ -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 = createEnlaceHookNext<ApiSchema, ApiError>(
815
+ const useAPI = enlaceHookNext<ApiSchema, ApiError>(
725
816
  "/api",
726
817
  {},
727
818
  {
@@ -752,7 +843,7 @@ await trigger({ body: data, serverRevalidate: true });
752
843
  ### Next.js Request Options
753
844
 
754
845
  ```typescript
755
- api.posts.get({
846
+ api.posts.$get({
756
847
  tags: ["posts"], // Next.js cache tags
757
848
  revalidate: 60, // ISR revalidation (seconds)
758
849
  revalidateTags: ["posts"], // Tags to invalidate after mutation
@@ -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 = createEnlaceHookNext<ApiSchema, ApiError>("/api");
861
+ const useAPI = enlaceHookNext<ApiSchema, ApiError>("/api");
771
862
  ```
772
863
 
773
864
  ---
774
865
 
775
866
  ## API Reference
776
867
 
777
- ### `createEnlaceHookReact<TSchema, TDefaultError>(baseUrl, options?, hookOptions?)`
868
+ ### `enlaceHookReact<TSchema, TDefaultError>(baseUrl, options?, hookOptions?)`
778
869
 
779
870
  Creates a React hook for making API calls.
780
871
 
781
- ### `createEnlaceHookNext<TSchema, TDefaultError>(baseUrl, options?, hookOptions?)`
872
+ ### `enlaceHookNext<TSchema, TDefaultError>(baseUrl, options?, hookOptions?)`
782
873
 
783
874
  Creates a Next.js hook with server revalidation support.
784
875
 
785
- ### `createEnlace<TSchema, TDefaultError>(baseUrl, options?, callbacks?)`
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
- ### `createEnlaceNext<TSchema, TDefaultError>(baseUrl, options?, nextOptions?)`
880
+ ### `enlaceNext<TSchema, TDefaultError>(baseUrl, options?, nextOptions?)`
790
881
 
791
882
  Creates a Next.js typed API client with caching support.
792
883
 
@@ -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 createEnlaceHookReact factory */
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 createEnlaceHookReact */
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,16 +118,16 @@ type EnlaceHook<TSchema, TDefaultError = unknown> = {
92
118
  * Called at module level to create a reusable hook.
93
119
  *
94
120
  * @example
95
- * const useAPI = createEnlaceHookReact<ApiSchema>('https://api.com');
121
+ * const useAPI = enlaceHookReact<ApiSchema>('https://api.com');
96
122
  *
97
123
  * // Query mode - auto-fetch (auto-tracks changes, no deps array needed)
98
- * const { loading, data, error } = useAPI((api) => api.posts.get({ query: { userId } }));
124
+ * const { loading, data, error } = useAPI((api) => api.posts.$get({ query: { userId } }));
99
125
  *
100
126
  * // Selector mode - typed trigger for lazy calls
101
- * const { trigger, loading, data, error } = useAPI((api) => api.posts.delete);
127
+ * const { trigger, loading, data, error } = useAPI((api) => api.posts.$delete);
102
128
  * onClick={() => trigger({ body: { id: 1 } })}
103
129
  */
104
- declare function createEnlaceHookReact<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions, hookOptions?: EnlaceHookOptions): EnlaceHook<TSchema, TDefaultError>;
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 createEnlaceNext) */
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
- * createEnlaceNext("http://localhost:3000/api/", {}, {
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 createEnlaceHookNext) - extends React's EnlaceHookOptions */
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 createEnlaceHookNext */
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,17 +188,17 @@ 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 = createEnlaceHookNext<ApiSchema>('https://api.com', {}, {
191
+ * const useAPI = enlaceHookNext<ApiSchema>('https://api.com', {}, {
166
192
  * serverRevalidator: (tags) => revalidateTagsAction(tags),
167
193
  * staleTime: 5000,
168
194
  * });
169
195
  *
170
196
  * // Query mode - auto-fetch
171
- * const { loading, data, error } = useAPI((api) => api.posts.get());
197
+ * const { loading, data, error } = useAPI((api) => api.posts.$get());
172
198
  *
173
199
  * // Selector mode - trigger for mutations
174
- * const { trigger } = useAPI((api) => api.posts.delete);
200
+ * const { trigger } = useAPI((api) => api.posts.$delete);
175
201
  */
176
- declare function createEnlaceHookNext<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions, hookOptions?: NextHookOptions): NextEnlaceHook<TSchema, TDefaultError>;
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, createEnlaceHookNext, createEnlaceHookReact };
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 };
@@ -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 createEnlaceHookReact factory */
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 createEnlaceHookReact */
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,16 +118,16 @@ type EnlaceHook<TSchema, TDefaultError = unknown> = {
92
118
  * Called at module level to create a reusable hook.
93
119
  *
94
120
  * @example
95
- * const useAPI = createEnlaceHookReact<ApiSchema>('https://api.com');
121
+ * const useAPI = enlaceHookReact<ApiSchema>('https://api.com');
96
122
  *
97
123
  * // Query mode - auto-fetch (auto-tracks changes, no deps array needed)
98
- * const { loading, data, error } = useAPI((api) => api.posts.get({ query: { userId } }));
124
+ * const { loading, data, error } = useAPI((api) => api.posts.$get({ query: { userId } }));
99
125
  *
100
126
  * // Selector mode - typed trigger for lazy calls
101
- * const { trigger, loading, data, error } = useAPI((api) => api.posts.delete);
127
+ * const { trigger, loading, data, error } = useAPI((api) => api.posts.$delete);
102
128
  * onClick={() => trigger({ body: { id: 1 } })}
103
129
  */
104
- declare function createEnlaceHookReact<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions, hookOptions?: EnlaceHookOptions): EnlaceHook<TSchema, TDefaultError>;
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 createEnlaceNext) */
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
- * createEnlaceNext("http://localhost:3000/api/", {}, {
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 createEnlaceHookNext) - extends React's EnlaceHookOptions */
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 createEnlaceHookNext */
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,17 +188,17 @@ 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 = createEnlaceHookNext<ApiSchema>('https://api.com', {}, {
191
+ * const useAPI = enlaceHookNext<ApiSchema>('https://api.com', {}, {
166
192
  * serverRevalidator: (tags) => revalidateTagsAction(tags),
167
193
  * staleTime: 5000,
168
194
  * });
169
195
  *
170
196
  * // Query mode - auto-fetch
171
- * const { loading, data, error } = useAPI((api) => api.posts.get());
197
+ * const { loading, data, error } = useAPI((api) => api.posts.$get());
172
198
  *
173
199
  * // Selector mode - trigger for mutations
174
- * const { trigger } = useAPI((api) => api.posts.delete);
200
+ * const { trigger } = useAPI((api) => api.posts.$delete);
175
201
  */
176
- declare function createEnlaceHookNext<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions, hookOptions?: NextHookOptions): NextEnlaceHook<TSchema, TDefaultError>;
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, createEnlaceHookNext, createEnlaceHookReact };
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 };
@@ -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
- createEnlaceHookNext: () => createEnlaceHookNext,
27
- createEnlaceHookReact: () => createEnlaceHookReact
26
+ enlaceHookNext: () => enlaceHookNext,
27
+ enlaceHookReact: () => enlaceHookReact
28
28
  });
29
29
  module.exports = __toCommonJS(hook_exports);
30
30
 
31
- // src/react/createEnlaceHookReact.ts
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/createEnlaceHookReact.ts
398
- function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
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.createEnlace)(baseUrl, defaultOptions, {
439
+ const api = (0, import_enlace_core.enlace)(baseUrl, defaultOptions, {
407
440
  onSuccess,
408
441
  onError
409
442
  });
@@ -429,13 +462,18 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
429
462
  }
430
463
  if (!trackingResult.trackedCall) {
431
464
  throw new Error(
432
- "useAPI query mode requires calling an HTTP method (get, post, etc.). Did you mean to use selector mode? Example: useAPI((api) => api.posts.get())"
465
+ "useAPI query mode requires calling an HTTP method ($get, $post, etc.). Did you mean to use selector mode? Example: useAPI((api) => api.posts.$get())"
433
466
  );
434
467
  }
435
468
  return useQueryMode(
436
469
  api,
437
470
  trackingResult.trackedCall,
438
- { autoGenerateTags, staleTime, enabled: queryOptions?.enabled ?? true }
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 createEnlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
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/createEnlaceHookNext.ts
506
- function createEnlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
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 = createEnlaceNext(
551
+ const api = enlaceNext(
514
552
  baseUrl,
515
553
  defaultOptions,
516
554
  {
@@ -541,13 +579,18 @@ function createEnlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
541
579
  }
542
580
  if (!trackedCall) {
543
581
  throw new Error(
544
- "useAPI query mode requires calling an HTTP method (get, post, etc.). Did you mean to use selector mode? Example: useAPI((api) => api.posts.get())"
582
+ "useAPI query mode requires calling an HTTP method ($get, $post, etc.). Did you mean to use selector mode? Example: useAPI((api) => api.posts.$get())"
545
583
  );
546
584
  }
547
585
  return useQueryMode(
548
586
  api,
549
587
  trackedCall,
550
- { autoGenerateTags, staleTime, enabled: queryOptions?.enabled ?? true }
588
+ {
589
+ autoGenerateTags,
590
+ staleTime,
591
+ enabled: queryOptions?.enabled ?? true,
592
+ pollingInterval: queryOptions?.pollingInterval
593
+ }
551
594
  );
552
595
  }
553
596
  return useEnlaceHook;
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  "use client";
3
3
 
4
- // src/react/createEnlaceHookReact.ts
4
+ // src/react/enlaceHookReact.ts
5
5
  import {
6
- createEnlace
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/createEnlaceHookReact.ts
373
- function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
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 = createEnlace(baseUrl, defaultOptions, {
414
+ const api = enlace(baseUrl, defaultOptions, {
382
415
  onSuccess,
383
416
  onError
384
417
  });
@@ -404,13 +437,18 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
404
437
  }
405
438
  if (!trackingResult.trackedCall) {
406
439
  throw new Error(
407
- "useAPI query mode requires calling an HTTP method (get, post, etc.). Did you mean to use selector mode? Example: useAPI((api) => api.posts.get())"
440
+ "useAPI query mode requires calling an HTTP method ($get, $post, etc.). Did you mean to use selector mode? Example: useAPI((api) => api.posts.$get())"
408
441
  );
409
442
  }
410
443
  return useQueryMode(
411
444
  api,
412
445
  trackingResult.trackedCall,
413
- { autoGenerateTags, staleTime, enabled: queryOptions?.enabled ?? true }
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 createEnlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
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/createEnlaceHookNext.ts
485
- function createEnlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
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 = createEnlaceNext(
530
+ const api = enlaceNext(
493
531
  baseUrl,
494
532
  defaultOptions,
495
533
  {
@@ -520,19 +558,24 @@ function createEnlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
520
558
  }
521
559
  if (!trackedCall) {
522
560
  throw new Error(
523
- "useAPI query mode requires calling an HTTP method (get, post, etc.). Did you mean to use selector mode? Example: useAPI((api) => api.posts.get())"
561
+ "useAPI query mode requires calling an HTTP method ($get, $post, etc.). Did you mean to use selector mode? Example: useAPI((api) => api.posts.$get())"
524
562
  );
525
563
  }
526
564
  return useQueryMode(
527
565
  api,
528
566
  trackedCall,
529
- { autoGenerateTags, staleTime, enabled: queryOptions?.enabled ?? true }
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
- createEnlaceHookNext,
537
- createEnlaceHookReact
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 createEnlaceHookReact factory */
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 createEnlaceNext) */
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
- * createEnlaceNext("http://localhost:3000/api/", {}, {
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 createEnlaceNext<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions | null, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, TDefaultError, NextRequestOptionsBase>;
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, createEnlaceNext };
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 createEnlaceHookReact factory */
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 createEnlaceNext) */
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
- * createEnlaceNext("http://localhost:3000/api/", {}, {
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 createEnlaceNext<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions | null, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, TDefaultError, NextRequestOptionsBase>;
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, createEnlaceNext };
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
- createEnlaceNext: () => createEnlaceNext
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 createEnlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
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 createEnlaceNext(baseUrl, defaultOptions = {}, nextOptions = {}) {
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
- createEnlaceNext
76
+ enlaceNext
77
77
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enlace",
3
- "version": "0.0.1-beta.13",
3
+ "version": "0.0.1-beta.15",
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.8"
21
+ "enlace-core": "0.0.1-beta.10"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "react": "^19"