@intrig/react 0.0.6 → 0.0.8
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/.babelrc +12 -0
- package/README.md +38 -3
- package/eslint.config.mjs +12 -0
- package/package.json +2 -6
- package/project.json +15 -0
- package/rollup.config.cjs +31 -0
- package/src/extra.ts +267 -0
- package/src/index.ts +4 -0
- package/src/intrig-context.ts +61 -0
- package/src/intrig-provider.tsx +539 -0
- package/src/logger.ts +57 -0
- package/src/media-type-utils.ts +198 -0
- package/src/network-state.tsx +538 -0
- package/tsconfig.json +10 -0
- package/tsconfig.lib.json +35 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
PropsWithChildren,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
useReducer,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import {
|
|
10
|
+
error,
|
|
11
|
+
ErrorState,
|
|
12
|
+
ErrorWithContext,
|
|
13
|
+
init,
|
|
14
|
+
IntrigHook,
|
|
15
|
+
isError,
|
|
16
|
+
isPending,
|
|
17
|
+
NetworkAction,
|
|
18
|
+
NetworkState,
|
|
19
|
+
pending,
|
|
20
|
+
Progress,
|
|
21
|
+
success,
|
|
22
|
+
} from './network-state';
|
|
23
|
+
import axios, {
|
|
24
|
+
Axios,
|
|
25
|
+
AxiosProgressEvent,
|
|
26
|
+
CreateAxiosDefaults,
|
|
27
|
+
isAxiosError,
|
|
28
|
+
} from 'axios';
|
|
29
|
+
import { ZodSchema } from 'zod';
|
|
30
|
+
import logger from './logger';
|
|
31
|
+
import { flushSync } from 'react-dom';
|
|
32
|
+
import { createParser } from 'eventsource-parser';
|
|
33
|
+
|
|
34
|
+
import { Context, RequestType, GlobalState } from './intrig-context';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Handles state updates for network requests based on the provided action.
|
|
38
|
+
*
|
|
39
|
+
* @param {GlobalState} state - The current state of the application.
|
|
40
|
+
* @param {NetworkAction<unknown>} action - The action containing source, operation, key, and state.
|
|
41
|
+
* @return {GlobalState} - The updated state after applying the action.
|
|
42
|
+
*/
|
|
43
|
+
function requestReducer(
|
|
44
|
+
state: GlobalState,
|
|
45
|
+
action: NetworkAction<unknown, unknown>,
|
|
46
|
+
): GlobalState {
|
|
47
|
+
return {
|
|
48
|
+
...state,
|
|
49
|
+
[`${action.source}:${action.operation}:${action.key}`]: action.state,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface DefaultConfigs extends CreateAxiosDefaults {
|
|
54
|
+
debounceDelay?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface IntrigProviderProps {
|
|
58
|
+
configs?: Record<string, DefaultConfigs>;
|
|
59
|
+
children: React.ReactNode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* IntrigProvider is a context provider component that sets up global state management
|
|
64
|
+
* and provides Axios instances for API requests.
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} props - The properties object.
|
|
67
|
+
* @param {React.ReactNode} props.children - The child components to be wrapped by the provider.
|
|
68
|
+
* @param {Object} [props.configs={}] - Configuration object for Axios instances.
|
|
69
|
+
* @param {Object} [props.configs.defaults={}] - Default configuration for Axios.
|
|
70
|
+
* @param {Object} [props.configs.petstore={}] - Configuration specific to the petstore API.
|
|
71
|
+
* @return {JSX.Element} A context provider component that wraps the provided children.
|
|
72
|
+
*/
|
|
73
|
+
export function IntrigProvider({
|
|
74
|
+
children,
|
|
75
|
+
configs = {},
|
|
76
|
+
}: IntrigProviderProps) {
|
|
77
|
+
const [state, dispatch] = useReducer(requestReducer, {} as GlobalState);
|
|
78
|
+
|
|
79
|
+
const axiosInstances: Record<string, Axios> = useMemo(() => {
|
|
80
|
+
return {
|
|
81
|
+
deamon_api: axios.create({
|
|
82
|
+
...(configs.defaults ?? {}),
|
|
83
|
+
...(configs['deamon_api'] ?? {}),
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
}, [configs]);
|
|
87
|
+
|
|
88
|
+
const contextValue = useMemo(() => {
|
|
89
|
+
async function execute<T, E = unknown>(
|
|
90
|
+
request: RequestType,
|
|
91
|
+
dispatch: (state: NetworkState<T, E>) => void,
|
|
92
|
+
schema: ZodSchema<T> | undefined,
|
|
93
|
+
errorSchema: ZodSchema<E> | undefined,
|
|
94
|
+
) {
|
|
95
|
+
try {
|
|
96
|
+
dispatch(pending());
|
|
97
|
+
const response = await axiosInstances[request.source].request(request);
|
|
98
|
+
|
|
99
|
+
if (response.status >= 200 && response.status < 300) {
|
|
100
|
+
if (
|
|
101
|
+
response.headers?.['content-type']?.includes('text/event-stream')
|
|
102
|
+
) {
|
|
103
|
+
const reader = response.data.getReader();
|
|
104
|
+
const decoder = new TextDecoder();
|
|
105
|
+
|
|
106
|
+
let lastMessage: any;
|
|
107
|
+
|
|
108
|
+
const parser = createParser({
|
|
109
|
+
onEvent(message) {
|
|
110
|
+
let decoded = message.data;
|
|
111
|
+
try {
|
|
112
|
+
let parsed = JSON.parse(decoded);
|
|
113
|
+
if (schema) {
|
|
114
|
+
const validated = schema.safeParse(parsed);
|
|
115
|
+
if (!validated.success) {
|
|
116
|
+
dispatch(
|
|
117
|
+
error(validated.error.issues, response.status, request),
|
|
118
|
+
);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
parsed = validated.data;
|
|
122
|
+
}
|
|
123
|
+
decoded = parsed;
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error(e);
|
|
126
|
+
}
|
|
127
|
+
lastMessage = decoded;
|
|
128
|
+
flushSync(() => dispatch(pending(undefined, decoded)));
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
while (true) {
|
|
133
|
+
const { done, value } = await reader.read();
|
|
134
|
+
if (done) {
|
|
135
|
+
flushSync(() => dispatch(success(lastMessage)));
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
parser.feed(decoder.decode(value, { stream: true }));
|
|
140
|
+
}
|
|
141
|
+
} else if (schema) {
|
|
142
|
+
const data = schema.safeParse(response.data);
|
|
143
|
+
if (!data.success) {
|
|
144
|
+
dispatch(error(data.error.issues, response.status, request));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
dispatch(success(data.data));
|
|
148
|
+
} else {
|
|
149
|
+
dispatch(success(response.data));
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
const { data } =
|
|
153
|
+
errorSchema?.safeParse(response.data ?? {}) ?? {};
|
|
154
|
+
//todo: handle error validation error.
|
|
155
|
+
dispatch(
|
|
156
|
+
error(
|
|
157
|
+
data ?? response.data ?? response.statusText,
|
|
158
|
+
response.status,
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
} catch (e: any) {
|
|
163
|
+
if (isAxiosError(e)) {
|
|
164
|
+
const { data } =
|
|
165
|
+
errorSchema?.safeParse(e.response?.data ?? {}) ?? {};
|
|
166
|
+
dispatch(
|
|
167
|
+
error(data ?? e.response?.data, e.response?.status, request),
|
|
168
|
+
);
|
|
169
|
+
} else {
|
|
170
|
+
dispatch(error(e));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
state,
|
|
177
|
+
dispatch,
|
|
178
|
+
filteredState: state,
|
|
179
|
+
configs,
|
|
180
|
+
execute,
|
|
181
|
+
};
|
|
182
|
+
}, [state, axiosInstances]);
|
|
183
|
+
|
|
184
|
+
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface StubType<P, B, T> {
|
|
188
|
+
<P, B, T>(
|
|
189
|
+
hook: IntrigHook<P, B, T>,
|
|
190
|
+
fn: (
|
|
191
|
+
params: P,
|
|
192
|
+
body: B,
|
|
193
|
+
dispatch: (state: NetworkState<T>) => void,
|
|
194
|
+
) => Promise<void>,
|
|
195
|
+
): void;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export type WithStubSupport<T> = T & {
|
|
199
|
+
stubs?: (stub: StubType<any, any, any>) => void;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export interface IntrigProviderStubProps {
|
|
203
|
+
configs?: DefaultConfigs;
|
|
204
|
+
stubs?: (stub: StubType<any, any, any>) => void;
|
|
205
|
+
children: React.ReactNode;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function IntrigProviderStub({
|
|
209
|
+
children,
|
|
210
|
+
configs = {},
|
|
211
|
+
stubs = () => {
|
|
212
|
+
// intentionally kept empty
|
|
213
|
+
},
|
|
214
|
+
}: IntrigProviderStubProps) {
|
|
215
|
+
const [state, dispatch] = useReducer(requestReducer, {} as GlobalState);
|
|
216
|
+
|
|
217
|
+
const collectedStubs = useMemo(() => {
|
|
218
|
+
const fns: Record<
|
|
219
|
+
string,
|
|
220
|
+
(
|
|
221
|
+
params: any,
|
|
222
|
+
body: any,
|
|
223
|
+
dispatch: (state: NetworkState<any>) => void,
|
|
224
|
+
) => Promise<void>
|
|
225
|
+
> = {};
|
|
226
|
+
function stub<P, B, T>(
|
|
227
|
+
hook: IntrigHook<P, B, T>,
|
|
228
|
+
fn: (
|
|
229
|
+
params: P,
|
|
230
|
+
body: B,
|
|
231
|
+
dispatch: (state: NetworkState<T>) => void,
|
|
232
|
+
) => Promise<void>,
|
|
233
|
+
) {
|
|
234
|
+
fns[hook.key] = fn;
|
|
235
|
+
}
|
|
236
|
+
stubs(stub);
|
|
237
|
+
return fns;
|
|
238
|
+
}, [stubs]);
|
|
239
|
+
|
|
240
|
+
const contextValue = useMemo(() => {
|
|
241
|
+
async function execute<T>(
|
|
242
|
+
request: RequestType,
|
|
243
|
+
dispatch: (state: NetworkState<T>) => void,
|
|
244
|
+
schema: ZodSchema<T> | undefined,
|
|
245
|
+
) {
|
|
246
|
+
const stub = collectedStubs[request.key];
|
|
247
|
+
|
|
248
|
+
if (stub) {
|
|
249
|
+
try {
|
|
250
|
+
await stub(request.params, request.data, dispatch);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
dispatch(error(e));
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
dispatch(init());
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
state,
|
|
261
|
+
dispatch,
|
|
262
|
+
filteredState: state,
|
|
263
|
+
configs,
|
|
264
|
+
execute,
|
|
265
|
+
};
|
|
266
|
+
}, [state, dispatch, configs, collectedStubs]);
|
|
267
|
+
|
|
268
|
+
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface StatusTrapProps {
|
|
272
|
+
type: 'pending' | 'error' | 'pending + error';
|
|
273
|
+
propagate?: boolean;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* StatusTrap component is used to track and manage network request states.
|
|
278
|
+
*
|
|
279
|
+
* @param {Object} props - The properties object.
|
|
280
|
+
* @param {React.ReactNode} props.children - The child elements to be rendered.
|
|
281
|
+
* @param {string} props.type - The type of network state to handle ("error", "pending", "pending + error").
|
|
282
|
+
* @param {boolean} [props.propagate=true] - Whether to propagate the event to the parent context.
|
|
283
|
+
* @return {React.ReactElement} The context provider component with filtered state and custom dispatch.
|
|
284
|
+
*/
|
|
285
|
+
export function StatusTrap({
|
|
286
|
+
children,
|
|
287
|
+
type,
|
|
288
|
+
propagate = true,
|
|
289
|
+
}: PropsWithChildren<StatusTrapProps>) {
|
|
290
|
+
const ctx = useContext(Context);
|
|
291
|
+
|
|
292
|
+
const [requests, setRequests] = useState<string[]>([]);
|
|
293
|
+
|
|
294
|
+
const shouldHandleEvent = useCallback(
|
|
295
|
+
(state: NetworkState) => {
|
|
296
|
+
switch (type) {
|
|
297
|
+
case 'error':
|
|
298
|
+
return isError(state);
|
|
299
|
+
case 'pending':
|
|
300
|
+
return isPending(state);
|
|
301
|
+
case 'pending + error':
|
|
302
|
+
return isPending(state) || isError(state);
|
|
303
|
+
default:
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
[type],
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const dispatch = useCallback(
|
|
311
|
+
(event: NetworkAction<any, any>) => {
|
|
312
|
+
if (!event.handled) {
|
|
313
|
+
if (shouldHandleEvent(event.state)) {
|
|
314
|
+
setRequests((prev) => [...prev, event.key]);
|
|
315
|
+
if (!propagate) {
|
|
316
|
+
ctx.dispatch({
|
|
317
|
+
...event,
|
|
318
|
+
handled: true,
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
setRequests((prev) => prev.filter((k) => k !== event.key));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
ctx.dispatch(event);
|
|
327
|
+
},
|
|
328
|
+
[ctx, propagate, shouldHandleEvent],
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const filteredState = useMemo(() => {
|
|
332
|
+
return Object.fromEntries(
|
|
333
|
+
Object.entries(ctx.state).filter(([key]) => requests.includes(key)),
|
|
334
|
+
);
|
|
335
|
+
}, [ctx.state, requests]);
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<Context.Provider
|
|
339
|
+
value={{
|
|
340
|
+
...ctx,
|
|
341
|
+
dispatch,
|
|
342
|
+
filteredState,
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
{children}
|
|
346
|
+
</Context.Provider>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface NetworkStateProps<T, E = unknown> {
|
|
351
|
+
key: string;
|
|
352
|
+
operation: string;
|
|
353
|
+
source: string;
|
|
354
|
+
schema?: ZodSchema<T>;
|
|
355
|
+
errorSchema?: ZodSchema<E>;
|
|
356
|
+
debounceDelay?: number;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* useNetworkState is a custom hook that manages the network state within the specified context.
|
|
361
|
+
* It handles making network requests, dispatching appropriate states based on the request lifecycle,
|
|
362
|
+
* and allows aborting ongoing requests.
|
|
363
|
+
*
|
|
364
|
+
* @param {Object} params - The parameters required to configure and use the network state.
|
|
365
|
+
* @param {string} params.key - A unique identifier for the network request.
|
|
366
|
+
* @param {string} params.operation - The operation type related to the request.
|
|
367
|
+
* @param {string} params.source - The source or endpoint for the network request.
|
|
368
|
+
* @param {Object} params.schema - The schema used for validating the response data.
|
|
369
|
+
* @param {number} [params.debounceDelay] - The debounce delay for executing the network request.
|
|
370
|
+
*
|
|
371
|
+
* @return {[NetworkState<T>, (request: AxiosRequestConfig) => void, () => void]}
|
|
372
|
+
* Returns a state object representing the current network state,
|
|
373
|
+
* a function to execute the network request, and a function to clear the request.
|
|
374
|
+
*/
|
|
375
|
+
export function useNetworkState<T, E = unknown>({
|
|
376
|
+
key,
|
|
377
|
+
operation,
|
|
378
|
+
source,
|
|
379
|
+
schema,
|
|
380
|
+
errorSchema,
|
|
381
|
+
debounceDelay: requestDebounceDelay,
|
|
382
|
+
}: NetworkStateProps<T>): [
|
|
383
|
+
NetworkState<T, E>,
|
|
384
|
+
(request: RequestType) => void,
|
|
385
|
+
clear: () => void,
|
|
386
|
+
(state: NetworkState<T, E>) => void,
|
|
387
|
+
] {
|
|
388
|
+
const context = useContext(Context);
|
|
389
|
+
|
|
390
|
+
const [abortController, setAbortController] = useState<AbortController>();
|
|
391
|
+
|
|
392
|
+
const networkState = useMemo(() => {
|
|
393
|
+
logger.info(`Updating status ${key} ${operation} ${source}`);
|
|
394
|
+
logger.debug('<=', context.state?.[`${source}:${operation}:${key}`]);
|
|
395
|
+
return (
|
|
396
|
+
(context.state?.[`${source}:${operation}:${key}`] as NetworkState<T>) ??
|
|
397
|
+
init()
|
|
398
|
+
);
|
|
399
|
+
}, [JSON.stringify(context.state?.[`${source}:${operation}:${key}`])]);
|
|
400
|
+
|
|
401
|
+
const dispatch = useCallback(
|
|
402
|
+
(state: NetworkState<T>) => {
|
|
403
|
+
context.dispatch({ key, operation, source, state });
|
|
404
|
+
},
|
|
405
|
+
[key, operation, source, context.dispatch],
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const debounceDelay = useMemo(() => {
|
|
409
|
+
return requestDebounceDelay ?? context.configs?.debounceDelay ?? 0;
|
|
410
|
+
}, [context.configs, requestDebounceDelay]);
|
|
411
|
+
|
|
412
|
+
const execute = useCallback(
|
|
413
|
+
async (request: RequestType) => {
|
|
414
|
+
logger.info(`Executing request ${key} ${operation} ${source}`);
|
|
415
|
+
logger.debug('=>', request);
|
|
416
|
+
|
|
417
|
+
const abortController = new AbortController();
|
|
418
|
+
setAbortController(abortController);
|
|
419
|
+
|
|
420
|
+
const requestConfig: RequestType = {
|
|
421
|
+
...request,
|
|
422
|
+
onUploadProgress(event: AxiosProgressEvent) {
|
|
423
|
+
dispatch(
|
|
424
|
+
pending({
|
|
425
|
+
type: 'upload',
|
|
426
|
+
loaded: event.loaded,
|
|
427
|
+
total: event.total,
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
430
|
+
request.onUploadProgress?.(event);
|
|
431
|
+
},
|
|
432
|
+
onDownloadProgress(event: AxiosProgressEvent) {
|
|
433
|
+
dispatch(
|
|
434
|
+
pending({
|
|
435
|
+
type: 'download',
|
|
436
|
+
loaded: event.loaded,
|
|
437
|
+
total: event.total,
|
|
438
|
+
}),
|
|
439
|
+
);
|
|
440
|
+
request.onDownloadProgress?.(event);
|
|
441
|
+
},
|
|
442
|
+
signal: abortController.signal,
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
await context.execute(
|
|
446
|
+
requestConfig,
|
|
447
|
+
dispatch,
|
|
448
|
+
schema,
|
|
449
|
+
errorSchema as any,
|
|
450
|
+
);
|
|
451
|
+
},
|
|
452
|
+
[networkState, context.dispatch, axios],
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const deboundedExecute = useMemo(
|
|
456
|
+
() => debounce(execute, debounceDelay ?? 0),
|
|
457
|
+
[execute],
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
const clear = useCallback(() => {
|
|
461
|
+
logger.info(`Clearing request ${key} ${operation} ${source}`);
|
|
462
|
+
dispatch(init());
|
|
463
|
+
setAbortController((abortController) => {
|
|
464
|
+
logger.info(`Aborting request ${key} ${operation} ${source}`);
|
|
465
|
+
abortController?.abort();
|
|
466
|
+
return undefined;
|
|
467
|
+
});
|
|
468
|
+
}, [dispatch, abortController]);
|
|
469
|
+
|
|
470
|
+
return [networkState, deboundedExecute, clear, dispatch];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function debounce<T extends (...args: any[]) => void>(func: T, delay: number) {
|
|
474
|
+
let timeoutId: any;
|
|
475
|
+
|
|
476
|
+
return (...args: Parameters<T>) => {
|
|
477
|
+
if (timeoutId) {
|
|
478
|
+
clearTimeout(timeoutId);
|
|
479
|
+
}
|
|
480
|
+
timeoutId = setTimeout(() => {
|
|
481
|
+
func(...args);
|
|
482
|
+
}, delay);
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Handles central error extraction from the provided context.
|
|
488
|
+
* It filters the state to retain error states and maps them to a structured error object with additional context information.
|
|
489
|
+
* @return {Object[]} An array of objects representing the error states with context information such as source, operation, and key.
|
|
490
|
+
*/
|
|
491
|
+
export function useCentralError() {
|
|
492
|
+
const ctx = useContext(Context);
|
|
493
|
+
|
|
494
|
+
return useMemo(() => {
|
|
495
|
+
return Object.entries(ctx.filteredState)
|
|
496
|
+
.filter(([, state]) => isError(state))
|
|
497
|
+
.map(([k, state]) => {
|
|
498
|
+
const [source, operation, key] = k.split(':');
|
|
499
|
+
return {
|
|
500
|
+
...(state as ErrorState<unknown>),
|
|
501
|
+
source,
|
|
502
|
+
operation,
|
|
503
|
+
key,
|
|
504
|
+
} satisfies ErrorWithContext;
|
|
505
|
+
});
|
|
506
|
+
}, [ctx.filteredState]);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Uses central pending state handling by aggregating pending states from context.
|
|
511
|
+
* It calculates the overall progress of pending states if any, or returns an initial state otherwise.
|
|
512
|
+
*
|
|
513
|
+
* @return {NetworkState} The aggregated network state based on the pending states and their progress.
|
|
514
|
+
*/
|
|
515
|
+
export function useCentralPendingState() {
|
|
516
|
+
const ctx = useContext(Context);
|
|
517
|
+
|
|
518
|
+
const result: NetworkState = useMemo(() => {
|
|
519
|
+
const pendingStates = Object.values(ctx.filteredState).filter(isPending);
|
|
520
|
+
if (!pendingStates.length) {
|
|
521
|
+
return init();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const progress = pendingStates
|
|
525
|
+
.filter((a) => a.progress)
|
|
526
|
+
.reduce(
|
|
527
|
+
(progress, current) => {
|
|
528
|
+
return {
|
|
529
|
+
total: progress.total + (current.progress?.total ?? 0),
|
|
530
|
+
loaded: progress.loaded + (current.progress?.loaded ?? 0),
|
|
531
|
+
};
|
|
532
|
+
},
|
|
533
|
+
{ total: 0, loaded: 0 } satisfies Progress,
|
|
534
|
+
);
|
|
535
|
+
return pending(progress.total ? progress : undefined);
|
|
536
|
+
}, [ctx.filteredState]);
|
|
537
|
+
|
|
538
|
+
return result;
|
|
539
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// logger.ts
|
|
2
|
+
|
|
3
|
+
import log, { LogLevelDesc } from 'loglevel';
|
|
4
|
+
|
|
5
|
+
// Extend the global interfaces
|
|
6
|
+
declare global {
|
|
7
|
+
interface window {
|
|
8
|
+
setLogLevel?: (level: LogLevelDesc) => void;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// 1) Build-time default via Vite (if available)
|
|
13
|
+
// Cast import.meta to any to avoid TS errors if env isn't typed
|
|
14
|
+
const buildDefault =
|
|
15
|
+
typeof import.meta !== 'undefined'
|
|
16
|
+
? ((import.meta as any).env?.VITE_LOG_LEVEL as string | undefined)
|
|
17
|
+
: undefined;
|
|
18
|
+
|
|
19
|
+
// 2) Stored default in localStorage
|
|
20
|
+
const storedLevel =
|
|
21
|
+
typeof localStorage !== 'undefined'
|
|
22
|
+
? (localStorage.getItem('LOG_LEVEL') as string | null)
|
|
23
|
+
: null;
|
|
24
|
+
|
|
25
|
+
// Determine initial log level: build-time → stored → 'error'
|
|
26
|
+
const defaultLevel: LogLevelDesc =
|
|
27
|
+
(buildDefault as LogLevelDesc) ?? (storedLevel as LogLevelDesc) ?? 'error';
|
|
28
|
+
|
|
29
|
+
// Apply initial level
|
|
30
|
+
log.setLevel(defaultLevel);
|
|
31
|
+
|
|
32
|
+
// Expose a console setter to change level at runtime
|
|
33
|
+
if (typeof window !== 'undefined') {
|
|
34
|
+
window.setLogLevel = (level: LogLevelDesc): void => {
|
|
35
|
+
log.setLevel(level);
|
|
36
|
+
try {
|
|
37
|
+
localStorage.setItem('LOG_LEVEL', String(level));
|
|
38
|
+
} catch {
|
|
39
|
+
// ignore if storage is unavailable
|
|
40
|
+
}
|
|
41
|
+
console.log(`✏️ loglevel set to '${level}'`);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Consistent wrapper API
|
|
46
|
+
export const logger = {
|
|
47
|
+
info: (msg: unknown, meta?: unknown): void =>
|
|
48
|
+
meta ? log.info(msg, meta) : log.info(msg),
|
|
49
|
+
warn: (msg: unknown, meta?: unknown): void =>
|
|
50
|
+
meta ? log.warn(msg, meta) : log.warn(msg),
|
|
51
|
+
error: (msg: unknown, meta?: unknown): void =>
|
|
52
|
+
meta ? log.error(msg, meta) : log.error(msg),
|
|
53
|
+
debug: (msg: unknown, meta?: unknown): void =>
|
|
54
|
+
meta ? log.debug(msg, meta) : log.debug(msg),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default logger;
|