@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 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`.
@@ -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.2.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
  }