@djvlc/runtime-host-react 1.1.2 → 1.1.3

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/dist/index.js CHANGED
@@ -1,8 +1,16 @@
1
1
  // src/react-runtime.ts
2
- import { useState, useCallback, useRef } from "react";
2
+ import { useState, useCallback, useRef, useMemo } from "react";
3
3
  import {
4
4
  createRuntime
5
5
  } from "@djvlc/runtime-core";
6
+ var defaultMetrics = {
7
+ initTime: 0,
8
+ loadTime: 0,
9
+ renderTime: 0,
10
+ totalTime: 0,
11
+ initTimestamp: null,
12
+ readyTimestamp: null
13
+ };
6
14
  function useReactRuntime(options) {
7
15
  const [runtime, setRuntime] = useState(null);
8
16
  const [loading, setLoading] = useState(true);
@@ -10,12 +18,29 @@ function useReactRuntime(options) {
10
18
  const [page, setPage] = useState(null);
11
19
  const [error, setError] = useState(null);
12
20
  const [hostApi, setHostApi] = useState(null);
21
+ const [metrics, setMetrics] = useState(defaultMetrics);
13
22
  const runtimeRef = useRef(null);
23
+ const timingRef = useRef({
24
+ startTime: 0,
25
+ initStartTime: 0,
26
+ loadStartTime: 0,
27
+ renderStartTime: 0
28
+ });
29
+ const isReady = useMemo(() => phase === "ready", [phase]);
30
+ const hasError = useMemo(() => phase === "error" || error !== null, [phase, error]);
14
31
  const init = useCallback(async () => {
15
32
  const container = options.containerRef?.current;
16
33
  if (!container) {
17
34
  throw new Error("Container element not found");
18
35
  }
36
+ timingRef.current.startTime = performance.now();
37
+ timingRef.current.initStartTime = timingRef.current.startTime;
38
+ if (options.enableMetrics) {
39
+ setMetrics((prev) => ({
40
+ ...prev,
41
+ initTimestamp: Date.now()
42
+ }));
43
+ }
19
44
  const runtimeInstance = createRuntime({
20
45
  ...options,
21
46
  container,
@@ -41,23 +66,46 @@ function useReactRuntime(options) {
41
66
  });
42
67
  await runtimeInstance.init();
43
68
  setHostApi(runtimeInstance.getHostApi());
69
+ if (options.enableMetrics) {
70
+ setMetrics((prev) => ({
71
+ ...prev,
72
+ initTime: performance.now() - timingRef.current.initStartTime
73
+ }));
74
+ }
44
75
  }, [options]);
45
76
  const load = useCallback(async () => {
46
77
  if (!runtimeRef.current) {
47
78
  throw new Error("Runtime not initialized");
48
79
  }
80
+ timingRef.current.loadStartTime = performance.now();
49
81
  const result = await runtimeRef.current.load();
50
82
  setPage(result);
51
83
  setHostApi(runtimeRef.current.getHostApi());
84
+ if (options.enableMetrics) {
85
+ setMetrics((prev) => ({
86
+ ...prev,
87
+ loadTime: performance.now() - timingRef.current.loadStartTime
88
+ }));
89
+ }
52
90
  return result;
53
- }, []);
91
+ }, [options.enableMetrics]);
54
92
  const render = useCallback(async () => {
55
93
  if (!runtimeRef.current) {
56
94
  throw new Error("Runtime not initialized");
57
95
  }
96
+ timingRef.current.renderStartTime = performance.now();
58
97
  await runtimeRef.current.render();
59
98
  setLoading(false);
60
- }, []);
99
+ if (options.enableMetrics) {
100
+ const now = performance.now();
101
+ setMetrics((prev) => ({
102
+ ...prev,
103
+ renderTime: now - timingRef.current.renderStartTime,
104
+ totalTime: now - timingRef.current.startTime,
105
+ readyTimestamp: Date.now()
106
+ }));
107
+ }
108
+ }, [options.enableMetrics]);
61
109
  const destroy = useCallback(() => {
62
110
  runtimeRef.current?.destroy();
63
111
  runtimeRef.current = null;
@@ -67,13 +115,43 @@ function useReactRuntime(options) {
67
115
  setError(null);
68
116
  setPhase("idle");
69
117
  setLoading(true);
118
+ setMetrics(defaultMetrics);
70
119
  }, []);
120
+ const reload = useCallback(async () => {
121
+ if (!runtimeRef.current) {
122
+ throw new Error("Runtime not initialized");
123
+ }
124
+ setError(null);
125
+ setLoading(true);
126
+ await load();
127
+ await render();
128
+ }, [load, render]);
71
129
  const setVariable = useCallback((key, value) => {
72
130
  runtimeRef.current?.setVariable(key, value);
73
131
  }, []);
132
+ const setVariables = useCallback((variables) => {
133
+ if (!runtimeRef.current) return;
134
+ Object.entries(variables).forEach(([key, value]) => {
135
+ runtimeRef.current?.setVariable(key, value);
136
+ });
137
+ }, []);
138
+ const getVariable = useCallback((key) => {
139
+ return runtimeRef.current?.getState().variables[key];
140
+ }, []);
74
141
  const refreshData = useCallback(async (queryId) => {
75
142
  await runtimeRef.current?.refreshData(queryId);
76
143
  }, []);
144
+ const executeAction = useCallback(async (actionType, params) => {
145
+ const api = hostApi;
146
+ if (!api) {
147
+ throw new Error("HostAPI not available");
148
+ }
149
+ const response = await api.executeAction(actionType, params || {});
150
+ if (response.success) {
151
+ return response.data;
152
+ }
153
+ throw new Error(response.errorMessage || "Action failed");
154
+ }, [hostApi]);
77
155
  return {
78
156
  runtime,
79
157
  loading,
@@ -81,12 +159,19 @@ function useReactRuntime(options) {
81
159
  page,
82
160
  error,
83
161
  hostApi,
162
+ isReady,
163
+ hasError,
164
+ metrics,
84
165
  init,
85
166
  load,
86
167
  render,
87
168
  destroy,
169
+ reload,
88
170
  setVariable,
89
- refreshData
171
+ setVariables,
172
+ getVariable,
173
+ refreshData,
174
+ executeAction
90
175
  };
91
176
  }
92
177
 
@@ -142,13 +227,20 @@ function DJVRenderer({
142
227
  onLoad,
143
228
  onError,
144
229
  onReady,
230
+ onPhaseChange,
145
231
  loadingComponent,
146
232
  errorComponent,
233
+ emptyComponent,
147
234
  children,
148
235
  className,
149
- style
236
+ style,
237
+ retryCount = 3,
238
+ retryDelay = 1e3,
239
+ timeout = 3e4
150
240
  }) {
151
241
  const containerRef = useRef2(null);
242
+ const mountedRef = useRef2(true);
243
+ const retryCountRef = useRef2(0);
152
244
  const [state, setState] = useState2({
153
245
  phase: "idle",
154
246
  page: null,
@@ -177,8 +269,8 @@ function DJVRenderer({
177
269
  const {
178
270
  runtime,
179
271
  loading,
180
- phase: _phase,
181
- page: _page,
272
+ phase,
273
+ page,
182
274
  error,
183
275
  hostApi,
184
276
  init,
@@ -186,27 +278,61 @@ function DJVRenderer({
186
278
  render,
187
279
  destroy
188
280
  } = useReactRuntime(options);
189
- void _phase;
190
- void _page;
191
- const initAndLoad = useCallback2(async () => {
192
- if (!containerRef.current) return;
281
+ useEffect(() => {
282
+ onPhaseChange?.(phase);
283
+ }, [phase, onPhaseChange]);
284
+ const initWithRetry = useCallback2(async () => {
285
+ if (!containerRef.current || !mountedRef.current) return;
286
+ const timeoutPromise = new Promise((_, reject) => {
287
+ setTimeout(() => reject(new Error("\u52A0\u8F7D\u8D85\u65F6")), timeout);
288
+ });
193
289
  try {
194
- await init();
195
- const pageData = await load();
196
- onLoad?.(pageData);
197
- await render();
198
- onReady?.();
199
- if (runtime) {
200
- runtime.onStateChange((newState) => {
201
- setState(newState);
202
- });
203
- }
290
+ await Promise.race([
291
+ (async () => {
292
+ await init();
293
+ const pageData = await load();
294
+ if (mountedRef.current) {
295
+ onLoad?.(pageData);
296
+ }
297
+ await render();
298
+ if (mountedRef.current) {
299
+ onReady?.();
300
+ retryCountRef.current = 0;
301
+ if (runtime) {
302
+ runtime.onStateChange((newState) => {
303
+ if (mountedRef.current) {
304
+ setState(newState);
305
+ }
306
+ });
307
+ }
308
+ }
309
+ })(),
310
+ timeoutPromise
311
+ ]);
204
312
  } catch (err) {
313
+ if (!mountedRef.current) return;
314
+ if (retryCountRef.current < retryCount) {
315
+ retryCountRef.current++;
316
+ if (debug) {
317
+ console.log(`[DJVRenderer] \u91CD\u8BD5 ${retryCountRef.current}/${retryCount}...`);
318
+ }
319
+ setTimeout(() => {
320
+ if (mountedRef.current) {
321
+ initWithRetry();
322
+ }
323
+ }, retryDelay);
324
+ }
205
325
  }
206
- }, [init, load, render, runtime, onLoad, onReady]);
326
+ }, [init, load, render, runtime, onLoad, onReady, timeout, retryCount, retryDelay, debug]);
327
+ const handleRetry = useCallback2(() => {
328
+ retryCountRef.current = 0;
329
+ initWithRetry();
330
+ }, [initWithRetry]);
207
331
  useEffect(() => {
208
- initAndLoad();
332
+ mountedRef.current = true;
333
+ initWithRetry();
209
334
  return () => {
335
+ mountedRef.current = false;
210
336
  destroy();
211
337
  };
212
338
  }, [pageUid]);
@@ -214,18 +340,43 @@ function DJVRenderer({
214
340
  if (!loading) return null;
215
341
  return loadingComponent || /* @__PURE__ */ jsxs("div", { className: "djvlc-loading", children: [
216
342
  /* @__PURE__ */ jsx2("div", { className: "djvlc-loading-spinner" }),
217
- /* @__PURE__ */ jsx2("span", { children: "\u52A0\u8F7D\u4E2D..." })
343
+ /* @__PURE__ */ jsx2("span", { children: "\u52A0\u8F7D\u4E2D..." }),
344
+ retryCountRef.current > 0 && /* @__PURE__ */ jsxs("span", { className: "djvlc-loading-retry", children: [
345
+ "\u91CD\u8BD5 ",
346
+ retryCountRef.current,
347
+ "/",
348
+ retryCount
349
+ ] })
218
350
  ] });
219
351
  };
220
352
  const renderError = () => {
221
353
  if (!error) return null;
222
354
  if (typeof errorComponent === "function") {
223
- return errorComponent(error);
355
+ return errorComponent(error, handleRetry);
356
+ }
357
+ return errorComponent || /* @__PURE__ */ jsxs("div", { className: "djvlc-error", children: [
358
+ /* @__PURE__ */ jsx2("div", { className: "djvlc-error-icon", children: "\u26A0\uFE0F" }),
359
+ /* @__PURE__ */ jsxs("span", { className: "djvlc-error-message", children: [
360
+ "\u52A0\u8F7D\u5931\u8D25\uFF1A",
361
+ error.message
362
+ ] }),
363
+ /* @__PURE__ */ jsx2(
364
+ "button",
365
+ {
366
+ className: "djvlc-error-retry-btn",
367
+ onClick: handleRetry,
368
+ type: "button",
369
+ children: "\u91CD\u8BD5"
370
+ }
371
+ )
372
+ ] });
373
+ };
374
+ const renderEmpty = () => {
375
+ if (loading || error || page) return null;
376
+ if (phase === "ready" && !page) {
377
+ return emptyComponent || /* @__PURE__ */ jsx2("div", { className: "djvlc-empty", children: /* @__PURE__ */ jsx2("span", { children: "\u6682\u65E0\u5185\u5BB9" }) });
224
378
  }
225
- return errorComponent || /* @__PURE__ */ jsx2("div", { className: "djvlc-error", children: /* @__PURE__ */ jsxs("span", { children: [
226
- "\u52A0\u8F7D\u5931\u8D25\uFF1A",
227
- error.message
228
- ] }) });
379
+ return null;
229
380
  };
230
381
  return /* @__PURE__ */ jsx2(RuntimeProvider, { runtime, state, hostApi, children: /* @__PURE__ */ jsxs(
231
382
  "div",
@@ -233,9 +384,12 @@ function DJVRenderer({
233
384
  ref: containerRef,
234
385
  className: `djvlc-renderer ${className || ""}`,
235
386
  style,
387
+ "data-phase": phase,
388
+ "data-page-uid": pageUid,
236
389
  children: [
237
390
  renderLoading(),
238
391
  renderError(),
392
+ renderEmpty(),
239
393
  children
240
394
  ]
241
395
  }
@@ -243,51 +397,94 @@ function DJVRenderer({
243
397
  }
244
398
 
245
399
  // src/components/DJVProvider.tsx
246
- import { useState as useState3, useEffect as useEffect2 } from "react";
400
+ import { useState as useState3, useEffect as useEffect2, useRef as useRef3 } from "react";
247
401
  import { jsx as jsx3 } from "react/jsx-runtime";
402
+ var defaultState2 = {
403
+ phase: "idle",
404
+ page: null,
405
+ variables: {},
406
+ queries: {},
407
+ components: /* @__PURE__ */ new Map(),
408
+ error: null,
409
+ destroyed: false
410
+ };
248
411
  function DJVProvider({
249
412
  runtime,
250
413
  hostApi,
251
- children
414
+ children,
415
+ className,
416
+ debug = false,
417
+ onStateChange,
418
+ onPhaseChange,
419
+ onError
252
420
  }) {
253
421
  const [state, setState] = useState3(
254
- runtime?.getState() || {
255
- phase: "idle",
256
- page: null,
257
- variables: {},
258
- queries: {},
259
- components: /* @__PURE__ */ new Map(),
260
- error: null,
261
- destroyed: false
262
- }
422
+ runtime?.getState() || defaultState2
263
423
  );
424
+ const prevPhaseRef = useRef3(state.phase);
264
425
  useEffect2(() => {
265
- if (!runtime) return;
426
+ if (!runtime) {
427
+ setState(defaultState2);
428
+ return;
429
+ }
430
+ setState(runtime.getState());
266
431
  const unsubscribe = runtime.onStateChange((newState) => {
267
432
  setState(newState);
433
+ onStateChange?.(newState);
434
+ if (newState.phase !== prevPhaseRef.current) {
435
+ onPhaseChange?.(newState.phase);
436
+ if (debug) {
437
+ console.log(`[DJVProvider] Phase changed: ${prevPhaseRef.current} -> ${newState.phase}`);
438
+ }
439
+ prevPhaseRef.current = newState.phase;
440
+ }
441
+ if (newState.error) {
442
+ onError?.(newState.error);
443
+ }
268
444
  });
269
445
  return () => {
270
446
  unsubscribe();
271
447
  };
272
- }, [runtime]);
273
- return /* @__PURE__ */ jsx3(RuntimeProvider, { runtime, state, hostApi, children: /* @__PURE__ */ jsx3("div", { className: "djvlc-provider", children }) });
448
+ }, [runtime, onStateChange, onPhaseChange, onError, debug]);
449
+ useEffect2(() => {
450
+ }, [hostApi]);
451
+ return /* @__PURE__ */ jsx3(RuntimeProvider, { runtime, state, hostApi, children: /* @__PURE__ */ jsx3(
452
+ "div",
453
+ {
454
+ className: ["djvlc-provider", className].filter(Boolean).join(" "),
455
+ "data-phase": state.phase,
456
+ children
457
+ }
458
+ ) });
274
459
  }
275
460
 
276
461
  // src/hooks/useDJVRuntime.ts
277
- import { useCallback as useCallback3, useState as useState4, useEffect as useEffect3, useMemo } from "react";
462
+ import { useCallback as useCallback3, useState as useState4, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef4 } from "react";
278
463
  function useDJVRuntime() {
279
464
  const context = useRuntimeContext();
280
- const loading = useMemo(() => {
465
+ const loading = useMemo2(() => {
281
466
  const phase = context.state.phase;
282
467
  return phase !== "ready" && phase !== "error";
283
468
  }, [context.state.phase]);
469
+ const isReady = context.state.phase === "ready";
470
+ const hasError = context.state.phase === "error" || context.state.error !== null;
471
+ const reload = useCallback3(async () => {
472
+ if (!context.runtime) {
473
+ throw new Error("Runtime not available");
474
+ }
475
+ await context.runtime.load();
476
+ await context.runtime.render();
477
+ }, [context.runtime]);
284
478
  return {
285
479
  runtime: context.runtime,
286
480
  state: context.state,
287
481
  loading,
288
482
  phase: context.state.phase,
289
483
  error: context.state.error,
290
- page: context.state.page
484
+ page: context.state.page,
485
+ isReady,
486
+ hasError,
487
+ reload
291
488
  };
292
489
  }
293
490
  function useHostApi() {
@@ -312,67 +509,143 @@ function useRuntimeStateWritable(key, defaultValue) {
312
509
  );
313
510
  return [value, setValue];
314
511
  }
315
- function useQuery(queryId) {
512
+ function useQuery(queryId, options) {
316
513
  const context = useRuntimeContext();
317
514
  const [loading, setLoading] = useState4(false);
318
515
  const [error, setError] = useState4(null);
516
+ const [lastUpdated, setLastUpdated] = useState4(null);
517
+ const mountedRef = useRef4(true);
319
518
  const data = context.state.queries[queryId];
320
519
  const refetch = useCallback3(async () => {
520
+ if (!mountedRef.current) return;
321
521
  setLoading(true);
322
522
  setError(null);
323
523
  try {
324
524
  await context.runtime?.refreshData(queryId);
525
+ if (mountedRef.current) {
526
+ setLastUpdated(Date.now());
527
+ }
325
528
  } catch (e) {
326
- setError(e);
529
+ if (mountedRef.current) {
530
+ setError(e);
531
+ }
327
532
  } finally {
328
- setLoading(false);
533
+ if (mountedRef.current) {
534
+ setLoading(false);
535
+ }
329
536
  }
330
537
  }, [context.runtime, queryId]);
538
+ useEffect3(() => {
539
+ mountedRef.current = true;
540
+ if (options?.refreshOnMount && context.runtime) {
541
+ refetch();
542
+ }
543
+ return () => {
544
+ mountedRef.current = false;
545
+ };
546
+ }, []);
547
+ useEffect3(() => {
548
+ if (!options?.refreshInterval || options.refreshInterval <= 0) return;
549
+ const timer = setInterval(() => {
550
+ refetch();
551
+ }, options.refreshInterval);
552
+ return () => clearInterval(timer);
553
+ }, [options?.refreshInterval, refetch]);
554
+ useEffect3(() => {
555
+ if (!options?.refreshOnFocus) return;
556
+ const handleFocus = () => {
557
+ refetch();
558
+ };
559
+ window.addEventListener("focus", handleFocus);
560
+ return () => window.removeEventListener("focus", handleFocus);
561
+ }, [options?.refreshOnFocus, refetch]);
331
562
  return {
332
563
  data,
333
564
  loading,
334
565
  error,
335
- refetch
566
+ refetch,
567
+ lastUpdated
336
568
  };
337
569
  }
338
- function useAction(actionType) {
570
+ function useAction(actionType, options) {
339
571
  const context = useRuntimeContext();
340
572
  const [loading, setLoading] = useState4(false);
341
573
  const [result, setResult] = useState4();
342
574
  const [error, setError] = useState4(null);
343
- const execute = useCallback3(
344
- async (params) => {
575
+ const [executionCount, setExecutionCount] = useState4(0);
576
+ const mountedRef = useRef4(true);
577
+ useEffect3(() => {
578
+ mountedRef.current = true;
579
+ return () => {
580
+ mountedRef.current = false;
581
+ };
582
+ }, []);
583
+ const reset = useCallback3(() => {
584
+ setResult(void 0);
585
+ setError(null);
586
+ setExecutionCount(0);
587
+ }, []);
588
+ const executeWithRetry = useCallback3(
589
+ async (params, retriesLeft) => {
345
590
  const hostApi = context.hostApi;
346
591
  if (!hostApi) {
347
592
  throw new Error("HostAPI not available");
348
593
  }
349
- setLoading(true);
350
- setError(null);
351
594
  try {
352
595
  const response = await hostApi.executeAction(
353
596
  actionType,
354
597
  params
355
598
  );
356
599
  if (response.success) {
357
- setResult(response.data);
358
600
  return response.data;
359
601
  } else {
360
- throw new Error(response.message || "Action failed");
602
+ throw new Error(response.errorMessage || "Action failed");
361
603
  }
362
604
  } catch (e) {
363
- setError(e);
605
+ if (retriesLeft > 0) {
606
+ await new Promise((resolve) => setTimeout(resolve, options?.retryDelay || 1e3));
607
+ return executeWithRetry(params, retriesLeft - 1);
608
+ }
609
+ throw e;
610
+ }
611
+ },
612
+ [context.hostApi, actionType, options?.retryDelay]
613
+ );
614
+ const execute = useCallback3(
615
+ async (params) => {
616
+ if (!mountedRef.current) return void 0;
617
+ setLoading(true);
618
+ setError(null);
619
+ setExecutionCount((c) => c + 1);
620
+ try {
621
+ const data = await executeWithRetry(params, options?.retryCount || 0);
622
+ if (mountedRef.current) {
623
+ setResult(data);
624
+ options?.onSuccess?.(data);
625
+ }
626
+ return data;
627
+ } catch (e) {
628
+ const err = e;
629
+ if (mountedRef.current) {
630
+ setError(err);
631
+ options?.onError?.(err);
632
+ }
364
633
  throw e;
365
634
  } finally {
366
- setLoading(false);
635
+ if (mountedRef.current) {
636
+ setLoading(false);
637
+ }
367
638
  }
368
639
  },
369
- [context.hostApi, actionType]
640
+ [executeWithRetry, options]
370
641
  );
371
642
  return {
372
643
  execute,
373
644
  loading,
374
645
  result,
375
- error
646
+ error,
647
+ reset,
648
+ executionCount
376
649
  };
377
650
  }
378
651
  function useData(queryId, params, options) {
@@ -380,42 +653,69 @@ function useData(queryId, params, options) {
380
653
  const [data, setData] = useState4();
381
654
  const [loading, setLoading] = useState4(false);
382
655
  const [error, setError] = useState4(null);
656
+ const [isFetched, setIsFetched] = useState4(false);
657
+ const mountedRef = useRef4(true);
658
+ const paramsRef = useRef4(params);
659
+ useEffect3(() => {
660
+ mountedRef.current = true;
661
+ return () => {
662
+ mountedRef.current = false;
663
+ };
664
+ }, []);
383
665
  const refetch = useCallback3(
384
666
  async (newParams) => {
385
667
  const hostApi = context.hostApi;
386
- if (!hostApi) {
387
- throw new Error("HostAPI not available");
388
- }
668
+ if (!hostApi || !mountedRef.current) return;
389
669
  setLoading(true);
390
670
  setError(null);
391
671
  try {
392
- const response = await hostApi.requestData(
672
+ const data2 = await hostApi.requestData(
393
673
  queryId,
394
- newParams || params
674
+ newParams || paramsRef.current
395
675
  );
396
- if (response.success) {
397
- setData(response.data);
398
- } else {
399
- throw new Error(response.message || "Query failed");
400
- }
676
+ if (!mountedRef.current) return;
677
+ setData(data2);
678
+ setIsFetched(true);
679
+ options?.onSuccess?.(data2);
401
680
  } catch (e) {
402
- setError(e);
681
+ if (mountedRef.current) {
682
+ const err = e;
683
+ setError(err);
684
+ options?.onError?.(err);
685
+ }
403
686
  } finally {
404
- setLoading(false);
687
+ if (mountedRef.current) {
688
+ setLoading(false);
689
+ }
405
690
  }
406
691
  },
407
- [context.hostApi, queryId, params]
692
+ [context.hostApi, queryId, options]
408
693
  );
694
+ useEffect3(() => {
695
+ const prevParams = paramsRef.current;
696
+ paramsRef.current = params;
697
+ if (options?.refreshOnParamsChange !== false && context.hostApi && isFetched && JSON.stringify(prevParams) !== JSON.stringify(params)) {
698
+ refetch();
699
+ }
700
+ }, [params, context.hostApi, options?.refreshOnParamsChange, isFetched, refetch]);
409
701
  useEffect3(() => {
410
702
  if (options?.immediate !== false && context.hostApi) {
411
703
  refetch();
412
704
  }
413
705
  }, [context.hostApi]);
706
+ useEffect3(() => {
707
+ if (!options?.refreshInterval || options.refreshInterval <= 0) return;
708
+ const timer = setInterval(() => {
709
+ refetch();
710
+ }, options.refreshInterval);
711
+ return () => clearInterval(timer);
712
+ }, [options?.refreshInterval, refetch]);
414
713
  return {
415
714
  data,
416
715
  loading,
417
716
  error,
418
- refetch
717
+ refetch,
718
+ isFetched
419
719
  };
420
720
  }
421
721
  function useRuntimeEvent(eventType, handler) {
@@ -429,19 +729,209 @@ function useRuntimeEvent(eventType, handler) {
429
729
  };
430
730
  }, [context.runtime, eventType, handler]);
431
731
  }
732
+ function usePageInfo() {
733
+ const context = useRuntimeContext();
734
+ const page = context.state.page;
735
+ return useMemo2(() => ({
736
+ /** 页面 UID */
737
+ pageUid: page?.pageUid,
738
+ /** 页面版本 ID */
739
+ pageVersionId: page?.pageVersionId,
740
+ /** Schema 版本 */
741
+ schemaVersion: page?.pageJson?.schemaVersion,
742
+ /** 页面标题 */
743
+ title: page?.title,
744
+ /** 页面配置 */
745
+ config: page?.config,
746
+ /** 页面是否已加载 */
747
+ isLoaded: page !== null
748
+ }), [page]);
749
+ }
750
+ function useComponentState(componentId) {
751
+ const context = useRuntimeContext();
752
+ const componentStatus = context.state.components.get(componentId);
753
+ return useMemo2(() => ({
754
+ /** 组件是否已加载 */
755
+ isLoaded: componentStatus?.status === "loaded",
756
+ /** 组件是否加载中 */
757
+ isLoading: componentStatus?.status === "loading",
758
+ /** 组件是否加载失败 */
759
+ hasError: componentStatus?.status === "failed",
760
+ /** 加载耗时 */
761
+ loadTime: componentStatus?.loadTime,
762
+ /** 组件信息 */
763
+ info: componentStatus
764
+ }), [componentStatus]);
765
+ }
766
+ function useLifecycle(options) {
767
+ const context = useRuntimeContext();
768
+ const [hasMounted, setHasMounted] = useState4(false);
769
+ const [hasReady, setHasReady] = useState4(false);
770
+ const prevPhaseRef = useRef4(context.state.phase);
771
+ const mountedRef = useRef4(true);
772
+ useEffect3(() => {
773
+ mountedRef.current = true;
774
+ return () => {
775
+ mountedRef.current = false;
776
+ };
777
+ }, []);
778
+ useEffect3(() => {
779
+ const currentPhase = context.state.phase;
780
+ if (currentPhase !== prevPhaseRef.current) {
781
+ options?.onPhaseChange?.(currentPhase);
782
+ prevPhaseRef.current = currentPhase;
783
+ }
784
+ if (!hasMounted && currentPhase !== "idle") {
785
+ setHasMounted(true);
786
+ Promise.resolve(options?.onMounted?.()).catch((error) => {
787
+ options?.onError?.(error);
788
+ });
789
+ }
790
+ if (!hasReady && currentPhase === "ready") {
791
+ setHasReady(true);
792
+ Promise.resolve(options?.onReady?.()).catch((error) => {
793
+ options?.onError?.(error);
794
+ });
795
+ }
796
+ if (currentPhase === "error" && context.state.error) {
797
+ options?.onError?.(context.state.error);
798
+ }
799
+ }, [context.state.phase, context.state.error, hasMounted, hasReady, options]);
800
+ return {
801
+ /** 当前阶段 */
802
+ phase: context.state.phase,
803
+ /** 是否已 mounted */
804
+ hasMounted,
805
+ /** 是否已 ready */
806
+ hasReady
807
+ };
808
+ }
809
+ function useWhen(condition, callback, options) {
810
+ const [executed, setExecuted] = useState4(false);
811
+ const { once = true } = options || {};
812
+ useEffect3(() => {
813
+ if (condition && (!once || !executed)) {
814
+ setExecuted(true);
815
+ callback();
816
+ }
817
+ }, [condition, callback, once, executed]);
818
+ return {
819
+ /** 是否已执行 */
820
+ executed
821
+ };
822
+ }
823
+ function useDebouncedAction(actionType, delay = 300) {
824
+ const { execute: rawExecute, loading, result, error } = useAction(actionType);
825
+ const timeoutRef = useRef4(null);
826
+ const mountedRef = useRef4(true);
827
+ useEffect3(() => {
828
+ mountedRef.current = true;
829
+ return () => {
830
+ mountedRef.current = false;
831
+ if (timeoutRef.current) {
832
+ clearTimeout(timeoutRef.current);
833
+ }
834
+ };
835
+ }, []);
836
+ const execute = useCallback3((params) => {
837
+ if (timeoutRef.current) {
838
+ clearTimeout(timeoutRef.current);
839
+ }
840
+ timeoutRef.current = setTimeout(() => {
841
+ if (mountedRef.current) {
842
+ rawExecute(params).catch(() => {
843
+ });
844
+ }
845
+ }, delay);
846
+ }, [rawExecute, delay]);
847
+ const cancel = useCallback3(() => {
848
+ if (timeoutRef.current) {
849
+ clearTimeout(timeoutRef.current);
850
+ timeoutRef.current = null;
851
+ }
852
+ }, []);
853
+ return {
854
+ execute,
855
+ loading,
856
+ result,
857
+ error,
858
+ cancel
859
+ };
860
+ }
861
+ function useAsync(asyncFn, deps = [], options) {
862
+ const [data, setData] = useState4();
863
+ const [loading, setLoading] = useState4(false);
864
+ const [error, setError] = useState4(null);
865
+ const mountedRef = useRef4(true);
866
+ useEffect3(() => {
867
+ mountedRef.current = true;
868
+ return () => {
869
+ mountedRef.current = false;
870
+ };
871
+ }, []);
872
+ const execute = useCallback3(async () => {
873
+ if (!mountedRef.current) return void 0;
874
+ setLoading(true);
875
+ setError(null);
876
+ try {
877
+ const result = await asyncFn();
878
+ if (mountedRef.current) {
879
+ setData(result);
880
+ options?.onSuccess?.(result);
881
+ }
882
+ return result;
883
+ } catch (e) {
884
+ const err = e;
885
+ if (mountedRef.current) {
886
+ setError(err);
887
+ options?.onError?.(err);
888
+ }
889
+ return void 0;
890
+ } finally {
891
+ if (mountedRef.current) {
892
+ setLoading(false);
893
+ }
894
+ }
895
+ }, [asyncFn, ...deps]);
896
+ useEffect3(() => {
897
+ if (options?.immediate !== false) {
898
+ execute();
899
+ }
900
+ }, []);
901
+ return {
902
+ data,
903
+ loading,
904
+ error,
905
+ execute
906
+ };
907
+ }
908
+ function usePrevious(value) {
909
+ const ref = useRef4();
910
+ useEffect3(() => {
911
+ ref.current = value;
912
+ }, [value]);
913
+ return ref.current;
914
+ }
432
915
  export {
433
916
  DJVProvider,
434
917
  DJVRenderer,
435
918
  RuntimeContext,
436
919
  RuntimeProvider,
437
920
  useAction,
921
+ useAsync,
922
+ useComponentState,
438
923
  useDJVRuntime,
439
924
  useData,
925
+ useDebouncedAction,
440
926
  useHostApi,
927
+ useLifecycle,
928
+ usePageInfo,
929
+ usePrevious,
441
930
  useQuery,
442
931
  useReactRuntime,
443
932
  useRuntimeContext,
444
933
  useRuntimeEvent,
445
934
  useRuntimeState,
446
- useRuntimeStateWritable
935
+ useRuntimeStateWritable,
936
+ useWhen
447
937
  };