@gears-frontx/react 0.2.0-alpha.0 → 0.2.0-alpha.2

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.d.cts CHANGED
@@ -366,10 +366,9 @@ declare function useApiSuspenseInfiniteQuery<TPage = unknown, TError = Error>(op
366
366
  /**
367
367
  * useApiStream - Declarative SSE streaming hook
368
368
  *
369
- * Accepts a StreamDescriptor from @gears-frontx/api and manages the EventSource
370
- * lifecycle: connects on mount, disconnects on unmount/descriptor change.
371
- * Returns the latest event, accumulated events, connection status, and
372
- * a manual disconnect function.
369
+ * Accepts a StreamDescriptor from @gears-frontx/api and returns the latest
370
+ * event, accumulated events, connection status, and a manual disconnect
371
+ * function.
373
372
  *
374
373
  * @example
375
374
  * ```tsx
@@ -395,17 +394,75 @@ interface ApiStreamOptions {
395
394
  }
396
395
  /** Return type of useApiStream. */
397
396
  interface ApiStreamResult<TEvent> {
398
- /** Latest event payload (always set in both modes). */
397
+ /** Latest event payload, set in both modes — `undefined` until the first event and after each connect attempt resets it. */
399
398
  data: TEvent | undefined;
400
399
  /** All received events when `mode: 'accumulate'`; empty array in `'latest'` mode. */
401
400
  events: TEvent[];
402
401
  /** Connection lifecycle status. */
403
402
  status: StreamStatus;
404
- /** Error if the connection failed. */
403
+ /** Last connect error — set when a connect attempt rejects, cleared when the next attempt starts. */
405
404
  error: Error | null;
406
405
  /** Manually close the connection. */
407
406
  disconnect: () => void;
408
407
  }
408
+ /**
409
+ * Manages the status lifecycle for a single SSE stream connection.
410
+ *
411
+ * **Status values**
412
+ * - `'idle'` — no connection is being attempted. This is the initial status
413
+ * before the first connect effect runs, and it is ordinarily the status
414
+ * while `enabled` is `false`. Consumers must not assume `'idle'` means "never
415
+ * connected" — a stream that is disabled after having connected returns to
416
+ * `'idle'` too, and `data`/`events` from the earlier connection are not
417
+ * cleared by that transition alone (they are cleared by mode/key changes,
418
+ * see below).
419
+ * - `'connecting'` — `descriptor.connect()` has been called and its promise
420
+ * has not yet settled.
421
+ * - `'connected'` — set both when `connect()` resolves (unless a disconnect
422
+ * was requested while it was pending) and, independently, whenever the
423
+ * `onEvent` callback delivers a new event. Because every event re-asserts
424
+ * `'connected'`, this status also serves as "still receiving events."
425
+ * - `'disconnected'` — reached via any of: the descriptor's `onComplete`
426
+ * callback firing (the stream ended normally), `connect()` rejecting while
427
+ * a disconnect was requested concurrently, or the manual `disconnect()`
428
+ * function being called. `disconnect()` has no `enabled` guard: called
429
+ * while the stream is disabled it still sets `'disconnected'`, which
430
+ * persists until the next connect-effect run.
431
+ * - `'error'` — `connect()` rejected and no disconnect was requested while it
432
+ * was pending. `error` holds the rejection, coerced to an `Error` if it
433
+ * wasn't one already.
434
+ *
435
+ * **Transitions**
436
+ * - Mount / `enabled` becomes `true` / `descriptor.key` or `mode` changes:
437
+ * `data`, `events`, and `error` are reset, then status goes to
438
+ * `'connecting'` and `connect()` is called.
439
+ * - `enabled` becomes `false` (or starts `false`): status is forced to
440
+ * `'idle'` and `connect()` is never called; no other field is reset by
441
+ * this transition alone.
442
+ * - Unmount, `descriptor.key` change, `mode` change, or `enabled` flipping to
443
+ * `false` while connected or connecting: the previous connection is torn
444
+ * down via `descriptor.disconnect()` once its `connect()` promise settles
445
+ * (or immediately skipped if it rejected). All four share the same cleanup
446
+ * path: the effect's cleanup runs on every dependency change and on unmount.
447
+ * - Calling `disconnect()`: if a connection id is already resolved, it is
448
+ * disconnected immediately and status becomes `'disconnected'`. If
449
+ * `connect()` is still pending, the request is recorded and honored when
450
+ * the promise settles — the newly resolved connection is torn down instead
451
+ * of being adopted, and status is `'disconnected'`. One caveat:
452
+ * `disconnect()` does not cancel the event callback, so a buffered or
453
+ * in-flight event landing after the call re-asserts `'connected'` — and in
454
+ * the pending-connect case the settle handler tears the connection down
455
+ * without touching status, so that stray `'connected'` persists.
456
+ * - Stream completion while `connect()` is still pending: the current
457
+ * implementation lets `onComplete` set `'disconnected'`, then the settle
458
+ * handler adopts the connection id and re-asserts `'connected'` anyway.
459
+ * This is known current behavior, not a durable guarantee: a pending
460
+ * upstream derived-status rewrite is expected to remove this transition.
461
+ *
462
+ * `enabled` (default `true`) is the only way to defer connecting past mount;
463
+ * flipping it back to `true` re-runs the same connect sequence as a fresh
464
+ * mount would.
465
+ */
409
466
  declare function useApiStream<TEvent>(descriptor: StreamDescriptor<TEvent>, options?: ApiStreamOptions): ApiStreamResult<TEvent>;
410
467
 
411
468
  /**
@@ -498,7 +555,7 @@ declare const MfeProvider: React__default.FC<MfeProviderProps>;
498
555
  * // bridge.executeActionsChain(chain);
499
556
  * // bridge.subscribeToProperty(propertyTypeId, callback);
500
557
  *
501
- * return <div>Domain: {bridge.domainId}</div>;
558
+ * return <div>Domain: {bridge.extDomainId}</div>;
502
559
  * }
503
560
  * ```
504
561
  */
package/dist/index.d.ts CHANGED
@@ -366,10 +366,9 @@ declare function useApiSuspenseInfiniteQuery<TPage = unknown, TError = Error>(op
366
366
  /**
367
367
  * useApiStream - Declarative SSE streaming hook
368
368
  *
369
- * Accepts a StreamDescriptor from @gears-frontx/api and manages the EventSource
370
- * lifecycle: connects on mount, disconnects on unmount/descriptor change.
371
- * Returns the latest event, accumulated events, connection status, and
372
- * a manual disconnect function.
369
+ * Accepts a StreamDescriptor from @gears-frontx/api and returns the latest
370
+ * event, accumulated events, connection status, and a manual disconnect
371
+ * function.
373
372
  *
374
373
  * @example
375
374
  * ```tsx
@@ -395,17 +394,75 @@ interface ApiStreamOptions {
395
394
  }
396
395
  /** Return type of useApiStream. */
397
396
  interface ApiStreamResult<TEvent> {
398
- /** Latest event payload (always set in both modes). */
397
+ /** Latest event payload, set in both modes — `undefined` until the first event and after each connect attempt resets it. */
399
398
  data: TEvent | undefined;
400
399
  /** All received events when `mode: 'accumulate'`; empty array in `'latest'` mode. */
401
400
  events: TEvent[];
402
401
  /** Connection lifecycle status. */
403
402
  status: StreamStatus;
404
- /** Error if the connection failed. */
403
+ /** Last connect error — set when a connect attempt rejects, cleared when the next attempt starts. */
405
404
  error: Error | null;
406
405
  /** Manually close the connection. */
407
406
  disconnect: () => void;
408
407
  }
408
+ /**
409
+ * Manages the status lifecycle for a single SSE stream connection.
410
+ *
411
+ * **Status values**
412
+ * - `'idle'` — no connection is being attempted. This is the initial status
413
+ * before the first connect effect runs, and it is ordinarily the status
414
+ * while `enabled` is `false`. Consumers must not assume `'idle'` means "never
415
+ * connected" — a stream that is disabled after having connected returns to
416
+ * `'idle'` too, and `data`/`events` from the earlier connection are not
417
+ * cleared by that transition alone (they are cleared by mode/key changes,
418
+ * see below).
419
+ * - `'connecting'` — `descriptor.connect()` has been called and its promise
420
+ * has not yet settled.
421
+ * - `'connected'` — set both when `connect()` resolves (unless a disconnect
422
+ * was requested while it was pending) and, independently, whenever the
423
+ * `onEvent` callback delivers a new event. Because every event re-asserts
424
+ * `'connected'`, this status also serves as "still receiving events."
425
+ * - `'disconnected'` — reached via any of: the descriptor's `onComplete`
426
+ * callback firing (the stream ended normally), `connect()` rejecting while
427
+ * a disconnect was requested concurrently, or the manual `disconnect()`
428
+ * function being called. `disconnect()` has no `enabled` guard: called
429
+ * while the stream is disabled it still sets `'disconnected'`, which
430
+ * persists until the next connect-effect run.
431
+ * - `'error'` — `connect()` rejected and no disconnect was requested while it
432
+ * was pending. `error` holds the rejection, coerced to an `Error` if it
433
+ * wasn't one already.
434
+ *
435
+ * **Transitions**
436
+ * - Mount / `enabled` becomes `true` / `descriptor.key` or `mode` changes:
437
+ * `data`, `events`, and `error` are reset, then status goes to
438
+ * `'connecting'` and `connect()` is called.
439
+ * - `enabled` becomes `false` (or starts `false`): status is forced to
440
+ * `'idle'` and `connect()` is never called; no other field is reset by
441
+ * this transition alone.
442
+ * - Unmount, `descriptor.key` change, `mode` change, or `enabled` flipping to
443
+ * `false` while connected or connecting: the previous connection is torn
444
+ * down via `descriptor.disconnect()` once its `connect()` promise settles
445
+ * (or immediately skipped if it rejected). All four share the same cleanup
446
+ * path: the effect's cleanup runs on every dependency change and on unmount.
447
+ * - Calling `disconnect()`: if a connection id is already resolved, it is
448
+ * disconnected immediately and status becomes `'disconnected'`. If
449
+ * `connect()` is still pending, the request is recorded and honored when
450
+ * the promise settles — the newly resolved connection is torn down instead
451
+ * of being adopted, and status is `'disconnected'`. One caveat:
452
+ * `disconnect()` does not cancel the event callback, so a buffered or
453
+ * in-flight event landing after the call re-asserts `'connected'` — and in
454
+ * the pending-connect case the settle handler tears the connection down
455
+ * without touching status, so that stray `'connected'` persists.
456
+ * - Stream completion while `connect()` is still pending: the current
457
+ * implementation lets `onComplete` set `'disconnected'`, then the settle
458
+ * handler adopts the connection id and re-asserts `'connected'` anyway.
459
+ * This is known current behavior, not a durable guarantee: a pending
460
+ * upstream derived-status rewrite is expected to remove this transition.
461
+ *
462
+ * `enabled` (default `true`) is the only way to defer connecting past mount;
463
+ * flipping it back to `true` re-runs the same connect sequence as a fresh
464
+ * mount would.
465
+ */
409
466
  declare function useApiStream<TEvent>(descriptor: StreamDescriptor<TEvent>, options?: ApiStreamOptions): ApiStreamResult<TEvent>;
410
467
 
411
468
  /**
@@ -498,7 +555,7 @@ declare const MfeProvider: React__default.FC<MfeProviderProps>;
498
555
  * // bridge.executeActionsChain(chain);
499
556
  * // bridge.subscribeToProperty(propertyTypeId, callback);
500
557
  *
501
- * return <div>Domain: {bridge.domainId}</div>;
558
+ * return <div>Domain: {bridge.extDomainId}</div>;
502
559
  * }
503
560
  * ```
504
561
  */
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/FrontXProvider.tsx
2
- import { useMemo as useMemo2, useEffect as useEffect2, useState as useState2 } from "react";
2
+ import { useMemo as useMemo2, useEffect as useEffect3, useState as useState2 } from "react";
3
3
  import { Provider as ReduxProvider } from "react-redux";
4
4
  import {
5
5
  createFrontXApp
@@ -41,7 +41,7 @@ var MfeProvider = ({ value, children }) => {
41
41
  import React, {
42
42
  createContext as createContext3,
43
43
  useContext as useContext3,
44
- useEffect,
44
+ useEffect as useEffect2,
45
45
  useLayoutEffect,
46
46
  useMemo,
47
47
  useState
@@ -164,13 +164,23 @@ function createQueryCache(queryClient) {
164
164
  };
165
165
  }
166
166
 
167
+ // src/hooks/useLatest.ts
168
+ import { useEffect, useRef } from "react";
169
+ function useLatest(value) {
170
+ const ref = useRef(value);
171
+ useEffect(() => {
172
+ ref.current = value;
173
+ });
174
+ return ref;
175
+ }
176
+
167
177
  // src/queryClient.tsx
168
178
  import { jsx as jsx2 } from "react/jsx-runtime";
169
179
  var APP_QUERY_CLIENT_SYMBOL = /* @__PURE__ */ Symbol.for("frontx:query-cache:app-client");
170
180
  var APP_QUERY_CLIENT_RESOLVER_SYMBOL = /* @__PURE__ */ Symbol.for("frontx:query-cache:app-client-resolver");
171
181
  var APP_QUERY_CLIENT_ACTIVATOR_SYMBOL = /* @__PURE__ */ Symbol.for("frontx:query-cache:app-client-activator");
172
182
  var FrontXQueryClientContext = createContext3(void 0);
173
- var useCommitEffect = typeof globalThis.window === "undefined" ? useEffect : useLayoutEffect;
183
+ var useCommitEffect = typeof globalThis.window === "undefined" ? useEffect2 : useLayoutEffect;
174
184
  function resolveFrontXQueryClient(app) {
175
185
  const clientApp = app;
176
186
  return clientApp[APP_QUERY_CLIENT_SYMBOL] ?? clientApp[APP_QUERY_CLIENT_RESOLVER_SYMBOL]?.();
@@ -409,18 +419,19 @@ function useFrontXSuspenseInfiniteQuery(options) {
409
419
  }
410
420
  function useFrontXMutation(options, callbackCtx) {
411
421
  useRequiredFrontXQueryClient();
412
- const latestRef = React.useRef({ options, callbackCtx });
413
- latestRef.current = { options, callbackCtx };
422
+ const latestRef = useLatest({ options, callbackCtx });
414
423
  const fetchAbortControllersRef = React.useRef(/* @__PURE__ */ new Set());
424
+ const abortFetchesOnUnmount = React.useEffectEvent(() => {
425
+ if (options.abortOnUnmount) {
426
+ for (const controller of fetchAbortControllersRef.current) {
427
+ controller.abort();
428
+ }
429
+ fetchAbortControllersRef.current.clear();
430
+ }
431
+ });
415
432
  React.useEffect(() => {
416
- const fetchAbortControllers = fetchAbortControllersRef.current;
417
433
  return () => {
418
- if (latestRef.current.options.abortOnUnmount) {
419
- for (const controller of fetchAbortControllers) {
420
- controller.abort();
421
- }
422
- fetchAbortControllers.clear();
423
- }
434
+ abortFetchesOnUnmount();
424
435
  };
425
436
  }, []);
426
437
  const mutationFn = React.useCallback((variables, context) => {
@@ -444,22 +455,22 @@ function useFrontXMutation(options, callbackCtx) {
444
455
  return latestRef.current.options.endpoint.fetch(variables, { signal: controller.signal }).finally(() => {
445
456
  fetchAbortControllersRef.current.delete(controller);
446
457
  });
447
- }, []);
458
+ }, [latestRef]);
448
459
  const onMutate = React.useCallback((variables, _context) => {
449
460
  const current = latestRef.current;
450
461
  if (!current.options.onMutate) {
451
462
  return void 0;
452
463
  }
453
464
  return current.options.onMutate(variables, current.callbackCtx);
454
- }, []);
465
+ }, [latestRef]);
455
466
  const onSuccess = React.useCallback((data, variables, context) => {
456
467
  const current = latestRef.current;
457
468
  return current.options.onSuccess?.(data, variables, context, current.callbackCtx);
458
- }, []);
469
+ }, [latestRef]);
459
470
  const onError = React.useCallback((error, variables, context) => {
460
471
  const current = latestRef.current;
461
472
  return current.options.onError?.(error, variables, context, current.callbackCtx);
462
- }, []);
473
+ }, [latestRef]);
463
474
  const onSettled = React.useCallback((data, error, variables, context) => {
464
475
  const current = latestRef.current;
465
476
  return current.options.onSettled?.(
@@ -469,7 +480,7 @@ function useFrontXMutation(options, callbackCtx) {
469
480
  context,
470
481
  current.callbackCtx
471
482
  );
472
- }, []);
483
+ }, [latestRef]);
473
484
  const mutation = useMutation({
474
485
  mutationFn,
475
486
  onMutate: options.onMutate ? onMutate : void 0,
@@ -520,7 +531,7 @@ var FrontXProvider = ({
520
531
  }, [providedApp, stableConfig]);
521
532
  const queryClient = useBootstrappedFrontXQueryClient(app);
522
533
  const deferQuerySubtree = hasFrontXQueryClientActivator(app) && queryClient === void 0;
523
- useEffect2(() => {
534
+ useEffect3(() => {
524
535
  if (queryClient || hasFrontXQueryClientActivator(app) || process.env.NODE_ENV === "production" || process.env.NODE_ENV === "test" || process.env.VITEST === "true") {
525
536
  return;
526
537
  }
@@ -528,7 +539,7 @@ var FrontXProvider = ({
528
539
  "[FrontXProvider] No query cache available. Add queryCache() or queryCacheShared() to your plugin composition. useApiQuery/useApiMutation will fail without it."
529
540
  );
530
541
  }, [app, queryClient]);
531
- useEffect2(() => {
542
+ useEffect3(() => {
532
543
  return () => {
533
544
  if (!providedApp) {
534
545
  app.destroy();
@@ -543,7 +554,7 @@ var FrontXProvider = ({
543
554
  };
544
555
 
545
556
  // src/hooks/useCanAccess.ts
546
- import { useEffect as useEffect3, useState as useState3 } from "react";
557
+ import { useEffect as useEffect4, useState as useState3 } from "react";
547
558
  function serializeRecordValue(value) {
548
559
  if (value === null) return "n:";
549
560
  if (typeof value === "string") return `s:${value}`;
@@ -560,24 +571,23 @@ function accessQueryKey(query) {
560
571
  function getAuthRuntime(app) {
561
572
  return app.auth ?? null;
562
573
  }
574
+ var DENIED_NO_AUTH = { allow: false, isResolving: false };
563
575
  function useCanAccess(query) {
564
576
  const app = useFrontX();
577
+ const auth2 = getAuthRuntime(app);
565
578
  const stableKey = accessQueryKey(query);
566
579
  const [stableQuery, setStableQuery] = useState3(() => query);
567
- const [storedKey, setStoredKey] = useState3(stableKey);
580
+ const [runState, setRunState] = useState3({ auth: auth2, stableKey });
568
581
  const [result, setResult] = useState3({ allow: false, isResolving: true });
569
- if (storedKey !== stableKey) {
570
- setStoredKey(stableKey);
582
+ if (runState.auth !== auth2 || runState.stableKey !== stableKey) {
583
+ setRunState({ auth: auth2, stableKey });
571
584
  setStableQuery(query);
572
585
  setResult({ allow: false, isResolving: true });
573
586
  }
574
- useEffect3(() => {
575
- const auth2 = getAuthRuntime(app);
587
+ useEffect4(() => {
576
588
  if (!auth2) {
577
- setResult({ allow: false, isResolving: false });
578
589
  return;
579
590
  }
580
- setResult({ allow: false, isResolving: true });
581
591
  let alive = true;
582
592
  const controller = new AbortController();
583
593
  void auth2.canAccess(stableQuery, { signal: controller.signal }).then((decision) => {
@@ -593,8 +603,8 @@ function useCanAccess(query) {
593
603
  alive = false;
594
604
  controller.abort();
595
605
  };
596
- }, [app, stableQuery]);
597
- return result;
606
+ }, [auth2, stableQuery]);
607
+ return auth2 ? result : DENIED_NO_AUTH;
598
608
  }
599
609
 
600
610
  // src/components/CanAccess.tsx
@@ -667,7 +677,7 @@ function useTranslation() {
667
677
  }
668
678
 
669
679
  // src/hooks/useScreenTranslations.ts
670
- import { useState as useState4, useEffect as useEffect4, useMemo as useMemo4, useCallback as useCallback2, useSyncExternalStore as useSyncExternalStore2 } from "react";
680
+ import { useState as useState4, useEffect as useEffect5, useMemo as useMemo4, useCallback as useCallback2, useSyncExternalStore as useSyncExternalStore2 } from "react";
671
681
  function isTranslationLoader(input) {
672
682
  return typeof input === "function";
673
683
  }
@@ -675,7 +685,6 @@ function useScreenTranslations(screensetId, screenId, translations) {
675
685
  const app = useFrontX();
676
686
  const { i18nRegistry: i18nRegistry2 } = app;
677
687
  const [loadedLanguage, setLoadedLanguage] = useState4(null);
678
- const [isLoading, setIsLoading] = useState4(false);
679
688
  const [error, setError] = useState4(null);
680
689
  const version = useSyncExternalStore2(
681
690
  useCallback2(
@@ -702,12 +711,11 @@ function useScreenTranslations(screensetId, screenId, translations) {
702
711
  return module.default;
703
712
  };
704
713
  }, [translations]);
705
- useEffect4(() => {
714
+ useEffect5(() => {
706
715
  if (!currentLanguage || currentLanguage === loadedLanguage) {
707
716
  return;
708
717
  }
709
718
  let cancelled = false;
710
- setIsLoading(true);
711
719
  const loadTranslations = async () => {
712
720
  try {
713
721
  const namespace = `screen.${screensetId}.${screenId}`;
@@ -716,13 +724,11 @@ function useScreenTranslations(screensetId, screenId, translations) {
716
724
  i18nRegistry2.register(namespace, currentLanguage, loadedTranslations);
717
725
  if (!cancelled) {
718
726
  setLoadedLanguage(currentLanguage);
719
- setIsLoading(false);
720
727
  setError(null);
721
728
  }
722
729
  } catch (err) {
723
730
  if (!cancelled) {
724
731
  setError(err instanceof Error ? err : new Error(String(err)));
725
- setIsLoading(false);
726
732
  }
727
733
  }
728
734
  };
@@ -731,7 +737,7 @@ function useScreenTranslations(screensetId, screenId, translations) {
731
737
  cancelled = true;
732
738
  };
733
739
  }, [screensetId, screenId, loader, i18nRegistry2, currentLanguage, loadedLanguage]);
734
- const isLoaded = currentLanguage !== null && currentLanguage === loadedLanguage && !isLoading;
740
+ const isLoaded = currentLanguage !== null && currentLanguage === loadedLanguage;
735
741
  return { isLoaded, error };
736
742
  }
737
743
 
@@ -842,25 +848,38 @@ function useApiMutation(options) {
842
848
  }
843
849
 
844
850
  // src/hooks/useApiStream.ts
845
- import { useEffect as useEffect5, useMemo as useMemo8, useRef, useState as useState5, useCallback as useCallback4 } from "react";
851
+ import { useEffect as useEffect6, useMemo as useMemo8, useRef as useRef2, useState as useState5, useCallback as useCallback4 } from "react";
846
852
  function useApiStream(descriptor, options) {
847
853
  const mode = options?.mode ?? "latest";
848
854
  const enabled = options?.enabled ?? true;
849
855
  const [data, setData] = useState5(void 0);
850
856
  const [events, setEvents] = useState5([]);
851
- const [status, setStatus] = useState5("idle");
857
+ const [status, setStatus] = useState5(() => enabled ? "connecting" : "idle");
852
858
  const [error, setError] = useState5(null);
853
- const connectPromiseRef = useRef(null);
854
- const connectionIdRef = useRef(null);
855
- const disconnectRequestedRef = useRef(false);
856
- const descriptorRef = useRef(descriptor);
857
- descriptorRef.current = descriptor;
859
+ const connectPromiseRef = useRef2(null);
860
+ const connectionIdRef = useRef2(null);
861
+ const disconnectRequestedRef = useRef2(false);
862
+ const descriptorRef = useLatest(descriptor);
858
863
  const descriptorKey = useMemo8(() => JSON.stringify(descriptor.key), [descriptor.key]);
859
- useEffect5(() => {
864
+ const resetKey = `${descriptorKey}:${mode}`;
865
+ const [prevResetKey, setPrevResetKey] = useState5(resetKey);
866
+ if (resetKey !== prevResetKey) {
867
+ setPrevResetKey(resetKey);
860
868
  setData(void 0);
861
869
  setEvents([]);
862
870
  setError(null);
863
- }, [descriptorKey, mode]);
871
+ }
872
+ const connectKey = `${resetKey}:${enabled}`;
873
+ const [prevConnectKey, setPrevConnectKey] = useState5(connectKey);
874
+ if (connectKey !== prevConnectKey) {
875
+ setPrevConnectKey(connectKey);
876
+ if (enabled) {
877
+ setData(void 0);
878
+ setEvents([]);
879
+ setStatus("connecting");
880
+ setError(null);
881
+ }
882
+ }
864
883
  const disconnect = useCallback4(() => {
865
884
  disconnectRequestedRef.current = true;
866
885
  if (connectionIdRef.current) {
@@ -869,19 +888,14 @@ function useApiStream(descriptor, options) {
869
888
  disconnectRequestedRef.current = false;
870
889
  }
871
890
  setStatus("disconnected");
872
- }, []);
873
- useEffect5(() => {
891
+ }, [descriptorRef]);
892
+ useEffect6(() => {
874
893
  if (!enabled) {
875
- setStatus("idle");
876
894
  return;
877
895
  }
878
896
  let cancelled = false;
879
897
  disconnectRequestedRef.current = false;
880
898
  const d = descriptorRef.current;
881
- setData(void 0);
882
- setEvents([]);
883
- setStatus("connecting");
884
- setError(null);
885
899
  const connectPromise = d.connect(
886
900
  (event) => {
887
901
  if (cancelled) return;
@@ -927,8 +941,8 @@ function useApiStream(descriptor, options) {
927
941
  connectPromiseRef.current = null;
928
942
  connectionIdRef.current = null;
929
943
  };
930
- }, [descriptorKey, enabled, mode]);
931
- return { data, events, status, error, disconnect };
944
+ }, [descriptorKey, enabled, mode, descriptorRef]);
945
+ return { data, events, status: enabled ? status : "idle", error, disconnect };
932
946
  }
933
947
 
934
948
  // src/hooks/useQueryCache.ts
@@ -974,7 +988,7 @@ function useHostAction(actionTypeId) {
974
988
  const chain = {
975
989
  action: {
976
990
  type: actionTypeId,
977
- target: bridge.domainId,
991
+ target: bridge.extDomainId,
978
992
  payload
979
993
  }
980
994
  };
@@ -988,7 +1002,7 @@ function useHostAction(actionTypeId) {
988
1002
  }
989
1003
 
990
1004
  // src/mfe/hooks/useDomainExtensions.ts
991
- import { useSyncExternalStore as useSyncExternalStore5, useCallback as useCallback7, useRef as useRef2 } from "react";
1005
+ import { useSyncExternalStore as useSyncExternalStore5, useCallback as useCallback7, useRef as useRef3 } from "react";
992
1006
  function useDomainExtensions(domainId) {
993
1007
  const app = useFrontX();
994
1008
  const registry = app.mfeRegistry;
@@ -1003,7 +1017,7 @@ function useDomainExtensions(domainId) {
1003
1017
  },
1004
1018
  [app.store]
1005
1019
  );
1006
- const cacheRef = useRef2({ ids: "", extensions: [] });
1020
+ const cacheRef = useRef3({ ids: "", extensions: [] });
1007
1021
  const getSnapshot = useCallback7(() => {
1008
1022
  const extensions = registry.getExtensionsForDomain(domainId);
1009
1023
  const ids = extensions.map((e) => e.id).join(",");
@@ -1016,7 +1030,7 @@ function useDomainExtensions(domainId) {
1016
1030
  }
1017
1031
 
1018
1032
  // src/mfe/hooks/useMountedExtensions.ts
1019
- import { useSyncExternalStore as useSyncExternalStore6, useCallback as useCallback8, useRef as useRef3 } from "react";
1033
+ import { useSyncExternalStore as useSyncExternalStore6, useCallback as useCallback8, useRef as useRef4 } from "react";
1020
1034
  function useMountedExtensions(domainId) {
1021
1035
  const app = useFrontX();
1022
1036
  const registry = app.mfeRegistry;
@@ -1031,7 +1045,7 @@ function useMountedExtensions(domainId) {
1031
1045
  },
1032
1046
  [app.store]
1033
1047
  );
1034
- const cacheRef = useRef3({ key: "", extensions: [] });
1048
+ const cacheRef = useRef4({ key: "", extensions: [] });
1035
1049
  const getSnapshot = useCallback8(() => {
1036
1050
  const ids = registry.getMountedExtensions(domainId);
1037
1051
  const resolved = ids.map((id) => registry.getExtension(id)).filter((ext) => ext !== void 0);
@@ -1045,7 +1059,7 @@ function useMountedExtensions(domainId) {
1045
1059
  }
1046
1060
 
1047
1061
  // src/mfe/hooks/useRegisteredPackages.ts
1048
- import { useSyncExternalStore as useSyncExternalStore7, useCallback as useCallback9, useRef as useRef4 } from "react";
1062
+ import { useSyncExternalStore as useSyncExternalStore7, useCallback as useCallback9, useRef as useRef5 } from "react";
1049
1063
  function useRegisteredPackages() {
1050
1064
  const app = useFrontX();
1051
1065
  const registry = app.mfeRegistry;
@@ -1060,7 +1074,7 @@ function useRegisteredPackages() {
1060
1074
  },
1061
1075
  [app.store]
1062
1076
  );
1063
- const cacheRef = useRef4({ packages: "", list: [] });
1077
+ const cacheRef = useRef5({ packages: "", list: [] });
1064
1078
  const getSnapshot = useCallback9(() => {
1065
1079
  const list = registry.getRegisteredPackages();
1066
1080
  const packages = list.join(",");
@@ -1180,7 +1194,7 @@ var ThemeAwareReactLifecycle = class {
1180
1194
  };
1181
1195
 
1182
1196
  // src/mfe/components/ExtensionDomainSlot.tsx
1183
- import { useEffect as useEffect6, useRef as useRef5, useState as useState6 } from "react";
1197
+ import { useEffect as useEffect7, useEffectEvent, useRef as useRef6, useState as useState6 } from "react";
1184
1198
  import { jsx as jsx5 } from "react/jsx-runtime";
1185
1199
  function ExtensionDomainSlot(props) {
1186
1200
  const {
@@ -1191,13 +1205,15 @@ function ExtensionDomainSlot(props) {
1191
1205
  onDetached,
1192
1206
  loadingComponent = null
1193
1207
  } = props;
1194
- const rootRef = useRef5(null);
1195
- const onAttachedRef = useRef5(onAttached);
1196
- const onDetachedRef = useRef5(onDetached);
1197
- onAttachedRef.current = onAttached;
1198
- onDetachedRef.current = onDetached;
1208
+ const rootRef = useRef6(null);
1209
+ const emitAttached = useEffectEvent((root) => {
1210
+ onAttached?.(root);
1211
+ });
1212
+ const emitDetached = useEffectEvent(() => {
1213
+ onDetached?.();
1214
+ });
1199
1215
  const [attached, setAttached] = useState6(false);
1200
- useEffect6(() => {
1216
+ useEffect7(() => {
1201
1217
  const root = rootRef.current;
1202
1218
  if (!root) {
1203
1219
  return;
@@ -1205,11 +1221,11 @@ function ExtensionDomainSlot(props) {
1205
1221
  const mounter = registry.getMounter(domainId);
1206
1222
  mounter.attach(root);
1207
1223
  setAttached(true);
1208
- onAttachedRef.current?.(root);
1224
+ emitAttached(root);
1209
1225
  return () => {
1210
1226
  void mounter.detach();
1211
1227
  setAttached(false);
1212
- onDetachedRef.current?.();
1228
+ emitDetached();
1213
1229
  };
1214
1230
  }, [registry, domainId]);
1215
1231
  return /* @__PURE__ */ jsx5(