@wp-typia/rest 0.2.0 → 0.3.1

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