@faasjs/react 8.0.0-beta.2 → 8.0.0-beta.21
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 +16 -68
- package/dist/index.d.ts +1557 -410
- package/dist/index.mjs +1765 -595
- package/package.json +18 -22
- package/dist/index.cjs +0 -652
package/dist/index.mjs
CHANGED
|
@@ -1,625 +1,1795 @@
|
|
|
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/generateId.ts
|
|
4
|
+
/**
|
|
5
|
+
* Generate a random identifier with an optional prefix.
|
|
6
|
+
*
|
|
7
|
+
* @param prefix - Prefix prepended to the generated identifier.
|
|
8
|
+
* @param length - Length of the generated identifier excluding `prefix`. Must be between `8` and `18`.
|
|
9
|
+
* @returns Generated identifier string.
|
|
10
|
+
* @throws {Error} When `length` is outside the supported `8` to `18` range.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const id = generateId('prefix-')
|
|
15
|
+
*
|
|
16
|
+
* id.startsWith('prefix-') // true
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
function generateId(prefix = "", length = 18) {
|
|
20
|
+
if (length < 8 || length > 18) throw new Error("Length must be 8 ~ 18");
|
|
21
|
+
return `${prefix}${Date.now().toString(36).padStart(8, "0")}${Math.random().toString(36).substring(2, length - 6).padEnd(length - 8, "0")}`;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/browser.ts
|
|
25
|
+
/**
|
|
26
|
+
* Wrapper class for HTTP responses from FaasJS functions.
|
|
27
|
+
*
|
|
28
|
+
* Provides a consistent interface for handling server responses with status code, headers,
|
|
29
|
+
* body, and parsed data. Automatically handles JSON serialization and status code defaults.
|
|
30
|
+
*
|
|
31
|
+
* @template T - The type of the data property for type-safe response handling
|
|
32
|
+
*
|
|
33
|
+
* @property {number} status - The HTTP status code of the response.
|
|
34
|
+
* Defaults to 200 if data or body is provided, 204 if neither is present.
|
|
35
|
+
* @property {ResponseHeaders} headers - The response headers as a key-value object.
|
|
36
|
+
* Empty object if no headers were provided.
|
|
37
|
+
* @property {any} body - The raw response body as a string or object.
|
|
38
|
+
* If data is provided without body, body is automatically set to JSON.stringify(data).
|
|
39
|
+
* @property {T} [data] - The parsed JSON data from the response.
|
|
40
|
+
* Optional property that contains the response payload when JSON is provided.
|
|
41
|
+
*
|
|
42
|
+
* Notes:
|
|
43
|
+
* - status defaults to 200 if data or body is present, 204 otherwise
|
|
44
|
+
* - body is automatically populated from data if not explicitly provided
|
|
45
|
+
* - headers defaults to an empty object if not provided
|
|
46
|
+
* - Use generic type parameter T for type-safe data access
|
|
47
|
+
* - Commonly used as the return type from client.action() method
|
|
48
|
+
* - Can be used in mock handlers to return structured responses
|
|
49
|
+
* - The data property is optional and may be undefined for responses without data
|
|
50
|
+
*
|
|
51
|
+
* @example Create successful response with data
|
|
52
|
+
* ```ts
|
|
53
|
+
* const response = new Response({
|
|
54
|
+
* status: 200,
|
|
55
|
+
* data: {
|
|
56
|
+
* id: 123,
|
|
57
|
+
* name: 'John Doe'
|
|
58
|
+
* }
|
|
59
|
+
* })
|
|
60
|
+
* console.log(response.status) // 200
|
|
61
|
+
* console.log(response.data.name) // 'John Doe'
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example Create response with type safety
|
|
65
|
+
* ```ts
|
|
66
|
+
* interface User {
|
|
67
|
+
* id: number
|
|
68
|
+
* name: string
|
|
69
|
+
* email: string
|
|
70
|
+
* }
|
|
71
|
+
*
|
|
72
|
+
* const response = new Response<User>({
|
|
73
|
+
* data: {
|
|
74
|
+
* id: 123,
|
|
75
|
+
* name: 'John',
|
|
76
|
+
* email: 'john@example.com'
|
|
77
|
+
* }
|
|
78
|
+
* })
|
|
79
|
+
* // TypeScript knows response.data.name is a string
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @example Create response with headers
|
|
83
|
+
* ```ts
|
|
84
|
+
* const response = new Response({
|
|
85
|
+
* status: 201,
|
|
86
|
+
* data: { created: true },
|
|
87
|
+
* headers: {
|
|
88
|
+
* 'Content-Type': 'application/json',
|
|
89
|
+
* 'X-Request-Id': 'req-123',
|
|
90
|
+
* 'X-Cache-Key': 'user-123'
|
|
91
|
+
* }
|
|
92
|
+
* })
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* @example Create response with custom body
|
|
96
|
+
* ```ts
|
|
97
|
+
* const response = new Response({
|
|
98
|
+
* status: 200,
|
|
99
|
+
* body: JSON.stringify({ custom: 'format' }),
|
|
100
|
+
* headers: { 'Content-Type': 'application/json' }
|
|
101
|
+
* })
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @example Create empty response (204 No Content)
|
|
105
|
+
* ```ts
|
|
106
|
+
* const response = new Response()
|
|
107
|
+
* // status: 204, headers: {}, body: undefined, data: undefined
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @example Create error response
|
|
111
|
+
* ```ts
|
|
112
|
+
* const response = new Response({
|
|
113
|
+
* status: 404,
|
|
114
|
+
* data: {
|
|
115
|
+
* error: {
|
|
116
|
+
* message: 'User not found',
|
|
117
|
+
* code: 'USER_NOT_FOUND'
|
|
118
|
+
* }
|
|
119
|
+
* }
|
|
120
|
+
* })
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* @example Use in mock handler
|
|
124
|
+
* ```ts
|
|
125
|
+
* setMock(async (action, params) => {
|
|
126
|
+
* if (action === 'user') {
|
|
127
|
+
* return new Response({
|
|
128
|
+
* status: 200,
|
|
129
|
+
* data: { id: params.id, name: 'Mock User' }
|
|
130
|
+
* })
|
|
131
|
+
* }
|
|
132
|
+
* return new Response({ status: 404, data: { error: 'Not found' } })
|
|
133
|
+
* })
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* @see ResponseProps for response property type
|
|
137
|
+
* @see ResponseError for error response handling
|
|
138
|
+
* @see FaasBrowserClient.action for method returning Response
|
|
139
|
+
*/
|
|
140
|
+
var Response = class {
|
|
141
|
+
/**
|
|
142
|
+
* HTTP status code exposed to callers.
|
|
143
|
+
*/
|
|
144
|
+
status;
|
|
145
|
+
/**
|
|
146
|
+
* Response headers keyed by header name.
|
|
147
|
+
*/
|
|
148
|
+
headers;
|
|
149
|
+
/**
|
|
150
|
+
* Raw response body.
|
|
151
|
+
*/
|
|
152
|
+
body;
|
|
153
|
+
/**
|
|
154
|
+
* Parsed response payload when JSON data is available.
|
|
155
|
+
*/
|
|
156
|
+
data;
|
|
157
|
+
/**
|
|
158
|
+
* Create a wrapped response object.
|
|
159
|
+
*
|
|
160
|
+
* @param props - Response properties including status, headers, body, and data.
|
|
161
|
+
* @param props.status - HTTP status code. Defaults to `200` when `data` or `body` exists, otherwise `204`.
|
|
162
|
+
* @param props.headers - Response headers keyed by header name.
|
|
163
|
+
* @param props.body - Raw response body to expose without additional parsing.
|
|
164
|
+
* @param props.data - Parsed response payload to expose on `response.data`.
|
|
165
|
+
* @returns Wrapped response instance.
|
|
166
|
+
*/
|
|
167
|
+
constructor(props = {}) {
|
|
168
|
+
this.status = props.status || (props.data || props.body ? 200 : 204);
|
|
169
|
+
this.headers = props.headers || {};
|
|
170
|
+
this.body = props.body;
|
|
171
|
+
if (props.data !== void 0) this.data = props.data;
|
|
172
|
+
if (props.data && !props.body) this.body = JSON.stringify(props.data);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Custom error class for handling HTTP response errors from FaasJS requests.
|
|
177
|
+
*
|
|
178
|
+
* Extends the built-in Error class to provide additional information about failed requests,
|
|
179
|
+
* including HTTP status code, response headers, response body, and the original error.
|
|
180
|
+
*
|
|
181
|
+
* @augments Error
|
|
182
|
+
*
|
|
183
|
+
* @property {number} status - The HTTP status code of the failed response. Defaults to 500 if not provided.
|
|
184
|
+
* @property {ResponseHeaders} headers - The response headers from the failed request.
|
|
185
|
+
* @property {any} body - The response body containing error details or the original error if available.
|
|
186
|
+
* @property {Error} [originalError] - The original Error object if this ResponseError was created from another Error.
|
|
187
|
+
*
|
|
188
|
+
* @example Basic error with message
|
|
189
|
+
* ```ts
|
|
190
|
+
* throw new ResponseError('User not found')
|
|
191
|
+
* // or inside action method:
|
|
192
|
+
* catch (error) {
|
|
193
|
+
* throw new ResponseError(error.message)
|
|
194
|
+
* }
|
|
195
|
+
* ```
|
|
196
|
+
*
|
|
197
|
+
* @example Error from existing Error
|
|
198
|
+
* ```ts
|
|
199
|
+
* try {
|
|
200
|
+
* await someOperation()
|
|
201
|
+
* } catch (error) {
|
|
202
|
+
* throw new ResponseError(error, {
|
|
203
|
+
* status: 500,
|
|
204
|
+
* headers: { 'X-Error-Type': 'internal' }
|
|
205
|
+
* })
|
|
206
|
+
* }
|
|
207
|
+
* ```
|
|
208
|
+
*
|
|
209
|
+
* @example Error with complete response details
|
|
210
|
+
* ```ts
|
|
211
|
+
* throw new ResponseError({
|
|
212
|
+
* message: 'Validation failed',
|
|
213
|
+
* status: 400,
|
|
214
|
+
* headers: { 'X-Error-Code': 'VALIDATION_ERROR' },
|
|
215
|
+
* body: {
|
|
216
|
+
* error: {
|
|
217
|
+
* message: 'Validation failed',
|
|
218
|
+
* fields: ['email', 'password']
|
|
219
|
+
* }
|
|
220
|
+
* }
|
|
221
|
+
* })
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* @example Handling ResponseError in client
|
|
225
|
+
* ```ts
|
|
226
|
+
* try {
|
|
227
|
+
* const response = await client.action('user', { id: 123 })
|
|
228
|
+
* console.log(response.data)
|
|
229
|
+
* } catch (error) {
|
|
230
|
+
* if (error instanceof ResponseError) {
|
|
231
|
+
* console.error(`Request failed: ${error.message}`)
|
|
232
|
+
* console.error(`Status: ${error.status}`)
|
|
233
|
+
* if (error.body) {
|
|
234
|
+
* console.error('Error details:', error.body)
|
|
235
|
+
* }
|
|
236
|
+
* if (error.headers['X-Request-Id']) {
|
|
237
|
+
* console.error('Request ID:', error.headers['X-Request-Id'])
|
|
238
|
+
* }
|
|
239
|
+
* }
|
|
240
|
+
* }
|
|
241
|
+
* ```
|
|
242
|
+
*
|
|
243
|
+
* @example Throwing ResponseError from mock
|
|
244
|
+
* ```ts
|
|
245
|
+
* setMock(async (action, params) => {
|
|
246
|
+
* if (action === 'login') {
|
|
247
|
+
* if (!params.email || !params.password) {
|
|
248
|
+
* throw new ResponseError({
|
|
249
|
+
* message: 'Email and password are required',
|
|
250
|
+
* status: 400,
|
|
251
|
+
* body: { error: 'missing_fields' }
|
|
252
|
+
* })
|
|
253
|
+
* }
|
|
254
|
+
* return { data: { token: 'abc123' } }
|
|
255
|
+
* }
|
|
256
|
+
* })
|
|
257
|
+
* ```
|
|
258
|
+
*
|
|
259
|
+
* Notes:
|
|
260
|
+
* - ResponseError is automatically thrown by the action method when the server returns an error (status >= 400)
|
|
261
|
+
* - The error message from server responses is extracted from body.error.message if available
|
|
262
|
+
* - When created from an Error object, the original error is preserved in the originalError property
|
|
263
|
+
* - The status property defaults to 500 if not explicitly provided
|
|
264
|
+
* - Use instanceof ResponseError to distinguish FaasJS errors from other JavaScript errors
|
|
265
|
+
* - The body property can contain structured error information from the server response
|
|
266
|
+
*
|
|
267
|
+
* @see FaasBrowserClient.action for how ResponseError is thrown in requests
|
|
268
|
+
* @see ResponseProps for the structure of response data
|
|
269
|
+
* @see setMock for mocking errors in tests
|
|
270
|
+
*/
|
|
271
|
+
var ResponseError = class extends Error {
|
|
272
|
+
/**
|
|
273
|
+
* HTTP status code reported for the failed request.
|
|
274
|
+
*/
|
|
275
|
+
status;
|
|
276
|
+
/**
|
|
277
|
+
* Response headers returned with the error.
|
|
278
|
+
*/
|
|
279
|
+
headers;
|
|
280
|
+
/**
|
|
281
|
+
* Raw error body or fallback error payload.
|
|
282
|
+
*/
|
|
283
|
+
body;
|
|
284
|
+
/**
|
|
285
|
+
* Original error used to construct this instance, when available.
|
|
286
|
+
*/
|
|
287
|
+
originalError;
|
|
288
|
+
constructor(data, options) {
|
|
289
|
+
let props;
|
|
290
|
+
if (typeof data === "string") props = {
|
|
291
|
+
message: data,
|
|
292
|
+
...options
|
|
293
|
+
};
|
|
294
|
+
else if (data instanceof Error || typeof data === "object" && data !== null && typeof data.constructor?.name === "string" && data.constructor.name.includes("Error")) props = {
|
|
295
|
+
message: data.message,
|
|
296
|
+
originalError: data,
|
|
297
|
+
...options
|
|
298
|
+
};
|
|
299
|
+
else props = data;
|
|
300
|
+
super(props.message);
|
|
301
|
+
this.status = props.status || 500;
|
|
302
|
+
this.headers = props.headers || {};
|
|
303
|
+
this.body = props.body || props.originalError || { error: { message: props.message } };
|
|
304
|
+
if (props.originalError) this.originalError = props.originalError;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
let mock = null;
|
|
308
|
+
/**
|
|
309
|
+
* Set the global mock handler used by all {@link FaasBrowserClient} instances.
|
|
310
|
+
*
|
|
311
|
+
* @param handler - Mock handler, can be:
|
|
312
|
+
* - MockHandler function: receives (action, params, options) and returns response data
|
|
313
|
+
* - ResponseProps object: static response data
|
|
314
|
+
* - Response instance: pre-configured Response object
|
|
315
|
+
* - null or undefined: clear mock
|
|
316
|
+
*
|
|
317
|
+
* @example Reset in Vitest shared setup
|
|
318
|
+
* ```ts
|
|
319
|
+
* import { afterEach } from 'vitest'
|
|
320
|
+
*
|
|
321
|
+
* afterEach(() => {
|
|
322
|
+
* setMock(null)
|
|
323
|
+
* })
|
|
324
|
+
* ```
|
|
325
|
+
*
|
|
326
|
+
* @example Use ResponseProps object
|
|
327
|
+
* ```ts
|
|
328
|
+
* setMock({
|
|
329
|
+
* data: { name: 'FaasJS' },
|
|
330
|
+
* })
|
|
331
|
+
*
|
|
332
|
+
* setMock({
|
|
333
|
+
* status: 500,
|
|
334
|
+
* data: { message: 'Internal Server Error' },
|
|
335
|
+
* })
|
|
336
|
+
* ```
|
|
337
|
+
*
|
|
338
|
+
* @example Use MockHandler function
|
|
339
|
+
* ```ts
|
|
340
|
+
* setMock(async (action) => {
|
|
341
|
+
* if (action === '/pages/users/get') {
|
|
342
|
+
* return { data: { id: 1, name: 'FaasJS' } }
|
|
343
|
+
* }
|
|
344
|
+
*
|
|
345
|
+
* return { status: 404, data: { message: 'Not Found' } }
|
|
346
|
+
* })
|
|
347
|
+
*
|
|
348
|
+
* const response = await client.action('/pages/users/get')
|
|
349
|
+
* ```
|
|
350
|
+
*
|
|
351
|
+
* @example Branch by action and params
|
|
352
|
+
* ```ts
|
|
353
|
+
* setMock(async (action, params) => {
|
|
354
|
+
* if (action === '/pages/users/get' && params?.id === 1) {
|
|
355
|
+
* return { data: { id: 1, name: 'Admin' } }
|
|
356
|
+
* }
|
|
357
|
+
*
|
|
358
|
+
* if (action === '/pages/users/get' && params?.id === 2) {
|
|
359
|
+
* return { data: { id: 2, name: 'Editor' } }
|
|
360
|
+
* }
|
|
361
|
+
*
|
|
362
|
+
* return { status: 404, data: { message: 'User not found' } }
|
|
363
|
+
* })
|
|
364
|
+
* ```
|
|
365
|
+
*
|
|
366
|
+
* @example Use Response instance
|
|
367
|
+
* ```ts
|
|
368
|
+
* setMock(new Response({
|
|
369
|
+
* status: 200,
|
|
370
|
+
* data: { result: 'success' }
|
|
371
|
+
* }))
|
|
372
|
+
* ```
|
|
373
|
+
*
|
|
374
|
+
* @example Streaming response
|
|
375
|
+
* ```ts
|
|
376
|
+
* setMock({
|
|
377
|
+
* body: new ReadableStream({
|
|
378
|
+
* start(controller) {
|
|
379
|
+
* controller.enqueue(new TextEncoder().encode('hello'))
|
|
380
|
+
* controller.enqueue(new TextEncoder().encode(' world'))
|
|
381
|
+
* controller.close()
|
|
382
|
+
* },
|
|
383
|
+
* }),
|
|
384
|
+
* })
|
|
385
|
+
* ```
|
|
386
|
+
*
|
|
387
|
+
* @example Clear mock
|
|
388
|
+
* ```ts
|
|
389
|
+
* setMock(null)
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* @example Handle errors
|
|
393
|
+
* ```ts
|
|
394
|
+
* setMock(async () => {
|
|
395
|
+
* throw new Error('Internal error')
|
|
396
|
+
* })
|
|
397
|
+
* // This will reject with ResponseError
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
function setMock(handler) {
|
|
401
|
+
mock = handler;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Browser client for FaasJS - provides HTTP client functionality for making API requests from web applications.
|
|
405
|
+
*
|
|
406
|
+
* @template PathOrData - Type parameter extending FaasActionUnionType for type-safe requests
|
|
407
|
+
*
|
|
408
|
+
* Features:
|
|
409
|
+
* - Type-safe API requests with TypeScript support
|
|
410
|
+
* - Built-in mock support for testing
|
|
411
|
+
* - Custom request function support
|
|
412
|
+
* - Request/response hooks (beforeRequest)
|
|
413
|
+
* - Automatic error handling with ResponseError
|
|
414
|
+
* - Streaming support for large responses
|
|
415
|
+
* - Multiple instance support with unique IDs
|
|
416
|
+
*
|
|
417
|
+
* Notes:
|
|
418
|
+
* - All requests are POST requests by default
|
|
419
|
+
* - Automatically adds X-FaasJS-Request-Id header for request tracking
|
|
420
|
+
* - baseUrl must end with '/' (will throw Error if not)
|
|
421
|
+
* - Supports global mock via setMock() for testing all instances
|
|
422
|
+
*
|
|
423
|
+
* @example Basic usage
|
|
424
|
+
* ```ts
|
|
425
|
+
* import { FaasBrowserClient } from '@faasjs/react'
|
|
426
|
+
*
|
|
427
|
+
* const client = new FaasBrowserClient('http://localhost:8080/')
|
|
428
|
+
* const response = await client.action('func', { key: 'value' })
|
|
429
|
+
* console.log(response.data)
|
|
430
|
+
* ```
|
|
431
|
+
*
|
|
432
|
+
* @example With custom headers and options
|
|
433
|
+
* ```ts
|
|
434
|
+
* const client = new FaasBrowserClient('https://api.example.com/', {
|
|
435
|
+
* headers: { 'X-API-Key': 'secret' },
|
|
436
|
+
* beforeRequest: async ({ action, params, headers }) => {
|
|
437
|
+
* console.log(`Calling ${action} with params:`, params)
|
|
438
|
+
* }
|
|
439
|
+
* })
|
|
440
|
+
* ```
|
|
441
|
+
*
|
|
442
|
+
* @example Multiple instances
|
|
443
|
+
* ```ts
|
|
444
|
+
* const apiClient = new FaasBrowserClient('https://api.example.com/')
|
|
445
|
+
* const localClient = new FaasBrowserClient('http://localhost:3000/')
|
|
446
|
+
*
|
|
447
|
+
* const apiData = await apiClient.action('users')
|
|
448
|
+
* const localData = await localClient.action('data')
|
|
449
|
+
* ```
|
|
450
|
+
*
|
|
451
|
+
* @example Error handling
|
|
452
|
+
* ```ts
|
|
453
|
+
* const client = new FaasBrowserClient('https://api.example.com/')
|
|
454
|
+
*
|
|
455
|
+
* try {
|
|
456
|
+
* const response = await client.action('user', { id: 123 })
|
|
457
|
+
* console.log(response.data)
|
|
458
|
+
* } catch (error) {
|
|
459
|
+
* if (error instanceof ResponseError) {
|
|
460
|
+
* console.error(`Request failed: ${error.message}`, error.status)
|
|
461
|
+
* } else {
|
|
462
|
+
* console.error('Unexpected error:', error)
|
|
463
|
+
* }
|
|
464
|
+
* }
|
|
465
|
+
* ```
|
|
466
|
+
*
|
|
467
|
+
* @throws {Error} When baseUrl does not end with '/'
|
|
468
|
+
*
|
|
469
|
+
* @see setMock for testing support
|
|
470
|
+
* @see ResponseError for error handling
|
|
471
|
+
*/
|
|
472
|
+
var FaasBrowserClient = class {
|
|
473
|
+
/**
|
|
474
|
+
* Unique identifier for this client instance.
|
|
475
|
+
*/
|
|
476
|
+
id;
|
|
477
|
+
/**
|
|
478
|
+
* Base URL used to build action request URLs.
|
|
479
|
+
*/
|
|
480
|
+
baseUrl;
|
|
481
|
+
/**
|
|
482
|
+
* Default request options merged into every request.
|
|
483
|
+
*/
|
|
484
|
+
defaultOptions;
|
|
485
|
+
/**
|
|
486
|
+
* Creates a new FaasBrowserClient instance.
|
|
487
|
+
*
|
|
488
|
+
* @param baseUrl - Base URL for all API requests. Must end with `/`. Defaults to `/` for relative requests.
|
|
489
|
+
* @param options - Default request options such as headers, hooks, request override, or stream mode.
|
|
490
|
+
* See {@link Options} for supported request fields such as `headers`, `beforeRequest`,
|
|
491
|
+
* `request`, `baseUrl`, and `stream`.
|
|
492
|
+
*
|
|
493
|
+
* @example Basic initialization
|
|
494
|
+
* ```ts
|
|
495
|
+
* const client = new FaasBrowserClient('/')
|
|
496
|
+
* ```
|
|
497
|
+
*
|
|
498
|
+
* @example With API endpoint
|
|
499
|
+
* ```ts
|
|
500
|
+
* const client = new FaasBrowserClient('https://api.example.com/')
|
|
501
|
+
* ```
|
|
502
|
+
*
|
|
503
|
+
* @example With custom headers
|
|
504
|
+
* ```ts
|
|
505
|
+
* const client = new FaasBrowserClient('https://api.example.com/', {
|
|
506
|
+
* headers: {
|
|
507
|
+
* 'Authorization': 'Bearer token123',
|
|
508
|
+
* 'X-Custom-Header': 'value'
|
|
509
|
+
* }
|
|
510
|
+
* })
|
|
511
|
+
* ```
|
|
512
|
+
*
|
|
513
|
+
* @example With beforeRequest hook
|
|
514
|
+
* ```ts
|
|
515
|
+
* const client = new FaasBrowserClient('https://api.example.com/', {
|
|
516
|
+
* beforeRequest: async ({ action, params, headers }) => {
|
|
517
|
+
* console.log(`Requesting ${action}`, params)
|
|
518
|
+
* // Modify headers before request
|
|
519
|
+
* headers['X-Timestamp'] = Date.now().toString()
|
|
520
|
+
* }
|
|
521
|
+
* })
|
|
522
|
+
* ```
|
|
523
|
+
*
|
|
524
|
+
* @example With custom request function
|
|
525
|
+
* ```ts
|
|
526
|
+
* import axios from 'axios'
|
|
527
|
+
*
|
|
528
|
+
* const client = new FaasBrowserClient('/', {
|
|
529
|
+
* request: async (url, options) => {
|
|
530
|
+
* const response = await axios.post(url, options.body, {
|
|
531
|
+
* headers: options.headers
|
|
532
|
+
* })
|
|
533
|
+
* return new Response({
|
|
534
|
+
* status: response.status,
|
|
535
|
+
* headers: response.headers,
|
|
536
|
+
* data: response.data
|
|
537
|
+
* })
|
|
538
|
+
* }
|
|
539
|
+
* })
|
|
540
|
+
* ```
|
|
541
|
+
*
|
|
542
|
+
* @throws {Error} When `baseUrl` does not end with `/`
|
|
543
|
+
*/
|
|
544
|
+
constructor(baseUrl = "/", options = Object.create(null)) {
|
|
545
|
+
if (baseUrl && !baseUrl.endsWith("/")) throw Error("[FaasJS] baseUrl should end with /");
|
|
546
|
+
this.id = `FBC-${generateId()}`;
|
|
547
|
+
this.baseUrl = baseUrl;
|
|
548
|
+
this.defaultOptions = options;
|
|
549
|
+
console.debug(`[FaasJS] Initialize with baseUrl: ${this.baseUrl}`);
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Makes a request to a FaasJS function.
|
|
553
|
+
*
|
|
554
|
+
* @template PathOrData - The function path or data type for type safety
|
|
555
|
+
* @param action - The function path to call. Converted to lowercase when constructing the URL.
|
|
556
|
+
* Must be a non-empty string.
|
|
557
|
+
* @param params - The parameters to send to the function. Will be serialized as JSON.
|
|
558
|
+
* Optional if the function accepts no parameters.
|
|
559
|
+
* @param options - Optional request options that override client defaults.
|
|
560
|
+
* Supports headers, beforeRequest hook, custom request function, baseUrl override, and streaming mode.
|
|
561
|
+
* See {@link Options} for supported request fields such as `headers`, `beforeRequest`,
|
|
562
|
+
* `request`, `baseUrl`, and `stream`.
|
|
563
|
+
*
|
|
564
|
+
* @returns A promise resolving to the wrapped FaasJS response. When `options.stream`
|
|
565
|
+
* is `true`, the runtime returns the native fetch response so callers can read the stream.
|
|
566
|
+
*
|
|
567
|
+
* @throws {Error} When action is not provided or is empty
|
|
568
|
+
* @throws {ResponseError} When the server returns an error response (status >= 400 or body.error exists)
|
|
569
|
+
* @throws {Error} When the request fails before a response is received
|
|
570
|
+
*
|
|
571
|
+
* Notes:
|
|
572
|
+
* - All requests are POST requests by default
|
|
573
|
+
* - Action path is automatically converted to lowercase
|
|
574
|
+
* - A unique request ID is generated for each request and sent in X-FaasJS-Request-Id header
|
|
575
|
+
* - Headers are merged from client defaults and request options (request options take precedence)
|
|
576
|
+
* - If a global mock is set via setMock(), it will be used instead of making real requests
|
|
577
|
+
* - If a custom request function is provided in options, it will be used instead of fetch
|
|
578
|
+
* - When stream option is true, returns the native fetch Response instead of a wrapped Response
|
|
579
|
+
* - Response body is automatically parsed as JSON when possible
|
|
580
|
+
* - Server errors (body.error) are automatically converted to ResponseError
|
|
581
|
+
*
|
|
582
|
+
* @example Basic request
|
|
583
|
+
* ```ts
|
|
584
|
+
* const response = await client.action('user', { id: 123 })
|
|
585
|
+
* console.log(response.data)
|
|
586
|
+
* ```
|
|
587
|
+
*
|
|
588
|
+
* @example With no parameters
|
|
589
|
+
* ```ts
|
|
590
|
+
* const response = await client.action('status')
|
|
591
|
+
* console.log(response.data.status)
|
|
592
|
+
* ```
|
|
593
|
+
*
|
|
594
|
+
* @example With custom options
|
|
595
|
+
* ```ts
|
|
596
|
+
* const response = await client.action('data', {
|
|
597
|
+
* limit: 10,
|
|
598
|
+
* offset: 0
|
|
599
|
+
* }, {
|
|
600
|
+
* headers: { 'X-Custom-Header': 'value' }
|
|
601
|
+
* })
|
|
602
|
+
* ```
|
|
603
|
+
*
|
|
604
|
+
* @example Streaming large response
|
|
605
|
+
* ```ts
|
|
606
|
+
* const response = await client.action('stream', {
|
|
607
|
+
* format: 'json'
|
|
608
|
+
* }, {
|
|
609
|
+
* stream: true
|
|
610
|
+
* })
|
|
611
|
+
* // response is native fetch Response with streaming support
|
|
612
|
+
* const reader = response.body.getReader()
|
|
613
|
+
* ```
|
|
614
|
+
*
|
|
615
|
+
* @example With type safety
|
|
616
|
+
* ```ts
|
|
617
|
+
* interface UserData {
|
|
618
|
+
* id: number
|
|
619
|
+
* name: string
|
|
620
|
+
* email: string
|
|
621
|
+
* }
|
|
622
|
+
*
|
|
623
|
+
* const response = await client.action<UserData>('user', { id: 123 })
|
|
624
|
+
* console.log(response.data.name) // TypeScript knows it's a string
|
|
625
|
+
* ```
|
|
626
|
+
*
|
|
627
|
+
* @example Handling errors
|
|
628
|
+
* ```ts
|
|
629
|
+
* try {
|
|
630
|
+
* const response = await client.action('user', { id: 123 })
|
|
631
|
+
* console.log(response.data)
|
|
632
|
+
* } catch (error) {
|
|
633
|
+
* if (error instanceof ResponseError) {
|
|
634
|
+
* console.error(`Server error: ${error.message}`, error.status)
|
|
635
|
+
* if (error.body) console.error('Error details:', error.body)
|
|
636
|
+
* } else {
|
|
637
|
+
* console.error('Network error:', error)
|
|
638
|
+
* }
|
|
639
|
+
* }
|
|
640
|
+
* ```
|
|
641
|
+
*
|
|
642
|
+
* @example Chaining requests
|
|
643
|
+
* ```ts
|
|
644
|
+
* const userId = await client.action('createUser', {
|
|
645
|
+
* name: 'John',
|
|
646
|
+
* email: 'john@example.com'
|
|
647
|
+
* })
|
|
648
|
+
*
|
|
649
|
+
* const profile = await client.action('getProfile', {
|
|
650
|
+
* userId: userId.data.id
|
|
651
|
+
* })
|
|
652
|
+
* ```
|
|
653
|
+
*/
|
|
654
|
+
async action(action, params, options) {
|
|
655
|
+
if (!action) throw Error("[FaasJS] action required");
|
|
656
|
+
const id = `F-${generateId()}`;
|
|
657
|
+
const url = `${(options?.baseUrl || this.baseUrl) + action.toLowerCase()}?_=${id}`;
|
|
658
|
+
if (!params) params = Object.create(null);
|
|
659
|
+
if (!options) options = Object.create(null);
|
|
660
|
+
const parsedOptions = {
|
|
661
|
+
method: "POST",
|
|
662
|
+
headers: { "Content-Type": "application/json; charset=UTF-8" },
|
|
663
|
+
mode: "cors",
|
|
664
|
+
credentials: "include",
|
|
665
|
+
body: JSON.stringify(params),
|
|
666
|
+
...this.defaultOptions,
|
|
667
|
+
...options
|
|
668
|
+
};
|
|
669
|
+
if (!parsedOptions.headers["X-FaasJS-Request-Id"] && !parsedOptions.headers["x-faasjs-request-id"]) parsedOptions.headers["X-FaasJS-Request-Id"] = id;
|
|
670
|
+
if (parsedOptions.beforeRequest) await parsedOptions.beforeRequest({
|
|
671
|
+
action,
|
|
672
|
+
params,
|
|
673
|
+
options: parsedOptions,
|
|
674
|
+
headers: parsedOptions.headers
|
|
675
|
+
});
|
|
676
|
+
if (mock) {
|
|
677
|
+
console.debug(`[FaasJS] Mock request: ${action} %j`, params);
|
|
678
|
+
if (typeof mock === "function") {
|
|
679
|
+
const response = await mock(action, params, parsedOptions);
|
|
680
|
+
if (response instanceof Error) return Promise.reject(new ResponseError(response));
|
|
681
|
+
if (response instanceof Response) return response;
|
|
682
|
+
return new Response(response || {});
|
|
683
|
+
}
|
|
684
|
+
if (mock instanceof Response) return mock;
|
|
685
|
+
return new Response(mock || {});
|
|
686
|
+
}
|
|
687
|
+
if (parsedOptions.request) return parsedOptions.request(url, parsedOptions);
|
|
688
|
+
if (parsedOptions.stream) return fetch(url, parsedOptions);
|
|
689
|
+
return fetch(url, parsedOptions).then(async (response) => {
|
|
690
|
+
const headers = {};
|
|
691
|
+
for (const values of response.headers) headers[values[0]] = values[1];
|
|
692
|
+
return response.text().then((res) => {
|
|
693
|
+
if (response.status >= 200 && response.status < 300) {
|
|
694
|
+
if (!res) return new Response({
|
|
695
|
+
status: response.status,
|
|
696
|
+
headers
|
|
697
|
+
});
|
|
698
|
+
const body = JSON.parse(res);
|
|
699
|
+
if (body.error?.message) return Promise.reject(new ResponseError({
|
|
700
|
+
message: body.error.message,
|
|
701
|
+
status: response.status,
|
|
702
|
+
headers,
|
|
703
|
+
body
|
|
704
|
+
}));
|
|
705
|
+
return new Response({
|
|
706
|
+
status: response.status,
|
|
707
|
+
headers,
|
|
708
|
+
body,
|
|
709
|
+
data: body.data
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const body = JSON.parse(res);
|
|
714
|
+
if (body.error?.message) return Promise.reject(new ResponseError({
|
|
715
|
+
message: body.error.message,
|
|
716
|
+
status: response.status,
|
|
717
|
+
headers,
|
|
718
|
+
body
|
|
719
|
+
}));
|
|
720
|
+
return Promise.reject(new ResponseError({
|
|
721
|
+
message: res,
|
|
722
|
+
status: response.status,
|
|
723
|
+
headers,
|
|
724
|
+
body
|
|
725
|
+
}));
|
|
726
|
+
} catch {
|
|
727
|
+
return Promise.reject(new ResponseError({
|
|
728
|
+
message: res,
|
|
729
|
+
status: response.status,
|
|
730
|
+
headers,
|
|
731
|
+
body: res
|
|
732
|
+
}));
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
//#endregion
|
|
739
|
+
//#region src/faas.ts
|
|
740
|
+
/**
|
|
741
|
+
* Call the currently configured FaasReactClient.
|
|
742
|
+
*
|
|
743
|
+
* This helper forwards the request to `getClient`. When the registered
|
|
744
|
+
* client defines `onError`, the hook is invoked before the promise rejects.
|
|
745
|
+
*
|
|
746
|
+
* @template PathOrData - Action path or response data type used for inference.
|
|
747
|
+
*
|
|
748
|
+
* @param action - Action path to invoke.
|
|
749
|
+
* @param params - Parameters sent to the action.
|
|
750
|
+
* @param options - Optional per-request overrides such as headers or base URL.
|
|
751
|
+
* See the request `Options` type for supported fields such as `headers`, `beforeRequest`,
|
|
752
|
+
* `request`, `baseUrl`, and `stream`.
|
|
753
|
+
* @returns Response returned by the active browser client.
|
|
754
|
+
* @throws {ResponseError} When the request fails and the active client does not recover inside `onError`.
|
|
755
|
+
*
|
|
756
|
+
* @example
|
|
757
|
+
* ```ts
|
|
758
|
+
* import { faas } from '@faasjs/react'
|
|
759
|
+
*
|
|
760
|
+
* const response = await faas('posts/get', { id: 1 })
|
|
761
|
+
*
|
|
762
|
+
* console.log(response.data.title)
|
|
763
|
+
* ```
|
|
764
|
+
*/
|
|
765
|
+
async function faas(action, params, options) {
|
|
766
|
+
const client = getClient(options?.baseUrl);
|
|
767
|
+
const onError = client.onError;
|
|
768
|
+
if (onError) return client.browserClient.action(action, params, options).catch(async (res) => {
|
|
769
|
+
await onError(action, params)(res);
|
|
770
|
+
return Promise.reject(res);
|
|
771
|
+
});
|
|
772
|
+
return client.browserClient.action(action, params, options);
|
|
773
|
+
}
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/equal.ts
|
|
776
|
+
const AsyncFunction = (async () => {}).constructor;
|
|
777
|
+
/**
|
|
778
|
+
* Compares two values for deep equality.
|
|
779
|
+
*
|
|
780
|
+
* This function checks if two values are deeply equal by comparing their types and contents.
|
|
781
|
+
* It handles various data types including primitives, arrays, dates, regular expressions, functions,
|
|
782
|
+
* maps, sets, and promises.
|
|
783
|
+
*
|
|
784
|
+
* @param a - The first value to compare.
|
|
785
|
+
* @param b - The second value to compare.
|
|
786
|
+
* @returns `true` if the values are deeply equal, `false` otherwise.
|
|
787
|
+
*
|
|
788
|
+
* @example
|
|
789
|
+
* ```ts
|
|
790
|
+
* import { equal } from '@faasjs/react'
|
|
791
|
+
*
|
|
792
|
+
* equal({ page: 1, filters: ['a'] }, { page: 1, filters: ['a'] }) // true
|
|
793
|
+
* equal({ page: 1 }, { page: 2 }) // false
|
|
794
|
+
* ```
|
|
795
|
+
*/
|
|
8
796
|
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
|
-
}
|
|
797
|
+
if (a === b) return true;
|
|
798
|
+
if ((a === null || a === void 0) && (b === null || b === void 0)) return true;
|
|
799
|
+
if (typeof a !== typeof b) return false;
|
|
800
|
+
if (a === null || a === void 0 || b === null || b === void 0) return false;
|
|
801
|
+
const ctor = a.constructor;
|
|
802
|
+
if (ctor !== b.constructor) return false;
|
|
803
|
+
switch (ctor) {
|
|
804
|
+
case String:
|
|
805
|
+
case Boolean: return a === b;
|
|
806
|
+
case Number: return Number.isNaN(a) && Number.isNaN(b) || a === b;
|
|
807
|
+
case Array:
|
|
808
|
+
if (a.length !== b.length) return false;
|
|
809
|
+
for (let i = 0; i < a.length; i++) if (!equal(a[i], b[i])) return false;
|
|
810
|
+
return true;
|
|
811
|
+
case Date: return a.getTime() === b.getTime();
|
|
812
|
+
case RegExp:
|
|
813
|
+
case Function:
|
|
814
|
+
case AsyncFunction: return a.toString() === b.toString();
|
|
815
|
+
case Map:
|
|
816
|
+
case Set: return equal(Array.from(a), Array.from(b));
|
|
817
|
+
case Promise: return a === b;
|
|
818
|
+
case Object:
|
|
819
|
+
for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) if (!equal(a[key], b[key])) return false;
|
|
820
|
+
return true;
|
|
821
|
+
default: throw Error(`Unsupported type: ${ctor}`);
|
|
822
|
+
}
|
|
49
823
|
}
|
|
824
|
+
/**
|
|
825
|
+
* Custom hook that memoizes a value using deep equality comparison.
|
|
826
|
+
*
|
|
827
|
+
* @param value - The value to be memoized.
|
|
828
|
+
* @returns The memoized value.
|
|
829
|
+
*
|
|
830
|
+
* @example
|
|
831
|
+
* ```tsx
|
|
832
|
+
* import { useEqualMemoize } from '@faasjs/react'
|
|
833
|
+
*
|
|
834
|
+
* function Filters({ filters }: { filters: Record<string, any> }) {
|
|
835
|
+
* const memoizedFilters = useEqualMemoize(filters)
|
|
836
|
+
*
|
|
837
|
+
* return <pre>{JSON.stringify(memoizedFilters)}</pre>
|
|
838
|
+
* }
|
|
839
|
+
* ```
|
|
840
|
+
*/
|
|
50
841
|
function useEqualMemoize(value) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
ref.current = value;
|
|
55
|
-
signalRef.current += 1;
|
|
56
|
-
}
|
|
57
|
-
return useMemo(() => ref.current, [signalRef.current]);
|
|
842
|
+
const ref = useRef(value);
|
|
843
|
+
if (!equal(value, ref.current)) ref.current = value;
|
|
844
|
+
return ref.current;
|
|
58
845
|
}
|
|
846
|
+
function useEqualSignal(value) {
|
|
847
|
+
const ref = useRef(value);
|
|
848
|
+
const signalRef = useRef(0);
|
|
849
|
+
if (!equal(value, ref.current)) {
|
|
850
|
+
ref.current = value;
|
|
851
|
+
signalRef.current += 1;
|
|
852
|
+
}
|
|
853
|
+
return signalRef.current;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Custom hook that works like `useEffect` but uses deep comparison on dependencies.
|
|
857
|
+
*
|
|
858
|
+
* @param callback - The effect callback function to run.
|
|
859
|
+
* @param dependencies - The list of dependencies for the effect.
|
|
860
|
+
* @returns The result of the `useEffect` hook with memoized dependencies.
|
|
861
|
+
*
|
|
862
|
+
* @example
|
|
863
|
+
* ```tsx
|
|
864
|
+
* import { useEqualEffect } from '@faasjs/react'
|
|
865
|
+
*
|
|
866
|
+
* function Page({ filters }: { filters: Record<string, any> }) {
|
|
867
|
+
* useEqualEffect(() => {
|
|
868
|
+
* console.log('filters changed', filters)
|
|
869
|
+
* }, [filters])
|
|
870
|
+
*
|
|
871
|
+
* return null
|
|
872
|
+
* }
|
|
873
|
+
* ```
|
|
874
|
+
*/
|
|
59
875
|
function useEqualEffect(callback, dependencies) {
|
|
60
|
-
|
|
876
|
+
return useEffect(callback, [useEqualSignal(dependencies)]);
|
|
61
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Custom hook that works like `useMemo` but uses deep comparison on dependencies.
|
|
880
|
+
*
|
|
881
|
+
* @template T - Memoized value type returned by the callback.
|
|
882
|
+
*
|
|
883
|
+
* @param callback - The callback function to run.
|
|
884
|
+
* @param dependencies - The list of dependencies.
|
|
885
|
+
* @returns The result of the `useMemo` hook with memoized dependencies.
|
|
886
|
+
*
|
|
887
|
+
* @example
|
|
888
|
+
* ```tsx
|
|
889
|
+
* import { useEqualMemo } from '@faasjs/react'
|
|
890
|
+
*
|
|
891
|
+
* function Page({ filters }: { filters: Record<string, any> }) {
|
|
892
|
+
* const queryString = useEqualMemo(() => JSON.stringify(filters), [filters])
|
|
893
|
+
*
|
|
894
|
+
* return <span>{queryString}</span>
|
|
895
|
+
* }
|
|
896
|
+
* ```
|
|
897
|
+
*/
|
|
62
898
|
function useEqualMemo(callback, dependencies) {
|
|
63
|
-
|
|
899
|
+
const signal = useEqualSignal(dependencies);
|
|
900
|
+
const callbackRef = useRef(callback);
|
|
901
|
+
callbackRef.current = callback;
|
|
902
|
+
return useMemo(() => {
|
|
903
|
+
return callbackRef.current();
|
|
904
|
+
}, [signal]);
|
|
64
905
|
}
|
|
906
|
+
/**
|
|
907
|
+
* Custom hook that works like `useCallback` but uses deep comparison on dependencies.
|
|
908
|
+
*
|
|
909
|
+
* @template T - Callback signature to memoize.
|
|
910
|
+
*
|
|
911
|
+
* @param callback - The callback function to run.
|
|
912
|
+
* @param dependencies - The list of dependencies.
|
|
913
|
+
* @returns The result of the `useCallback` hook with memoized dependencies.
|
|
914
|
+
*
|
|
915
|
+
* @example
|
|
916
|
+
* ```tsx
|
|
917
|
+
* import { useEqualCallback } from '@faasjs/react'
|
|
918
|
+
*
|
|
919
|
+
* function Search({ filters }: { filters: Record<string, any> }) {
|
|
920
|
+
* const handleSubmit = useEqualCallback(() => {
|
|
921
|
+
* console.log(filters)
|
|
922
|
+
* }, [filters])
|
|
923
|
+
*
|
|
924
|
+
* return <button onClick={handleSubmit}>Search</button>
|
|
925
|
+
* }
|
|
926
|
+
* ```
|
|
927
|
+
*/
|
|
65
928
|
function useEqualCallback(callback, dependencies) {
|
|
66
|
-
|
|
67
|
-
(...args) => callback(...args),
|
|
68
|
-
useEqualMemoize(dependencies)
|
|
69
|
-
);
|
|
929
|
+
return useCallback((...args) => callback(...args), [useEqualSignal(dependencies)]);
|
|
70
930
|
}
|
|
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
|
-
|
|
109
|
-
|
|
931
|
+
/**
|
|
932
|
+
* Fetch FaasJS data and inject the result into a render prop or child element.
|
|
933
|
+
*
|
|
934
|
+
* The wrapper defers rendering `children` or `render` until the first request
|
|
935
|
+
* completes, then keeps passing the latest request state to the rendered output.
|
|
936
|
+
*
|
|
937
|
+
* @param props - Wrapper props controlling the request and rendered fallback.
|
|
938
|
+
* @param props.render - Render prop that receives the resolved Faas request state.
|
|
939
|
+
* @param props.children - Child element cloned with injected Faas request state.
|
|
940
|
+
* @param props.fallback - Element rendered before the first successful load.
|
|
941
|
+
* @param props.action - Action path to request.
|
|
942
|
+
* @param props.params - Params sent to the action.
|
|
943
|
+
* @param props.onDataChange - Callback invoked when the resolved data value changes.
|
|
944
|
+
* @param props.data - Controlled data value used instead of internal state.
|
|
945
|
+
* @param props.setData - Controlled setter used instead of internal state.
|
|
946
|
+
* @param props.baseUrl - Base URL override used for this wrapper instance.
|
|
947
|
+
*
|
|
948
|
+
* @example
|
|
949
|
+
* ```tsx
|
|
950
|
+
* import { FaasDataWrapper } from '@faasjs/react'
|
|
951
|
+
*
|
|
952
|
+
* type User = {
|
|
953
|
+
* name: string
|
|
954
|
+
* }
|
|
955
|
+
*
|
|
956
|
+
* function UserView(props: {
|
|
957
|
+
* data?: User
|
|
958
|
+
* error?: Error
|
|
959
|
+
* reload?: () => void
|
|
960
|
+
* }) {
|
|
961
|
+
* if (props.error) {
|
|
962
|
+
* return (
|
|
963
|
+
* <div>
|
|
964
|
+
* <p>Failed to load user: {props.error.message}</p>
|
|
965
|
+
* <button type="button" onClick={() => props.reload?.()}>
|
|
966
|
+
* Retry
|
|
967
|
+
* </button>
|
|
968
|
+
* </div>
|
|
969
|
+
* )
|
|
970
|
+
* }
|
|
971
|
+
*
|
|
972
|
+
* return <div>Hello, {props.data?.name}</div>
|
|
973
|
+
* }
|
|
974
|
+
*
|
|
975
|
+
* // Render-prop mode
|
|
976
|
+
* export function UserProfile(props: { id: number }) {
|
|
977
|
+
* return (
|
|
978
|
+
* <FaasDataWrapper<User>
|
|
979
|
+
* action="/pages/users/get"
|
|
980
|
+
* params={{ id: props.id }}
|
|
981
|
+
* fallback={<div>Loading user...</div>}
|
|
982
|
+
* render={({ data, error, reload }) => {
|
|
983
|
+
* if (error) {
|
|
984
|
+
* return (
|
|
985
|
+
* <div>
|
|
986
|
+
* <p>Failed to load user: {error.message}</p>
|
|
987
|
+
* <button type="button" onClick={() => reload()}>
|
|
988
|
+
* Retry
|
|
989
|
+
* </button>
|
|
990
|
+
* </div>
|
|
991
|
+
* )
|
|
992
|
+
* }
|
|
993
|
+
*
|
|
994
|
+
* return <div>Hello, {data.name}</div>
|
|
995
|
+
* }}
|
|
996
|
+
* />
|
|
997
|
+
* )
|
|
998
|
+
* }
|
|
999
|
+
*
|
|
1000
|
+
* // Children injection mode
|
|
1001
|
+
* export function UserProfileWithChildren(props: { id: number }) {
|
|
1002
|
+
* return (
|
|
1003
|
+
* <FaasDataWrapper<User>
|
|
1004
|
+
* action="/pages/users/get"
|
|
1005
|
+
* params={{ id: props.id }}
|
|
1006
|
+
* fallback={<div>Loading user...</div>}
|
|
1007
|
+
* >
|
|
1008
|
+
* <UserView />
|
|
1009
|
+
* </FaasDataWrapper>
|
|
1010
|
+
* )
|
|
1011
|
+
* }
|
|
1012
|
+
* ```
|
|
1013
|
+
*
|
|
1014
|
+
* When a ref is provided, it exposes the current Faas request state imperatively.
|
|
1015
|
+
*/
|
|
1016
|
+
const FaasDataWrapper = forwardRef((props, ref) => {
|
|
1017
|
+
const requestOptions = {
|
|
1018
|
+
...props.data !== void 0 ? { data: props.data } : {},
|
|
1019
|
+
...props.setData ? { setData: props.setData } : {}
|
|
1020
|
+
};
|
|
1021
|
+
const request = getClient(props.baseUrl).useFaas(props.action, props.params ?? {}, requestOptions);
|
|
1022
|
+
const [loaded, setLoaded] = useState(false);
|
|
1023
|
+
useImperativeHandle(ref, () => request, [request]);
|
|
1024
|
+
useEqualEffect(() => {
|
|
1025
|
+
if (!request.loading) setLoaded((prev) => prev === false ? true : prev);
|
|
1026
|
+
}, [request.loading]);
|
|
1027
|
+
useEqualEffect(() => {
|
|
1028
|
+
if (props.onDataChange) props.onDataChange(request);
|
|
1029
|
+
}, [request.data]);
|
|
1030
|
+
return useEqualMemo(() => {
|
|
1031
|
+
if (loaded) {
|
|
1032
|
+
if (props.children) return cloneElement(props.children, request);
|
|
1033
|
+
if (props.render) return props.render(request);
|
|
1034
|
+
}
|
|
1035
|
+
return props.fallback || null;
|
|
1036
|
+
}, [
|
|
1037
|
+
loaded,
|
|
1038
|
+
request.action,
|
|
1039
|
+
request.params,
|
|
1040
|
+
request.data,
|
|
1041
|
+
request.error,
|
|
1042
|
+
request.loading
|
|
1043
|
+
]);
|
|
110
1044
|
});
|
|
111
|
-
|
|
112
|
-
|
|
1045
|
+
Object.assign(FaasDataWrapper, { displayName: "FaasDataWrapper" });
|
|
1046
|
+
/**
|
|
1047
|
+
* Wrap a component with {@link FaasDataWrapper} and inject Faas request state as props.
|
|
1048
|
+
*
|
|
1049
|
+
* `withFaasData` is most useful for wrapper-style exports or compatibility with
|
|
1050
|
+
* an existing component boundary. For new code, prefer `useFaas` or
|
|
1051
|
+
* `FaasDataWrapper` when they express the request ownership more directly.
|
|
1052
|
+
*
|
|
1053
|
+
* @template PathOrData - Action path or response data type used for inference.
|
|
1054
|
+
* @template TComponentProps - Component props including injected Faas data fields.
|
|
1055
|
+
* @param Component - Component that consumes injected Faas data props.
|
|
1056
|
+
* @param faasProps - Request configuration forwarded to `FaasDataWrapper`.
|
|
1057
|
+
* @returns Component that accepts the original props minus the injected Faas data fields.
|
|
1058
|
+
*
|
|
1059
|
+
* @example
|
|
1060
|
+
* ```tsx
|
|
1061
|
+
* import { withFaasData } from '@faasjs/react'
|
|
1062
|
+
*
|
|
1063
|
+
* const MyComponent = withFaasData(
|
|
1064
|
+
* ({ data, error, reload }) => {
|
|
1065
|
+
* if (error) {
|
|
1066
|
+
* return (
|
|
1067
|
+
* <button type="button" onClick={() => reload()}>
|
|
1068
|
+
* Retry
|
|
1069
|
+
* </button>
|
|
1070
|
+
* )
|
|
1071
|
+
* }
|
|
1072
|
+
*
|
|
1073
|
+
* return <div>{data.name}</div>
|
|
1074
|
+
* },
|
|
1075
|
+
* { action: '/pages/users/get', params: { id: 1 } },
|
|
1076
|
+
* )
|
|
1077
|
+
* ```
|
|
1078
|
+
*/
|
|
1079
|
+
function withFaasData(Component, faasProps) {
|
|
1080
|
+
return (props) => /* @__PURE__ */ jsx(FaasDataWrapper, {
|
|
1081
|
+
...faasProps,
|
|
1082
|
+
children: /* @__PURE__ */ jsx(Component, { ...props })
|
|
1083
|
+
});
|
|
113
1084
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
1085
|
+
//#endregion
|
|
1086
|
+
//#region src/useFaasRequest.ts
|
|
1087
|
+
function useFaasRequest({ action, defaultParams, options, beforeSend, onSuccess, send }) {
|
|
1088
|
+
const [loading, setLoading] = useState(true);
|
|
1089
|
+
const [error, setError] = useState();
|
|
1090
|
+
const [params, setParams] = useState(defaultParams);
|
|
1091
|
+
const [reloadTimes, setReloadTimes] = useState(0);
|
|
1092
|
+
const [skip, setSkip] = useState(typeof options.skip === "function" ? options.skip(defaultParams) : options.skip);
|
|
1093
|
+
const promiseRef = useRef(null);
|
|
1094
|
+
const controllerRef = useRef(null);
|
|
1095
|
+
const failedOnceRef = useRef(false);
|
|
1096
|
+
const pendingReloadsRef = useRef(/* @__PURE__ */ new Map());
|
|
1097
|
+
const reloadCounterRef = useRef(0);
|
|
1098
|
+
const beforeSendRef = useRef(beforeSend);
|
|
1099
|
+
const onSuccessRef = useRef(onSuccess);
|
|
1100
|
+
const sendRef = useRef(send);
|
|
1101
|
+
beforeSendRef.current = beforeSend;
|
|
1102
|
+
onSuccessRef.current = onSuccess;
|
|
1103
|
+
sendRef.current = send;
|
|
1104
|
+
useEqualEffect(() => {
|
|
1105
|
+
setSkip(typeof options.skip === "function" ? options.skip(params) : options.skip);
|
|
1106
|
+
}, [typeof options.skip === "function" ? params : options.skip]);
|
|
1107
|
+
useEqualEffect(() => {
|
|
1108
|
+
if (!equal(defaultParams, params)) setParams(defaultParams);
|
|
1109
|
+
}, [defaultParams]);
|
|
1110
|
+
useEqualEffect(() => {
|
|
1111
|
+
if (!action || skip) {
|
|
1112
|
+
setLoading(false);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
setLoading(true);
|
|
1116
|
+
beforeSendRef.current?.();
|
|
1117
|
+
failedOnceRef.current = false;
|
|
1118
|
+
const controller = new AbortController();
|
|
1119
|
+
controllerRef.current = controller;
|
|
1120
|
+
const client = getClient(options.baseUrl);
|
|
1121
|
+
const requestParams = options.params || params;
|
|
1122
|
+
const rejectPending = (reason) => {
|
|
1123
|
+
for (const { reject } of pendingReloadsRef.current.values()) reject(reason);
|
|
1124
|
+
pendingReloadsRef.current.clear();
|
|
1125
|
+
};
|
|
1126
|
+
const resolvePending = (value) => {
|
|
1127
|
+
for (const { resolve } of pendingReloadsRef.current.values()) resolve(value);
|
|
1128
|
+
pendingReloadsRef.current.clear();
|
|
1129
|
+
};
|
|
1130
|
+
const run = () => {
|
|
1131
|
+
sendRef.current({
|
|
1132
|
+
action,
|
|
1133
|
+
params: requestParams,
|
|
1134
|
+
signal: controller.signal,
|
|
1135
|
+
client,
|
|
1136
|
+
setPromise: (promise) => {
|
|
1137
|
+
promiseRef.current = promise;
|
|
1138
|
+
}
|
|
1139
|
+
}).then((result) => {
|
|
1140
|
+
failedOnceRef.current = false;
|
|
1141
|
+
setError(null);
|
|
1142
|
+
onSuccessRef.current?.(result);
|
|
1143
|
+
setLoading(false);
|
|
1144
|
+
resolvePending(result);
|
|
1145
|
+
}).catch(async (e) => {
|
|
1146
|
+
if (typeof e?.message === "string" && e.message.toLowerCase().includes("aborted")) return;
|
|
1147
|
+
if (!failedOnceRef.current && typeof e?.message === "string" && e.message.includes("Failed to fetch")) {
|
|
1148
|
+
failedOnceRef.current = true;
|
|
1149
|
+
console.warn(`FaasReactClient: ${e.message} retry...`);
|
|
1150
|
+
run();
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
let nextError = e;
|
|
1154
|
+
if (client.onError) try {
|
|
1155
|
+
await client.onError(action, requestParams || Object.create(null))(e);
|
|
1156
|
+
} catch (newError) {
|
|
1157
|
+
nextError = newError;
|
|
1158
|
+
}
|
|
1159
|
+
setError(nextError);
|
|
1160
|
+
setLoading(false);
|
|
1161
|
+
rejectPending(nextError);
|
|
1162
|
+
});
|
|
1163
|
+
};
|
|
1164
|
+
if (options.debounce) {
|
|
1165
|
+
const timeout = setTimeout(run, options.debounce);
|
|
1166
|
+
return () => {
|
|
1167
|
+
clearTimeout(timeout);
|
|
1168
|
+
controllerRef.current?.abort();
|
|
1169
|
+
setLoading(false);
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
run();
|
|
1173
|
+
return () => {
|
|
1174
|
+
controllerRef.current?.abort();
|
|
1175
|
+
setLoading(false);
|
|
1176
|
+
};
|
|
1177
|
+
}, [
|
|
1178
|
+
action,
|
|
1179
|
+
options.params || params,
|
|
1180
|
+
reloadTimes,
|
|
1181
|
+
skip
|
|
1182
|
+
]);
|
|
1183
|
+
return {
|
|
1184
|
+
loading,
|
|
1185
|
+
error,
|
|
1186
|
+
params,
|
|
1187
|
+
reloadTimes,
|
|
1188
|
+
reload: useEqualCallback((nextParams) => {
|
|
1189
|
+
if (skip) setSkip(false);
|
|
1190
|
+
if (nextParams) setParams(nextParams);
|
|
1191
|
+
const reloadCounter = ++reloadCounterRef.current;
|
|
1192
|
+
return new Promise((resolve, reject) => {
|
|
1193
|
+
pendingReloadsRef.current.set(reloadCounter, {
|
|
1194
|
+
resolve,
|
|
1195
|
+
reject
|
|
1196
|
+
});
|
|
1197
|
+
setReloadTimes((prev) => prev + 1);
|
|
1198
|
+
});
|
|
1199
|
+
}, [skip]),
|
|
1200
|
+
promiseRef,
|
|
1201
|
+
setError,
|
|
1202
|
+
setLoading
|
|
1203
|
+
};
|
|
124
1204
|
}
|
|
1205
|
+
//#endregion
|
|
1206
|
+
//#region src/useFaas.tsx
|
|
1207
|
+
/**
|
|
1208
|
+
* Request FaasJS data and keep request state in React state.
|
|
1209
|
+
*
|
|
1210
|
+
* `useFaas` is the default hook for standard FaasJS request-response flows in React.
|
|
1211
|
+
* It sends an initial request unless `skip` is enabled, and returns request state
|
|
1212
|
+
* plus helpers for reloading, updating data, and handling errors.
|
|
1213
|
+
*
|
|
1214
|
+
* @template PathOrData - Action path or response data type used for inference.
|
|
1215
|
+
*
|
|
1216
|
+
* @param action - Action path to invoke.
|
|
1217
|
+
* @param defaultParams - Params used for the initial request and future reloads.
|
|
1218
|
+
* @param options - Optional hook configuration such as controlled data, debounce, and skip logic.
|
|
1219
|
+
* @param options.params - Request params override used without mutating the hook's stored params state.
|
|
1220
|
+
* @param options.data - Controlled data value used instead of the hook's internal state.
|
|
1221
|
+
* @param options.setData - Controlled setter used instead of the hook's internal `setData`.
|
|
1222
|
+
* @param options.skip - Boolean or predicate that suppresses the automatic request until `reload()` runs.
|
|
1223
|
+
* @param options.debounce - Milliseconds to wait before sending the latest request.
|
|
1224
|
+
* @param options.baseUrl - Base URL override used for this hook instance.
|
|
1225
|
+
* @returns Request state and helper methods described by {@link FaasDataInjection}.
|
|
1226
|
+
*
|
|
1227
|
+
* @example
|
|
1228
|
+
* ```tsx
|
|
1229
|
+
* import { useFaas } from '@faasjs/react'
|
|
1230
|
+
*
|
|
1231
|
+
* function Profile({ id }: { id: number }) {
|
|
1232
|
+
* const { data, error, loading, reload } = useFaas('/pages/users/get', { id })
|
|
1233
|
+
*
|
|
1234
|
+
* if (loading) return <div>Loading...</div>
|
|
1235
|
+
*
|
|
1236
|
+
* if (error) {
|
|
1237
|
+
* return (
|
|
1238
|
+
* <div>
|
|
1239
|
+
* <div>Load failed: {error.message}</div>
|
|
1240
|
+
* <button type="button" onClick={() => reload()}>
|
|
1241
|
+
* Retry
|
|
1242
|
+
* </button>
|
|
1243
|
+
* </div>
|
|
1244
|
+
* )
|
|
1245
|
+
* }
|
|
1246
|
+
*
|
|
1247
|
+
* return (
|
|
1248
|
+
* <div>
|
|
1249
|
+
* <span>{data.name}</span>
|
|
1250
|
+
* <button type="button" onClick={() => reload()}>
|
|
1251
|
+
* Refresh
|
|
1252
|
+
* </button>
|
|
1253
|
+
* </div>
|
|
1254
|
+
* )
|
|
1255
|
+
* }
|
|
1256
|
+
* ```
|
|
1257
|
+
*/
|
|
125
1258
|
function useFaas(action, defaultParams, options = {}) {
|
|
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
|
-
{ signal: controllerRef.current.signal }
|
|
162
|
-
);
|
|
163
|
-
promiseRef.current = request;
|
|
164
|
-
request.then((r) => {
|
|
165
|
-
setFails(0);
|
|
166
|
-
setError(null);
|
|
167
|
-
options.setData ? options.setData(r.data) : setData(r.data);
|
|
168
|
-
setLoading(false);
|
|
169
|
-
for (const { resolve } of pendingReloadsRef.current.values())
|
|
170
|
-
resolve(r.data);
|
|
171
|
-
pendingReloadsRef.current.clear();
|
|
172
|
-
}).catch(async (e) => {
|
|
173
|
-
if (typeof e?.message === "string" && e.message.toLowerCase().indexOf("aborted") >= 0)
|
|
174
|
-
return;
|
|
175
|
-
if (!fails && typeof e?.message === "string" && e.message.indexOf("Failed to fetch") >= 0) {
|
|
176
|
-
console.warn(`FaasReactClient: ${e.message} retry...`);
|
|
177
|
-
setFails(1);
|
|
178
|
-
return send();
|
|
179
|
-
}
|
|
180
|
-
let error2 = e;
|
|
181
|
-
if (client.onError)
|
|
182
|
-
try {
|
|
183
|
-
await client.onError(action, params)(e);
|
|
184
|
-
} catch (newError) {
|
|
185
|
-
error2 = newError;
|
|
186
|
-
}
|
|
187
|
-
setError(error2);
|
|
188
|
-
setLoading(false);
|
|
189
|
-
for (const { reject } of pendingReloadsRef.current.values())
|
|
190
|
-
reject(error2);
|
|
191
|
-
pendingReloadsRef.current.clear();
|
|
192
|
-
return;
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
if (options.debounce) {
|
|
196
|
-
const timeout = setTimeout(send, options.debounce);
|
|
197
|
-
return () => {
|
|
198
|
-
clearTimeout(timeout);
|
|
199
|
-
controllerRef.current?.abort();
|
|
200
|
-
setLoading(false);
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
send();
|
|
204
|
-
return () => {
|
|
205
|
-
controllerRef.current?.abort();
|
|
206
|
-
setLoading(false);
|
|
207
|
-
};
|
|
208
|
-
}, [action, options.params || params, reloadTimes, skip]);
|
|
209
|
-
const reload = useEqualCallback(
|
|
210
|
-
(params2) => {
|
|
211
|
-
if (skip) setSkip(false);
|
|
212
|
-
if (params2) setParams(params2);
|
|
213
|
-
const reloadCounter = ++reloadCounterRef.current;
|
|
214
|
-
setReloadTimes((prev) => prev + 1);
|
|
215
|
-
return new Promise((resolve, reject) => {
|
|
216
|
-
pendingReloadsRef.current.set(reloadCounter, { resolve, reject });
|
|
217
|
-
setReloadTimes((prev) => prev + 1);
|
|
218
|
-
});
|
|
219
|
-
},
|
|
220
|
-
[params, skip]
|
|
221
|
-
);
|
|
222
|
-
return {
|
|
223
|
-
action,
|
|
224
|
-
params,
|
|
225
|
-
loading,
|
|
226
|
-
data: options.data || data,
|
|
227
|
-
reloadTimes,
|
|
228
|
-
error,
|
|
229
|
-
promise: promiseRef.current,
|
|
230
|
-
reload,
|
|
231
|
-
setData: options.setData || setData,
|
|
232
|
-
setLoading,
|
|
233
|
-
setPromise: (newPromise) => typeof newPromise === "function" ? newPromise(promiseRef.current) : promiseRef.current = newPromise,
|
|
234
|
-
setError
|
|
235
|
-
};
|
|
1259
|
+
const [data, setData] = useState();
|
|
1260
|
+
const localSetData = setData;
|
|
1261
|
+
const request = useFaasRequest({
|
|
1262
|
+
action,
|
|
1263
|
+
defaultParams,
|
|
1264
|
+
options,
|
|
1265
|
+
onSuccess: (nextData) => {
|
|
1266
|
+
if (options.setData) options.setData(nextData);
|
|
1267
|
+
else localSetData(nextData);
|
|
1268
|
+
},
|
|
1269
|
+
send: ({ action, params, signal, client, setPromise }) => {
|
|
1270
|
+
const promise = client.faas(action, params, { signal });
|
|
1271
|
+
setPromise(promise);
|
|
1272
|
+
return promise.then((response) => response.data);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
const currentData = options.data ?? data;
|
|
1276
|
+
const currentPromise = request.promiseRef.current ?? Promise.resolve({});
|
|
1277
|
+
const updateData = options.setData ?? localSetData;
|
|
1278
|
+
return {
|
|
1279
|
+
action,
|
|
1280
|
+
params: request.params,
|
|
1281
|
+
loading: request.loading,
|
|
1282
|
+
data: currentData,
|
|
1283
|
+
reloadTimes: request.reloadTimes,
|
|
1284
|
+
error: request.error,
|
|
1285
|
+
promise: currentPromise,
|
|
1286
|
+
reload: request.reload,
|
|
1287
|
+
setData: updateData,
|
|
1288
|
+
setLoading: request.setLoading,
|
|
1289
|
+
setPromise: (newPromise) => {
|
|
1290
|
+
request.promiseRef.current = typeof newPromise === "function" ? newPromise(currentPromise) : newPromise;
|
|
1291
|
+
},
|
|
1292
|
+
setError: request.setError
|
|
1293
|
+
};
|
|
236
1294
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
1295
|
+
//#endregion
|
|
1296
|
+
//#region src/client.tsx
|
|
1297
|
+
const clients = {};
|
|
1298
|
+
/**
|
|
1299
|
+
* Create and register a FaasReactClient instance.
|
|
1300
|
+
*
|
|
1301
|
+
* The returned client is stored by `baseUrl` and becomes the default client
|
|
1302
|
+
* used by helpers such as {@link faas} and {@link useFaas}.
|
|
1303
|
+
*
|
|
1304
|
+
* @param options - Client configuration including base URL, default request options, and error hooks.
|
|
1305
|
+
* @param options.baseUrl - Base URL used to register and route the client instance.
|
|
1306
|
+
* @param options.options - Default browser-client request options forwarded to `FaasBrowserClient`.
|
|
1307
|
+
* @param options.onError - Hook factory used to handle failed `faas` and `useFaas` requests.
|
|
1308
|
+
* See {@link Options} for supported browser-client request fields such as `headers`,
|
|
1309
|
+
* `beforeRequest`, `request`, `baseUrl`, and `stream`.
|
|
1310
|
+
* @returns Registered FaasReactClient instance.
|
|
1311
|
+
*
|
|
1312
|
+
* @example
|
|
1313
|
+
* ```ts
|
|
1314
|
+
* import { FaasReactClient, ResponseError } from '@faasjs/react'
|
|
1315
|
+
*
|
|
1316
|
+
* const client = FaasReactClient({
|
|
1317
|
+
* baseUrl: 'http://localhost:8080/api/',
|
|
1318
|
+
* onError: (action, params) => async (res) => {
|
|
1319
|
+
* if (res instanceof ResponseError) {
|
|
1320
|
+
* reportErrorToSentry(res, {
|
|
1321
|
+
* tags: { action },
|
|
1322
|
+
* extra: { params },
|
|
1323
|
+
* })
|
|
1324
|
+
* }
|
|
1325
|
+
* },
|
|
1326
|
+
* })
|
|
1327
|
+
* ```
|
|
1328
|
+
*/
|
|
1329
|
+
function FaasReactClient(options = { baseUrl: "/" }) {
|
|
1330
|
+
const { baseUrl, options: clientOptions, onError } = options;
|
|
1331
|
+
const resolvedBaseUrl = baseUrl ?? "/";
|
|
1332
|
+
const client = new FaasBrowserClient(resolvedBaseUrl, clientOptions);
|
|
1333
|
+
function withBaseUrl(options) {
|
|
1334
|
+
if (options?.baseUrl) return options;
|
|
1335
|
+
return {
|
|
1336
|
+
...options ?? {},
|
|
1337
|
+
baseUrl: resolvedBaseUrl
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
const reactClient = {
|
|
1341
|
+
id: client.id,
|
|
1342
|
+
faas: async (action, params, requestOptions) => faas(action, params, withBaseUrl(requestOptions)),
|
|
1343
|
+
useFaas: (action, defaultParams, requestOptions) => useFaas(action, defaultParams, withBaseUrl(requestOptions)),
|
|
1344
|
+
FaasDataWrapper: (props) => /* @__PURE__ */ jsx(FaasDataWrapper, {
|
|
1345
|
+
...props,
|
|
1346
|
+
baseUrl: resolvedBaseUrl
|
|
1347
|
+
}),
|
|
1348
|
+
...onError ? { onError } : {},
|
|
1349
|
+
browserClient: client
|
|
1350
|
+
};
|
|
1351
|
+
clients[resolvedBaseUrl] = reactClient;
|
|
1352
|
+
return reactClient;
|
|
252
1353
|
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Get a registered FaasReactClient instance.
|
|
1356
|
+
*
|
|
1357
|
+
* When `host` is omitted, the first registered client is returned. If no client
|
|
1358
|
+
* has been created yet, a default client is initialized automatically.
|
|
1359
|
+
* Use `getClient` only for special cases such as multiple Faas clients with
|
|
1360
|
+
* different base URLs. In normal single-client app code, prefer the default
|
|
1361
|
+
* `faas`, `useFaas`, or `FaasReactClient` setup directly.
|
|
1362
|
+
*
|
|
1363
|
+
* @param host - Registered base URL to look up. Omit it to use the default client.
|
|
1364
|
+
* @returns Registered or newly created FaasReactClient instance.
|
|
1365
|
+
*
|
|
1366
|
+
* @example
|
|
1367
|
+
* ```ts
|
|
1368
|
+
* import { FaasReactClient, getClient } from '@faasjs/react'
|
|
1369
|
+
*
|
|
1370
|
+
* FaasReactClient({
|
|
1371
|
+
* baseUrl: 'https://service-a.example.com/api/',
|
|
1372
|
+
* })
|
|
1373
|
+
*
|
|
1374
|
+
* FaasReactClient({
|
|
1375
|
+
* baseUrl: 'https://service-b.example.com/api/',
|
|
1376
|
+
* })
|
|
1377
|
+
*
|
|
1378
|
+
* const client = getClient('https://service-b.example.com/api/')
|
|
1379
|
+
*
|
|
1380
|
+
* await client.faas('/pages/posts/get', { id: 1 })
|
|
1381
|
+
* ```
|
|
1382
|
+
*/
|
|
253
1383
|
function getClient(host) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
1384
|
+
const client = clients[host || Object.keys(clients)[0]];
|
|
1385
|
+
if (!client) {
|
|
1386
|
+
console.warn("FaasReactClient is not initialized manually, use default.");
|
|
1387
|
+
return FaasReactClient();
|
|
1388
|
+
}
|
|
1389
|
+
return client;
|
|
260
1390
|
}
|
|
1391
|
+
//#endregion
|
|
1392
|
+
//#region src/constant.ts
|
|
1393
|
+
/**
|
|
1394
|
+
* Returns a constant value that is created by the given function.
|
|
1395
|
+
*
|
|
1396
|
+
* @template T - Constant value type returned by the initializer.
|
|
1397
|
+
* @param fn - Initializer that runs only once for the current component instance.
|
|
1398
|
+
*
|
|
1399
|
+
* @example
|
|
1400
|
+
* ```tsx
|
|
1401
|
+
* import { useConstant } from '@faasjs/react'
|
|
1402
|
+
*
|
|
1403
|
+
* function Page() {
|
|
1404
|
+
* const requestId = useConstant(() => crypto.randomUUID())
|
|
1405
|
+
*
|
|
1406
|
+
* return <span>{requestId}</span>
|
|
1407
|
+
* }
|
|
1408
|
+
* ```
|
|
1409
|
+
*/
|
|
261
1410
|
function useConstant(fn) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
return ref.current.v;
|
|
1411
|
+
const ref = useRef(null);
|
|
1412
|
+
if (!ref.current) ref.current = { v: fn() };
|
|
1413
|
+
return ref.current.v;
|
|
267
1414
|
}
|
|
268
|
-
|
|
1415
|
+
//#endregion
|
|
1416
|
+
//#region src/ErrorBoundary.tsx
|
|
1417
|
+
/**
|
|
1418
|
+
* React error boundary with an optional custom fallback element.
|
|
1419
|
+
*
|
|
1420
|
+
* The boundary renders its children until a descendant throws. After that it
|
|
1421
|
+
* either clones `errorChildren` with injected error details or renders a simple
|
|
1422
|
+
* built-in fallback.
|
|
1423
|
+
*
|
|
1424
|
+
* @example
|
|
1425
|
+
* ```tsx
|
|
1426
|
+
* import { ErrorBoundary } from '@faasjs/react'
|
|
1427
|
+
*
|
|
1428
|
+
* function Fallback({ errorMessage }: { errorMessage?: string }) {
|
|
1429
|
+
* return <div>{errorMessage}</div>
|
|
1430
|
+
* }
|
|
1431
|
+
*
|
|
1432
|
+
* <ErrorBoundary errorChildren={<Fallback />}>
|
|
1433
|
+
* <DangerousWidget />
|
|
1434
|
+
* </ErrorBoundary>
|
|
1435
|
+
* ```
|
|
1436
|
+
*/
|
|
269
1437
|
var ErrorBoundary = class extends Component {
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
1438
|
+
/**
|
|
1439
|
+
* Stable display name used by React DevTools.
|
|
1440
|
+
*/
|
|
1441
|
+
static displayName = "ErrorBoundary";
|
|
1442
|
+
/**
|
|
1443
|
+
* Create an error boundary with empty error state.
|
|
1444
|
+
*
|
|
1445
|
+
* @param props - Boundary props.
|
|
1446
|
+
* @param props.children - Descendant elements protected by the boundary.
|
|
1447
|
+
* @param props.onError - Callback invoked after a render error is captured.
|
|
1448
|
+
* @param props.errorChildren - Custom fallback element that receives error details.
|
|
1449
|
+
*/
|
|
1450
|
+
constructor(props) {
|
|
1451
|
+
super(props);
|
|
1452
|
+
this.state = {
|
|
1453
|
+
error: null,
|
|
1454
|
+
info: { componentStack: "" }
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Capture rendering errors from descendant components.
|
|
1459
|
+
*
|
|
1460
|
+
* @param error - Caught render error.
|
|
1461
|
+
* @param info - React component stack metadata.
|
|
1462
|
+
*/
|
|
1463
|
+
componentDidCatch(error, info) {
|
|
1464
|
+
this.setState({
|
|
1465
|
+
error,
|
|
1466
|
+
info
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Render children or the configured fallback for the captured error.
|
|
1471
|
+
*/
|
|
1472
|
+
render() {
|
|
1473
|
+
const { error, info } = this.state;
|
|
1474
|
+
const errorMessage = String(error ?? "");
|
|
1475
|
+
const errorDescription = info.componentStack || void 0;
|
|
1476
|
+
if (error) {
|
|
1477
|
+
if (this.props.onError) this.props.onError(error, info);
|
|
1478
|
+
if (this.props.errorChildren) return cloneElement(this.props.errorChildren, {
|
|
1479
|
+
error,
|
|
1480
|
+
info,
|
|
1481
|
+
errorMessage,
|
|
1482
|
+
...errorDescription ? { errorDescription } : {}
|
|
1483
|
+
});
|
|
1484
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", { children: errorMessage }), /* @__PURE__ */ jsx("pre", { children: errorDescription })] });
|
|
1485
|
+
}
|
|
1486
|
+
return this.props.children ?? null;
|
|
1487
|
+
}
|
|
305
1488
|
};
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
1489
|
+
//#endregion
|
|
1490
|
+
//#region src/OptionalWrapper.tsx
|
|
1491
|
+
/**
|
|
1492
|
+
* Conditionally wrap children with another component.
|
|
1493
|
+
*
|
|
1494
|
+
* @param props - Wrapper condition, wrapper component, and child content.
|
|
1495
|
+
* @param props.condition - When `true`, wrap children with `Wrapper`.
|
|
1496
|
+
* @param props.Wrapper - Component used as the wrapper when the condition passes.
|
|
1497
|
+
* @param props.wrapperProps - Props forwarded to the wrapper component.
|
|
1498
|
+
* @param props.children - Content rendered directly or inside the wrapper.
|
|
1499
|
+
* @returns Wrapped children or the original children when `condition` is false.
|
|
1500
|
+
*
|
|
1501
|
+
* @example
|
|
1502
|
+
* ```tsx
|
|
1503
|
+
* import { OptionalWrapper } from '@faasjs/react'
|
|
1504
|
+
*
|
|
1505
|
+
* const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
1506
|
+
* <div className='wrapper'>{children}</div>
|
|
1507
|
+
* )
|
|
1508
|
+
*
|
|
1509
|
+
* const App = () => (
|
|
1510
|
+
* <OptionalWrapper condition={true} Wrapper={Wrapper}>
|
|
1511
|
+
* <span>Test</span>
|
|
1512
|
+
* </OptionalWrapper>
|
|
1513
|
+
* )
|
|
1514
|
+
* ```
|
|
1515
|
+
*/
|
|
1516
|
+
function OptionalWrapper(props) {
|
|
1517
|
+
const { condition, Wrapper, wrapperProps, children } = props;
|
|
1518
|
+
if (condition) return /* @__PURE__ */ jsx(Wrapper, {
|
|
1519
|
+
...wrapperProps,
|
|
1520
|
+
children
|
|
1521
|
+
});
|
|
1522
|
+
return children;
|
|
313
1523
|
}
|
|
1524
|
+
OptionalWrapper.displayName = "OptionalWrapper";
|
|
1525
|
+
//#endregion
|
|
1526
|
+
//#region src/splittingState.tsx
|
|
1527
|
+
/**
|
|
1528
|
+
* Create local state entries and matching setters for each key in an object.
|
|
1529
|
+
*
|
|
1530
|
+
* @template T - A generic type that extends a record with string keys and any values.
|
|
1531
|
+
* @param initialStates - Object whose keys become state values and `setXxx` setters.
|
|
1532
|
+
* @returns Object containing the original keys plus generated setter functions.
|
|
1533
|
+
*
|
|
1534
|
+
* @example
|
|
1535
|
+
* ```tsx
|
|
1536
|
+
* function Counter() {
|
|
1537
|
+
* const { count, setCount, name, setName } = useSplittingState({ count: 0, name: 'John' })
|
|
1538
|
+
*
|
|
1539
|
+
* return <>{name}: {count}</>
|
|
1540
|
+
* }
|
|
1541
|
+
* ```
|
|
1542
|
+
*/
|
|
314
1543
|
function useSplittingState(initialStates) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
1544
|
+
const states = {};
|
|
1545
|
+
for (const key of Object.keys(initialStates)) {
|
|
1546
|
+
const state = useState(initialStates[key]);
|
|
1547
|
+
Object.assign(states, {
|
|
1548
|
+
[key]: state[0],
|
|
1549
|
+
[`set${String(key).charAt(0).toUpperCase()}${String(key).slice(1)}`]: state[1]
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
return states;
|
|
324
1553
|
}
|
|
1554
|
+
//#endregion
|
|
1555
|
+
//#region src/splittingContext.tsx
|
|
1556
|
+
/**
|
|
1557
|
+
* Create a context whose keys can be consumed independently.
|
|
1558
|
+
*
|
|
1559
|
+
* `createSplittingContext` returns a `Provider` and a `use` hook. Each key in
|
|
1560
|
+
* the provided shape is backed by a separate React context so readers only
|
|
1561
|
+
* subscribe to the values they access.
|
|
1562
|
+
*
|
|
1563
|
+
* @template T - Context value shape exposed by the provider and hook.
|
|
1564
|
+
* @param defaultValue - Default value map or key list used to create split contexts.
|
|
1565
|
+
* @returns Provider and hook helpers for the split context.
|
|
1566
|
+
*
|
|
1567
|
+
* @example
|
|
1568
|
+
* ```tsx
|
|
1569
|
+
* const { Provider, use } = createSplittingContext<{
|
|
1570
|
+
* value: number
|
|
1571
|
+
* setValue: React.Dispatch<React.SetStateAction<number>>
|
|
1572
|
+
* }>({
|
|
1573
|
+
* value: 0,
|
|
1574
|
+
* setValue: null,
|
|
1575
|
+
* })
|
|
1576
|
+
*
|
|
1577
|
+
* function ReaderComponent() {
|
|
1578
|
+
* const { value } = use()
|
|
1579
|
+
*
|
|
1580
|
+
* return <div>{value}</div>
|
|
1581
|
+
* }
|
|
1582
|
+
*
|
|
1583
|
+
* function WriterComponent() {
|
|
1584
|
+
* const { setValue } = use()
|
|
1585
|
+
*
|
|
1586
|
+
* return (
|
|
1587
|
+
* <button type='button' onClick={() => setValue((p: number) => p + 1)}>
|
|
1588
|
+
* Change
|
|
1589
|
+
* </button>
|
|
1590
|
+
* )
|
|
1591
|
+
* }
|
|
1592
|
+
*
|
|
1593
|
+
* function App() {
|
|
1594
|
+
* const [value, setValue] = useState(0)
|
|
1595
|
+
*
|
|
1596
|
+
* return (
|
|
1597
|
+
* <Provider value={{ value, setValue }}>
|
|
1598
|
+
* <ReaderComponent />
|
|
1599
|
+
* <WriterComponent />
|
|
1600
|
+
* </Provider>
|
|
1601
|
+
* )
|
|
1602
|
+
* }
|
|
1603
|
+
* ```
|
|
1604
|
+
*/
|
|
325
1605
|
function createSplittingContext(defaultValue) {
|
|
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
|
-
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
return Object.freeze(obj);
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
use.whyDidYouRender = true;
|
|
368
|
-
return {
|
|
369
|
-
Provider,
|
|
370
|
-
use
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// src/Form/context.tsx
|
|
375
|
-
var FormContext = createSplittingContext([
|
|
376
|
-
"items",
|
|
377
|
-
"onSubmit",
|
|
378
|
-
"Elements",
|
|
379
|
-
"lang",
|
|
380
|
-
"rules",
|
|
381
|
-
"submitting",
|
|
382
|
-
"setSubmitting",
|
|
383
|
-
"values",
|
|
384
|
-
"setValues",
|
|
385
|
-
"errors",
|
|
386
|
-
"setErrors",
|
|
387
|
-
"valuesRef"
|
|
388
|
-
]);
|
|
389
|
-
var FormContextProvider = FormContext.Provider;
|
|
390
|
-
var useFormContext = FormContext.use;
|
|
391
|
-
function processValue(input, rules) {
|
|
392
|
-
switch (rules?.type) {
|
|
393
|
-
case "number":
|
|
394
|
-
return Number(input);
|
|
395
|
-
case "string":
|
|
396
|
-
return String(input);
|
|
397
|
-
default:
|
|
398
|
-
return input;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
function FormInput({
|
|
402
|
-
name,
|
|
403
|
-
rules,
|
|
404
|
-
...rest
|
|
405
|
-
}) {
|
|
406
|
-
const { Elements, values, setValues } = useFormContext();
|
|
407
|
-
const value = values?.[name];
|
|
408
|
-
if (rest.Input) {
|
|
409
|
-
return /* @__PURE__ */ jsx(
|
|
410
|
-
rest.Input,
|
|
411
|
-
{
|
|
412
|
-
name,
|
|
413
|
-
value,
|
|
414
|
-
onChange: (v) => setValues((prev) => ({
|
|
415
|
-
...prev,
|
|
416
|
-
[name]: processValue(v, rules)
|
|
417
|
-
})),
|
|
418
|
-
...rest.props
|
|
419
|
-
}
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
return /* @__PURE__ */ jsx(
|
|
423
|
-
Elements.Input,
|
|
424
|
-
{
|
|
425
|
-
name,
|
|
426
|
-
value,
|
|
427
|
-
onChange: (v) => setValues((prev) => ({
|
|
428
|
-
...prev,
|
|
429
|
-
[name]: processValue(v, rules)
|
|
430
|
-
})),
|
|
431
|
-
...rest.props
|
|
432
|
-
}
|
|
433
|
-
);
|
|
1606
|
+
const keys = Array.isArray(defaultValue) ? defaultValue : Object.keys(defaultValue);
|
|
1607
|
+
const defaultValues = Array.isArray(defaultValue) ? keys.reduce((prev, cur) => {
|
|
1608
|
+
prev[cur] = null;
|
|
1609
|
+
return prev;
|
|
1610
|
+
}, {}) : defaultValue;
|
|
1611
|
+
const contexts = {};
|
|
1612
|
+
for (const key of keys) contexts[key] = createContext(defaultValues[key]);
|
|
1613
|
+
function Provider(props) {
|
|
1614
|
+
const states = props.initializeStates ? useSplittingState(props.initializeStates) : {};
|
|
1615
|
+
let children = props.memo ? useEqualMemo(() => props.children, props.memo === true ? [] : props.memo) : props.children;
|
|
1616
|
+
for (const key of keys) {
|
|
1617
|
+
const Context = contexts[key];
|
|
1618
|
+
const value = props.value?.[key] ?? states[key] ?? defaultValues[key];
|
|
1619
|
+
children = /* @__PURE__ */ jsx(Context.Provider, {
|
|
1620
|
+
value,
|
|
1621
|
+
children
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
return children;
|
|
1625
|
+
}
|
|
1626
|
+
Provider.displayName = "SplittingContextProvider";
|
|
1627
|
+
function use() {
|
|
1628
|
+
return useConstant(() => {
|
|
1629
|
+
const obj = Object.create(null);
|
|
1630
|
+
for (const key of Object.keys(contexts)) Object.defineProperty(obj, key, { get: () => {
|
|
1631
|
+
if (!contexts[key]) throw new Error(`Context for key "${key}" is undefined`);
|
|
1632
|
+
return useContext(contexts[key]);
|
|
1633
|
+
} });
|
|
1634
|
+
return Object.freeze(obj);
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
return {
|
|
1638
|
+
Provider,
|
|
1639
|
+
use
|
|
1640
|
+
};
|
|
434
1641
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
1642
|
+
//#endregion
|
|
1643
|
+
//#region src/useFaasStream.tsx
|
|
1644
|
+
/**
|
|
1645
|
+
* Stream a FaasJS response into React state.
|
|
1646
|
+
*
|
|
1647
|
+
* `useFaasStream` is the default hook for streaming FaasJS responses in React.
|
|
1648
|
+
* It sends a streaming request, appends decoded text chunks to `data`, and
|
|
1649
|
+
* exposes reload helpers for retrying the same action.
|
|
1650
|
+
*
|
|
1651
|
+
* @param action - Action path to invoke.
|
|
1652
|
+
* @param defaultParams - Params used for the initial request and future reloads.
|
|
1653
|
+
* @param options - Optional hook configuration such as controlled data, debounce, and skip logic.
|
|
1654
|
+
* @param options.params - Request params override used without mutating the hook's stored params state.
|
|
1655
|
+
* @param options.data - Controlled stream text used instead of the hook's internal state.
|
|
1656
|
+
* @param options.setData - Controlled setter used instead of the hook's internal `setData`.
|
|
1657
|
+
* @param options.skip - Boolean or predicate that suppresses the automatic request until `reload()` runs.
|
|
1658
|
+
* @param options.debounce - Milliseconds to wait before sending the latest request.
|
|
1659
|
+
* @param options.baseUrl - Base URL override used for this hook instance.
|
|
1660
|
+
* @returns Streaming request state and helper methods described by {@link UseFaasStreamResult}.
|
|
1661
|
+
*
|
|
1662
|
+
* @example
|
|
1663
|
+
* ```tsx
|
|
1664
|
+
* import { useFaasStream } from '@faasjs/react'
|
|
1665
|
+
*
|
|
1666
|
+
* function Chat({ prompt }: { prompt: string }) {
|
|
1667
|
+
* const { data, error, loading, reload } = useFaasStream('/pages/chat/stream', { prompt })
|
|
1668
|
+
*
|
|
1669
|
+
* if (loading) return <div>Streaming...</div>
|
|
1670
|
+
*
|
|
1671
|
+
* if (error) {
|
|
1672
|
+
* return (
|
|
1673
|
+
* <div>
|
|
1674
|
+
* <div>Stream failed: {error.message}</div>
|
|
1675
|
+
* <button type="button" onClick={() => reload()}>
|
|
1676
|
+
* Retry
|
|
1677
|
+
* </button>
|
|
1678
|
+
* </div>
|
|
1679
|
+
* )
|
|
1680
|
+
* }
|
|
1681
|
+
*
|
|
1682
|
+
* return <pre>{data}</pre>
|
|
1683
|
+
* }
|
|
1684
|
+
* ```
|
|
1685
|
+
*/
|
|
1686
|
+
function useFaasStream(action, defaultParams, options = {}) {
|
|
1687
|
+
const [data, setData] = useState(options.data || "");
|
|
1688
|
+
const request = useFaasRequest({
|
|
1689
|
+
action,
|
|
1690
|
+
defaultParams,
|
|
1691
|
+
options,
|
|
1692
|
+
beforeSend: () => setData(""),
|
|
1693
|
+
send: async ({ action, params, signal, client }) => {
|
|
1694
|
+
const response = await client.browserClient.action(action, params, {
|
|
1695
|
+
signal,
|
|
1696
|
+
stream: true
|
|
1697
|
+
});
|
|
1698
|
+
if (!response.body) throw new Error("Response body is null");
|
|
1699
|
+
const reader = response.body.getReader();
|
|
1700
|
+
const decoder = new TextDecoder();
|
|
1701
|
+
let accumulatedText = "";
|
|
1702
|
+
try {
|
|
1703
|
+
while (true) {
|
|
1704
|
+
const { done, value } = await reader.read();
|
|
1705
|
+
if (done) break;
|
|
1706
|
+
accumulatedText += decoder.decode(value, { stream: true });
|
|
1707
|
+
setData(accumulatedText);
|
|
1708
|
+
}
|
|
1709
|
+
return accumulatedText;
|
|
1710
|
+
} catch (error) {
|
|
1711
|
+
reader.releaseLock();
|
|
1712
|
+
throw error;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
return {
|
|
1717
|
+
action,
|
|
1718
|
+
params: request.params,
|
|
1719
|
+
loading: request.loading,
|
|
1720
|
+
reloadTimes: request.reloadTimes,
|
|
1721
|
+
data: options.data || data,
|
|
1722
|
+
error: request.error,
|
|
1723
|
+
reload: request.reload,
|
|
1724
|
+
setData: options.setData || setData,
|
|
1725
|
+
setLoading: request.setLoading,
|
|
1726
|
+
setError: request.setError
|
|
1727
|
+
};
|
|
441
1728
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
Object.assign(FormButtonElement, { whyDidYouRender: true });
|
|
463
|
-
var FormInputElement = forwardRef(({ onChange, ...props }, ref) => /* @__PURE__ */ jsx("input", { ...props, onChange: (e) => onChange(e.target.value), ref }));
|
|
464
|
-
FormInputElement.displayName = "FormInputElement";
|
|
465
|
-
Object.assign(FormInputElement, { whyDidYouRender: true });
|
|
466
|
-
var FormLabelElement = ({
|
|
467
|
-
name,
|
|
468
|
-
title,
|
|
469
|
-
description,
|
|
470
|
-
error,
|
|
471
|
-
children
|
|
472
|
-
}) => {
|
|
473
|
-
return /* @__PURE__ */ jsxs("label", { children: [
|
|
474
|
-
title ?? name,
|
|
475
|
-
children,
|
|
476
|
-
description,
|
|
477
|
-
error && /* @__PURE__ */ jsx("div", { style: { color: "red" }, children: error.message })
|
|
478
|
-
] });
|
|
479
|
-
};
|
|
480
|
-
FormLabelElement.displayName = "FormLabelElement";
|
|
481
|
-
FormLabelElement.whyDidYouRender = true;
|
|
482
|
-
|
|
483
|
-
// src/Form/elements/index.ts
|
|
484
|
-
var FormDefaultElements = {
|
|
485
|
-
Label: FormLabelElement,
|
|
486
|
-
Input: FormInputElement,
|
|
487
|
-
Button: FormButtonElement
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
// src/Form/rules.ts
|
|
491
|
-
var FormDefaultRules = {
|
|
492
|
-
required: async (value, _, lang) => {
|
|
493
|
-
if (value === null || value === void 0 || value === "" || Number.isNaN(value)) {
|
|
494
|
-
throw Error(lang?.required);
|
|
495
|
-
}
|
|
496
|
-
},
|
|
497
|
-
type: async (value, options, lang) => {
|
|
498
|
-
switch (options) {
|
|
499
|
-
case "string":
|
|
500
|
-
if (typeof value !== "string") throw Error(lang?.string);
|
|
501
|
-
break;
|
|
502
|
-
case "number":
|
|
503
|
-
if (Number.isNaN(Number(value))) throw Error(lang?.number);
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
},
|
|
507
|
-
custom: async (value, options) => {
|
|
508
|
-
return options(value);
|
|
509
|
-
}
|
|
510
|
-
};
|
|
511
|
-
async function validValues(rules, items, values, lang) {
|
|
512
|
-
const errors = {};
|
|
513
|
-
for (const item of items) {
|
|
514
|
-
const value = values[item.name];
|
|
515
|
-
const rulesOptions = item.rules;
|
|
516
|
-
if (rulesOptions) {
|
|
517
|
-
for (const [name, options] of Object.entries(rulesOptions)) {
|
|
518
|
-
try {
|
|
519
|
-
await rules[name](value, options, lang);
|
|
520
|
-
} catch (error) {
|
|
521
|
-
errors[item.name] = error;
|
|
522
|
-
break;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
return errors;
|
|
528
|
-
}
|
|
529
|
-
function FormFooter() {
|
|
530
|
-
const {
|
|
531
|
-
submitting,
|
|
532
|
-
setSubmitting,
|
|
533
|
-
onSubmit,
|
|
534
|
-
valuesRef,
|
|
535
|
-
Elements,
|
|
536
|
-
items,
|
|
537
|
-
setErrors,
|
|
538
|
-
lang,
|
|
539
|
-
rules
|
|
540
|
-
} = useFormContext();
|
|
541
|
-
const handleSubmit = useCallback(async () => {
|
|
542
|
-
setSubmitting(true);
|
|
543
|
-
setErrors({});
|
|
544
|
-
const errors = await validValues(rules, items, valuesRef.current, lang);
|
|
545
|
-
if (Object.keys(errors).length) {
|
|
546
|
-
setErrors(errors);
|
|
547
|
-
setSubmitting(false);
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
onSubmit(valuesRef.current).finally(() => setSubmitting(false));
|
|
551
|
-
}, [setSubmitting, setErrors, rules, items, lang, onSubmit]);
|
|
552
|
-
const MemoizedButton = useMemo(
|
|
553
|
-
() => /* @__PURE__ */ jsx(Elements.Button, { submitting, submit: handleSubmit, children: lang.submit }),
|
|
554
|
-
[submitting, handleSubmit, lang.submit, Elements.Button]
|
|
555
|
-
);
|
|
556
|
-
return MemoizedButton;
|
|
557
|
-
}
|
|
558
|
-
FormFooter.displayName = "FormFooter";
|
|
559
|
-
FormFooter.whyDidYouRender = true;
|
|
560
|
-
|
|
561
|
-
// src/Form/lang.ts
|
|
562
|
-
var FormDefaultLang = {
|
|
563
|
-
submit: "Submit",
|
|
564
|
-
required: "This field is required",
|
|
565
|
-
string: "This field must be a string",
|
|
566
|
-
number: "This field must be a number"
|
|
567
|
-
};
|
|
568
|
-
function mergeValues(items, defaultValues = {}) {
|
|
569
|
-
const values = {};
|
|
570
|
-
for (const item of items)
|
|
571
|
-
values[item.name] = defaultValues[item.name] ?? "";
|
|
572
|
-
return values;
|
|
573
|
-
}
|
|
574
|
-
function FormContainer({
|
|
575
|
-
defaultValues,
|
|
576
|
-
Elements,
|
|
577
|
-
rules,
|
|
578
|
-
lang,
|
|
579
|
-
items,
|
|
580
|
-
...props
|
|
581
|
-
}) {
|
|
582
|
-
const [values, setValues, valuesRef] = useStateRef(
|
|
583
|
-
mergeValues(items, defaultValues)
|
|
584
|
-
);
|
|
585
|
-
return /* @__PURE__ */ jsxs(
|
|
586
|
-
FormContextProvider,
|
|
587
|
-
{
|
|
588
|
-
initializeStates: {
|
|
589
|
-
errors: {},
|
|
590
|
-
submitting: false
|
|
591
|
-
},
|
|
592
|
-
value: {
|
|
593
|
-
Elements: Object.assign(FormDefaultElements, Elements),
|
|
594
|
-
lang: Object.assign(FormDefaultLang, lang),
|
|
595
|
-
rules: Object.assign(FormDefaultRules, rules),
|
|
596
|
-
items,
|
|
597
|
-
values,
|
|
598
|
-
setValues,
|
|
599
|
-
valuesRef,
|
|
600
|
-
...props
|
|
601
|
-
},
|
|
602
|
-
memo: true,
|
|
603
|
-
children: [
|
|
604
|
-
/* @__PURE__ */ jsx(FormBody, {}),
|
|
605
|
-
/* @__PURE__ */ jsx(FormFooter, {})
|
|
606
|
-
]
|
|
607
|
-
}
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
FormContainer.displayName = "FormContainer";
|
|
611
|
-
FormContainer.whyDidYouRender = true;
|
|
612
|
-
var OptionalWrapper = ({ condition, Wrapper, wrapperProps, children }) => {
|
|
613
|
-
return condition ? /* @__PURE__ */ jsx(Wrapper, { ...wrapperProps, children }) : /* @__PURE__ */ jsx(Fragment, { children });
|
|
614
|
-
};
|
|
615
|
-
OptionalWrapper.displayName = "OptionalWrapper";
|
|
616
|
-
OptionalWrapper.whyDidYouRender = true;
|
|
1729
|
+
//#endregion
|
|
1730
|
+
//#region src/usePrevious.ts
|
|
1731
|
+
/**
|
|
1732
|
+
* Hook to store the previous value of a state or prop.
|
|
1733
|
+
*
|
|
1734
|
+
* @template T - The type of the value.
|
|
1735
|
+
* @param value - The current value to track.
|
|
1736
|
+
* @returns Previous value from the prior render, or `undefined` on the first render.
|
|
1737
|
+
*
|
|
1738
|
+
* @example
|
|
1739
|
+
* ```tsx
|
|
1740
|
+
* import { usePrevious } from '@faasjs/react'
|
|
1741
|
+
*
|
|
1742
|
+
* function Counter({ count }: { count: number }) {
|
|
1743
|
+
* const previous = usePrevious(count)
|
|
1744
|
+
*
|
|
1745
|
+
* return <span>{previous} -> {count}</span>
|
|
1746
|
+
* }
|
|
1747
|
+
* ```
|
|
1748
|
+
*/
|
|
617
1749
|
function usePrevious(value) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
1750
|
+
const ref = useRef(void 0);
|
|
1751
|
+
useEffect(() => {
|
|
1752
|
+
ref.current = value;
|
|
1753
|
+
});
|
|
1754
|
+
return ref.current;
|
|
1755
|
+
}
|
|
1756
|
+
//#endregion
|
|
1757
|
+
//#region src/useStateRef.ts
|
|
1758
|
+
/**
|
|
1759
|
+
* Custom hook that returns a stateful value and a ref to that value.
|
|
1760
|
+
*
|
|
1761
|
+
* @template T - The type of the value.
|
|
1762
|
+
* @param initialValue - Initial state value. When omitted, state starts as `null`.
|
|
1763
|
+
* @returns Tuple containing the current state, the state setter, and a ref that always points at the latest state.
|
|
1764
|
+
*
|
|
1765
|
+
* @example
|
|
1766
|
+
* ```tsx
|
|
1767
|
+
* import { useStateRef } from '@faasjs/react'
|
|
1768
|
+
*
|
|
1769
|
+
* function MyComponent() {
|
|
1770
|
+
* const [value, setValue, ref] = useStateRef(0)
|
|
1771
|
+
*
|
|
1772
|
+
* return (
|
|
1773
|
+
* <div>
|
|
1774
|
+
* <p>Value: {value}</p>
|
|
1775
|
+
* <button onClick={() => setValue(value + 1)}>Increment</button>
|
|
1776
|
+
* <button onClick={() => console.log(ref.current)}>Submit</button>
|
|
1777
|
+
* </div>
|
|
1778
|
+
* )
|
|
1779
|
+
* }
|
|
1780
|
+
* ```
|
|
1781
|
+
*/
|
|
1782
|
+
function useStateRef(initialValue) {
|
|
1783
|
+
const [state, setState] = useState(initialValue ?? null);
|
|
1784
|
+
const ref = useRef(state);
|
|
1785
|
+
useEffect(() => {
|
|
1786
|
+
ref.current = state;
|
|
1787
|
+
}, [state]);
|
|
1788
|
+
return [
|
|
1789
|
+
state,
|
|
1790
|
+
setState,
|
|
1791
|
+
ref
|
|
1792
|
+
];
|
|
623
1793
|
}
|
|
624
|
-
|
|
625
|
-
export { ErrorBoundary, FaasDataWrapper, FaasReactClient,
|
|
1794
|
+
//#endregion
|
|
1795
|
+
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 };
|