@faasjs/react 8.0.0-beta.3 → 8.0.0-beta.30
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 +13 -69
- package/dist/index.d.ts +855 -455
- package/dist/index.mjs +1396 -710
- package/package.json +19 -23
- package/dist/index.cjs +0 -769
package/dist/index.mjs
CHANGED
|
@@ -1,741 +1,1427 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
1
|
+
import { Component, cloneElement, createContext, forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/generate-id/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* Generate a random identifier with an optional prefix.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} [prefix] - Prefix prepended to the generated identifier.
|
|
8
|
+
* @param {number} [length] - Length of the generated identifier excluding `prefix`. Must be between `8` and `18`.
|
|
9
|
+
* @returns {string} Generated identifier string.
|
|
10
|
+
* @throws {Error} When `length` is outside the supported `8` to `18` range.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { generateId } from '@faasjs/react'
|
|
15
|
+
*
|
|
16
|
+
* const id = generateId('prefix-')
|
|
17
|
+
*
|
|
18
|
+
* id.startsWith('prefix-') // true
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
function generateId(prefix = "", length = 18) {
|
|
22
|
+
if (length < 8 || length > 18) throw new Error("Length must be 8 ~ 18");
|
|
23
|
+
return `${prefix}${Date.now().toString(36).padStart(8, "0")}${Math.random().toString(36).substring(2, length - 6).padEnd(length - 8, "0")}`;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/browser/response.ts
|
|
27
|
+
/**
|
|
28
|
+
* Wrapper class for HTTP responses from FaasJS functions.
|
|
29
|
+
*
|
|
30
|
+
* Provides a consistent interface for handling server responses with status code, headers,
|
|
31
|
+
* body, and parsed data. Automatically handles JSON serialization and status code defaults.
|
|
32
|
+
*
|
|
33
|
+
* @template T - The type of the data property for type-safe response handling
|
|
34
|
+
*/
|
|
35
|
+
var Response = class {
|
|
36
|
+
/**
|
|
37
|
+
* HTTP status code exposed to callers.
|
|
38
|
+
*/
|
|
39
|
+
status;
|
|
40
|
+
/**
|
|
41
|
+
* Response headers keyed by header name.
|
|
42
|
+
*/
|
|
43
|
+
headers;
|
|
44
|
+
/**
|
|
45
|
+
* Raw response body.
|
|
46
|
+
*/
|
|
47
|
+
body;
|
|
48
|
+
/**
|
|
49
|
+
* Parsed response payload when JSON data is available.
|
|
50
|
+
*/
|
|
51
|
+
data;
|
|
52
|
+
/**
|
|
53
|
+
* Create a wrapped response object.
|
|
54
|
+
*/
|
|
55
|
+
constructor(props = {}) {
|
|
56
|
+
this.status = props.status || (props.data || props.body ? 200 : 204);
|
|
57
|
+
this.headers = props.headers || {};
|
|
58
|
+
this.body = props.body;
|
|
59
|
+
if (props.data !== void 0) this.data = props.data;
|
|
60
|
+
if (props.data && !props.body) this.body = JSON.stringify(props.data);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Custom error class for handling HTTP response errors from FaasJS requests.
|
|
65
|
+
*
|
|
66
|
+
* Extends the built-in Error class to provide additional information about failed requests,
|
|
67
|
+
* including HTTP status code, response headers, response body, and the original error.
|
|
68
|
+
*/
|
|
69
|
+
var ResponseError = class extends Error {
|
|
70
|
+
/**
|
|
71
|
+
* HTTP status code reported for the failed request.
|
|
72
|
+
*/
|
|
73
|
+
status;
|
|
74
|
+
/**
|
|
75
|
+
* Response headers returned with the error.
|
|
76
|
+
*/
|
|
77
|
+
headers;
|
|
78
|
+
/**
|
|
79
|
+
* Raw error body or fallback error payload.
|
|
80
|
+
*/
|
|
81
|
+
body;
|
|
82
|
+
/**
|
|
83
|
+
* Original error used to construct this instance, when available.
|
|
84
|
+
*/
|
|
85
|
+
originalError;
|
|
86
|
+
constructor(data, options) {
|
|
87
|
+
let props;
|
|
88
|
+
if (typeof data === "string") props = {
|
|
89
|
+
message: data,
|
|
90
|
+
...options
|
|
91
|
+
};
|
|
92
|
+
else if (data instanceof Error || typeof data === "object" && data !== null && typeof data.constructor?.name === "string" && data.constructor.name.includes("Error")) props = {
|
|
93
|
+
message: data.message,
|
|
94
|
+
originalError: data,
|
|
95
|
+
...options
|
|
96
|
+
};
|
|
97
|
+
else props = data;
|
|
98
|
+
super(props.message);
|
|
99
|
+
this.status = props.status || 500;
|
|
100
|
+
this.headers = props.headers || {};
|
|
101
|
+
this.body = props.body || props.originalError || { error: { message: props.message } };
|
|
102
|
+
if (props.originalError) this.originalError = props.originalError;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/browser/mock.ts
|
|
107
|
+
let mock = null;
|
|
108
|
+
/**
|
|
109
|
+
* Set the global mock handler used by all {@link FaasBrowserClient} instances.
|
|
110
|
+
*/
|
|
111
|
+
function setMock(handler) {
|
|
112
|
+
mock = handler;
|
|
113
|
+
}
|
|
114
|
+
function normalizeMockResponse(response) {
|
|
115
|
+
if (response instanceof Error) throw new ResponseError(response);
|
|
116
|
+
if (response instanceof Response) {
|
|
117
|
+
if (typeof ReadableStream !== "undefined" && response.body instanceof ReadableStream && response.body.locked === false) {
|
|
118
|
+
const [nextBody, currentBody] = response.body.tee();
|
|
119
|
+
response.body = nextBody;
|
|
120
|
+
const clonedResponse = {
|
|
121
|
+
status: response.status,
|
|
122
|
+
headers: response.headers,
|
|
123
|
+
body: currentBody
|
|
124
|
+
};
|
|
125
|
+
if (response.data !== void 0) clonedResponse.data = response.data;
|
|
126
|
+
return new Response({ ...clonedResponse });
|
|
127
|
+
}
|
|
128
|
+
return response;
|
|
129
|
+
}
|
|
130
|
+
if (response && typeof ReadableStream !== "undefined" && response.body instanceof ReadableStream && response.body.locked === false) {
|
|
131
|
+
const [nextBody, currentBody] = response.body.tee();
|
|
132
|
+
response.body = nextBody;
|
|
133
|
+
return new Response({
|
|
134
|
+
...response,
|
|
135
|
+
body: currentBody
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return new Response(response || {});
|
|
139
|
+
}
|
|
140
|
+
async function resolveMockResponse(action, params, options) {
|
|
141
|
+
if (typeof mock === "function") return normalizeMockResponse(await mock(action, params, options));
|
|
142
|
+
return normalizeMockResponse(mock);
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/browser/helpers.ts
|
|
146
|
+
function buildActionUrl(action, baseUrl, options, requestId) {
|
|
147
|
+
return `${(options?.baseUrl || baseUrl) + action.toLowerCase()}?_=${requestId}`;
|
|
148
|
+
}
|
|
149
|
+
function buildActionOptions(defaultOptions, options, params, requestId) {
|
|
150
|
+
const resolvedOptions = {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { "Content-Type": "application/json; charset=UTF-8" },
|
|
153
|
+
mode: "cors",
|
|
154
|
+
credentials: "include",
|
|
155
|
+
body: JSON.stringify(params),
|
|
156
|
+
...defaultOptions,
|
|
157
|
+
...options || Object.create(null)
|
|
158
|
+
};
|
|
159
|
+
if (!resolvedOptions.headers["X-FaasJS-Request-Id"] && !resolvedOptions.headers["x-faasjs-request-id"]) resolvedOptions.headers["X-FaasJS-Request-Id"] = requestId;
|
|
160
|
+
return resolvedOptions;
|
|
161
|
+
}
|
|
162
|
+
async function runBeforeRequest(action, params, options) {
|
|
163
|
+
if (!options.beforeRequest) return;
|
|
164
|
+
await options.beforeRequest({
|
|
165
|
+
action,
|
|
166
|
+
params,
|
|
167
|
+
options,
|
|
168
|
+
headers: options.headers
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function toResponseHeaders(headers) {
|
|
172
|
+
const responseHeaders = {};
|
|
173
|
+
for (const [key, value] of headers) responseHeaders[key] = value;
|
|
174
|
+
return responseHeaders;
|
|
175
|
+
}
|
|
176
|
+
function parseSuccessfulResponse(status, headers, text) {
|
|
177
|
+
if (!text) return new Response({
|
|
178
|
+
status,
|
|
179
|
+
headers
|
|
180
|
+
});
|
|
181
|
+
const body = JSON.parse(text);
|
|
182
|
+
if (body.error?.message) throw new ResponseError({
|
|
183
|
+
message: body.error.message,
|
|
184
|
+
status,
|
|
185
|
+
headers,
|
|
186
|
+
body
|
|
187
|
+
});
|
|
188
|
+
return new Response({
|
|
189
|
+
status,
|
|
190
|
+
headers,
|
|
191
|
+
body,
|
|
192
|
+
data: body.data
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function parseFailedResponse(status, headers, text) {
|
|
196
|
+
try {
|
|
197
|
+
const body = JSON.parse(text);
|
|
198
|
+
if (body.error?.message) throw new ResponseError({
|
|
199
|
+
message: body.error.message,
|
|
200
|
+
status,
|
|
201
|
+
headers,
|
|
202
|
+
body
|
|
203
|
+
});
|
|
204
|
+
throw new ResponseError({
|
|
205
|
+
message: text,
|
|
206
|
+
status,
|
|
207
|
+
headers,
|
|
208
|
+
body
|
|
209
|
+
});
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error instanceof ResponseError) throw error;
|
|
212
|
+
throw new ResponseError({
|
|
213
|
+
message: text,
|
|
214
|
+
status,
|
|
215
|
+
headers,
|
|
216
|
+
body: text
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function parseFetchResponse(response) {
|
|
221
|
+
const headers = toResponseHeaders(response.headers);
|
|
222
|
+
const text = await response.text();
|
|
223
|
+
if (response.status >= 200 && response.status < 300) return parseSuccessfulResponse(response.status, headers, text);
|
|
224
|
+
return parseFailedResponse(response.status, headers, text);
|
|
225
|
+
}
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/browser/client.ts
|
|
228
|
+
/**
|
|
229
|
+
* Browser client for FaasJS - provides HTTP client functionality for making API requests from web applications.
|
|
230
|
+
*/
|
|
231
|
+
var FaasBrowserClient = class {
|
|
232
|
+
/**
|
|
233
|
+
* Unique identifier for this client instance.
|
|
234
|
+
*/
|
|
235
|
+
id;
|
|
236
|
+
/**
|
|
237
|
+
* Base URL used to build action request URLs.
|
|
238
|
+
*/
|
|
239
|
+
baseUrl;
|
|
240
|
+
/**
|
|
241
|
+
* Default request options merged into every request.
|
|
242
|
+
*/
|
|
243
|
+
defaultOptions;
|
|
244
|
+
/**
|
|
245
|
+
* Creates a new FaasBrowserClient instance.
|
|
246
|
+
*/
|
|
247
|
+
constructor(baseUrl = "/", options = Object.create(null)) {
|
|
248
|
+
if (baseUrl && !baseUrl.endsWith("/")) throw Error("[FaasJS] baseUrl should end with /");
|
|
249
|
+
this.id = `FBC-${generateId()}`;
|
|
250
|
+
this.baseUrl = baseUrl;
|
|
251
|
+
this.defaultOptions = options;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Makes a request to a FaasJS function.
|
|
255
|
+
*/
|
|
256
|
+
async action(action, params, options) {
|
|
257
|
+
if (!action) throw Error("[FaasJS] action required");
|
|
258
|
+
if (!params) params = Object.create(null);
|
|
259
|
+
const requestId = `F-${generateId()}`;
|
|
260
|
+
const url = buildActionUrl(action, this.baseUrl, options, requestId);
|
|
261
|
+
const resolvedOptions = buildActionOptions(this.defaultOptions, options, params, requestId);
|
|
262
|
+
await runBeforeRequest(action, params, resolvedOptions);
|
|
263
|
+
if (mock) return resolveMockResponse(action, params, resolvedOptions);
|
|
264
|
+
if (resolvedOptions.request) return resolvedOptions.request(url, resolvedOptions);
|
|
265
|
+
if (resolvedOptions.stream) return fetch(url, resolvedOptions);
|
|
266
|
+
return parseFetchResponse(await fetch(url, resolvedOptions));
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/faas/index.ts
|
|
271
|
+
/**
|
|
272
|
+
* Call the currently configured FaasReactClient.
|
|
273
|
+
*
|
|
274
|
+
* This helper forwards the request to `getClient`. When the registered
|
|
275
|
+
* client defines `onError`, the hook is invoked before the promise rejects.
|
|
276
|
+
*
|
|
277
|
+
* @template Path - Action path or response data type used for inference.
|
|
278
|
+
*
|
|
279
|
+
* @param {Path} action - Action path to invoke.
|
|
280
|
+
* @param {FaasParams<Path>} params - Parameters sent to the action.
|
|
281
|
+
* @param {Options} [options] - Optional per-request overrides such as headers or base URL.
|
|
282
|
+
* See the browser-client `Options` type for supported fields such as `headers`, `beforeRequest`,
|
|
283
|
+
* `request`, `baseUrl`, and `stream`.
|
|
284
|
+
* @returns {Promise<Response<FaasData<Path>>>} Response returned by the active browser client.
|
|
285
|
+
* @throws {ResponseError} When the request fails and the active client does not recover inside `onError`.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```ts
|
|
289
|
+
* import { faas } from '@faasjs/react'
|
|
290
|
+
*
|
|
291
|
+
* const response = await faas('posts/get', { id: 1 })
|
|
292
|
+
*
|
|
293
|
+
* console.log(response.data.title)
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
async function faas(action, params, options) {
|
|
297
|
+
const client = getClient(options?.baseUrl);
|
|
298
|
+
const onError = client.onError;
|
|
299
|
+
if (onError) return client.browserClient.action(action, params, options).catch(async (res) => {
|
|
300
|
+
await onError(action, params)(res);
|
|
301
|
+
return Promise.reject(res);
|
|
302
|
+
});
|
|
303
|
+
return client.browserClient.action(action, params, options);
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/equal/index.ts
|
|
307
|
+
const AsyncFunction = (async () => {}).constructor;
|
|
308
|
+
/**
|
|
309
|
+
* Compares two values for deep equality.
|
|
310
|
+
*
|
|
311
|
+
* This function checks if two values are deeply equal by comparing their types and contents.
|
|
312
|
+
* It handles various data types including primitives, arrays, dates, regular expressions, functions,
|
|
313
|
+
* maps, sets, and promises.
|
|
314
|
+
*
|
|
315
|
+
* @param {any} a - The first value to compare.
|
|
316
|
+
* @param {any} b - The second value to compare.
|
|
317
|
+
* @returns {boolean} `true` if the values are deeply equal, `false` otherwise.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```ts
|
|
321
|
+
* import { equal } from '@faasjs/react'
|
|
322
|
+
*
|
|
323
|
+
* equal({ page: 1, filters: ['a'] }, { page: 1, filters: ['a'] }) // true
|
|
324
|
+
* equal({ page: 1 }, { page: 2 }) // false
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
8
327
|
function equal(a, b) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return a.toString() === b.toString();
|
|
36
|
-
case Map:
|
|
37
|
-
case Set:
|
|
38
|
-
return equal(Array.from(a), Array.from(b));
|
|
39
|
-
case Promise:
|
|
40
|
-
return a === b;
|
|
41
|
-
case Object: {
|
|
42
|
-
for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]))
|
|
43
|
-
if (!equal(a[key], b[key])) return false;
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
default:
|
|
47
|
-
throw Error(`Unsupported type: ${ctor}`);
|
|
48
|
-
}
|
|
328
|
+
if (a === b) return true;
|
|
329
|
+
if ((a === null || a === void 0) && (b === null || b === void 0)) return true;
|
|
330
|
+
if (typeof a !== typeof b) return false;
|
|
331
|
+
if (a === null || a === void 0 || b === null || b === void 0) return false;
|
|
332
|
+
const ctor = a.constructor;
|
|
333
|
+
if (ctor !== b.constructor) return false;
|
|
334
|
+
switch (ctor) {
|
|
335
|
+
case String:
|
|
336
|
+
case Boolean: return a === b;
|
|
337
|
+
case Number: return Number.isNaN(a) && Number.isNaN(b) || a === b;
|
|
338
|
+
case Array:
|
|
339
|
+
if (a.length !== b.length) return false;
|
|
340
|
+
for (let i = 0; i < a.length; i++) if (!equal(a[i], b[i])) return false;
|
|
341
|
+
return true;
|
|
342
|
+
case Date: return a.getTime() === b.getTime();
|
|
343
|
+
case RegExp:
|
|
344
|
+
case Function:
|
|
345
|
+
case AsyncFunction: return a.toString() === b.toString();
|
|
346
|
+
case Map:
|
|
347
|
+
case Set: return equal(Array.from(a), Array.from(b));
|
|
348
|
+
case Promise: return a === b;
|
|
349
|
+
case Object:
|
|
350
|
+
for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) if (!equal(a[key], b[key])) return false;
|
|
351
|
+
return true;
|
|
352
|
+
default: throw Error(`Unsupported type: ${ctor}`);
|
|
353
|
+
}
|
|
49
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Custom hook that memoizes a value using deep equality comparison.
|
|
357
|
+
*
|
|
358
|
+
* @param {any} value - The value to be memoized.
|
|
359
|
+
* @returns {any} The memoized value.
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```tsx
|
|
363
|
+
* import { useEqualMemoize } from '@faasjs/react'
|
|
364
|
+
*
|
|
365
|
+
* function Filters({ filters }: { filters: Record<string, any> }) {
|
|
366
|
+
* const memoizedFilters = useEqualMemoize(filters)
|
|
367
|
+
*
|
|
368
|
+
* return <pre>{JSON.stringify(memoizedFilters)}</pre>
|
|
369
|
+
* }
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
50
372
|
function useEqualMemoize(value) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
ref.current = value;
|
|
55
|
-
signalRef.current += 1;
|
|
56
|
-
}
|
|
57
|
-
return useMemo(() => ref.current, [signalRef.current]);
|
|
373
|
+
const ref = useRef(value);
|
|
374
|
+
if (!equal(value, ref.current)) ref.current = value;
|
|
375
|
+
return ref.current;
|
|
58
376
|
}
|
|
377
|
+
function useEqualSignal(value) {
|
|
378
|
+
const ref = useRef(value);
|
|
379
|
+
const signalRef = useRef(0);
|
|
380
|
+
if (!equal(value, ref.current)) {
|
|
381
|
+
ref.current = value;
|
|
382
|
+
signalRef.current += 1;
|
|
383
|
+
}
|
|
384
|
+
return signalRef.current;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Custom hook that works like `useEffect` but uses deep comparison on dependencies.
|
|
388
|
+
*
|
|
389
|
+
* @param {React.EffectCallback} callback - The effect callback function to run.
|
|
390
|
+
* @param {any[]} dependencies - The list of dependencies for the effect.
|
|
391
|
+
* @returns {void} The result of the `useEffect` hook with memoized dependencies.
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* ```tsx
|
|
395
|
+
* import { useEqualEffect } from '@faasjs/react'
|
|
396
|
+
*
|
|
397
|
+
* function Page({ filters }: { filters: Record<string, any> }) {
|
|
398
|
+
* useEqualEffect(() => {
|
|
399
|
+
* console.log('filters changed', filters)
|
|
400
|
+
* }, [filters])
|
|
401
|
+
*
|
|
402
|
+
* return null
|
|
403
|
+
* }
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
59
406
|
function useEqualEffect(callback, dependencies) {
|
|
60
|
-
|
|
407
|
+
return useEffect(callback, [useEqualSignal(dependencies)]);
|
|
61
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Custom hook that works like `useMemo` but uses deep comparison on dependencies.
|
|
411
|
+
*
|
|
412
|
+
* @template T - Memoized value type returned by the callback.
|
|
413
|
+
*
|
|
414
|
+
* @param {() => T} callback - The callback function to run.
|
|
415
|
+
* @param {any[]} dependencies - The list of dependencies.
|
|
416
|
+
* @returns {T} The result of the `useMemo` hook with memoized dependencies.
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* ```tsx
|
|
420
|
+
* import { useEqualMemo } from '@faasjs/react'
|
|
421
|
+
*
|
|
422
|
+
* function Page({ filters }: { filters: Record<string, any> }) {
|
|
423
|
+
* const queryString = useEqualMemo(() => JSON.stringify(filters), [filters])
|
|
424
|
+
*
|
|
425
|
+
* return <span>{queryString}</span>
|
|
426
|
+
* }
|
|
427
|
+
* ```
|
|
428
|
+
*/
|
|
62
429
|
function useEqualMemo(callback, dependencies) {
|
|
63
|
-
|
|
430
|
+
const signal = useEqualSignal(dependencies);
|
|
431
|
+
const callbackRef = useRef(callback);
|
|
432
|
+
callbackRef.current = callback;
|
|
433
|
+
return useMemo(() => {
|
|
434
|
+
return callbackRef.current();
|
|
435
|
+
}, [signal]);
|
|
64
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* Custom hook that works like `useCallback` but uses deep comparison on dependencies.
|
|
439
|
+
*
|
|
440
|
+
* @template T - Callback signature to memoize.
|
|
441
|
+
*
|
|
442
|
+
* @param {T} callback - The callback function to run.
|
|
443
|
+
* @param {any[]} dependencies - The list of dependencies.
|
|
444
|
+
* @returns {T} The result of the `useCallback` hook with memoized dependencies.
|
|
445
|
+
*
|
|
446
|
+
* @example
|
|
447
|
+
* ```tsx
|
|
448
|
+
* import { useEqualCallback } from '@faasjs/react'
|
|
449
|
+
*
|
|
450
|
+
* function Search({ filters }: { filters: Record<string, any> }) {
|
|
451
|
+
* const handleSubmit = useEqualCallback(() => {
|
|
452
|
+
* console.log(filters)
|
|
453
|
+
* }, [filters])
|
|
454
|
+
*
|
|
455
|
+
* return <button onClick={handleSubmit}>Search</button>
|
|
456
|
+
* }
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
65
459
|
function useEqualCallback(callback, dependencies) {
|
|
66
|
-
|
|
67
|
-
(...args) => callback(...args),
|
|
68
|
-
useEqualMemoize(dependencies)
|
|
69
|
-
);
|
|
460
|
+
return useCallback((...args) => callback(...args), [useEqualSignal(dependencies)]);
|
|
70
461
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
|
|
462
|
+
/**
|
|
463
|
+
* Fetch FaasJS data and inject the result into a render prop or child element.
|
|
464
|
+
*
|
|
465
|
+
* The wrapper defers rendering `children` or `render` until the first request
|
|
466
|
+
* completes, then keeps passing the latest request state to the rendered output.
|
|
467
|
+
*
|
|
468
|
+
* @param {FaasDataWrapperProps<Path>} props - Wrapper props controlling the request and rendered fallback.
|
|
469
|
+
* @param {(args: FaasDataInjection<Path>) => JSX.Element | JSX.Element[]} [props.render] - Render prop that receives the resolved Faas request state.
|
|
470
|
+
* @param {React.ReactElement<Partial<FaasDataInjection<Path>>>} [props.children] - Child element cloned with injected Faas request state.
|
|
471
|
+
* @param {JSX.Element | false} [props.fallback] - Element rendered before the first successful load.
|
|
472
|
+
* @param {Path} props.action - Action path to request.
|
|
473
|
+
* @param {FaasParams<Path>} [props.params] - Params sent to the action.
|
|
474
|
+
* @param {number | false} [props.polling] - Milliseconds to wait after each completed request before refreshing data in the background.
|
|
475
|
+
* @param {(args: FaasDataInjection<Path>) => void} [props.onDataChange] - Callback invoked when the resolved data value changes.
|
|
476
|
+
* @param {FaasData<Path>} [props.data] - Controlled data value used instead of internal state.
|
|
477
|
+
* @param {React.Dispatch<React.SetStateAction<FaasData<Path>>>} [props.setData] - Controlled setter used instead of internal state.
|
|
478
|
+
* @param {BaseUrl} [props.baseUrl] - Base URL override used for this wrapper instance.
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```tsx
|
|
482
|
+
* import { FaasDataWrapper } from '@faasjs/react'
|
|
483
|
+
*
|
|
484
|
+
* type User = {
|
|
485
|
+
* name: string
|
|
486
|
+
* }
|
|
487
|
+
*
|
|
488
|
+
* function UserView(props: {
|
|
489
|
+
* data?: User
|
|
490
|
+
* error?: Error
|
|
491
|
+
* reload?: () => void
|
|
492
|
+
* }) {
|
|
493
|
+
* if (props.error) {
|
|
494
|
+
* return (
|
|
495
|
+
* <div>
|
|
496
|
+
* <p>Failed to load user: {props.error.message}</p>
|
|
497
|
+
* <button type="button" onClick={() => props.reload?.()}>
|
|
498
|
+
* Retry
|
|
499
|
+
* </button>
|
|
500
|
+
* </div>
|
|
501
|
+
* )
|
|
502
|
+
* }
|
|
503
|
+
*
|
|
504
|
+
* return <div>Hello, {props.data?.name}</div>
|
|
505
|
+
* }
|
|
506
|
+
*
|
|
507
|
+
* // Render-prop mode
|
|
508
|
+
* export function UserProfile(props: { id: number }) {
|
|
509
|
+
* return (
|
|
510
|
+
* <FaasDataWrapper<User>
|
|
511
|
+
* action="/pages/users/get"
|
|
512
|
+
* params={{ id: props.id }}
|
|
513
|
+
* fallback={<div>Loading user...</div>}
|
|
514
|
+
* render={({ data, error, reload }) => {
|
|
515
|
+
* if (error) {
|
|
516
|
+
* return (
|
|
517
|
+
* <div>
|
|
518
|
+
* <p>Failed to load user: {error.message}</p>
|
|
519
|
+
* <button type="button" onClick={() => reload()}>
|
|
520
|
+
* Retry
|
|
521
|
+
* </button>
|
|
522
|
+
* </div>
|
|
523
|
+
* )
|
|
524
|
+
* }
|
|
525
|
+
*
|
|
526
|
+
* return <div>Hello, {data.name}</div>
|
|
527
|
+
* }}
|
|
528
|
+
* />
|
|
529
|
+
* )
|
|
530
|
+
* }
|
|
531
|
+
*
|
|
532
|
+
* // Children injection mode
|
|
533
|
+
* export function UserProfileWithChildren(props: { id: number }) {
|
|
534
|
+
* return (
|
|
535
|
+
* <FaasDataWrapper<User>
|
|
536
|
+
* action="/pages/users/get"
|
|
537
|
+
* params={{ id: props.id }}
|
|
538
|
+
* fallback={<div>Loading user...</div>}
|
|
539
|
+
* >
|
|
540
|
+
* <UserView />
|
|
541
|
+
* </FaasDataWrapper>
|
|
542
|
+
* )
|
|
543
|
+
* }
|
|
544
|
+
* ```
|
|
545
|
+
*
|
|
546
|
+
* When a ref is provided, it exposes the current Faas request state imperatively.
|
|
547
|
+
*/
|
|
548
|
+
const FaasDataWrapper = forwardRef((props, ref) => {
|
|
549
|
+
const requestOptions = {
|
|
550
|
+
...props.data !== void 0 ? { data: props.data } : {},
|
|
551
|
+
...props.setData ? { setData: props.setData } : {},
|
|
552
|
+
...props.polling !== void 0 ? { polling: props.polling } : {}
|
|
553
|
+
};
|
|
554
|
+
const request = getClient(props.baseUrl).useFaas(props.action, props.params ?? {}, requestOptions);
|
|
555
|
+
const [loaded, setLoaded] = useState(false);
|
|
556
|
+
useImperativeHandle(ref, () => request, [request]);
|
|
557
|
+
useEqualEffect(() => {
|
|
558
|
+
if (!request.loading) setLoaded((prev) => prev === false ? true : prev);
|
|
559
|
+
}, [request.loading]);
|
|
560
|
+
useEqualEffect(() => {
|
|
561
|
+
if (props.onDataChange) props.onDataChange(request);
|
|
562
|
+
}, [request.data]);
|
|
563
|
+
return useEqualMemo(() => {
|
|
564
|
+
if (loaded) {
|
|
565
|
+
if (props.children) return cloneElement(props.children, request);
|
|
566
|
+
if (props.render) return props.render(request);
|
|
567
|
+
}
|
|
568
|
+
return props.fallback || null;
|
|
569
|
+
}, [
|
|
570
|
+
loaded,
|
|
571
|
+
request.action,
|
|
572
|
+
request.params,
|
|
573
|
+
request.data,
|
|
574
|
+
request.error,
|
|
575
|
+
request.loading,
|
|
576
|
+
request.refreshing
|
|
577
|
+
]);
|
|
109
578
|
});
|
|
110
|
-
|
|
111
|
-
|
|
579
|
+
Object.assign(FaasDataWrapper, { displayName: "FaasDataWrapper" });
|
|
580
|
+
/**
|
|
581
|
+
* Wrap a component with {@link FaasDataWrapper} and inject Faas request state as props.
|
|
582
|
+
*
|
|
583
|
+
* `withFaasData` is most useful for wrapper-style exports or when you want to
|
|
584
|
+
* preserve an existing component boundary. For new code, prefer `useFaas` or
|
|
585
|
+
* `FaasDataWrapper` when they express the request ownership more directly.
|
|
586
|
+
*
|
|
587
|
+
* @template Path - Action path or response data type used for inference.
|
|
588
|
+
* @template TComponentProps - Component props including injected Faas data fields.
|
|
589
|
+
* @param {React.FC<TComponentProps>} Component - Component that consumes injected Faas data props.
|
|
590
|
+
* @param {FaasDataWrapperProps<Path>} faasProps - Request configuration forwarded to `FaasDataWrapper`.
|
|
591
|
+
* @returns {React.FC<Omit<TComponentProps, keyof FaasDataInjection<Path>> & Record<string, any>>} Component that accepts the original props minus the injected Faas data fields.
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```tsx
|
|
595
|
+
* import { withFaasData } from '@faasjs/react'
|
|
596
|
+
*
|
|
597
|
+
* const MyComponent = withFaasData(
|
|
598
|
+
* ({ data, error, reload }) => {
|
|
599
|
+
* if (error) {
|
|
600
|
+
* return (
|
|
601
|
+
* <button type="button" onClick={() => reload()}>
|
|
602
|
+
* Retry
|
|
603
|
+
* </button>
|
|
604
|
+
* )
|
|
605
|
+
* }
|
|
606
|
+
*
|
|
607
|
+
* return <div>{data.name}</div>
|
|
608
|
+
* },
|
|
609
|
+
* { action: '/pages/users/get', params: { id: 1 } },
|
|
610
|
+
* )
|
|
611
|
+
* ```
|
|
612
|
+
*/
|
|
613
|
+
function withFaasData(Component, faasProps) {
|
|
614
|
+
return (props) => /* @__PURE__ */ jsx(FaasDataWrapper, {
|
|
615
|
+
...faasProps,
|
|
616
|
+
children: /* @__PURE__ */ jsx(Component, { ...props })
|
|
617
|
+
});
|
|
112
618
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/useFaasRequest.ts
|
|
621
|
+
/**
|
|
622
|
+
* Run the shared request lifecycle used by the higher-level FaasJS React hooks.
|
|
623
|
+
*
|
|
624
|
+
* It manages loading state, background refresh state, abort signals, debounce timing,
|
|
625
|
+
* retry-on-fetch-failure, polling, and queued reload promises while delegating the
|
|
626
|
+
* actual transport to `send`.
|
|
627
|
+
*
|
|
628
|
+
* @template Path - Action path used for request params inference.
|
|
629
|
+
* @template TData - Data type returned by `send` and resolved by `reload()`.
|
|
630
|
+
* @param {UseFaasRequestArgs<Path>} args - Request lifecycle configuration.
|
|
631
|
+
* @param {string} args.action - Action path or request key used to trigger the lifecycle.
|
|
632
|
+
* @param {FaasParams<Path>} args.defaultParams - Initial params value stored by the lifecycle.
|
|
633
|
+
* @param {Pick<SharedUseFaasOptions<FaasParams<Path>, FaasData<Path>>, 'params' | 'skip' | 'debounce' | 'polling' | 'baseUrl'>} args.options - Shared request options used by the lifecycle.
|
|
634
|
+
* @param {(args: { silent: boolean }) => void} [args.beforeSend] - Optional callback invoked immediately before a request starts.
|
|
635
|
+
* @param {(result: FaasData<Path>) => void} [args.onSuccess] - Optional callback invoked after a successful response.
|
|
636
|
+
* @param {UseFaasRequestArgs<Path>['send']} args.send - Transport function responsible for creating and resolving the request.
|
|
637
|
+
* @returns Shared request state, reload helpers, and refs used by `useFaas` and `useFaasStream`.
|
|
638
|
+
* @example
|
|
639
|
+
* ```ts
|
|
640
|
+
* function useUserRequest(id: number) {
|
|
641
|
+
* return useFaasRequest({
|
|
642
|
+
* action: '/pages/users/get',
|
|
643
|
+
* defaultParams: { id },
|
|
644
|
+
* options: {},
|
|
645
|
+
* send: async ({ action, params, signal, client, setPromise }) => {
|
|
646
|
+
* const promise = client.faas(action, params, { signal })
|
|
647
|
+
*
|
|
648
|
+
* setPromise(promise)
|
|
649
|
+
*
|
|
650
|
+
* return (await promise).data as { name: string }
|
|
651
|
+
* },
|
|
652
|
+
* })
|
|
653
|
+
* }
|
|
654
|
+
* ```
|
|
655
|
+
*/
|
|
656
|
+
function useFaasRequest({ action, defaultParams, options, beforeSend, onSuccess, send }) {
|
|
657
|
+
const [loading, setLoading] = useState(true);
|
|
658
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
659
|
+
const [error, setError] = useState();
|
|
660
|
+
const [params, setParams] = useState(defaultParams);
|
|
661
|
+
const [requestTrigger, setRequestTrigger] = useState({
|
|
662
|
+
times: 0,
|
|
663
|
+
silent: false
|
|
664
|
+
});
|
|
665
|
+
const [skip, setSkip] = useState(typeof options.skip === "function" ? options.skip(defaultParams) : options.skip);
|
|
666
|
+
const promiseRef = useRef(null);
|
|
667
|
+
const controllerRef = useRef(null);
|
|
668
|
+
const failedOnceRef = useRef(false);
|
|
669
|
+
const pendingReloadsRef = useRef(/* @__PURE__ */ new Map());
|
|
670
|
+
const reloadCounterRef = useRef(0);
|
|
671
|
+
const requestVersionRef = useRef(0);
|
|
672
|
+
const handledRequestTriggerTimesRef = useRef(-1);
|
|
673
|
+
const pollingTimerRef = useRef(null);
|
|
674
|
+
const hasLoadedRef = useRef(false);
|
|
675
|
+
const beforeSendRef = useRef(beforeSend);
|
|
676
|
+
const onSuccessRef = useRef(onSuccess);
|
|
677
|
+
const sendRef = useRef(send);
|
|
678
|
+
beforeSendRef.current = beforeSend;
|
|
679
|
+
onSuccessRef.current = onSuccess;
|
|
680
|
+
sendRef.current = send;
|
|
681
|
+
useEqualEffect(() => {
|
|
682
|
+
setSkip(typeof options.skip === "function" ? options.skip(params) : options.skip);
|
|
683
|
+
}, [typeof options.skip === "function" ? params : options.skip]);
|
|
684
|
+
useEqualEffect(() => {
|
|
685
|
+
if (!equal(defaultParams, params)) setParams(defaultParams);
|
|
686
|
+
}, [defaultParams]);
|
|
687
|
+
useEqualEffect(() => {
|
|
688
|
+
if (!action || skip) {
|
|
689
|
+
setLoading(false);
|
|
690
|
+
setRefreshing(false);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const isSilentRequest = requestTrigger.times !== handledRequestTriggerTimesRef.current && requestTrigger.silent && hasLoadedRef.current;
|
|
694
|
+
handledRequestTriggerTimesRef.current = requestTrigger.times;
|
|
695
|
+
if (isSilentRequest) setRefreshing(true);
|
|
696
|
+
else setLoading(true);
|
|
697
|
+
beforeSendRef.current?.({ silent: isSilentRequest });
|
|
698
|
+
failedOnceRef.current = false;
|
|
699
|
+
const controller = new AbortController();
|
|
700
|
+
const requestVersion = ++requestVersionRef.current;
|
|
701
|
+
controllerRef.current = controller;
|
|
702
|
+
const client = getClient(options.baseUrl);
|
|
703
|
+
const requestParams = options.params || params;
|
|
704
|
+
const clearPollingTimer = () => {
|
|
705
|
+
if (!pollingTimerRef.current) return;
|
|
706
|
+
clearTimeout(pollingTimerRef.current);
|
|
707
|
+
pollingTimerRef.current = null;
|
|
708
|
+
};
|
|
709
|
+
const schedulePolling = () => {
|
|
710
|
+
clearPollingTimer();
|
|
711
|
+
if (!options.polling || options.polling <= 0 || !isCurrentRequest()) return;
|
|
712
|
+
pollingTimerRef.current = setTimeout(() => {
|
|
713
|
+
if (!isCurrentRequest()) return;
|
|
714
|
+
setRequestTrigger((prev) => ({
|
|
715
|
+
times: prev.times + 1,
|
|
716
|
+
silent: true
|
|
717
|
+
}));
|
|
718
|
+
}, options.polling);
|
|
719
|
+
};
|
|
720
|
+
const rejectPending = (reason) => {
|
|
721
|
+
for (const { reject } of pendingReloadsRef.current.values()) reject(reason);
|
|
722
|
+
pendingReloadsRef.current.clear();
|
|
723
|
+
};
|
|
724
|
+
const resolvePending = (value) => {
|
|
725
|
+
for (const { resolve } of pendingReloadsRef.current.values()) resolve(value);
|
|
726
|
+
pendingReloadsRef.current.clear();
|
|
727
|
+
};
|
|
728
|
+
const isCurrentRequest = () => requestVersion === requestVersionRef.current && controllerRef.current === controller;
|
|
729
|
+
const run = () => {
|
|
730
|
+
sendRef.current({
|
|
731
|
+
action,
|
|
732
|
+
params: requestParams,
|
|
733
|
+
signal: controller.signal,
|
|
734
|
+
client,
|
|
735
|
+
setPromise: (promise) => {
|
|
736
|
+
promiseRef.current = promise;
|
|
737
|
+
}
|
|
738
|
+
}).then((result) => {
|
|
739
|
+
if (!isCurrentRequest()) return;
|
|
740
|
+
failedOnceRef.current = false;
|
|
741
|
+
setError(null);
|
|
742
|
+
onSuccessRef.current?.(result);
|
|
743
|
+
hasLoadedRef.current = true;
|
|
744
|
+
setLoading(false);
|
|
745
|
+
setRefreshing(false);
|
|
746
|
+
resolvePending(result);
|
|
747
|
+
schedulePolling();
|
|
748
|
+
}).catch(async (e) => {
|
|
749
|
+
if (!isCurrentRequest()) return;
|
|
750
|
+
if (typeof e?.message === "string" && e.message.toLowerCase().includes("aborted")) return;
|
|
751
|
+
if (!failedOnceRef.current && typeof e?.message === "string" && e.message.includes("Failed to fetch")) {
|
|
752
|
+
failedOnceRef.current = true;
|
|
753
|
+
console.warn(`[FaasJS] FaasReactClient: ${e.message} retry...`);
|
|
754
|
+
run();
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
let nextError = e;
|
|
758
|
+
if (client.onError) try {
|
|
759
|
+
await client.onError(action, requestParams || Object.create(null))(e);
|
|
760
|
+
} catch (newError) {
|
|
761
|
+
nextError = newError;
|
|
762
|
+
}
|
|
763
|
+
if (!isCurrentRequest()) return;
|
|
764
|
+
setError(nextError);
|
|
765
|
+
setLoading(false);
|
|
766
|
+
setRefreshing(false);
|
|
767
|
+
rejectPending(nextError);
|
|
768
|
+
schedulePolling();
|
|
769
|
+
});
|
|
770
|
+
};
|
|
771
|
+
if (options.debounce) {
|
|
772
|
+
const timeout = setTimeout(run, options.debounce);
|
|
773
|
+
return () => {
|
|
774
|
+
clearTimeout(timeout);
|
|
775
|
+
clearPollingTimer();
|
|
776
|
+
if (controllerRef.current === controller) controllerRef.current = null;
|
|
777
|
+
controller.abort();
|
|
778
|
+
setLoading(false);
|
|
779
|
+
setRefreshing(false);
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
run();
|
|
783
|
+
return () => {
|
|
784
|
+
clearPollingTimer();
|
|
785
|
+
if (controllerRef.current === controller) controllerRef.current = null;
|
|
786
|
+
controller.abort();
|
|
787
|
+
setLoading(false);
|
|
788
|
+
setRefreshing(false);
|
|
789
|
+
};
|
|
790
|
+
}, [
|
|
791
|
+
action,
|
|
792
|
+
options.params || params,
|
|
793
|
+
requestTrigger,
|
|
794
|
+
skip,
|
|
795
|
+
options.debounce,
|
|
796
|
+
options.polling,
|
|
797
|
+
options.baseUrl
|
|
798
|
+
]);
|
|
799
|
+
const reload = useEqualCallback((nextParams, reloadOptions) => {
|
|
800
|
+
if (skip) setSkip(false);
|
|
801
|
+
if (nextParams) setParams(nextParams);
|
|
802
|
+
const reloadCounter = ++reloadCounterRef.current;
|
|
803
|
+
return new Promise((resolve, reject) => {
|
|
804
|
+
pendingReloadsRef.current.set(reloadCounter, {
|
|
805
|
+
resolve,
|
|
806
|
+
reject
|
|
807
|
+
});
|
|
808
|
+
setRequestTrigger((prev) => ({
|
|
809
|
+
times: prev.times + 1,
|
|
810
|
+
silent: Boolean(reloadOptions?.silent)
|
|
811
|
+
}));
|
|
812
|
+
});
|
|
813
|
+
}, [skip]);
|
|
814
|
+
return {
|
|
815
|
+
loading,
|
|
816
|
+
refreshing,
|
|
817
|
+
error,
|
|
818
|
+
params,
|
|
819
|
+
reloadTimes: requestTrigger.times,
|
|
820
|
+
reload,
|
|
821
|
+
promiseRef,
|
|
822
|
+
setError,
|
|
823
|
+
setLoading
|
|
824
|
+
};
|
|
123
825
|
}
|
|
826
|
+
//#endregion
|
|
827
|
+
//#region src/useFaas/index.tsx
|
|
828
|
+
/**
|
|
829
|
+
* Request FaasJS data and keep request state in React state.
|
|
830
|
+
*
|
|
831
|
+
* `useFaas` is the default hook for standard FaasJS request-response flows in React.
|
|
832
|
+
* It sends an initial request unless `skip` is enabled, and returns request state
|
|
833
|
+
* plus helpers for reloading, background refreshing, updating data, and handling errors.
|
|
834
|
+
*
|
|
835
|
+
* @template Path - Action path or response data type used for inference.
|
|
836
|
+
*
|
|
837
|
+
* @param {Path} action - Action path to invoke.
|
|
838
|
+
* @param {FaasParams<Path>} defaultParams - Params used for the initial request and future reloads.
|
|
839
|
+
* @param {UseFaasOptions<Path>} [options] - Optional hook configuration such as controlled data, skip logic, debounce timing, polling, and base URL overrides.
|
|
840
|
+
* See the `UseFaasOptions` type for `params`, `data`, `setData`, `skip`, `debounce`, `polling`, and `baseUrl`.
|
|
841
|
+
* @returns {FaasDataInjection<Path>} Request state and helper methods described by {@link FaasDataInjection}.
|
|
842
|
+
*
|
|
843
|
+
* @example
|
|
844
|
+
* ```tsx
|
|
845
|
+
* import { useFaas } from '@faasjs/react'
|
|
846
|
+
*
|
|
847
|
+
* function Profile({ id }: { id: number }) {
|
|
848
|
+
* const { data, error, loading, reload } = useFaas('/pages/users/get', { id })
|
|
849
|
+
*
|
|
850
|
+
* if (loading) return <div>Loading...</div>
|
|
851
|
+
*
|
|
852
|
+
* if (error) {
|
|
853
|
+
* return (
|
|
854
|
+
* <div>
|
|
855
|
+
* <div>Load failed: {error.message}</div>
|
|
856
|
+
* <button type="button" onClick={() => reload()}>
|
|
857
|
+
* Retry
|
|
858
|
+
* </button>
|
|
859
|
+
* </div>
|
|
860
|
+
* )
|
|
861
|
+
* }
|
|
862
|
+
*
|
|
863
|
+
* return (
|
|
864
|
+
* <div>
|
|
865
|
+
* <span>{data.name}</span>
|
|
866
|
+
* <button type="button" onClick={() => reload()}>
|
|
867
|
+
* Refresh
|
|
868
|
+
* </button>
|
|
869
|
+
* </div>
|
|
870
|
+
* )
|
|
871
|
+
* }
|
|
872
|
+
* ```
|
|
873
|
+
*/
|
|
124
874
|
function useFaas(action, defaultParams, options = {}) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
);
|
|
162
|
-
promiseRef.current = request;
|
|
163
|
-
request.then((r) => {
|
|
164
|
-
setFails(0);
|
|
165
|
-
setError(null);
|
|
166
|
-
options.setData ? options.setData(r.data) : setData(r.data);
|
|
167
|
-
setLoading(false);
|
|
168
|
-
for (const { resolve } of pendingReloadsRef.current.values())
|
|
169
|
-
resolve(r.data);
|
|
170
|
-
pendingReloadsRef.current.clear();
|
|
171
|
-
}).catch(async (e) => {
|
|
172
|
-
if (typeof e?.message === "string" && e.message.toLowerCase().indexOf("aborted") >= 0)
|
|
173
|
-
return;
|
|
174
|
-
if (!fails && typeof e?.message === "string" && e.message.indexOf("Failed to fetch") >= 0) {
|
|
175
|
-
console.warn(`FaasReactClient: ${e.message} retry...`);
|
|
176
|
-
setFails(1);
|
|
177
|
-
return send();
|
|
178
|
-
}
|
|
179
|
-
let error2 = e;
|
|
180
|
-
if (client.onError)
|
|
181
|
-
try {
|
|
182
|
-
await client.onError(action, params)(e);
|
|
183
|
-
} catch (newError) {
|
|
184
|
-
error2 = newError;
|
|
185
|
-
}
|
|
186
|
-
setError(error2);
|
|
187
|
-
setLoading(false);
|
|
188
|
-
for (const { reject } of pendingReloadsRef.current.values())
|
|
189
|
-
reject(error2);
|
|
190
|
-
pendingReloadsRef.current.clear();
|
|
191
|
-
return;
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
if (options.debounce) {
|
|
195
|
-
const timeout = setTimeout(send, options.debounce);
|
|
196
|
-
return () => {
|
|
197
|
-
clearTimeout(timeout);
|
|
198
|
-
controllerRef.current?.abort();
|
|
199
|
-
setLoading(false);
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
send();
|
|
203
|
-
return () => {
|
|
204
|
-
controllerRef.current?.abort();
|
|
205
|
-
setLoading(false);
|
|
206
|
-
};
|
|
207
|
-
}, [action, options.params || params, reloadTimes, skip]);
|
|
208
|
-
const reload = useEqualCallback(
|
|
209
|
-
(params2) => {
|
|
210
|
-
if (skip) setSkip(false);
|
|
211
|
-
if (params2) setParams(params2);
|
|
212
|
-
const reloadCounter = ++reloadCounterRef.current;
|
|
213
|
-
setReloadTimes((prev) => prev + 1);
|
|
214
|
-
return new Promise((resolve, reject) => {
|
|
215
|
-
pendingReloadsRef.current.set(reloadCounter, { resolve, reject });
|
|
216
|
-
setReloadTimes((prev) => prev + 1);
|
|
217
|
-
});
|
|
218
|
-
},
|
|
219
|
-
[params, skip]
|
|
220
|
-
);
|
|
221
|
-
return {
|
|
222
|
-
action,
|
|
223
|
-
params,
|
|
224
|
-
loading,
|
|
225
|
-
data: options.data || data,
|
|
226
|
-
reloadTimes,
|
|
227
|
-
error,
|
|
228
|
-
promise: promiseRef.current,
|
|
229
|
-
reload,
|
|
230
|
-
setData: options.setData || setData,
|
|
231
|
-
setLoading,
|
|
232
|
-
setPromise: (newPromise) => typeof newPromise === "function" ? newPromise(promiseRef.current) : promiseRef.current = newPromise,
|
|
233
|
-
setError
|
|
234
|
-
};
|
|
875
|
+
const [data, setData] = useState();
|
|
876
|
+
const localSetData = setData;
|
|
877
|
+
const request = useFaasRequest({
|
|
878
|
+
action,
|
|
879
|
+
defaultParams,
|
|
880
|
+
options,
|
|
881
|
+
onSuccess: (nextData) => {
|
|
882
|
+
if (options.setData) options.setData(nextData);
|
|
883
|
+
else localSetData(nextData);
|
|
884
|
+
},
|
|
885
|
+
send: async ({ action, params, signal, client, setPromise }) => {
|
|
886
|
+
const promise = client.faas(action, params, { signal });
|
|
887
|
+
setPromise(promise);
|
|
888
|
+
return (await promise).data;
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
const currentData = options.data ?? data;
|
|
892
|
+
const currentPromise = request.promiseRef.current ?? Promise.resolve({});
|
|
893
|
+
const updateData = options.setData ?? localSetData;
|
|
894
|
+
return {
|
|
895
|
+
action,
|
|
896
|
+
params: request.params,
|
|
897
|
+
loading: request.loading,
|
|
898
|
+
refreshing: request.refreshing,
|
|
899
|
+
data: currentData,
|
|
900
|
+
reloadTimes: request.reloadTimes,
|
|
901
|
+
error: request.error,
|
|
902
|
+
promise: currentPromise,
|
|
903
|
+
reload: request.reload,
|
|
904
|
+
setData: updateData,
|
|
905
|
+
setLoading: request.setLoading,
|
|
906
|
+
setPromise: (newPromise) => {
|
|
907
|
+
request.promiseRef.current = typeof newPromise === "function" ? newPromise(currentPromise) : newPromise;
|
|
908
|
+
},
|
|
909
|
+
setError: request.setError
|
|
910
|
+
};
|
|
235
911
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/client.tsx
|
|
914
|
+
const clients = {};
|
|
915
|
+
/**
|
|
916
|
+
* Create and register a FaasReactClient instance.
|
|
917
|
+
*
|
|
918
|
+
* The returned client is stored by `baseUrl` and becomes the default client
|
|
919
|
+
* used by helpers such as {@link faas} and {@link useFaas}.
|
|
920
|
+
*
|
|
921
|
+
* @param {FaasReactClientOptions} [options] - Client configuration including base URL, default request options, and error hooks.
|
|
922
|
+
* @param {BaseUrl} [options.baseUrl] - Base URL used to register and route the client instance.
|
|
923
|
+
* @param {Options} [options.options] - Default browser-client request options forwarded to `FaasBrowserClient`.
|
|
924
|
+
* @param {OnError} [options.onError] - Hook factory used to handle failed `faas` and `useFaas` requests.
|
|
925
|
+
* See {@link Options} for supported browser-client request fields such as `headers`,
|
|
926
|
+
* `beforeRequest`, `request`, `baseUrl`, and `stream`.
|
|
927
|
+
* @returns {FaasReactClientInstance} Registered FaasReactClient instance.
|
|
928
|
+
*
|
|
929
|
+
* @example
|
|
930
|
+
* ```ts
|
|
931
|
+
* import { FaasReactClient, ResponseError } from '@faasjs/react'
|
|
932
|
+
*
|
|
933
|
+
* const client = FaasReactClient({
|
|
934
|
+
* baseUrl: 'http://localhost:8080/api/',
|
|
935
|
+
* onError: (action, params) => async (res) => {
|
|
936
|
+
* if (res instanceof ResponseError) {
|
|
937
|
+
* reportErrorToSentry(res, {
|
|
938
|
+
* tags: { action },
|
|
939
|
+
* extra: { params },
|
|
940
|
+
* })
|
|
941
|
+
* }
|
|
942
|
+
* },
|
|
943
|
+
* })
|
|
944
|
+
* ```
|
|
945
|
+
*/
|
|
946
|
+
function FaasReactClient(options = { baseUrl: "/" }) {
|
|
947
|
+
const { baseUrl, options: clientOptions, onError } = options;
|
|
948
|
+
const resolvedBaseUrl = baseUrl ?? "/";
|
|
949
|
+
const client = new FaasBrowserClient(resolvedBaseUrl, clientOptions);
|
|
950
|
+
function withBaseUrl(options) {
|
|
951
|
+
if (options?.baseUrl) return options;
|
|
952
|
+
return {
|
|
953
|
+
...options ?? {},
|
|
954
|
+
baseUrl: resolvedBaseUrl
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
const reactClient = {
|
|
958
|
+
id: client.id,
|
|
959
|
+
faas: async (action, params, requestOptions) => faas(action, params, withBaseUrl(requestOptions)),
|
|
960
|
+
useFaas: (action, defaultParams, requestOptions) => useFaas(action, defaultParams, withBaseUrl(requestOptions)),
|
|
961
|
+
FaasDataWrapper: (props) => /* @__PURE__ */ jsx(FaasDataWrapper, {
|
|
962
|
+
...props,
|
|
963
|
+
baseUrl: resolvedBaseUrl
|
|
964
|
+
}),
|
|
965
|
+
...onError ? { onError } : {},
|
|
966
|
+
browserClient: client
|
|
967
|
+
};
|
|
968
|
+
clients[resolvedBaseUrl] = reactClient;
|
|
969
|
+
return reactClient;
|
|
251
970
|
}
|
|
971
|
+
/**
|
|
972
|
+
* Get a registered FaasReactClient instance.
|
|
973
|
+
*
|
|
974
|
+
* When `host` is omitted, the first registered client is returned. If no client
|
|
975
|
+
* has been created yet, a default client is initialized automatically.
|
|
976
|
+
* Use `getClient` only for special cases such as multiple Faas clients with
|
|
977
|
+
* different base URLs. In normal single-client app code, prefer the default
|
|
978
|
+
* `faas`, `useFaas`, or `FaasReactClient` setup directly.
|
|
979
|
+
*
|
|
980
|
+
* @param {string} [host] - Registered base URL to look up. Omit it to use the default client.
|
|
981
|
+
* @returns {FaasReactClientInstance} Registered or newly created FaasReactClient instance.
|
|
982
|
+
*
|
|
983
|
+
* @example
|
|
984
|
+
* ```ts
|
|
985
|
+
* import { FaasReactClient, getClient } from '@faasjs/react'
|
|
986
|
+
*
|
|
987
|
+
* FaasReactClient({
|
|
988
|
+
* baseUrl: 'https://service-a.example.com/api/',
|
|
989
|
+
* })
|
|
990
|
+
*
|
|
991
|
+
* FaasReactClient({
|
|
992
|
+
* baseUrl: 'https://service-b.example.com/api/',
|
|
993
|
+
* })
|
|
994
|
+
*
|
|
995
|
+
* const client = getClient('https://service-b.example.com/api/')
|
|
996
|
+
*
|
|
997
|
+
* await client.faas('/pages/posts/get', { id: 1 })
|
|
998
|
+
* ```
|
|
999
|
+
*/
|
|
252
1000
|
function getClient(host) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
1001
|
+
const client = clients[host || Object.keys(clients)[0]];
|
|
1002
|
+
if (!client) {
|
|
1003
|
+
console.warn("[FaasJS] FaasReactClient is not initialized manually, use default.");
|
|
1004
|
+
return FaasReactClient();
|
|
1005
|
+
}
|
|
1006
|
+
return client;
|
|
259
1007
|
}
|
|
1008
|
+
//#endregion
|
|
1009
|
+
//#region src/constants/index.ts
|
|
1010
|
+
/**
|
|
1011
|
+
* Returns a constant value that is created by the given function.
|
|
1012
|
+
*
|
|
1013
|
+
* @template T - Constant value type returned by the initializer.
|
|
1014
|
+
* @param {() => T} fn - Initializer that runs only once for the current component instance.
|
|
1015
|
+
* @returns {T} Stable value returned by the initializer.
|
|
1016
|
+
*
|
|
1017
|
+
* @example
|
|
1018
|
+
* ```tsx
|
|
1019
|
+
* import { useConstant } from '@faasjs/react'
|
|
1020
|
+
*
|
|
1021
|
+
* function Page() {
|
|
1022
|
+
* const requestId = useConstant(() => crypto.randomUUID())
|
|
1023
|
+
*
|
|
1024
|
+
* return <span>{requestId}</span>
|
|
1025
|
+
* }
|
|
1026
|
+
* ```
|
|
1027
|
+
*/
|
|
260
1028
|
function useConstant(fn) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
return ref.current.v;
|
|
1029
|
+
const ref = useRef(null);
|
|
1030
|
+
if (!ref.current) ref.current = { v: fn() };
|
|
1031
|
+
return ref.current.v;
|
|
266
1032
|
}
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region src/ErrorBoundary/index.tsx
|
|
1035
|
+
/**
|
|
1036
|
+
* React error boundary with an optional custom fallback element.
|
|
1037
|
+
*
|
|
1038
|
+
* The boundary renders its children until a descendant throws. After that it
|
|
1039
|
+
* either clones `errorChildren` with injected error details or renders a simple
|
|
1040
|
+
* built-in fallback.
|
|
1041
|
+
*
|
|
1042
|
+
* @example
|
|
1043
|
+
* ```tsx
|
|
1044
|
+
* import { ErrorBoundary } from '@faasjs/react'
|
|
1045
|
+
*
|
|
1046
|
+
* function Fallback({ errorMessage }: { errorMessage?: string }) {
|
|
1047
|
+
* return <div>{errorMessage}</div>
|
|
1048
|
+
* }
|
|
1049
|
+
*
|
|
1050
|
+
* <ErrorBoundary errorChildren={<Fallback />}>
|
|
1051
|
+
* <DangerousWidget />
|
|
1052
|
+
* </ErrorBoundary>
|
|
1053
|
+
* ```
|
|
1054
|
+
*/
|
|
267
1055
|
var ErrorBoundary = class extends Component {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
1056
|
+
/**
|
|
1057
|
+
* Stable display name used by React DevTools.
|
|
1058
|
+
*/
|
|
1059
|
+
static displayName = "ErrorBoundary";
|
|
1060
|
+
/**
|
|
1061
|
+
* Create an error boundary with empty error state.
|
|
1062
|
+
*
|
|
1063
|
+
* @param {ErrorBoundaryProps} props - Boundary props.
|
|
1064
|
+
* @param {ReactNode} [props.children] - Descendant elements protected by the boundary.
|
|
1065
|
+
* @param {(error: Error | null, info: any) => void} [props.onError] - Callback invoked after a render error is captured.
|
|
1066
|
+
* @param {ReactElement<ErrorChildrenProps>} [props.errorChildren] - Custom fallback element that receives error details.
|
|
1067
|
+
*/
|
|
1068
|
+
constructor(props) {
|
|
1069
|
+
super(props);
|
|
1070
|
+
this.state = {
|
|
1071
|
+
error: null,
|
|
1072
|
+
info: { componentStack: "" }
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Capture rendering errors from descendant components.
|
|
1077
|
+
*
|
|
1078
|
+
* @param {Error} error - Caught render error.
|
|
1079
|
+
* @param {ErrorInfo} info - React component stack metadata.
|
|
1080
|
+
*/
|
|
1081
|
+
componentDidCatch(error, info) {
|
|
1082
|
+
this.setState({
|
|
1083
|
+
error,
|
|
1084
|
+
info
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Render children or the configured fallback for the captured error.
|
|
1089
|
+
*/
|
|
1090
|
+
render() {
|
|
1091
|
+
const { error, info } = this.state;
|
|
1092
|
+
const errorMessage = String(error ?? "");
|
|
1093
|
+
const errorDescription = info.componentStack || void 0;
|
|
1094
|
+
if (error) {
|
|
1095
|
+
if (this.props.onError) this.props.onError(error, info);
|
|
1096
|
+
if (this.props.errorChildren) return cloneElement(this.props.errorChildren, {
|
|
1097
|
+
error,
|
|
1098
|
+
info,
|
|
1099
|
+
errorMessage,
|
|
1100
|
+
...errorDescription ? { errorDescription } : {}
|
|
1101
|
+
});
|
|
1102
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", { children: errorMessage }), /* @__PURE__ */ jsx("pre", { children: errorDescription })] });
|
|
1103
|
+
}
|
|
1104
|
+
return this.props.children ?? null;
|
|
1105
|
+
}
|
|
302
1106
|
};
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
1107
|
+
//#endregion
|
|
1108
|
+
//#region src/OptionalWrapper/index.tsx
|
|
1109
|
+
/**
|
|
1110
|
+
* Conditionally wrap children with another component.
|
|
1111
|
+
*
|
|
1112
|
+
* @param {OptionalWrapperProps} props - Wrapper condition, wrapper component, and child content.
|
|
1113
|
+
* @param {boolean} props.condition - When `true`, wrap children with `Wrapper`.
|
|
1114
|
+
* @param {OptionalWrapperProps['Wrapper']} props.Wrapper - Component used as the wrapper when the condition passes.
|
|
1115
|
+
* @param {OptionalWrapperProps['wrapperProps']} [props.wrapperProps] - Props forwarded to the wrapper component.
|
|
1116
|
+
* @param {ReactNode} props.children - Content rendered directly or inside the wrapper.
|
|
1117
|
+
* @returns {ReactNode} Wrapped children or the original children when `condition` is false.
|
|
1118
|
+
*
|
|
1119
|
+
* @example
|
|
1120
|
+
* ```tsx
|
|
1121
|
+
* import { OptionalWrapper } from '@faasjs/react'
|
|
1122
|
+
*
|
|
1123
|
+
* const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
1124
|
+
* <div className='wrapper'>{children}</div>
|
|
1125
|
+
* )
|
|
1126
|
+
*
|
|
1127
|
+
* const App = () => (
|
|
1128
|
+
* <OptionalWrapper condition={true} Wrapper={Wrapper}>
|
|
1129
|
+
* <span>Test</span>
|
|
1130
|
+
* </OptionalWrapper>
|
|
1131
|
+
* )
|
|
1132
|
+
* ```
|
|
1133
|
+
*/
|
|
1134
|
+
function OptionalWrapper(props) {
|
|
1135
|
+
const { condition, Wrapper, wrapperProps, children } = props;
|
|
1136
|
+
if (condition) return /* @__PURE__ */ jsx(Wrapper, {
|
|
1137
|
+
...wrapperProps,
|
|
1138
|
+
children
|
|
1139
|
+
});
|
|
1140
|
+
return children;
|
|
310
1141
|
}
|
|
1142
|
+
OptionalWrapper.displayName = "OptionalWrapper";
|
|
1143
|
+
//#endregion
|
|
1144
|
+
//#region src/splitting-state/index.tsx
|
|
1145
|
+
/**
|
|
1146
|
+
* Create local state entries and matching setters for each key in an object.
|
|
1147
|
+
*
|
|
1148
|
+
* @template T - A generic type that extends a record with string keys and any values.
|
|
1149
|
+
* @param {T} initialStates - Object whose keys become state values and `setXxx` setters.
|
|
1150
|
+
* @returns {StatesWithSetters<T>} Object containing the original keys plus generated setter functions.
|
|
1151
|
+
*
|
|
1152
|
+
* @example
|
|
1153
|
+
* ```tsx
|
|
1154
|
+
* function Counter() {
|
|
1155
|
+
* const { count, setCount, name, setName } = useSplittingState({ count: 0, name: 'John' })
|
|
1156
|
+
*
|
|
1157
|
+
* return <>{name}: {count}</>
|
|
1158
|
+
* }
|
|
1159
|
+
* ```
|
|
1160
|
+
*/
|
|
311
1161
|
function useSplittingState(initialStates) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
1162
|
+
const states = {};
|
|
1163
|
+
for (const key of Object.keys(initialStates)) {
|
|
1164
|
+
const state = useState(initialStates[key]);
|
|
1165
|
+
Object.assign(states, {
|
|
1166
|
+
[key]: state[0],
|
|
1167
|
+
[`set${String(key).charAt(0).toUpperCase()}${String(key).slice(1)}`]: state[1]
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
return states;
|
|
321
1171
|
}
|
|
1172
|
+
//#endregion
|
|
1173
|
+
//#region src/splitting-context/index.tsx
|
|
1174
|
+
/**
|
|
1175
|
+
* Create a context whose keys can be consumed independently.
|
|
1176
|
+
*
|
|
1177
|
+
* `createSplittingContext` returns a `Provider` and a `use` hook. Each key in
|
|
1178
|
+
* the provided shape is backed by a separate React context so readers only
|
|
1179
|
+
* subscribe to the values they access.
|
|
1180
|
+
*
|
|
1181
|
+
* @template T - Context value shape exposed by the provider and hook.
|
|
1182
|
+
* @param {Record<string, any> | (keyof T)[]} defaultValue - Default value map or key list used to create split contexts.
|
|
1183
|
+
* @returns {{ Provider<NewT extends T = T>(this: void, props: { value?: Partial<NewT>; children: ReactNode; memo?: true | any[]; initializeStates?: Partial<NewT> }): ReactNode; use<NewT extends T = T>(this: void): Readonly<NewT> }} Provider and hook helpers for the split context.
|
|
1184
|
+
*
|
|
1185
|
+
* @example
|
|
1186
|
+
* ```tsx
|
|
1187
|
+
* const { Provider, use } = createSplittingContext<{
|
|
1188
|
+
* value: number
|
|
1189
|
+
* setValue: React.Dispatch<React.SetStateAction<number>>
|
|
1190
|
+
* }>({
|
|
1191
|
+
* value: 0,
|
|
1192
|
+
* setValue: null,
|
|
1193
|
+
* })
|
|
1194
|
+
*
|
|
1195
|
+
* function ReaderComponent() {
|
|
1196
|
+
* const { value } = use()
|
|
1197
|
+
*
|
|
1198
|
+
* return <div>{value}</div>
|
|
1199
|
+
* }
|
|
1200
|
+
*
|
|
1201
|
+
* function WriterComponent() {
|
|
1202
|
+
* const { setValue } = use()
|
|
1203
|
+
*
|
|
1204
|
+
* return (
|
|
1205
|
+
* <button type='button' onClick={() => setValue((p: number) => p + 1)}>
|
|
1206
|
+
* Change
|
|
1207
|
+
* </button>
|
|
1208
|
+
* )
|
|
1209
|
+
* }
|
|
1210
|
+
*
|
|
1211
|
+
* function App() {
|
|
1212
|
+
* const [value, setValue] = useState(0)
|
|
1213
|
+
*
|
|
1214
|
+
* return (
|
|
1215
|
+
* <Provider value={{ value, setValue }}>
|
|
1216
|
+
* <ReaderComponent />
|
|
1217
|
+
* <WriterComponent />
|
|
1218
|
+
* </Provider>
|
|
1219
|
+
* )
|
|
1220
|
+
* }
|
|
1221
|
+
* ```
|
|
1222
|
+
*/
|
|
322
1223
|
function createSplittingContext(defaultValue) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
return Object.freeze(obj);
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
return {
|
|
364
|
-
Provider,
|
|
365
|
-
use
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// src/Form/context.tsx
|
|
370
|
-
var FormContext = createSplittingContext([
|
|
371
|
-
"items",
|
|
372
|
-
"onSubmit",
|
|
373
|
-
"Elements",
|
|
374
|
-
"lang",
|
|
375
|
-
"rules",
|
|
376
|
-
"submitting",
|
|
377
|
-
"setSubmitting",
|
|
378
|
-
"values",
|
|
379
|
-
"setValues",
|
|
380
|
-
"errors",
|
|
381
|
-
"setErrors",
|
|
382
|
-
"valuesRef"
|
|
383
|
-
]);
|
|
384
|
-
var FormContextProvider = FormContext.Provider;
|
|
385
|
-
var useFormContext = FormContext.use;
|
|
386
|
-
function processValue(input, rules) {
|
|
387
|
-
switch (rules?.type) {
|
|
388
|
-
case "number":
|
|
389
|
-
return Number(input);
|
|
390
|
-
case "string":
|
|
391
|
-
return String(input);
|
|
392
|
-
default:
|
|
393
|
-
return input;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
function FormInput({
|
|
397
|
-
name,
|
|
398
|
-
rules,
|
|
399
|
-
...rest
|
|
400
|
-
}) {
|
|
401
|
-
const { Elements, values, setValues } = useFormContext();
|
|
402
|
-
const value = values?.[name];
|
|
403
|
-
if (rest.Input) {
|
|
404
|
-
return /* @__PURE__ */ jsx(
|
|
405
|
-
rest.Input,
|
|
406
|
-
{
|
|
407
|
-
name,
|
|
408
|
-
value,
|
|
409
|
-
onChange: (v) => setValues((prev) => ({
|
|
410
|
-
...prev,
|
|
411
|
-
[name]: processValue(v, rules)
|
|
412
|
-
})),
|
|
413
|
-
...rest.props
|
|
414
|
-
}
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
return /* @__PURE__ */ jsx(
|
|
418
|
-
Elements.Input,
|
|
419
|
-
{
|
|
420
|
-
name,
|
|
421
|
-
value,
|
|
422
|
-
onChange: (v) => setValues((prev) => ({
|
|
423
|
-
...prev,
|
|
424
|
-
[name]: processValue(v, rules)
|
|
425
|
-
})),
|
|
426
|
-
...rest.props
|
|
427
|
-
}
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
FormInput.displayName = "FormInput";
|
|
431
|
-
function FormItem(props) {
|
|
432
|
-
const { Elements, errors } = useFormContext();
|
|
433
|
-
const Label = props.label?.Label ?? Elements.Label;
|
|
434
|
-
return /* @__PURE__ */ jsx(Label, { name: props.name, ...props.label, error: errors[props.name], children: /* @__PURE__ */ jsx(FormInput, { name: props.name, rules: props.rules, ...props.input }) });
|
|
435
|
-
}
|
|
436
|
-
FormItem.displayName = "FormItem";
|
|
437
|
-
function FormBody() {
|
|
438
|
-
const { items } = useFormContext();
|
|
439
|
-
return items.map((item) => /* @__PURE__ */ jsx(FormItem, { ...item }, item.name));
|
|
440
|
-
}
|
|
441
|
-
FormBody.displayName = "FormBody";
|
|
442
|
-
var FormButtonElement = forwardRef(({ children, submit, submitting, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
443
|
-
"button",
|
|
444
|
-
{
|
|
445
|
-
type: "button",
|
|
446
|
-
disabled: submitting,
|
|
447
|
-
onClick: submit,
|
|
448
|
-
...props,
|
|
449
|
-
ref,
|
|
450
|
-
children
|
|
451
|
-
}
|
|
452
|
-
));
|
|
453
|
-
FormButtonElement.displayName = "FormButtonElement";
|
|
454
|
-
var FormInputElement = forwardRef(({ onChange, ...props }, ref) => /* @__PURE__ */ jsx("input", { ...props, onChange: (e) => onChange(e.target.value), ref }));
|
|
455
|
-
FormInputElement.displayName = "FormInputElement";
|
|
456
|
-
var FormLabelElement = ({
|
|
457
|
-
name,
|
|
458
|
-
title,
|
|
459
|
-
description,
|
|
460
|
-
error,
|
|
461
|
-
children
|
|
462
|
-
}) => {
|
|
463
|
-
return /* @__PURE__ */ jsxs("label", { children: [
|
|
464
|
-
title ?? name,
|
|
465
|
-
children,
|
|
466
|
-
description,
|
|
467
|
-
error && /* @__PURE__ */ jsx("div", { style: { color: "red" }, children: error.message })
|
|
468
|
-
] });
|
|
469
|
-
};
|
|
470
|
-
FormLabelElement.displayName = "FormLabelElement";
|
|
471
|
-
|
|
472
|
-
// src/Form/elements/index.ts
|
|
473
|
-
var FormDefaultElements = {
|
|
474
|
-
Label: FormLabelElement,
|
|
475
|
-
Input: FormInputElement,
|
|
476
|
-
Button: FormButtonElement
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
// src/Form/rules.ts
|
|
480
|
-
var FormDefaultRules = {
|
|
481
|
-
required: async (value, _, lang) => {
|
|
482
|
-
if (value === null || value === void 0 || value === "" || Number.isNaN(value)) {
|
|
483
|
-
throw Error(lang?.required);
|
|
484
|
-
}
|
|
485
|
-
},
|
|
486
|
-
type: async (value, options, lang) => {
|
|
487
|
-
switch (options) {
|
|
488
|
-
case "string":
|
|
489
|
-
if (typeof value !== "string") throw Error(lang?.string);
|
|
490
|
-
break;
|
|
491
|
-
case "number":
|
|
492
|
-
if (Number.isNaN(Number(value))) throw Error(lang?.number);
|
|
493
|
-
break;
|
|
494
|
-
}
|
|
495
|
-
},
|
|
496
|
-
custom: async (value, options) => {
|
|
497
|
-
return options(value);
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
async function validValues(rules, items, values, lang) {
|
|
501
|
-
const errors = {};
|
|
502
|
-
for (const item of items) {
|
|
503
|
-
const value = values[item.name];
|
|
504
|
-
const rulesOptions = item.rules;
|
|
505
|
-
if (rulesOptions) {
|
|
506
|
-
for (const [name, options] of Object.entries(rulesOptions)) {
|
|
507
|
-
try {
|
|
508
|
-
await rules[name](value, options, lang);
|
|
509
|
-
} catch (error) {
|
|
510
|
-
errors[item.name] = error;
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return errors;
|
|
517
|
-
}
|
|
518
|
-
function FormFooter() {
|
|
519
|
-
const {
|
|
520
|
-
submitting,
|
|
521
|
-
setSubmitting,
|
|
522
|
-
onSubmit,
|
|
523
|
-
valuesRef,
|
|
524
|
-
Elements,
|
|
525
|
-
items,
|
|
526
|
-
setErrors,
|
|
527
|
-
lang,
|
|
528
|
-
rules
|
|
529
|
-
} = useFormContext();
|
|
530
|
-
const handleSubmit = useCallback(async () => {
|
|
531
|
-
setSubmitting(true);
|
|
532
|
-
setErrors({});
|
|
533
|
-
const errors = await validValues(rules, items, valuesRef.current, lang);
|
|
534
|
-
if (Object.keys(errors).length) {
|
|
535
|
-
setErrors(errors);
|
|
536
|
-
setSubmitting(false);
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
onSubmit(valuesRef.current).finally(() => setSubmitting(false));
|
|
540
|
-
}, [setSubmitting, setErrors, rules, items, lang, onSubmit]);
|
|
541
|
-
const MemoizedButton = useMemo(
|
|
542
|
-
() => /* @__PURE__ */ jsx(Elements.Button, { submitting, submit: handleSubmit, children: lang.submit }),
|
|
543
|
-
[submitting, handleSubmit, lang.submit, Elements.Button]
|
|
544
|
-
);
|
|
545
|
-
return MemoizedButton;
|
|
546
|
-
}
|
|
547
|
-
FormFooter.displayName = "FormFooter";
|
|
548
|
-
|
|
549
|
-
// src/Form/lang.ts
|
|
550
|
-
var FormDefaultLang = {
|
|
551
|
-
submit: "Submit",
|
|
552
|
-
required: "This field is required",
|
|
553
|
-
string: "This field must be a string",
|
|
554
|
-
number: "This field must be a number"
|
|
555
|
-
};
|
|
556
|
-
function mergeValues(items, defaultValues = {}) {
|
|
557
|
-
const values = {};
|
|
558
|
-
for (const item of items)
|
|
559
|
-
values[item.name] = defaultValues[item.name] ?? "";
|
|
560
|
-
return values;
|
|
561
|
-
}
|
|
562
|
-
function FormContainer({
|
|
563
|
-
defaultValues,
|
|
564
|
-
Elements,
|
|
565
|
-
rules,
|
|
566
|
-
lang,
|
|
567
|
-
items,
|
|
568
|
-
...props
|
|
569
|
-
}) {
|
|
570
|
-
const [values, setValues, valuesRef] = useStateRef(
|
|
571
|
-
mergeValues(items, defaultValues)
|
|
572
|
-
);
|
|
573
|
-
return /* @__PURE__ */ jsxs(
|
|
574
|
-
FormContextProvider,
|
|
575
|
-
{
|
|
576
|
-
initializeStates: {
|
|
577
|
-
errors: {},
|
|
578
|
-
submitting: false
|
|
579
|
-
},
|
|
580
|
-
value: {
|
|
581
|
-
Elements: Object.assign(FormDefaultElements, Elements),
|
|
582
|
-
lang: Object.assign(FormDefaultLang, lang),
|
|
583
|
-
rules: Object.assign(FormDefaultRules, rules),
|
|
584
|
-
items,
|
|
585
|
-
values,
|
|
586
|
-
setValues,
|
|
587
|
-
valuesRef,
|
|
588
|
-
...props
|
|
589
|
-
},
|
|
590
|
-
memo: true,
|
|
591
|
-
children: [
|
|
592
|
-
/* @__PURE__ */ jsx(FormBody, {}),
|
|
593
|
-
/* @__PURE__ */ jsx(FormFooter, {})
|
|
594
|
-
]
|
|
595
|
-
}
|
|
596
|
-
);
|
|
1224
|
+
const keys = Array.isArray(defaultValue) ? defaultValue : Object.keys(defaultValue);
|
|
1225
|
+
const defaultValues = Array.isArray(defaultValue) ? keys.reduce((prev, cur) => {
|
|
1226
|
+
prev[cur] = null;
|
|
1227
|
+
return prev;
|
|
1228
|
+
}, {}) : defaultValue;
|
|
1229
|
+
const contexts = {};
|
|
1230
|
+
for (const key of keys) contexts[key] = createContext(defaultValues[key]);
|
|
1231
|
+
function Provider(props) {
|
|
1232
|
+
const states = props.initializeStates ? useSplittingState(props.initializeStates) : {};
|
|
1233
|
+
let children = props.memo ? useEqualMemo(() => props.children, props.memo === true ? [] : props.memo) : props.children;
|
|
1234
|
+
for (const key of keys) {
|
|
1235
|
+
const Context = contexts[key];
|
|
1236
|
+
const value = props.value?.[key] ?? states[key] ?? defaultValues[key];
|
|
1237
|
+
children = /* @__PURE__ */ jsx(Context.Provider, {
|
|
1238
|
+
value,
|
|
1239
|
+
children
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
return children;
|
|
1243
|
+
}
|
|
1244
|
+
Provider.displayName = "SplittingContextProvider";
|
|
1245
|
+
function use() {
|
|
1246
|
+
return useConstant(() => {
|
|
1247
|
+
const obj = Object.create(null);
|
|
1248
|
+
for (const key of Object.keys(contexts)) Object.defineProperty(obj, key, { get: () => {
|
|
1249
|
+
if (!contexts[key]) throw new Error(`Context for key "${key}" is undefined`);
|
|
1250
|
+
return useContext(contexts[key]);
|
|
1251
|
+
} });
|
|
1252
|
+
return Object.freeze(obj);
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
return {
|
|
1256
|
+
Provider,
|
|
1257
|
+
use
|
|
1258
|
+
};
|
|
597
1259
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
}
|
|
608
|
-
|
|
1260
|
+
//#endregion
|
|
1261
|
+
//#region src/useFaasStream/index.tsx
|
|
1262
|
+
/**
|
|
1263
|
+
* Stream a FaasJS response into React state.
|
|
1264
|
+
*
|
|
1265
|
+
* `useFaasStream` is the default hook for streaming FaasJS responses in React.
|
|
1266
|
+
* It sends a streaming request, appends decoded text chunks to `data`, and
|
|
1267
|
+
* exposes reload helpers for retrying the same action.
|
|
1268
|
+
*
|
|
1269
|
+
* @param {string} action - Action path to invoke.
|
|
1270
|
+
* @param {Record<string, any>} defaultParams - Params used for the initial request and future reloads.
|
|
1271
|
+
* @param {UseFaasStreamOptions} [options] - Optional hook configuration such as controlled stream text, skip logic, debounce timing, polling, and base URL overrides.
|
|
1272
|
+
* See the `UseFaasStreamOptions` type for `params`, `data`, `setData`, `skip`, `debounce`, `polling`, and `baseUrl`.
|
|
1273
|
+
* @returns {UseFaasStreamResult} Streaming request state and helper methods described by {@link UseFaasStreamResult}.
|
|
1274
|
+
*
|
|
1275
|
+
* @example
|
|
1276
|
+
* ```tsx
|
|
1277
|
+
* import { useFaasStream } from '@faasjs/react'
|
|
1278
|
+
*
|
|
1279
|
+
* function Chat({ prompt }: { prompt: string }) {
|
|
1280
|
+
* const { data, error, loading, reload } = useFaasStream('/pages/chat/stream', { prompt })
|
|
1281
|
+
*
|
|
1282
|
+
* if (loading) return <div>Streaming...</div>
|
|
1283
|
+
*
|
|
1284
|
+
* if (error) {
|
|
1285
|
+
* return (
|
|
1286
|
+
* <div>
|
|
1287
|
+
* <div>Stream failed: {error.message}</div>
|
|
1288
|
+
* <button type="button" onClick={() => reload()}>
|
|
1289
|
+
* Retry
|
|
1290
|
+
* </button>
|
|
1291
|
+
* </div>
|
|
1292
|
+
* )
|
|
1293
|
+
* }
|
|
1294
|
+
*
|
|
1295
|
+
* return <pre>{data}</pre>
|
|
1296
|
+
* }
|
|
1297
|
+
* ```
|
|
1298
|
+
*/
|
|
609
1299
|
function useFaasStream(action, defaultParams, options = {}) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
}).catch(async (e) => {
|
|
672
|
-
if (typeof e?.message === "string" && e.message.toLowerCase().indexOf("aborted") >= 0)
|
|
673
|
-
return;
|
|
674
|
-
if (!fails && typeof e?.message === "string" && e.message.indexOf("Failed to fetch") >= 0) {
|
|
675
|
-
console.warn(`FaasReactClient: ${e.message} retry...`);
|
|
676
|
-
setFails(1);
|
|
677
|
-
return send();
|
|
678
|
-
}
|
|
679
|
-
let error2 = e;
|
|
680
|
-
if (client.onError)
|
|
681
|
-
try {
|
|
682
|
-
await client.onError(action, params)(e);
|
|
683
|
-
} catch (newError) {
|
|
684
|
-
error2 = newError;
|
|
685
|
-
}
|
|
686
|
-
setError(error2);
|
|
687
|
-
setLoading(false);
|
|
688
|
-
for (const { reject } of pendingReloadsRef.current.values())
|
|
689
|
-
reject(error2);
|
|
690
|
-
pendingReloadsRef.current.clear();
|
|
691
|
-
return;
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
if (options.debounce) {
|
|
695
|
-
const timeout = setTimeout(send, options.debounce);
|
|
696
|
-
return () => {
|
|
697
|
-
clearTimeout(timeout);
|
|
698
|
-
controllerRef.current?.abort();
|
|
699
|
-
setLoading(false);
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
send();
|
|
703
|
-
return () => {
|
|
704
|
-
controllerRef.current?.abort();
|
|
705
|
-
setLoading(false);
|
|
706
|
-
};
|
|
707
|
-
}, [action, options.params || params, reloadTimes, skip]);
|
|
708
|
-
const reload = useEqualCallback(
|
|
709
|
-
(params2) => {
|
|
710
|
-
if (skip) setSkip(false);
|
|
711
|
-
if (params2) setParams(params2);
|
|
712
|
-
const reloadCounter = ++reloadCounterRef.current;
|
|
713
|
-
return new Promise((resolve, reject) => {
|
|
714
|
-
pendingReloadsRef.current.set(reloadCounter, { resolve, reject });
|
|
715
|
-
setReloadTimes((prev) => prev + 1);
|
|
716
|
-
});
|
|
717
|
-
},
|
|
718
|
-
[params, skip]
|
|
719
|
-
);
|
|
720
|
-
return {
|
|
721
|
-
action,
|
|
722
|
-
params,
|
|
723
|
-
loading,
|
|
724
|
-
data: options.data || data,
|
|
725
|
-
reloadTimes,
|
|
726
|
-
error,
|
|
727
|
-
reload,
|
|
728
|
-
setData: options.setData || setData,
|
|
729
|
-
setLoading,
|
|
730
|
-
setError
|
|
731
|
-
};
|
|
1300
|
+
const [data, setData] = useState(options.data ?? "");
|
|
1301
|
+
const updateData = options.setData ?? setData;
|
|
1302
|
+
const request = useFaasRequest({
|
|
1303
|
+
action,
|
|
1304
|
+
defaultParams,
|
|
1305
|
+
options,
|
|
1306
|
+
beforeSend: ({ silent }) => {
|
|
1307
|
+
if (!silent) updateData("");
|
|
1308
|
+
},
|
|
1309
|
+
send: async ({ action, params, signal, client }) => {
|
|
1310
|
+
const response = await client.browserClient.action(action, params, {
|
|
1311
|
+
signal,
|
|
1312
|
+
stream: true
|
|
1313
|
+
});
|
|
1314
|
+
if (!response.body) throw new Error("Response body is null");
|
|
1315
|
+
const reader = response.body.getReader();
|
|
1316
|
+
const decoder = new TextDecoder();
|
|
1317
|
+
let accumulatedText = "";
|
|
1318
|
+
const onAbort = () => {
|
|
1319
|
+
reader.cancel().catch(() => void 0);
|
|
1320
|
+
};
|
|
1321
|
+
if (signal.aborted) {
|
|
1322
|
+
onAbort();
|
|
1323
|
+
throw new Error("Request aborted");
|
|
1324
|
+
}
|
|
1325
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1326
|
+
try {
|
|
1327
|
+
while (true) {
|
|
1328
|
+
if (signal.aborted) throw new Error("Request aborted");
|
|
1329
|
+
const { done, value } = await reader.read();
|
|
1330
|
+
if (done) break;
|
|
1331
|
+
accumulatedText += decoder.decode(value, { stream: true });
|
|
1332
|
+
updateData(accumulatedText);
|
|
1333
|
+
}
|
|
1334
|
+
accumulatedText += decoder.decode();
|
|
1335
|
+
return accumulatedText;
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
if (signal.aborted) throw new Error("Request aborted");
|
|
1338
|
+
throw error;
|
|
1339
|
+
} finally {
|
|
1340
|
+
signal.removeEventListener("abort", onAbort);
|
|
1341
|
+
try {
|
|
1342
|
+
reader.releaseLock();
|
|
1343
|
+
} catch {}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
return {
|
|
1348
|
+
action,
|
|
1349
|
+
params: request.params,
|
|
1350
|
+
loading: request.loading,
|
|
1351
|
+
refreshing: request.refreshing,
|
|
1352
|
+
reloadTimes: request.reloadTimes,
|
|
1353
|
+
data: options.data ?? data,
|
|
1354
|
+
error: request.error,
|
|
1355
|
+
reload: request.reload,
|
|
1356
|
+
setData: updateData,
|
|
1357
|
+
setLoading: request.setLoading,
|
|
1358
|
+
setError: request.setError
|
|
1359
|
+
};
|
|
732
1360
|
}
|
|
1361
|
+
//#endregion
|
|
1362
|
+
//#region src/usePrevious/index.ts
|
|
1363
|
+
/**
|
|
1364
|
+
* Hook to store the previous value of a state or prop.
|
|
1365
|
+
*
|
|
1366
|
+
* @template T - The type of the value.
|
|
1367
|
+
* @param {T} value - The current value to track.
|
|
1368
|
+
* @returns {T | undefined} Previous value from the prior render, or `undefined` on the first render.
|
|
1369
|
+
*
|
|
1370
|
+
* @example
|
|
1371
|
+
* ```tsx
|
|
1372
|
+
* import { usePrevious } from '@faasjs/react'
|
|
1373
|
+
*
|
|
1374
|
+
* function Counter({ count }: { count: number }) {
|
|
1375
|
+
* const previous = usePrevious(count)
|
|
1376
|
+
*
|
|
1377
|
+
* return <span>{previous} -> {count}</span>
|
|
1378
|
+
* }
|
|
1379
|
+
* ```
|
|
1380
|
+
*/
|
|
733
1381
|
function usePrevious(value) {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1382
|
+
const ref = useRef(void 0);
|
|
1383
|
+
useEffect(() => {
|
|
1384
|
+
ref.current = value;
|
|
1385
|
+
});
|
|
1386
|
+
return ref.current;
|
|
1387
|
+
}
|
|
1388
|
+
//#endregion
|
|
1389
|
+
//#region src/useStateRef/index.ts
|
|
1390
|
+
/**
|
|
1391
|
+
* Custom hook that returns a stateful value and a ref to that value.
|
|
1392
|
+
*
|
|
1393
|
+
* @template T - The type of the value.
|
|
1394
|
+
* @param {T} [initialValue] - Initial state value. When omitted, state starts as `null`.
|
|
1395
|
+
* @returns {[T | null, Dispatch<SetStateAction<T | null>>, RefObject<T | null>]} Tuple containing the current state, the state setter, and a ref that always points at the latest state.
|
|
1396
|
+
*
|
|
1397
|
+
* @example
|
|
1398
|
+
* ```tsx
|
|
1399
|
+
* import { useStateRef } from '@faasjs/react'
|
|
1400
|
+
*
|
|
1401
|
+
* function MyComponent() {
|
|
1402
|
+
* const [value, setValue, ref] = useStateRef(0)
|
|
1403
|
+
*
|
|
1404
|
+
* return (
|
|
1405
|
+
* <div>
|
|
1406
|
+
* <p>Value: {value}</p>
|
|
1407
|
+
* <button onClick={() => setValue(value + 1)}>Increment</button>
|
|
1408
|
+
* <button onClick={() => console.log(ref.current)}>Submit</button>
|
|
1409
|
+
* </div>
|
|
1410
|
+
* )
|
|
1411
|
+
* }
|
|
1412
|
+
* ```
|
|
1413
|
+
*/
|
|
1414
|
+
function useStateRef(initialValue) {
|
|
1415
|
+
const [state, setState] = useState(initialValue ?? null);
|
|
1416
|
+
const ref = useRef(state);
|
|
1417
|
+
useEffect(() => {
|
|
1418
|
+
ref.current = state;
|
|
1419
|
+
}, [state]);
|
|
1420
|
+
return [
|
|
1421
|
+
state,
|
|
1422
|
+
setState,
|
|
1423
|
+
ref
|
|
1424
|
+
];
|
|
739
1425
|
}
|
|
740
|
-
|
|
741
|
-
export { ErrorBoundary, FaasDataWrapper, FaasReactClient,
|
|
1426
|
+
//#endregion
|
|
1427
|
+
export { ErrorBoundary, FaasBrowserClient, FaasDataWrapper, FaasReactClient, OptionalWrapper, Response, ResponseError, createSplittingContext, equal, faas, generateId, getClient, setMock, useConstant, useEqualCallback, useEqualEffect, useEqualMemo, useEqualMemoize, useFaas, useFaasStream, usePrevious, useSplittingState, useStateRef, withFaasData };
|