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