enlace 0.0.1-beta.10 → 0.0.1-beta.12
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 +137 -29
- package/dist/hook/index.d.mts +22 -17
- package/dist/hook/index.d.ts +22 -17
- package/dist/hook/index.js +68 -33
- package/dist/hook/index.mjs +71 -34
- package/dist/index.d.mts +19 -14
- package/dist/index.d.ts +19 -14
- package/dist/index.js +10 -6
- package/dist/index.mjs +10 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,37 +85,108 @@ api.users[123].get(); // GET /users/123
|
|
|
85
85
|
api.users[123].profile.get(); // GET /users/123/profile
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
### Endpoint
|
|
88
|
+
### Endpoint Types
|
|
89
89
|
|
|
90
|
-
The `Endpoint` type
|
|
90
|
+
The `Endpoint` type helpers let you define response data, request body, query params, formData, and error types.
|
|
91
|
+
|
|
92
|
+
#### `Endpoint<TData, TBody?, TError?>`
|
|
93
|
+
|
|
94
|
+
For endpoints with JSON body:
|
|
91
95
|
|
|
92
96
|
```typescript
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
import { Endpoint } from "enlace";
|
|
98
|
+
|
|
99
|
+
type ApiSchema = {
|
|
100
|
+
posts: {
|
|
101
|
+
$get: Post[]; // Direct type (simplest)
|
|
102
|
+
$post: Endpoint<Post, CreatePost>; // Data + Body
|
|
103
|
+
$put: Endpoint<Post, UpdatePost, ValidationError>; // Data + Body + Error
|
|
104
|
+
$delete: void; // void response
|
|
105
|
+
$patch: Endpoint<Post, never, NotFoundError>; // Custom error without body
|
|
106
|
+
};
|
|
107
|
+
};
|
|
95
108
|
```
|
|
96
109
|
|
|
97
|
-
|
|
110
|
+
#### `EndpointWithQuery<TData, TQuery, TError?>`
|
|
111
|
+
|
|
112
|
+
For endpoints with typed query parameters:
|
|
98
113
|
|
|
99
114
|
```typescript
|
|
115
|
+
import { EndpointWithQuery } from "enlace";
|
|
116
|
+
|
|
100
117
|
type ApiSchema = {
|
|
118
|
+
users: {
|
|
119
|
+
$get: EndpointWithQuery<User[], { page: number; limit: number; search?: string }>;
|
|
120
|
+
};
|
|
101
121
|
posts: {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
122
|
+
$get: EndpointWithQuery<Post[], { status: "draft" | "published" }, ApiError>;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
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
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### `EndpointWithFormData<TData, TFormData, TError?>`
|
|
132
|
+
|
|
133
|
+
For file uploads (multipart/form-data):
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { EndpointWithFormData } from "enlace";
|
|
137
|
+
|
|
138
|
+
type ApiSchema = {
|
|
139
|
+
uploads: {
|
|
140
|
+
$post: EndpointWithFormData<Upload, { file: Blob | File; name: string }>;
|
|
141
|
+
};
|
|
142
|
+
avatars: {
|
|
143
|
+
$post: EndpointWithFormData<Avatar, { image: File }, UploadError>;
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Usage - formData is automatically converted to FormData
|
|
148
|
+
const { trigger } = useAPI((api) => api.uploads.post);
|
|
149
|
+
trigger({
|
|
150
|
+
formData: {
|
|
151
|
+
file: selectedFile, // File object
|
|
152
|
+
name: "document.pdf", // String - converted automatically
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
// → Sends as multipart/form-data
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**FormData conversion rules:**
|
|
105
159
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
160
|
+
| Type | Conversion |
|
|
161
|
+
|------|------------|
|
|
162
|
+
| `File` / `Blob` | Appended directly |
|
|
163
|
+
| `string` / `number` / `boolean` | Converted to string |
|
|
164
|
+
| `object` (nested) | JSON stringified |
|
|
165
|
+
| `array` of primitives | Each item appended separately |
|
|
166
|
+
| `array` of files | Each file appended with same key |
|
|
109
167
|
|
|
110
|
-
|
|
111
|
-
// Overrides global error type for this endpoint
|
|
112
|
-
$put: Endpoint<Post, UpdatePost, ValidationError>;
|
|
168
|
+
#### `EndpointFull<T>`
|
|
113
169
|
|
|
114
|
-
|
|
115
|
-
$delete: void;
|
|
170
|
+
Object-style for complex endpoints:
|
|
116
171
|
|
|
117
|
-
|
|
118
|
-
|
|
172
|
+
```typescript
|
|
173
|
+
import { EndpointFull } from "enlace";
|
|
174
|
+
|
|
175
|
+
type ApiSchema = {
|
|
176
|
+
products: {
|
|
177
|
+
$post: EndpointFull<{
|
|
178
|
+
data: Product;
|
|
179
|
+
body: CreateProduct;
|
|
180
|
+
query: { categoryId: string };
|
|
181
|
+
error: ValidationError;
|
|
182
|
+
}>;
|
|
183
|
+
};
|
|
184
|
+
files: {
|
|
185
|
+
$post: EndpointFull<{
|
|
186
|
+
data: FileUpload;
|
|
187
|
+
formData: { file: File; description: string };
|
|
188
|
+
query: { folder: string };
|
|
189
|
+
}>;
|
|
119
190
|
};
|
|
120
191
|
};
|
|
121
192
|
```
|
|
@@ -270,7 +341,7 @@ function PostList({ posts }: { posts: Post[] }) {
|
|
|
270
341
|
|
|
271
342
|
const handleDelete = (postId: number) => {
|
|
272
343
|
// Pass the actual ID when triggering
|
|
273
|
-
trigger({
|
|
344
|
+
trigger({ params: { id: postId } });
|
|
274
345
|
};
|
|
275
346
|
|
|
276
347
|
return (
|
|
@@ -295,7 +366,7 @@ const { trigger } = useAPI(
|
|
|
295
366
|
(api) => api.users[":userId"].posts[":postId"].delete
|
|
296
367
|
);
|
|
297
368
|
|
|
298
|
-
trigger({
|
|
369
|
+
trigger({ params: { userId: "1", postId: "42" } });
|
|
299
370
|
// → DELETE /users/1/posts/42
|
|
300
371
|
```
|
|
301
372
|
|
|
@@ -305,7 +376,7 @@ trigger({ pathParams: { userId: "1", postId: "42" } });
|
|
|
305
376
|
const { trigger } = useAPI((api) => api.products[":id"].patch);
|
|
306
377
|
|
|
307
378
|
trigger({
|
|
308
|
-
|
|
379
|
+
params: { id: "123" },
|
|
309
380
|
body: { name: "Updated Product" },
|
|
310
381
|
});
|
|
311
382
|
// → PATCH /products/123 with body
|
|
@@ -541,12 +612,13 @@ type UseEnlaceSelectorResult<TMethod> = {
|
|
|
541
612
|
|
|
542
613
|
```typescript
|
|
543
614
|
type RequestOptions = {
|
|
544
|
-
query?:
|
|
545
|
-
body?: TBody; // Request body
|
|
615
|
+
query?: TQuery; // Query parameters (typed when using EndpointWithQuery/EndpointFull)
|
|
616
|
+
body?: TBody; // Request body (JSON)
|
|
617
|
+
formData?: TFormData; // FormData fields (auto-converted, for file uploads)
|
|
546
618
|
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>); // Request headers
|
|
547
619
|
tags?: string[]; // Cache tags (GET only)
|
|
548
620
|
revalidateTags?: string[]; // Tags to invalidate after mutation
|
|
549
|
-
|
|
621
|
+
params?: Record<string, string | number>; // Dynamic path parameters
|
|
550
622
|
};
|
|
551
623
|
```
|
|
552
624
|
|
|
@@ -623,7 +695,7 @@ const useAPI = createEnlaceHookNext<ApiSchema, ApiError>(
|
|
|
623
695
|
"/api",
|
|
624
696
|
{},
|
|
625
697
|
{
|
|
626
|
-
|
|
698
|
+
serverRevalidator: revalidateAction,
|
|
627
699
|
}
|
|
628
700
|
);
|
|
629
701
|
```
|
|
@@ -637,13 +709,46 @@ function CreatePost() {
|
|
|
637
709
|
const handleCreate = () => {
|
|
638
710
|
trigger({
|
|
639
711
|
body: { title: "New Post" },
|
|
640
|
-
revalidateTags: ["posts"], // Passed to
|
|
641
|
-
revalidatePaths: ["/posts"], // Passed to
|
|
712
|
+
revalidateTags: ["posts"], // Passed to serverRevalidator
|
|
713
|
+
revalidatePaths: ["/posts"], // Passed to serverRevalidator
|
|
642
714
|
});
|
|
643
715
|
};
|
|
644
716
|
}
|
|
645
717
|
```
|
|
646
718
|
|
|
719
|
+
### CSR-Heavy Projects
|
|
720
|
+
|
|
721
|
+
For projects that primarily use client-side rendering with minimal SSR, you can disable server-side revalidation by default:
|
|
722
|
+
|
|
723
|
+
```typescript
|
|
724
|
+
const useAPI = createEnlaceHookNext<ApiSchema, ApiError>(
|
|
725
|
+
"/api",
|
|
726
|
+
{},
|
|
727
|
+
{
|
|
728
|
+
serverRevalidator: revalidateAction,
|
|
729
|
+
skipServerRevalidation: true, // Disable server revalidation by default
|
|
730
|
+
}
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
// Mutations won't trigger server revalidation by default
|
|
734
|
+
await trigger({ body: { title: "New Post" } });
|
|
735
|
+
|
|
736
|
+
// Opt-in to server revalidation when needed
|
|
737
|
+
await trigger({ body: { title: "New Post" }, serverRevalidate: true });
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
### Per-Request Server Revalidation Control
|
|
741
|
+
|
|
742
|
+
Override the global setting for individual requests:
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
// Skip server revalidation for this request
|
|
746
|
+
await trigger({ body: data, serverRevalidate: false });
|
|
747
|
+
|
|
748
|
+
// Force server revalidation for this request
|
|
749
|
+
await trigger({ body: data, serverRevalidate: true });
|
|
750
|
+
```
|
|
751
|
+
|
|
647
752
|
### Next.js Request Options
|
|
648
753
|
|
|
649
754
|
```typescript
|
|
@@ -652,7 +757,7 @@ api.posts.get({
|
|
|
652
757
|
revalidate: 60, // ISR revalidation (seconds)
|
|
653
758
|
revalidateTags: ["posts"], // Tags to invalidate after mutation
|
|
654
759
|
revalidatePaths: ["/"], // Paths to revalidate after mutation
|
|
655
|
-
|
|
760
|
+
serverRevalidate: true, // Control server-side revalidation per-request
|
|
656
761
|
});
|
|
657
762
|
```
|
|
658
763
|
|
|
@@ -710,7 +815,10 @@ type EnlaceHookOptions = {
|
|
|
710
815
|
|
|
711
816
|
### Re-exports from enlace-core
|
|
712
817
|
|
|
713
|
-
- `Endpoint` — Type helper for
|
|
818
|
+
- `Endpoint` — Type helper for endpoints with JSON body
|
|
819
|
+
- `EndpointWithQuery` — Type helper for endpoints with typed query params
|
|
820
|
+
- `EndpointWithFormData` — Type helper for file upload endpoints
|
|
821
|
+
- `EndpointFull` — Object-style type helper for complex endpoints
|
|
714
822
|
- `EnlaceResponse` — Response type
|
|
715
823
|
- `EnlaceOptions` — Fetch options type
|
|
716
824
|
|
package/dist/hook/index.d.mts
CHANGED
|
@@ -15,9 +15,9 @@ type ReactRequestOptionsBase = {
|
|
|
15
15
|
* Used to replace :paramName placeholders in the URL path.
|
|
16
16
|
* @example
|
|
17
17
|
* // With path api.products[':id'].delete
|
|
18
|
-
* trigger({
|
|
18
|
+
* trigger({ params: { id: '123' } }) // → DELETE /products/123
|
|
19
19
|
*/
|
|
20
|
-
|
|
20
|
+
params?: Record<string, string | number>;
|
|
21
21
|
};
|
|
22
22
|
/** Options for query mode hooks */
|
|
23
23
|
type UseEnlaceQueryOptions = {
|
|
@@ -108,7 +108,7 @@ declare function createEnlaceHookReact<TSchema = unknown, TDefaultError = unknow
|
|
|
108
108
|
* @param tags - Cache tags to revalidate
|
|
109
109
|
* @param paths - URL paths to revalidate
|
|
110
110
|
*/
|
|
111
|
-
type
|
|
111
|
+
type ServerRevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
|
|
112
112
|
/** Next.js-specific options (third argument for createEnlaceNext) */
|
|
113
113
|
type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
|
|
114
114
|
/**
|
|
@@ -117,32 +117,37 @@ type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateT
|
|
|
117
117
|
* @example
|
|
118
118
|
* ```ts
|
|
119
119
|
* createEnlaceNext("http://localhost:3000/api/", {}, {
|
|
120
|
-
*
|
|
120
|
+
* serverRevalidator: (tags, paths) => revalidateServerAction(tags, paths)
|
|
121
121
|
* });
|
|
122
122
|
* ```
|
|
123
123
|
*/
|
|
124
|
-
|
|
124
|
+
serverRevalidator?: ServerRevalidateHandler;
|
|
125
|
+
/**
|
|
126
|
+
* Skip server-side revalidation by default for all mutations.
|
|
127
|
+
* Individual requests can override with serverRevalidate: true.
|
|
128
|
+
* Useful for CSR-heavy apps where server cache invalidation is rarely needed.
|
|
129
|
+
* @default false
|
|
130
|
+
*/
|
|
131
|
+
skipServerRevalidation?: boolean;
|
|
125
132
|
};
|
|
126
133
|
/** Next.js hook options (third argument for createEnlaceHookNext) - extends React's EnlaceHookOptions */
|
|
127
|
-
type NextHookOptions = EnlaceHookOptions & Pick<NextOptions, "
|
|
134
|
+
type NextHookOptions = EnlaceHookOptions & Pick<NextOptions, "serverRevalidator" | "skipServerRevalidation">;
|
|
128
135
|
/** Per-request options for Next.js fetch - extends React's base options */
|
|
129
136
|
type NextRequestOptionsBase = ReactRequestOptionsBase & {
|
|
130
137
|
/** Time in seconds to revalidate, or false to disable */
|
|
131
138
|
revalidate?: number | false;
|
|
132
139
|
/**
|
|
133
|
-
* URL paths to revalidate after mutation
|
|
134
|
-
*
|
|
135
|
-
* You must implement the revalidation logic in the revalidator.
|
|
140
|
+
* URL paths to revalidate after mutation.
|
|
141
|
+
* Passed to the serverRevalidator handler.
|
|
136
142
|
*/
|
|
137
143
|
revalidatePaths?: string[];
|
|
138
144
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* Eg. you don't fetch any data on server component and you might want to skip the overhead of revalidation.
|
|
145
|
+
* Control server-side revalidation for this specific request.
|
|
146
|
+
* - true: Force server revalidation
|
|
147
|
+
* - false: Skip server revalidation
|
|
148
|
+
* When undefined, follows the global skipServerRevalidation setting.
|
|
144
149
|
*/
|
|
145
|
-
|
|
150
|
+
serverRevalidate?: boolean;
|
|
146
151
|
};
|
|
147
152
|
type NextQueryFn<TSchema, TData, TError, TDefaultError = unknown> = QueryFn<TSchema, TData, TError, TDefaultError, NextRequestOptionsBase>;
|
|
148
153
|
type NextSelectorFn<TSchema, TMethod, TDefaultError = unknown> = SelectorFn<TSchema, TMethod, TDefaultError, NextRequestOptionsBase>;
|
|
@@ -154,11 +159,11 @@ type NextEnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
|
154
159
|
|
|
155
160
|
/**
|
|
156
161
|
* Creates a React hook for making API calls in Next.js Client Components.
|
|
157
|
-
* Uses Next.js-specific features like
|
|
162
|
+
* Uses Next.js-specific features like serverRevalidator for server-side cache invalidation.
|
|
158
163
|
*
|
|
159
164
|
* @example
|
|
160
165
|
* const useAPI = createEnlaceHookNext<ApiSchema>('https://api.com', {}, {
|
|
161
|
-
*
|
|
166
|
+
* serverRevalidator: (tags) => revalidateTagsAction(tags),
|
|
162
167
|
* staleTime: 5000,
|
|
163
168
|
* });
|
|
164
169
|
*
|
package/dist/hook/index.d.ts
CHANGED
|
@@ -15,9 +15,9 @@ type ReactRequestOptionsBase = {
|
|
|
15
15
|
* Used to replace :paramName placeholders in the URL path.
|
|
16
16
|
* @example
|
|
17
17
|
* // With path api.products[':id'].delete
|
|
18
|
-
* trigger({
|
|
18
|
+
* trigger({ params: { id: '123' } }) // → DELETE /products/123
|
|
19
19
|
*/
|
|
20
|
-
|
|
20
|
+
params?: Record<string, string | number>;
|
|
21
21
|
};
|
|
22
22
|
/** Options for query mode hooks */
|
|
23
23
|
type UseEnlaceQueryOptions = {
|
|
@@ -108,7 +108,7 @@ declare function createEnlaceHookReact<TSchema = unknown, TDefaultError = unknow
|
|
|
108
108
|
* @param tags - Cache tags to revalidate
|
|
109
109
|
* @param paths - URL paths to revalidate
|
|
110
110
|
*/
|
|
111
|
-
type
|
|
111
|
+
type ServerRevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
|
|
112
112
|
/** Next.js-specific options (third argument for createEnlaceNext) */
|
|
113
113
|
type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
|
|
114
114
|
/**
|
|
@@ -117,32 +117,37 @@ type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateT
|
|
|
117
117
|
* @example
|
|
118
118
|
* ```ts
|
|
119
119
|
* createEnlaceNext("http://localhost:3000/api/", {}, {
|
|
120
|
-
*
|
|
120
|
+
* serverRevalidator: (tags, paths) => revalidateServerAction(tags, paths)
|
|
121
121
|
* });
|
|
122
122
|
* ```
|
|
123
123
|
*/
|
|
124
|
-
|
|
124
|
+
serverRevalidator?: ServerRevalidateHandler;
|
|
125
|
+
/**
|
|
126
|
+
* Skip server-side revalidation by default for all mutations.
|
|
127
|
+
* Individual requests can override with serverRevalidate: true.
|
|
128
|
+
* Useful for CSR-heavy apps where server cache invalidation is rarely needed.
|
|
129
|
+
* @default false
|
|
130
|
+
*/
|
|
131
|
+
skipServerRevalidation?: boolean;
|
|
125
132
|
};
|
|
126
133
|
/** Next.js hook options (third argument for createEnlaceHookNext) - extends React's EnlaceHookOptions */
|
|
127
|
-
type NextHookOptions = EnlaceHookOptions & Pick<NextOptions, "
|
|
134
|
+
type NextHookOptions = EnlaceHookOptions & Pick<NextOptions, "serverRevalidator" | "skipServerRevalidation">;
|
|
128
135
|
/** Per-request options for Next.js fetch - extends React's base options */
|
|
129
136
|
type NextRequestOptionsBase = ReactRequestOptionsBase & {
|
|
130
137
|
/** Time in seconds to revalidate, or false to disable */
|
|
131
138
|
revalidate?: number | false;
|
|
132
139
|
/**
|
|
133
|
-
* URL paths to revalidate after mutation
|
|
134
|
-
*
|
|
135
|
-
* You must implement the revalidation logic in the revalidator.
|
|
140
|
+
* URL paths to revalidate after mutation.
|
|
141
|
+
* Passed to the serverRevalidator handler.
|
|
136
142
|
*/
|
|
137
143
|
revalidatePaths?: string[];
|
|
138
144
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* Eg. you don't fetch any data on server component and you might want to skip the overhead of revalidation.
|
|
145
|
+
* Control server-side revalidation for this specific request.
|
|
146
|
+
* - true: Force server revalidation
|
|
147
|
+
* - false: Skip server revalidation
|
|
148
|
+
* When undefined, follows the global skipServerRevalidation setting.
|
|
144
149
|
*/
|
|
145
|
-
|
|
150
|
+
serverRevalidate?: boolean;
|
|
146
151
|
};
|
|
147
152
|
type NextQueryFn<TSchema, TData, TError, TDefaultError = unknown> = QueryFn<TSchema, TData, TError, TDefaultError, NextRequestOptionsBase>;
|
|
148
153
|
type NextSelectorFn<TSchema, TMethod, TDefaultError = unknown> = SelectorFn<TSchema, TMethod, TDefaultError, NextRequestOptionsBase>;
|
|
@@ -154,11 +159,11 @@ type NextEnlaceHook<TSchema, TDefaultError = unknown> = {
|
|
|
154
159
|
|
|
155
160
|
/**
|
|
156
161
|
* Creates a React hook for making API calls in Next.js Client Components.
|
|
157
|
-
* Uses Next.js-specific features like
|
|
162
|
+
* Uses Next.js-specific features like serverRevalidator for server-side cache invalidation.
|
|
158
163
|
*
|
|
159
164
|
* @example
|
|
160
165
|
* const useAPI = createEnlaceHookNext<ApiSchema>('https://api.com', {}, {
|
|
161
|
-
*
|
|
166
|
+
* serverRevalidator: (tags) => revalidateTagsAction(tags),
|
|
162
167
|
* staleTime: 5000,
|
|
163
168
|
* });
|
|
164
169
|
*
|
package/dist/hook/index.js
CHANGED
|
@@ -49,7 +49,8 @@ function hookReducer(state, action) {
|
|
|
49
49
|
return {
|
|
50
50
|
...state,
|
|
51
51
|
loading: state.data === void 0,
|
|
52
|
-
fetching: true
|
|
52
|
+
fetching: true,
|
|
53
|
+
error: void 0
|
|
53
54
|
};
|
|
54
55
|
case "FETCH_SUCCESS":
|
|
55
56
|
return {
|
|
@@ -78,12 +79,21 @@ function generateTags(path) {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
// src/utils/sortObjectKeys.ts
|
|
81
|
-
function sortObjectKeys(obj) {
|
|
82
|
+
function sortObjectKeys(obj, seen = /* @__PURE__ */ new WeakSet()) {
|
|
82
83
|
if (obj === null || typeof obj !== "object") return obj;
|
|
83
|
-
if (
|
|
84
|
+
if (seen.has(obj)) {
|
|
85
|
+
return "[Circular]";
|
|
86
|
+
}
|
|
87
|
+
seen.add(obj);
|
|
88
|
+
if (Array.isArray(obj)) {
|
|
89
|
+
return obj.map((item) => sortObjectKeys(item, seen));
|
|
90
|
+
}
|
|
84
91
|
return Object.keys(obj).sort().reduce(
|
|
85
92
|
(sorted, key) => {
|
|
86
|
-
sorted[key] = sortObjectKeys(
|
|
93
|
+
sorted[key] = sortObjectKeys(
|
|
94
|
+
obj[key],
|
|
95
|
+
seen
|
|
96
|
+
);
|
|
87
97
|
return sorted;
|
|
88
98
|
},
|
|
89
99
|
{}
|
|
@@ -149,10 +159,9 @@ function clearCacheByTags(tags) {
|
|
|
149
159
|
cache.forEach((entry) => {
|
|
150
160
|
const hasMatch = entry.tags.some((tag) => tags.includes(tag));
|
|
151
161
|
if (hasMatch) {
|
|
152
|
-
entry.data = void 0;
|
|
153
|
-
entry.error = void 0;
|
|
154
162
|
entry.timestamp = 0;
|
|
155
163
|
delete entry.promise;
|
|
164
|
+
entry.subscribers.forEach((cb) => cb());
|
|
156
165
|
}
|
|
157
166
|
});
|
|
158
167
|
}
|
|
@@ -169,12 +178,12 @@ function onRevalidate(callback) {
|
|
|
169
178
|
}
|
|
170
179
|
|
|
171
180
|
// src/react/useQueryMode.ts
|
|
172
|
-
function resolvePath(path,
|
|
173
|
-
if (!
|
|
181
|
+
function resolvePath(path, params) {
|
|
182
|
+
if (!params) return path;
|
|
174
183
|
return path.map((segment) => {
|
|
175
184
|
if (segment.startsWith(":")) {
|
|
176
185
|
const paramName = segment.slice(1);
|
|
177
|
-
const value =
|
|
186
|
+
const value = params[paramName];
|
|
178
187
|
if (value === void 0) {
|
|
179
188
|
throw new Error(`Missing path parameter: ${paramName}`);
|
|
180
189
|
}
|
|
@@ -187,16 +196,14 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
187
196
|
const { autoGenerateTags, staleTime, enabled } = options;
|
|
188
197
|
const queryKey = createQueryKey(trackedCall);
|
|
189
198
|
const requestOptions = trackedCall.options;
|
|
190
|
-
const resolvedPath = resolvePath(
|
|
191
|
-
trackedCall.path,
|
|
192
|
-
requestOptions?.pathParams
|
|
193
|
-
);
|
|
199
|
+
const resolvedPath = resolvePath(trackedCall.path, requestOptions?.params);
|
|
194
200
|
const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
|
|
195
201
|
const getCacheState = (includeNeedsFetch = false) => {
|
|
196
202
|
const cached = getCache(queryKey);
|
|
197
203
|
const hasCachedData = cached?.data !== void 0;
|
|
198
204
|
const isFetching = !!cached?.promise;
|
|
199
|
-
const
|
|
205
|
+
const stale = isStale(queryKey, staleTime);
|
|
206
|
+
const needsFetch = includeNeedsFetch && (!hasCachedData || stale);
|
|
200
207
|
return {
|
|
201
208
|
loading: !hasCachedData && (isFetching || needsFetch),
|
|
202
209
|
fetching: isFetching || needsFetch,
|
|
@@ -240,6 +247,15 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
240
247
|
tags: queryTags
|
|
241
248
|
});
|
|
242
249
|
}
|
|
250
|
+
}).catch((err) => {
|
|
251
|
+
if (mountedRef.current) {
|
|
252
|
+
setCache(queryKey, {
|
|
253
|
+
data: void 0,
|
|
254
|
+
error: err,
|
|
255
|
+
timestamp: Date.now(),
|
|
256
|
+
tags: queryTags
|
|
257
|
+
});
|
|
258
|
+
}
|
|
243
259
|
});
|
|
244
260
|
setCache(queryKey, {
|
|
245
261
|
promise: fetchPromise,
|
|
@@ -310,12 +326,12 @@ function createTrackingProxy(onTrack) {
|
|
|
310
326
|
|
|
311
327
|
// src/react/useSelectorMode.ts
|
|
312
328
|
var import_react2 = require("react");
|
|
313
|
-
function resolvePath2(path,
|
|
314
|
-
if (!
|
|
329
|
+
function resolvePath2(path, params) {
|
|
330
|
+
if (!params) return path;
|
|
315
331
|
return path.map((segment) => {
|
|
316
332
|
if (segment.startsWith(":")) {
|
|
317
333
|
const paramName = segment.slice(1);
|
|
318
|
-
const value =
|
|
334
|
+
const value = params[paramName];
|
|
319
335
|
if (value === void 0) {
|
|
320
336
|
throw new Error(`Missing path parameter: ${paramName}`);
|
|
321
337
|
}
|
|
@@ -345,7 +361,7 @@ function useSelectorMode(config) {
|
|
|
345
361
|
triggerRef.current = (async (...args) => {
|
|
346
362
|
dispatch({ type: "FETCH_START" });
|
|
347
363
|
const options = args[0];
|
|
348
|
-
const resolvedPath = resolvePath2(pathRef.current, options?.
|
|
364
|
+
const resolvedPath = resolvePath2(pathRef.current, options?.params);
|
|
349
365
|
let res;
|
|
350
366
|
if (hasPathParams(pathRef.current)) {
|
|
351
367
|
let current = apiRef.current;
|
|
@@ -384,7 +400,10 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
384
400
|
onSuccess,
|
|
385
401
|
onError
|
|
386
402
|
} = hookOptions;
|
|
387
|
-
const api = (0, import_enlace_core.createEnlace)(baseUrl, defaultOptions, {
|
|
403
|
+
const api = (0, import_enlace_core.createEnlace)(baseUrl, defaultOptions, {
|
|
404
|
+
onSuccess,
|
|
405
|
+
onError
|
|
406
|
+
});
|
|
388
407
|
function useEnlaceHook(selectorOrQuery, queryOptions) {
|
|
389
408
|
let trackingResult = {
|
|
390
409
|
trackedCall: null,
|
|
@@ -394,9 +413,7 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
394
413
|
const trackingProxy = createTrackingProxy((result2) => {
|
|
395
414
|
trackingResult = result2;
|
|
396
415
|
});
|
|
397
|
-
const result = selectorOrQuery(
|
|
398
|
-
trackingProxy
|
|
399
|
-
);
|
|
416
|
+
const result = selectorOrQuery(trackingProxy);
|
|
400
417
|
if (typeof result === "function") {
|
|
401
418
|
const actualResult = selectorOrQuery(api);
|
|
402
419
|
return useSelectorMode({
|
|
@@ -407,6 +424,11 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
407
424
|
autoRevalidateTags
|
|
408
425
|
});
|
|
409
426
|
}
|
|
427
|
+
if (!trackingResult.trackedCall) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
"useAPI query mode requires calling an HTTP method (get, post, etc.). Did you mean to use selector mode? Example: useAPI((api) => api.posts.get())"
|
|
430
|
+
);
|
|
431
|
+
}
|
|
410
432
|
return useQueryMode(
|
|
411
433
|
api,
|
|
412
434
|
trackingResult.trackedCall,
|
|
@@ -425,18 +447,22 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
425
447
|
const {
|
|
426
448
|
autoGenerateTags = true,
|
|
427
449
|
autoRevalidateTags = true,
|
|
428
|
-
|
|
450
|
+
skipServerRevalidation = false,
|
|
451
|
+
serverRevalidator,
|
|
429
452
|
onSuccess,
|
|
430
453
|
...coreOptions
|
|
431
454
|
} = combinedOptions;
|
|
432
455
|
const isGet = method === "GET";
|
|
433
456
|
const autoTags = generateTags(path);
|
|
434
457
|
const nextOnSuccess = (payload) => {
|
|
435
|
-
if (!isGet
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
458
|
+
if (!isGet) {
|
|
459
|
+
const shouldRevalidateServer = requestOptions?.serverRevalidate ?? !skipServerRevalidation;
|
|
460
|
+
if (shouldRevalidateServer) {
|
|
461
|
+
const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
|
|
462
|
+
const revalidatePaths = requestOptions?.revalidatePaths ?? [];
|
|
463
|
+
if (revalidateTags.length || revalidatePaths.length) {
|
|
464
|
+
serverRevalidator?.(revalidateTags, revalidatePaths);
|
|
465
|
+
}
|
|
440
466
|
}
|
|
441
467
|
}
|
|
442
468
|
onSuccess?.(payload);
|
|
@@ -481,11 +507,15 @@ function createEnlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
481
507
|
staleTime = 0,
|
|
482
508
|
...nextOptions
|
|
483
509
|
} = hookOptions;
|
|
484
|
-
const api = createEnlaceNext(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
510
|
+
const api = createEnlaceNext(
|
|
511
|
+
baseUrl,
|
|
512
|
+
defaultOptions,
|
|
513
|
+
{
|
|
514
|
+
autoGenerateTags,
|
|
515
|
+
autoRevalidateTags,
|
|
516
|
+
...nextOptions
|
|
517
|
+
}
|
|
518
|
+
);
|
|
489
519
|
function useEnlaceHook(selectorOrQuery, queryOptions) {
|
|
490
520
|
let trackedCall = null;
|
|
491
521
|
let selectorPath = null;
|
|
@@ -506,6 +536,11 @@ function createEnlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
506
536
|
autoRevalidateTags
|
|
507
537
|
});
|
|
508
538
|
}
|
|
539
|
+
if (!trackedCall) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
"useAPI query mode requires calling an HTTP method (get, post, etc.). Did you mean to use selector mode? Example: useAPI((api) => api.posts.get())"
|
|
542
|
+
);
|
|
543
|
+
}
|
|
509
544
|
return useQueryMode(
|
|
510
545
|
api,
|
|
511
546
|
trackedCall,
|
package/dist/hook/index.mjs
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
4
|
// src/react/createEnlaceHookReact.ts
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createEnlace
|
|
7
|
+
} from "enlace-core";
|
|
6
8
|
|
|
7
9
|
// src/react/useQueryMode.ts
|
|
8
10
|
import { useRef, useReducer, useEffect } from "react";
|
|
@@ -22,7 +24,8 @@ function hookReducer(state, action) {
|
|
|
22
24
|
return {
|
|
23
25
|
...state,
|
|
24
26
|
loading: state.data === void 0,
|
|
25
|
-
fetching: true
|
|
27
|
+
fetching: true,
|
|
28
|
+
error: void 0
|
|
26
29
|
};
|
|
27
30
|
case "FETCH_SUCCESS":
|
|
28
31
|
return {
|
|
@@ -51,12 +54,21 @@ function generateTags(path) {
|
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
// src/utils/sortObjectKeys.ts
|
|
54
|
-
function sortObjectKeys(obj) {
|
|
57
|
+
function sortObjectKeys(obj, seen = /* @__PURE__ */ new WeakSet()) {
|
|
55
58
|
if (obj === null || typeof obj !== "object") return obj;
|
|
56
|
-
if (
|
|
59
|
+
if (seen.has(obj)) {
|
|
60
|
+
return "[Circular]";
|
|
61
|
+
}
|
|
62
|
+
seen.add(obj);
|
|
63
|
+
if (Array.isArray(obj)) {
|
|
64
|
+
return obj.map((item) => sortObjectKeys(item, seen));
|
|
65
|
+
}
|
|
57
66
|
return Object.keys(obj).sort().reduce(
|
|
58
67
|
(sorted, key) => {
|
|
59
|
-
sorted[key] = sortObjectKeys(
|
|
68
|
+
sorted[key] = sortObjectKeys(
|
|
69
|
+
obj[key],
|
|
70
|
+
seen
|
|
71
|
+
);
|
|
60
72
|
return sorted;
|
|
61
73
|
},
|
|
62
74
|
{}
|
|
@@ -122,10 +134,9 @@ function clearCacheByTags(tags) {
|
|
|
122
134
|
cache.forEach((entry) => {
|
|
123
135
|
const hasMatch = entry.tags.some((tag) => tags.includes(tag));
|
|
124
136
|
if (hasMatch) {
|
|
125
|
-
entry.data = void 0;
|
|
126
|
-
entry.error = void 0;
|
|
127
137
|
entry.timestamp = 0;
|
|
128
138
|
delete entry.promise;
|
|
139
|
+
entry.subscribers.forEach((cb) => cb());
|
|
129
140
|
}
|
|
130
141
|
});
|
|
131
142
|
}
|
|
@@ -142,12 +153,12 @@ function onRevalidate(callback) {
|
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
// src/react/useQueryMode.ts
|
|
145
|
-
function resolvePath(path,
|
|
146
|
-
if (!
|
|
156
|
+
function resolvePath(path, params) {
|
|
157
|
+
if (!params) return path;
|
|
147
158
|
return path.map((segment) => {
|
|
148
159
|
if (segment.startsWith(":")) {
|
|
149
160
|
const paramName = segment.slice(1);
|
|
150
|
-
const value =
|
|
161
|
+
const value = params[paramName];
|
|
151
162
|
if (value === void 0) {
|
|
152
163
|
throw new Error(`Missing path parameter: ${paramName}`);
|
|
153
164
|
}
|
|
@@ -160,16 +171,14 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
160
171
|
const { autoGenerateTags, staleTime, enabled } = options;
|
|
161
172
|
const queryKey = createQueryKey(trackedCall);
|
|
162
173
|
const requestOptions = trackedCall.options;
|
|
163
|
-
const resolvedPath = resolvePath(
|
|
164
|
-
trackedCall.path,
|
|
165
|
-
requestOptions?.pathParams
|
|
166
|
-
);
|
|
174
|
+
const resolvedPath = resolvePath(trackedCall.path, requestOptions?.params);
|
|
167
175
|
const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
|
|
168
176
|
const getCacheState = (includeNeedsFetch = false) => {
|
|
169
177
|
const cached = getCache(queryKey);
|
|
170
178
|
const hasCachedData = cached?.data !== void 0;
|
|
171
179
|
const isFetching = !!cached?.promise;
|
|
172
|
-
const
|
|
180
|
+
const stale = isStale(queryKey, staleTime);
|
|
181
|
+
const needsFetch = includeNeedsFetch && (!hasCachedData || stale);
|
|
173
182
|
return {
|
|
174
183
|
loading: !hasCachedData && (isFetching || needsFetch),
|
|
175
184
|
fetching: isFetching || needsFetch,
|
|
@@ -213,6 +222,15 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
213
222
|
tags: queryTags
|
|
214
223
|
});
|
|
215
224
|
}
|
|
225
|
+
}).catch((err) => {
|
|
226
|
+
if (mountedRef.current) {
|
|
227
|
+
setCache(queryKey, {
|
|
228
|
+
data: void 0,
|
|
229
|
+
error: err,
|
|
230
|
+
timestamp: Date.now(),
|
|
231
|
+
tags: queryTags
|
|
232
|
+
});
|
|
233
|
+
}
|
|
216
234
|
});
|
|
217
235
|
setCache(queryKey, {
|
|
218
236
|
promise: fetchPromise,
|
|
@@ -283,12 +301,12 @@ function createTrackingProxy(onTrack) {
|
|
|
283
301
|
|
|
284
302
|
// src/react/useSelectorMode.ts
|
|
285
303
|
import { useRef as useRef2, useReducer as useReducer2 } from "react";
|
|
286
|
-
function resolvePath2(path,
|
|
287
|
-
if (!
|
|
304
|
+
function resolvePath2(path, params) {
|
|
305
|
+
if (!params) return path;
|
|
288
306
|
return path.map((segment) => {
|
|
289
307
|
if (segment.startsWith(":")) {
|
|
290
308
|
const paramName = segment.slice(1);
|
|
291
|
-
const value =
|
|
309
|
+
const value = params[paramName];
|
|
292
310
|
if (value === void 0) {
|
|
293
311
|
throw new Error(`Missing path parameter: ${paramName}`);
|
|
294
312
|
}
|
|
@@ -318,7 +336,7 @@ function useSelectorMode(config) {
|
|
|
318
336
|
triggerRef.current = (async (...args) => {
|
|
319
337
|
dispatch({ type: "FETCH_START" });
|
|
320
338
|
const options = args[0];
|
|
321
|
-
const resolvedPath = resolvePath2(pathRef.current, options?.
|
|
339
|
+
const resolvedPath = resolvePath2(pathRef.current, options?.params);
|
|
322
340
|
let res;
|
|
323
341
|
if (hasPathParams(pathRef.current)) {
|
|
324
342
|
let current = apiRef.current;
|
|
@@ -357,7 +375,10 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
357
375
|
onSuccess,
|
|
358
376
|
onError
|
|
359
377
|
} = hookOptions;
|
|
360
|
-
const api = createEnlace(baseUrl, defaultOptions, {
|
|
378
|
+
const api = createEnlace(baseUrl, defaultOptions, {
|
|
379
|
+
onSuccess,
|
|
380
|
+
onError
|
|
381
|
+
});
|
|
361
382
|
function useEnlaceHook(selectorOrQuery, queryOptions) {
|
|
362
383
|
let trackingResult = {
|
|
363
384
|
trackedCall: null,
|
|
@@ -367,9 +388,7 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
367
388
|
const trackingProxy = createTrackingProxy((result2) => {
|
|
368
389
|
trackingResult = result2;
|
|
369
390
|
});
|
|
370
|
-
const result = selectorOrQuery(
|
|
371
|
-
trackingProxy
|
|
372
|
-
);
|
|
391
|
+
const result = selectorOrQuery(trackingProxy);
|
|
373
392
|
if (typeof result === "function") {
|
|
374
393
|
const actualResult = selectorOrQuery(api);
|
|
375
394
|
return useSelectorMode({
|
|
@@ -380,6 +399,11 @@ function createEnlaceHookReact(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
380
399
|
autoRevalidateTags
|
|
381
400
|
});
|
|
382
401
|
}
|
|
402
|
+
if (!trackingResult.trackedCall) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
"useAPI query mode requires calling an HTTP method (get, post, etc.). Did you mean to use selector mode? Example: useAPI((api) => api.posts.get())"
|
|
405
|
+
);
|
|
406
|
+
}
|
|
383
407
|
return useQueryMode(
|
|
384
408
|
api,
|
|
385
409
|
trackingResult.trackedCall,
|
|
@@ -402,18 +426,22 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
402
426
|
const {
|
|
403
427
|
autoGenerateTags = true,
|
|
404
428
|
autoRevalidateTags = true,
|
|
405
|
-
|
|
429
|
+
skipServerRevalidation = false,
|
|
430
|
+
serverRevalidator,
|
|
406
431
|
onSuccess,
|
|
407
432
|
...coreOptions
|
|
408
433
|
} = combinedOptions;
|
|
409
434
|
const isGet = method === "GET";
|
|
410
435
|
const autoTags = generateTags(path);
|
|
411
436
|
const nextOnSuccess = (payload) => {
|
|
412
|
-
if (!isGet
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
437
|
+
if (!isGet) {
|
|
438
|
+
const shouldRevalidateServer = requestOptions?.serverRevalidate ?? !skipServerRevalidation;
|
|
439
|
+
if (shouldRevalidateServer) {
|
|
440
|
+
const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
|
|
441
|
+
const revalidatePaths = requestOptions?.revalidatePaths ?? [];
|
|
442
|
+
if (revalidateTags.length || revalidatePaths.length) {
|
|
443
|
+
serverRevalidator?.(revalidateTags, revalidatePaths);
|
|
444
|
+
}
|
|
417
445
|
}
|
|
418
446
|
}
|
|
419
447
|
onSuccess?.(payload);
|
|
@@ -458,11 +486,15 @@ function createEnlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
458
486
|
staleTime = 0,
|
|
459
487
|
...nextOptions
|
|
460
488
|
} = hookOptions;
|
|
461
|
-
const api = createEnlaceNext(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
489
|
+
const api = createEnlaceNext(
|
|
490
|
+
baseUrl,
|
|
491
|
+
defaultOptions,
|
|
492
|
+
{
|
|
493
|
+
autoGenerateTags,
|
|
494
|
+
autoRevalidateTags,
|
|
495
|
+
...nextOptions
|
|
496
|
+
}
|
|
497
|
+
);
|
|
466
498
|
function useEnlaceHook(selectorOrQuery, queryOptions) {
|
|
467
499
|
let trackedCall = null;
|
|
468
500
|
let selectorPath = null;
|
|
@@ -483,6 +515,11 @@ function createEnlaceHookNext(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
483
515
|
autoRevalidateTags
|
|
484
516
|
});
|
|
485
517
|
}
|
|
518
|
+
if (!trackedCall) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
"useAPI query mode requires calling an HTTP method (get, post, etc.). Did you mean to use selector mode? Example: useAPI((api) => api.posts.get())"
|
|
521
|
+
);
|
|
522
|
+
}
|
|
486
523
|
return useQueryMode(
|
|
487
524
|
api,
|
|
488
525
|
trackedCall,
|
package/dist/index.d.mts
CHANGED
|
@@ -16,9 +16,9 @@ type ReactRequestOptionsBase = {
|
|
|
16
16
|
* Used to replace :paramName placeholders in the URL path.
|
|
17
17
|
* @example
|
|
18
18
|
* // With path api.products[':id'].delete
|
|
19
|
-
* trigger({
|
|
19
|
+
* trigger({ params: { id: '123' } }) // → DELETE /products/123
|
|
20
20
|
*/
|
|
21
|
-
|
|
21
|
+
params?: Record<string, string | number>;
|
|
22
22
|
};
|
|
23
23
|
/** Options for createEnlaceHookReact factory */
|
|
24
24
|
type EnlaceHookOptions = {
|
|
@@ -43,7 +43,7 @@ type EnlaceHookOptions = {
|
|
|
43
43
|
* @param tags - Cache tags to revalidate
|
|
44
44
|
* @param paths - URL paths to revalidate
|
|
45
45
|
*/
|
|
46
|
-
type
|
|
46
|
+
type ServerRevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
|
|
47
47
|
/** Next.js-specific options (third argument for createEnlaceNext) */
|
|
48
48
|
type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
|
|
49
49
|
/**
|
|
@@ -52,30 +52,35 @@ type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateT
|
|
|
52
52
|
* @example
|
|
53
53
|
* ```ts
|
|
54
54
|
* createEnlaceNext("http://localhost:3000/api/", {}, {
|
|
55
|
-
*
|
|
55
|
+
* serverRevalidator: (tags, paths) => revalidateServerAction(tags, paths)
|
|
56
56
|
* });
|
|
57
57
|
* ```
|
|
58
58
|
*/
|
|
59
|
-
|
|
59
|
+
serverRevalidator?: ServerRevalidateHandler;
|
|
60
|
+
/**
|
|
61
|
+
* Skip server-side revalidation by default for all mutations.
|
|
62
|
+
* Individual requests can override with serverRevalidate: true.
|
|
63
|
+
* Useful for CSR-heavy apps where server cache invalidation is rarely needed.
|
|
64
|
+
* @default false
|
|
65
|
+
*/
|
|
66
|
+
skipServerRevalidation?: boolean;
|
|
60
67
|
};
|
|
61
68
|
/** Per-request options for Next.js fetch - extends React's base options */
|
|
62
69
|
type NextRequestOptionsBase = ReactRequestOptionsBase & {
|
|
63
70
|
/** Time in seconds to revalidate, or false to disable */
|
|
64
71
|
revalidate?: number | false;
|
|
65
72
|
/**
|
|
66
|
-
* URL paths to revalidate after mutation
|
|
67
|
-
*
|
|
68
|
-
* You must implement the revalidation logic in the revalidator.
|
|
73
|
+
* URL paths to revalidate after mutation.
|
|
74
|
+
* Passed to the serverRevalidator handler.
|
|
69
75
|
*/
|
|
70
76
|
revalidatePaths?: string[];
|
|
71
77
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* Eg. you don't fetch any data on server component and you might want to skip the overhead of revalidation.
|
|
78
|
+
* Control server-side revalidation for this specific request.
|
|
79
|
+
* - true: Force server revalidation
|
|
80
|
+
* - false: Skip server revalidation
|
|
81
|
+
* When undefined, follows the global skipServerRevalidation setting.
|
|
77
82
|
*/
|
|
78
|
-
|
|
83
|
+
serverRevalidate?: boolean;
|
|
79
84
|
};
|
|
80
85
|
|
|
81
86
|
declare function createEnlaceNext<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions | null, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, TDefaultError, NextRequestOptionsBase>;
|
package/dist/index.d.ts
CHANGED
|
@@ -16,9 +16,9 @@ type ReactRequestOptionsBase = {
|
|
|
16
16
|
* Used to replace :paramName placeholders in the URL path.
|
|
17
17
|
* @example
|
|
18
18
|
* // With path api.products[':id'].delete
|
|
19
|
-
* trigger({
|
|
19
|
+
* trigger({ params: { id: '123' } }) // → DELETE /products/123
|
|
20
20
|
*/
|
|
21
|
-
|
|
21
|
+
params?: Record<string, string | number>;
|
|
22
22
|
};
|
|
23
23
|
/** Options for createEnlaceHookReact factory */
|
|
24
24
|
type EnlaceHookOptions = {
|
|
@@ -43,7 +43,7 @@ type EnlaceHookOptions = {
|
|
|
43
43
|
* @param tags - Cache tags to revalidate
|
|
44
44
|
* @param paths - URL paths to revalidate
|
|
45
45
|
*/
|
|
46
|
-
type
|
|
46
|
+
type ServerRevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
|
|
47
47
|
/** Next.js-specific options (third argument for createEnlaceNext) */
|
|
48
48
|
type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
|
|
49
49
|
/**
|
|
@@ -52,30 +52,35 @@ type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateT
|
|
|
52
52
|
* @example
|
|
53
53
|
* ```ts
|
|
54
54
|
* createEnlaceNext("http://localhost:3000/api/", {}, {
|
|
55
|
-
*
|
|
55
|
+
* serverRevalidator: (tags, paths) => revalidateServerAction(tags, paths)
|
|
56
56
|
* });
|
|
57
57
|
* ```
|
|
58
58
|
*/
|
|
59
|
-
|
|
59
|
+
serverRevalidator?: ServerRevalidateHandler;
|
|
60
|
+
/**
|
|
61
|
+
* Skip server-side revalidation by default for all mutations.
|
|
62
|
+
* Individual requests can override with serverRevalidate: true.
|
|
63
|
+
* Useful for CSR-heavy apps where server cache invalidation is rarely needed.
|
|
64
|
+
* @default false
|
|
65
|
+
*/
|
|
66
|
+
skipServerRevalidation?: boolean;
|
|
60
67
|
};
|
|
61
68
|
/** Per-request options for Next.js fetch - extends React's base options */
|
|
62
69
|
type NextRequestOptionsBase = ReactRequestOptionsBase & {
|
|
63
70
|
/** Time in seconds to revalidate, or false to disable */
|
|
64
71
|
revalidate?: number | false;
|
|
65
72
|
/**
|
|
66
|
-
* URL paths to revalidate after mutation
|
|
67
|
-
*
|
|
68
|
-
* You must implement the revalidation logic in the revalidator.
|
|
73
|
+
* URL paths to revalidate after mutation.
|
|
74
|
+
* Passed to the serverRevalidator handler.
|
|
69
75
|
*/
|
|
70
76
|
revalidatePaths?: string[];
|
|
71
77
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* Eg. you don't fetch any data on server component and you might want to skip the overhead of revalidation.
|
|
78
|
+
* Control server-side revalidation for this specific request.
|
|
79
|
+
* - true: Force server revalidation
|
|
80
|
+
* - false: Skip server revalidation
|
|
81
|
+
* When undefined, follows the global skipServerRevalidation setting.
|
|
77
82
|
*/
|
|
78
|
-
|
|
83
|
+
serverRevalidate?: boolean;
|
|
79
84
|
};
|
|
80
85
|
|
|
81
86
|
declare function createEnlaceNext<TSchema = unknown, TDefaultError = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions | null, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, TDefaultError, NextRequestOptionsBase>;
|
package/dist/index.js
CHANGED
|
@@ -42,18 +42,22 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
42
42
|
const {
|
|
43
43
|
autoGenerateTags = true,
|
|
44
44
|
autoRevalidateTags = true,
|
|
45
|
-
|
|
45
|
+
skipServerRevalidation = false,
|
|
46
|
+
serverRevalidator,
|
|
46
47
|
onSuccess,
|
|
47
48
|
...coreOptions
|
|
48
49
|
} = combinedOptions;
|
|
49
50
|
const isGet = method === "GET";
|
|
50
51
|
const autoTags = generateTags(path);
|
|
51
52
|
const nextOnSuccess = (payload) => {
|
|
52
|
-
if (!isGet
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
if (!isGet) {
|
|
54
|
+
const shouldRevalidateServer = requestOptions?.serverRevalidate ?? !skipServerRevalidation;
|
|
55
|
+
if (shouldRevalidateServer) {
|
|
56
|
+
const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
|
|
57
|
+
const revalidatePaths = requestOptions?.revalidatePaths ?? [];
|
|
58
|
+
if (revalidateTags.length || revalidatePaths.length) {
|
|
59
|
+
serverRevalidator?.(revalidateTags, revalidatePaths);
|
|
60
|
+
}
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
onSuccess?.(payload);
|
package/dist/index.mjs
CHANGED
|
@@ -21,18 +21,22 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
21
21
|
const {
|
|
22
22
|
autoGenerateTags = true,
|
|
23
23
|
autoRevalidateTags = true,
|
|
24
|
-
|
|
24
|
+
skipServerRevalidation = false,
|
|
25
|
+
serverRevalidator,
|
|
25
26
|
onSuccess,
|
|
26
27
|
...coreOptions
|
|
27
28
|
} = combinedOptions;
|
|
28
29
|
const isGet = method === "GET";
|
|
29
30
|
const autoTags = generateTags(path);
|
|
30
31
|
const nextOnSuccess = (payload) => {
|
|
31
|
-
if (!isGet
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
if (!isGet) {
|
|
33
|
+
const shouldRevalidateServer = requestOptions?.serverRevalidate ?? !skipServerRevalidation;
|
|
34
|
+
if (shouldRevalidateServer) {
|
|
35
|
+
const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
|
|
36
|
+
const revalidatePaths = requestOptions?.revalidatePaths ?? [];
|
|
37
|
+
if (revalidateTags.length || revalidatePaths.length) {
|
|
38
|
+
serverRevalidator?.(revalidateTags, revalidatePaths);
|
|
39
|
+
}
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
onSuccess?.(payload);
|