@wp-typia/rest 0.2.0 → 0.3.0
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 +67 -0
- package/dist/react.d.ts +67 -0
- package/dist/react.js +556 -0
- package/package.json +17 -1
package/README.md
CHANGED
|
@@ -7,10 +7,21 @@ This package focuses on:
|
|
|
7
7
|
- validated `apiFetch` wrappers
|
|
8
8
|
- typed endpoint helpers
|
|
9
9
|
- canonical WordPress REST route URL resolution
|
|
10
|
+
- a React/data convenience layer at `@wp-typia/rest/react`
|
|
10
11
|
- optional query/header decoder helpers that can wrap Typia-generated HTTP decoders
|
|
11
12
|
|
|
12
13
|
It does not include any WordPress PHP bridge logic. Generated PHP route code stays in `@wp-typia/create` templates.
|
|
13
14
|
|
|
15
|
+
If you need a backend-neutral consumer instead of WordPress-specific route
|
|
16
|
+
resolution, use `@wp-typia/api-client`.
|
|
17
|
+
|
|
18
|
+
The root `@wp-typia/rest` entry stays transport-oriented. If you want query and
|
|
19
|
+
mutation hooks on top of those WordPress helpers, use the React-only subpath:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { useEndpointMutation, useEndpointQuery } from "@wp-typia/rest/react";
|
|
23
|
+
```
|
|
24
|
+
|
|
14
25
|
Typical usage:
|
|
15
26
|
|
|
16
27
|
```ts
|
|
@@ -44,3 +55,59 @@ const decodeQuery = createQueryDecoder(
|
|
|
44
55
|
typia.http.createValidateQuery<MyQuery>()
|
|
45
56
|
);
|
|
46
57
|
```
|
|
58
|
+
|
|
59
|
+
## `@wp-typia/rest/react`
|
|
60
|
+
|
|
61
|
+
The `./react` subpath adds a small cache client and React hook layer on top of
|
|
62
|
+
`callEndpoint(...)`:
|
|
63
|
+
|
|
64
|
+
- `createEndpointDataClient()`
|
|
65
|
+
- `EndpointDataProvider`
|
|
66
|
+
- `useEndpointDataClient()`
|
|
67
|
+
- `useEndpointQuery(endpoint, request, options?)`
|
|
68
|
+
- `useEndpointMutation(endpoint, options?)`
|
|
69
|
+
|
|
70
|
+
`useEndpointQuery(...)` is GET-only in this first pass. Mutations and explicit
|
|
71
|
+
non-query calls go through `useEndpointMutation(...)`.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import {
|
|
75
|
+
useEndpointMutation,
|
|
76
|
+
useEndpointQuery,
|
|
77
|
+
} from "@wp-typia/rest/react";
|
|
78
|
+
|
|
79
|
+
const query = useEndpointQuery(stateEndpoint, request, {
|
|
80
|
+
staleTime: 30_000,
|
|
81
|
+
resolveCallOptions: () => ({
|
|
82
|
+
requestOptions: {
|
|
83
|
+
headers: {
|
|
84
|
+
"X-WP-Nonce": resolveRestNonce(),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const mutation = useEndpointMutation(writeStateEndpoint, {
|
|
91
|
+
invalidate: { endpoint: stateEndpoint, request },
|
|
92
|
+
resolveCallOptions: () => ({
|
|
93
|
+
requestOptions: {
|
|
94
|
+
headers: {
|
|
95
|
+
"X-WP-Nonce": resolveRestNonce(),
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The refresh model is explicit:
|
|
103
|
+
|
|
104
|
+
- query hooks re-evaluate the latest `request` and `resolveCallOptions()` on
|
|
105
|
+
each execution
|
|
106
|
+
- mutation hooks use the latest variables passed to `mutate(...)` and the latest
|
|
107
|
+
`resolveCallOptions(variables)`
|
|
108
|
+
- stale nonces or public tokens do not trigger automatic retries; callers should
|
|
109
|
+
refresh auth state and then call `refetch()` or `mutate()` again
|
|
110
|
+
|
|
111
|
+
For persistence scaffolds generated by `@wp-typia/create`, `src/api.ts` remains
|
|
112
|
+
the WordPress call helper layer and `src/data.ts` adds block-specific wrappers
|
|
113
|
+
around `@wp-typia/rest/react`.
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createElement } from "@wordpress/element";
|
|
2
|
+
import type { ApiFetch } from "@wordpress/api-fetch";
|
|
3
|
+
import { type ApiEndpoint, type EndpointCallOptions, type ValidationResult } from "./client.js";
|
|
4
|
+
type EndpointDataUpdater<T> = T | ((current: T | undefined) => T | undefined);
|
|
5
|
+
export interface EndpointDataClient {
|
|
6
|
+
invalidate<Req, Res>(endpoint: ApiEndpoint<Req, Res>, request?: Req): void;
|
|
7
|
+
refetch<Req, Res>(endpoint: ApiEndpoint<Req, Res>, request?: Req): Promise<void>;
|
|
8
|
+
getData<Req, Res>(endpoint: ApiEndpoint<Req, Res>, request: Req): Res | undefined;
|
|
9
|
+
setData<Req, Res>(endpoint: ApiEndpoint<Req, Res>, request: Req, next: EndpointDataUpdater<Res>): void;
|
|
10
|
+
}
|
|
11
|
+
export interface EndpointInvalidateTarget<E extends ApiEndpoint<any, any> = ApiEndpoint<any, any>> {
|
|
12
|
+
endpoint: E;
|
|
13
|
+
request?: E extends ApiEndpoint<infer Req, any> ? Req : never;
|
|
14
|
+
}
|
|
15
|
+
type EndpointInvalidateTargets = EndpointInvalidateTarget | readonly EndpointInvalidateTarget[] | undefined;
|
|
16
|
+
export interface UseEndpointQueryOptions<Req, Res, Selected = Res> {
|
|
17
|
+
client?: EndpointDataClient;
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
fetchFn?: ApiFetch;
|
|
20
|
+
initialData?: Res;
|
|
21
|
+
onError?: (error: unknown) => void | Promise<void>;
|
|
22
|
+
onSuccess?: (data: Selected, validation: ValidationResult<Res>) => void | Promise<void>;
|
|
23
|
+
resolveCallOptions?: () => EndpointCallOptions | undefined;
|
|
24
|
+
select?: (data: Res) => Selected;
|
|
25
|
+
staleTime?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface UseEndpointQueryResult<Res, Selected = Res> {
|
|
28
|
+
data: Selected | undefined;
|
|
29
|
+
error: unknown;
|
|
30
|
+
isFetching: boolean;
|
|
31
|
+
isLoading: boolean;
|
|
32
|
+
refetch: () => Promise<ValidationResult<Res>>;
|
|
33
|
+
validation: ValidationResult<Res> | null;
|
|
34
|
+
}
|
|
35
|
+
export interface UseEndpointMutationOptions<Req, Res, Context = unknown> {
|
|
36
|
+
client?: EndpointDataClient;
|
|
37
|
+
fetchFn?: ApiFetch;
|
|
38
|
+
invalidate?: EndpointInvalidateTargets | ((data: Res | undefined, variables: Req, validation: ValidationResult<Res>) => EndpointInvalidateTargets);
|
|
39
|
+
onError?: (error: unknown, variables: Req, client: EndpointDataClient, context: Context | undefined) => void | Promise<void>;
|
|
40
|
+
onMutate?: (variables: Req, client: EndpointDataClient) => Context | Promise<Context>;
|
|
41
|
+
onSettled?: (result: {
|
|
42
|
+
data: Res | undefined;
|
|
43
|
+
error: unknown;
|
|
44
|
+
validation: ValidationResult<Res> | null;
|
|
45
|
+
}, variables: Req, client: EndpointDataClient, context: Context | undefined) => void | Promise<void>;
|
|
46
|
+
onSuccess?: (data: Res | undefined, variables: Req, validation: ValidationResult<Res>, client: EndpointDataClient, context: Context | undefined) => void | Promise<void>;
|
|
47
|
+
resolveCallOptions?: (variables: Req) => EndpointCallOptions | undefined;
|
|
48
|
+
}
|
|
49
|
+
export interface UseEndpointMutationResult<Req, Res> {
|
|
50
|
+
data: Res | undefined;
|
|
51
|
+
error: unknown;
|
|
52
|
+
isPending: boolean;
|
|
53
|
+
mutate: (variables: Req) => void;
|
|
54
|
+
mutateAsync: (variables: Req) => Promise<ValidationResult<Res>>;
|
|
55
|
+
reset: () => void;
|
|
56
|
+
validation: ValidationResult<Res> | null;
|
|
57
|
+
}
|
|
58
|
+
export interface EndpointDataProviderProps {
|
|
59
|
+
children?: unknown;
|
|
60
|
+
client: EndpointDataClient;
|
|
61
|
+
}
|
|
62
|
+
export declare function createEndpointDataClient(): EndpointDataClient;
|
|
63
|
+
export declare function EndpointDataProvider({ children, client, }: EndpointDataProviderProps): ReturnType<typeof createElement>;
|
|
64
|
+
export declare function useEndpointDataClient(): EndpointDataClient;
|
|
65
|
+
export declare function useEndpointQuery<Req, Res, Selected = Res>(endpoint: ApiEndpoint<Req, Res>, request: Req, options?: UseEndpointQueryOptions<Req, Res, Selected>): UseEndpointQueryResult<Res, Selected>;
|
|
66
|
+
export declare function useEndpointMutation<Req, Res, Context = unknown>(endpoint: ApiEndpoint<Req, Res>, options?: UseEndpointMutationOptions<Req, Res, Context>): UseEndpointMutationResult<Req, Res>;
|
|
67
|
+
export {};
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { createContext, createElement, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "@wordpress/element";
|
|
2
|
+
import { callEndpoint, } from "./client.js";
|
|
3
|
+
const EMPTY_SNAPSHOT = {
|
|
4
|
+
data: undefined,
|
|
5
|
+
error: null,
|
|
6
|
+
invalidatedAt: 0,
|
|
7
|
+
isFetching: false,
|
|
8
|
+
updatedAt: 0,
|
|
9
|
+
validation: null,
|
|
10
|
+
};
|
|
11
|
+
const EndpointDataClientContext = createContext(null);
|
|
12
|
+
function isPlainObject(value) {
|
|
13
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const prototype = Object.getPrototypeOf(value);
|
|
17
|
+
return prototype === Object.prototype || prototype === null;
|
|
18
|
+
}
|
|
19
|
+
function normalizeCacheValue(value) {
|
|
20
|
+
if (value === undefined) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
if (value === null ||
|
|
24
|
+
typeof value === "boolean" ||
|
|
25
|
+
typeof value === "number" ||
|
|
26
|
+
typeof value === "string") {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === "bigint") {
|
|
30
|
+
return { __bigint: String(value) };
|
|
31
|
+
}
|
|
32
|
+
if (value instanceof URLSearchParams) {
|
|
33
|
+
return {
|
|
34
|
+
__urlSearchParams: [...value.entries()].sort(([leftKey, leftValue], [rightKey, rightValue]) => leftKey === rightKey
|
|
35
|
+
? leftValue.localeCompare(rightValue)
|
|
36
|
+
: leftKey.localeCompare(rightKey)),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
return value.map((item) => normalizeCacheValue(item));
|
|
41
|
+
}
|
|
42
|
+
if (isPlainObject(value)) {
|
|
43
|
+
return Object.fromEntries(Object.entries(value)
|
|
44
|
+
.filter(([, item]) => item !== undefined)
|
|
45
|
+
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
|
|
46
|
+
.map(([key, item]) => [key, normalizeCacheValue(item)]));
|
|
47
|
+
}
|
|
48
|
+
if (value instanceof Date) {
|
|
49
|
+
return { __date: value.toISOString() };
|
|
50
|
+
}
|
|
51
|
+
return String(value);
|
|
52
|
+
}
|
|
53
|
+
function createEndpointPrefix(endpoint) {
|
|
54
|
+
return `${endpoint.method} ${endpoint.path}`;
|
|
55
|
+
}
|
|
56
|
+
function createCacheKey(endpoint, request) {
|
|
57
|
+
const requestValidation = endpoint.validateRequest(request);
|
|
58
|
+
const normalizedRequest = requestValidation.isValid
|
|
59
|
+
? requestValidation.data ?? request
|
|
60
|
+
: request;
|
|
61
|
+
return {
|
|
62
|
+
cacheKey: `${createEndpointPrefix(endpoint)}::${JSON.stringify(normalizeCacheValue(normalizedRequest))}`,
|
|
63
|
+
requestValidation,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function normalizeInvalidateTargets(targets) {
|
|
67
|
+
if (!targets) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
return (Array.isArray(targets) ? targets : [targets]);
|
|
71
|
+
}
|
|
72
|
+
function getOrCreateEntry(entries, cacheKey) {
|
|
73
|
+
const existing = entries.get(cacheKey);
|
|
74
|
+
if (existing) {
|
|
75
|
+
return existing;
|
|
76
|
+
}
|
|
77
|
+
const created = {
|
|
78
|
+
...EMPTY_SNAPSHOT,
|
|
79
|
+
listeners: new Set(),
|
|
80
|
+
promise: null,
|
|
81
|
+
refetchers: new Set(),
|
|
82
|
+
snapshot: EMPTY_SNAPSHOT,
|
|
83
|
+
};
|
|
84
|
+
entries.set(cacheKey, created);
|
|
85
|
+
return created;
|
|
86
|
+
}
|
|
87
|
+
function syncSnapshot(entry) {
|
|
88
|
+
entry.snapshot = {
|
|
89
|
+
data: entry.data,
|
|
90
|
+
error: entry.error,
|
|
91
|
+
invalidatedAt: entry.invalidatedAt,
|
|
92
|
+
isFetching: entry.isFetching,
|
|
93
|
+
updatedAt: entry.updatedAt,
|
|
94
|
+
validation: entry.validation,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function isEntryStale(entry, staleTime) {
|
|
98
|
+
if (entry.updatedAt === 0) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (entry.invalidatedAt > entry.updatedAt) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (staleTime === 0) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return Date.now() - entry.updatedAt > staleTime;
|
|
108
|
+
}
|
|
109
|
+
function asInternalClient(client) {
|
|
110
|
+
return client;
|
|
111
|
+
}
|
|
112
|
+
export function createEndpointDataClient() {
|
|
113
|
+
const entries = new Map();
|
|
114
|
+
function notify(cacheKey) {
|
|
115
|
+
const entry = entries.get(cacheKey);
|
|
116
|
+
if (!entry) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const listener of entry.listeners) {
|
|
120
|
+
listener();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const client = {
|
|
124
|
+
invalidate(endpoint, request) {
|
|
125
|
+
if (request !== undefined) {
|
|
126
|
+
const { cacheKey } = createCacheKey(endpoint, request);
|
|
127
|
+
const entry = entries.get(cacheKey);
|
|
128
|
+
if (!entry) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
entry.invalidatedAt = Date.now();
|
|
132
|
+
syncSnapshot(entry);
|
|
133
|
+
notify(cacheKey);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const prefix = createEndpointPrefix(endpoint);
|
|
137
|
+
for (const [cacheKey, entry] of entries.entries()) {
|
|
138
|
+
if (!cacheKey.startsWith(`${prefix}::`)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
entry.invalidatedAt = Date.now();
|
|
142
|
+
syncSnapshot(entry);
|
|
143
|
+
notify(cacheKey);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
async refetch(endpoint, request) {
|
|
147
|
+
const callbacks = new Set();
|
|
148
|
+
if (request !== undefined) {
|
|
149
|
+
const { cacheKey } = createCacheKey(endpoint, request);
|
|
150
|
+
for (const refetcher of entries.get(cacheKey)?.refetchers ?? []) {
|
|
151
|
+
callbacks.add(refetcher);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const prefix = createEndpointPrefix(endpoint);
|
|
156
|
+
for (const [cacheKey, entry] of entries.entries()) {
|
|
157
|
+
if (!cacheKey.startsWith(`${prefix}::`)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (const refetcher of entry.refetchers) {
|
|
161
|
+
callbacks.add(refetcher);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
await Promise.all([...callbacks].map((refetcher) => refetcher()));
|
|
166
|
+
},
|
|
167
|
+
getData(endpoint, request) {
|
|
168
|
+
const { cacheKey } = createCacheKey(endpoint, request);
|
|
169
|
+
return entries.get(cacheKey)?.data;
|
|
170
|
+
},
|
|
171
|
+
setData(endpoint, request, next) {
|
|
172
|
+
const { cacheKey } = createCacheKey(endpoint, request);
|
|
173
|
+
const entry = getOrCreateEntry(entries, cacheKey);
|
|
174
|
+
const resolvedNext = typeof next === "function"
|
|
175
|
+
? next(entry.data)
|
|
176
|
+
: next;
|
|
177
|
+
entry.data = resolvedNext;
|
|
178
|
+
entry.error = null;
|
|
179
|
+
entry.updatedAt = Date.now();
|
|
180
|
+
entry.validation = {
|
|
181
|
+
data: resolvedNext,
|
|
182
|
+
errors: [],
|
|
183
|
+
isValid: true,
|
|
184
|
+
};
|
|
185
|
+
syncSnapshot(entry);
|
|
186
|
+
notify(cacheKey);
|
|
187
|
+
},
|
|
188
|
+
__getSnapshot(cacheKey) {
|
|
189
|
+
const entry = entries.get(cacheKey);
|
|
190
|
+
if (!entry) {
|
|
191
|
+
return EMPTY_SNAPSHOT;
|
|
192
|
+
}
|
|
193
|
+
return entry.snapshot;
|
|
194
|
+
},
|
|
195
|
+
__publishValidation(cacheKey, validation) {
|
|
196
|
+
const entry = getOrCreateEntry(entries, cacheKey);
|
|
197
|
+
entry.error = null;
|
|
198
|
+
entry.updatedAt = Date.now();
|
|
199
|
+
entry.validation = validation;
|
|
200
|
+
if (validation.isValid) {
|
|
201
|
+
entry.data = validation.data;
|
|
202
|
+
}
|
|
203
|
+
syncSnapshot(entry);
|
|
204
|
+
notify(cacheKey);
|
|
205
|
+
},
|
|
206
|
+
__registerRefetcher(cacheKey, refetcher) {
|
|
207
|
+
const entry = getOrCreateEntry(entries, cacheKey);
|
|
208
|
+
entry.refetchers.add(refetcher);
|
|
209
|
+
return () => {
|
|
210
|
+
entry.refetchers.delete(refetcher);
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
async __runQuery(cacheKey, execute, { force = false, staleTime }) {
|
|
214
|
+
const entry = getOrCreateEntry(entries, cacheKey);
|
|
215
|
+
if (entry.promise) {
|
|
216
|
+
return entry.promise;
|
|
217
|
+
}
|
|
218
|
+
if (!force) {
|
|
219
|
+
if (!isEntryStale(entry, staleTime) && entry.validation) {
|
|
220
|
+
return entry.validation;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
entry.error = null;
|
|
224
|
+
entry.isFetching = true;
|
|
225
|
+
syncSnapshot(entry);
|
|
226
|
+
notify(cacheKey);
|
|
227
|
+
const startedAt = Date.now();
|
|
228
|
+
const promise = execute()
|
|
229
|
+
.then((validation) => {
|
|
230
|
+
entry.error = null;
|
|
231
|
+
if (entry.invalidatedAt <= startedAt) {
|
|
232
|
+
entry.updatedAt = Date.now();
|
|
233
|
+
entry.validation = validation;
|
|
234
|
+
if (validation.isValid) {
|
|
235
|
+
entry.data = validation.data;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
syncSnapshot(entry);
|
|
239
|
+
return validation;
|
|
240
|
+
})
|
|
241
|
+
.catch((error) => {
|
|
242
|
+
entry.error = error;
|
|
243
|
+
if (entry.invalidatedAt <= startedAt) {
|
|
244
|
+
entry.updatedAt = Date.now();
|
|
245
|
+
}
|
|
246
|
+
syncSnapshot(entry);
|
|
247
|
+
throw error;
|
|
248
|
+
})
|
|
249
|
+
.finally(() => {
|
|
250
|
+
entry.isFetching = false;
|
|
251
|
+
entry.promise = null;
|
|
252
|
+
syncSnapshot(entry);
|
|
253
|
+
notify(cacheKey);
|
|
254
|
+
});
|
|
255
|
+
entry.promise = promise;
|
|
256
|
+
return promise;
|
|
257
|
+
},
|
|
258
|
+
__seedData(cacheKey, data) {
|
|
259
|
+
const entry = getOrCreateEntry(entries, cacheKey);
|
|
260
|
+
if (entry.validation) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
entry.data = data;
|
|
264
|
+
entry.error = null;
|
|
265
|
+
entry.updatedAt = Date.now();
|
|
266
|
+
entry.validation = {
|
|
267
|
+
data,
|
|
268
|
+
errors: [],
|
|
269
|
+
isValid: true,
|
|
270
|
+
};
|
|
271
|
+
syncSnapshot(entry);
|
|
272
|
+
notify(cacheKey);
|
|
273
|
+
},
|
|
274
|
+
__subscribe(cacheKey, listener) {
|
|
275
|
+
const entry = getOrCreateEntry(entries, cacheKey);
|
|
276
|
+
entry.listeners.add(listener);
|
|
277
|
+
return () => {
|
|
278
|
+
entry.listeners.delete(listener);
|
|
279
|
+
};
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
return client;
|
|
283
|
+
}
|
|
284
|
+
const defaultEndpointDataClient = createEndpointDataClient();
|
|
285
|
+
export function EndpointDataProvider({ children, client, }) {
|
|
286
|
+
return createElement(EndpointDataClientContext.Provider, { value: client }, children);
|
|
287
|
+
}
|
|
288
|
+
export function useEndpointDataClient() {
|
|
289
|
+
return useContext(EndpointDataClientContext) ?? defaultEndpointDataClient;
|
|
290
|
+
}
|
|
291
|
+
export function useEndpointQuery(endpoint, request, options = {}) {
|
|
292
|
+
if (endpoint.method !== "GET") {
|
|
293
|
+
throw new Error("useEndpointQuery only supports GET endpoints in v1.");
|
|
294
|
+
}
|
|
295
|
+
const defaultClient = useEndpointDataClient();
|
|
296
|
+
const client = asInternalClient(options.client ?? defaultClient);
|
|
297
|
+
const { enabled = true, fetchFn, initialData, onError, onSuccess, resolveCallOptions, select, staleTime = 0, } = options;
|
|
298
|
+
const prepared = useMemo(() => createCacheKey(endpoint, request), [endpoint, request]);
|
|
299
|
+
const snapshot = useSyncExternalStore((listener) => client.__subscribe(prepared.cacheKey, listener), () => client.__getSnapshot(prepared.cacheKey), () => client.__getSnapshot(prepared.cacheKey));
|
|
300
|
+
const latestRef = useRef({
|
|
301
|
+
cacheKey: prepared.cacheKey,
|
|
302
|
+
client,
|
|
303
|
+
endpoint,
|
|
304
|
+
fetchFn,
|
|
305
|
+
onError,
|
|
306
|
+
onSuccess,
|
|
307
|
+
request,
|
|
308
|
+
requestValidation: prepared.requestValidation,
|
|
309
|
+
resolveCallOptions,
|
|
310
|
+
select,
|
|
311
|
+
staleTime,
|
|
312
|
+
});
|
|
313
|
+
latestRef.current = {
|
|
314
|
+
cacheKey: prepared.cacheKey,
|
|
315
|
+
client,
|
|
316
|
+
endpoint,
|
|
317
|
+
fetchFn,
|
|
318
|
+
onError,
|
|
319
|
+
onSuccess,
|
|
320
|
+
request,
|
|
321
|
+
requestValidation: prepared.requestValidation,
|
|
322
|
+
resolveCallOptions,
|
|
323
|
+
select,
|
|
324
|
+
staleTime,
|
|
325
|
+
};
|
|
326
|
+
const refetchRef = useRef();
|
|
327
|
+
const executeQueryRef = useRef();
|
|
328
|
+
const hasAutoFetchedZeroStaleRef = useRef(false);
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
hasAutoFetchedZeroStaleRef.current = false;
|
|
331
|
+
}, [enabled, prepared.cacheKey, staleTime]);
|
|
332
|
+
// Keep these callbacks stable while still reading the latest runtime inputs
|
|
333
|
+
// from latestRef.current on each execution.
|
|
334
|
+
if (!executeQueryRef.current) {
|
|
335
|
+
executeQueryRef.current = async (force) => {
|
|
336
|
+
const latest = latestRef.current;
|
|
337
|
+
if (!latest.requestValidation.isValid) {
|
|
338
|
+
const invalidValidation = latest.requestValidation;
|
|
339
|
+
latest.client.__publishValidation(latest.cacheKey, invalidValidation);
|
|
340
|
+
return invalidValidation;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const callOptions = latest.resolveCallOptions?.();
|
|
344
|
+
const validation = await latest.client.__runQuery(latest.cacheKey, () => callEndpoint(latest.endpoint, latest.request, {
|
|
345
|
+
fetchFn: callOptions?.fetchFn ?? latest.fetchFn,
|
|
346
|
+
requestOptions: callOptions?.requestOptions,
|
|
347
|
+
}), { force, staleTime: latest.staleTime });
|
|
348
|
+
if (validation.isValid) {
|
|
349
|
+
const selected = latest.select !== undefined
|
|
350
|
+
? latest.select(validation.data)
|
|
351
|
+
: validation.data;
|
|
352
|
+
await latest.onSuccess?.(selected, validation);
|
|
353
|
+
}
|
|
354
|
+
return validation;
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
await latest.onError?.(error);
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const executeQuery = executeQueryRef.current;
|
|
363
|
+
if (!refetchRef.current) {
|
|
364
|
+
refetchRef.current = () => executeQuery(true);
|
|
365
|
+
}
|
|
366
|
+
const refetch = refetchRef.current;
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
return client.__registerRefetcher(prepared.cacheKey, () => refetch());
|
|
369
|
+
}, [client, prepared.cacheKey, refetch]);
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
if (initialData === undefined || snapshot.validation) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
client.__seedData(prepared.cacheKey, initialData);
|
|
375
|
+
}, [client, initialData, prepared.cacheKey, snapshot.validation]);
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
if (!enabled) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (!prepared.requestValidation.isValid) {
|
|
381
|
+
if (snapshot.validation?.isValid === false) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
client.__publishValidation(prepared.cacheKey, prepared.requestValidation);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (snapshot.isFetching) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const shouldFetch = snapshot.updatedAt === 0
|
|
391
|
+
? initialData === undefined
|
|
392
|
+
: snapshot.invalidatedAt > snapshot.updatedAt
|
|
393
|
+
? true
|
|
394
|
+
: snapshot.error !== null
|
|
395
|
+
? false
|
|
396
|
+
: staleTime === 0
|
|
397
|
+
? !hasAutoFetchedZeroStaleRef.current
|
|
398
|
+
: Date.now() - snapshot.updatedAt > staleTime;
|
|
399
|
+
if (!shouldFetch) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (staleTime === 0) {
|
|
403
|
+
hasAutoFetchedZeroStaleRef.current = true;
|
|
404
|
+
}
|
|
405
|
+
void executeQuery(false).catch(() => { });
|
|
406
|
+
}, [
|
|
407
|
+
client,
|
|
408
|
+
enabled,
|
|
409
|
+
executeQuery,
|
|
410
|
+
initialData,
|
|
411
|
+
prepared.cacheKey,
|
|
412
|
+
prepared.requestValidation.isValid,
|
|
413
|
+
snapshot.isFetching,
|
|
414
|
+
refetch,
|
|
415
|
+
snapshot.invalidatedAt,
|
|
416
|
+
snapshot.updatedAt,
|
|
417
|
+
staleTime,
|
|
418
|
+
]);
|
|
419
|
+
const data = useMemo(() => {
|
|
420
|
+
const rawData = snapshot.data === undefined && snapshot.validation === null
|
|
421
|
+
? initialData
|
|
422
|
+
: snapshot.data;
|
|
423
|
+
if (rawData === undefined) {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
return select !== undefined
|
|
427
|
+
? select(rawData)
|
|
428
|
+
: rawData;
|
|
429
|
+
}, [initialData, select, snapshot.data, snapshot.validation]);
|
|
430
|
+
return {
|
|
431
|
+
data,
|
|
432
|
+
error: snapshot.error,
|
|
433
|
+
isFetching: snapshot.isFetching,
|
|
434
|
+
isLoading: snapshot.isFetching &&
|
|
435
|
+
data === undefined,
|
|
436
|
+
refetch,
|
|
437
|
+
validation: snapshot.validation,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
export function useEndpointMutation(endpoint, options = {}) {
|
|
441
|
+
const defaultClient = useEndpointDataClient();
|
|
442
|
+
const client = options.client ?? defaultClient;
|
|
443
|
+
const { fetchFn, invalidate, onError, onMutate, onSettled, onSuccess, resolveCallOptions, } = options;
|
|
444
|
+
const [data, setData] = useState(undefined);
|
|
445
|
+
const [error, setError] = useState(null);
|
|
446
|
+
const [isPending, setIsPending] = useState(false);
|
|
447
|
+
const [validation, setValidation] = useState(null);
|
|
448
|
+
const pendingCountRef = useRef(0);
|
|
449
|
+
const latestRef = useRef({
|
|
450
|
+
client,
|
|
451
|
+
endpoint,
|
|
452
|
+
fetchFn,
|
|
453
|
+
invalidate,
|
|
454
|
+
onError,
|
|
455
|
+
onMutate,
|
|
456
|
+
onSettled,
|
|
457
|
+
onSuccess,
|
|
458
|
+
resolveCallOptions,
|
|
459
|
+
});
|
|
460
|
+
latestRef.current = {
|
|
461
|
+
client,
|
|
462
|
+
endpoint,
|
|
463
|
+
fetchFn,
|
|
464
|
+
invalidate,
|
|
465
|
+
onError,
|
|
466
|
+
onMutate,
|
|
467
|
+
onSettled,
|
|
468
|
+
onSuccess,
|
|
469
|
+
resolveCallOptions,
|
|
470
|
+
};
|
|
471
|
+
const mutateAsyncRef = useRef();
|
|
472
|
+
if (!mutateAsyncRef.current) {
|
|
473
|
+
mutateAsyncRef.current = async (variables) => {
|
|
474
|
+
const latest = latestRef.current;
|
|
475
|
+
pendingCountRef.current += 1;
|
|
476
|
+
setIsPending(true);
|
|
477
|
+
setError(null);
|
|
478
|
+
setValidation(null);
|
|
479
|
+
let context;
|
|
480
|
+
try {
|
|
481
|
+
context = latest.onMutate
|
|
482
|
+
? await latest.onMutate(variables, latest.client)
|
|
483
|
+
: undefined;
|
|
484
|
+
const callOptions = latest.resolveCallOptions?.(variables);
|
|
485
|
+
const result = await callEndpoint(latest.endpoint, variables, {
|
|
486
|
+
fetchFn: callOptions?.fetchFn ?? latest.fetchFn,
|
|
487
|
+
requestOptions: callOptions?.requestOptions,
|
|
488
|
+
});
|
|
489
|
+
setValidation(result);
|
|
490
|
+
if (result.isValid) {
|
|
491
|
+
setData(result.data);
|
|
492
|
+
await latest.onSuccess?.(result.data, variables, result, latest.client, context);
|
|
493
|
+
const targets = normalizeInvalidateTargets(typeof latest.invalidate === "function"
|
|
494
|
+
? latest.invalidate(result.data, variables, result)
|
|
495
|
+
: latest.invalidate);
|
|
496
|
+
for (const target of targets) {
|
|
497
|
+
latest.client.invalidate(target.endpoint, target.request);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
setData(undefined);
|
|
502
|
+
setError(result);
|
|
503
|
+
await latest.onError?.(result, variables, latest.client, context);
|
|
504
|
+
const targets = normalizeInvalidateTargets(typeof latest.invalidate === "function"
|
|
505
|
+
? latest.invalidate(undefined, variables, result)
|
|
506
|
+
: latest.invalidate);
|
|
507
|
+
for (const target of targets) {
|
|
508
|
+
latest.client.invalidate(target.endpoint, target.request);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
await latest.onSettled?.({
|
|
512
|
+
data: result.isValid ? result.data : undefined,
|
|
513
|
+
error: result.isValid ? null : result,
|
|
514
|
+
validation: result,
|
|
515
|
+
}, variables, latest.client, context);
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
catch (nextError) {
|
|
519
|
+
setData(undefined);
|
|
520
|
+
setError(nextError);
|
|
521
|
+
setValidation(null);
|
|
522
|
+
await latest.onError?.(nextError, variables, latest.client, context);
|
|
523
|
+
await latest.onSettled?.({
|
|
524
|
+
data: undefined,
|
|
525
|
+
error: nextError,
|
|
526
|
+
validation: null,
|
|
527
|
+
}, variables, latest.client, context);
|
|
528
|
+
throw nextError;
|
|
529
|
+
}
|
|
530
|
+
finally {
|
|
531
|
+
pendingCountRef.current = Math.max(0, pendingCountRef.current - 1);
|
|
532
|
+
setIsPending(pendingCountRef.current > 0);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
const mutateAsync = mutateAsyncRef.current;
|
|
537
|
+
const mutate = (variables) => {
|
|
538
|
+
void mutateAsync(variables).catch(() => { });
|
|
539
|
+
};
|
|
540
|
+
const reset = () => {
|
|
541
|
+
pendingCountRef.current = 0;
|
|
542
|
+
setData(undefined);
|
|
543
|
+
setError(null);
|
|
544
|
+
setIsPending(false);
|
|
545
|
+
setValidation(null);
|
|
546
|
+
};
|
|
547
|
+
return {
|
|
548
|
+
data,
|
|
549
|
+
error,
|
|
550
|
+
isPending,
|
|
551
|
+
mutate,
|
|
552
|
+
mutateAsync,
|
|
553
|
+
reset,
|
|
554
|
+
validation,
|
|
555
|
+
};
|
|
556
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wp-typia/rest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Typed WordPress REST helpers powered by Typia validation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
"import": "./dist/index.js",
|
|
22
22
|
"default": "./dist/index.js"
|
|
23
23
|
},
|
|
24
|
+
"./react": {
|
|
25
|
+
"types": "./dist/react.d.ts",
|
|
26
|
+
"import": "./dist/react.js",
|
|
27
|
+
"default": "./dist/react.js"
|
|
28
|
+
},
|
|
24
29
|
"./package.json": "./package.json"
|
|
25
30
|
},
|
|
26
31
|
"files": [
|
|
@@ -32,6 +37,7 @@
|
|
|
32
37
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && bun ./scripts/fix-dist-imports.mjs",
|
|
33
38
|
"clean": "rm -rf dist",
|
|
34
39
|
"test": "bun run build && bun test tests",
|
|
40
|
+
"test:coverage": "bun run build && bun test tests --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir=coverage",
|
|
35
41
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
36
42
|
},
|
|
37
43
|
"author": "imjlk",
|
|
@@ -57,7 +63,17 @@
|
|
|
57
63
|
"@typia/interface": "^12.0.1",
|
|
58
64
|
"@wordpress/api-fetch": "^7.42.0"
|
|
59
65
|
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@wordpress/element": "^6.29.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@wordpress/element": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
60
74
|
"devDependencies": {
|
|
75
|
+
"@wordpress/element": "^6.29.0",
|
|
76
|
+
"happy-dom": "^20.8.9",
|
|
61
77
|
"typescript": "^5.9.2"
|
|
62
78
|
}
|
|
63
79
|
}
|