@granite-js/micro-frontend 2.3.1 → 2.4.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @granite-js/micro-frontend
2
2
 
3
+ ## 2.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d1f3f02: Add `getSessions()` and `onSessionsChanged()` for observing ordered session snapshots outside React. Snapshots include session identity, app name, scheme, and native presentation visibility, and update as native events arrive without waiting for React commits or app disposal.
8
+
9
+ Keep session tracking active between consumer subscriptions and let `useMicroFrontendSessions()` read the same runtime-owned state, including sessions opened before it mounts. Existing lifecycle callbacks retain their commit and disposal timing.
10
+
11
+ ### Patch Changes
12
+
13
+ - 9c09be7: Prevent iOS Portal hosts from dispatching duplicate Fabric touch events when another React Native root owns the touched view. Preserve touch handling for plain hosted content and nested Portal hosts.
14
+ - d0a4bf4: Mount teleported iOS Portal content under an `RCTRootComponentView` anchor so react-native-screens treats hosted screens as root-mounted and stops attaching a second `RCTSurfaceTouchHandler` per screen. Hosted content now gets a single touch handler and container-relative page coordinates, like content under a regular React Native root.
15
+ - @granite-js/utils@2.4.0
16
+
17
+ ## 2.3.2
18
+
19
+ ### Patch Changes
20
+
21
+ - f92bbc7: Register pending host components with the configured app scheme so route evaluation works before an initial URL is available and does not inherit another app's scheme.
22
+ - @granite-js/utils@2.3.2
23
+
3
24
  ## 2.3.1
4
25
 
5
26
  ### Patch Changes
package/README.md CHANGED
@@ -217,12 +217,48 @@ route best-effort native preload failures to its observability provider.
217
217
 
218
218
  The public runtime API is:
219
219
 
220
- | API | Responsibility |
221
- | -------------------------- | --------------------------------------------------------------- |
222
- | `preloadApp(appName)` | Load and evaluate one app without importing an exposed module. |
223
- | `importApp(request)` | Ensure the app is evaluated and import `appName/exposedModule`. |
224
- | `evaluateScript(filePath)` | Evaluate a local file or Android packaged asset in the retained runtime. |
225
- | `onEvent(listener)` | Subscribe to native open, close, and visibility events. |
220
+ | API | Responsibility |
221
+ | ----------------------------- | ------------------------------------------------------------------------ |
222
+ | `preloadApp(appName)` | Load and evaluate one app without importing an exposed module. |
223
+ | `importApp(request)` | Ensure the app is evaluated and import `appName/exposedModule`. |
224
+ | `evaluateScript(filePath)` | Evaluate a local file or Android packaged asset in the retained runtime. |
225
+ | `onEvent(listener)` | Subscribe to native open, close, and visibility events. |
226
+ | `getSessions()` | Read the current immutable session snapshot. |
227
+ | `onSessionsChanged(listener)` | Subscribe to session snapshot changes. |
228
+
229
+ Session snapshots contain `sessionId`, `appName`, `scheme`, and `isVisible`.
230
+ Sessions remain in opening order, including separate entries when the same app
231
+ has multiple sessions. A newly opened session starts with `isVisible: false`;
232
+ native visibility events update it, and `closeApp` removes it. Preloading an app
233
+ does not create a session.
234
+
235
+ ```ts
236
+ const subscription = runtime.onSessionsChanged((sessions) => {
237
+ updateSessionStatus(sessions);
238
+ });
239
+ updateSessionStatus(runtime.getSessions());
240
+
241
+ // Stop observing when the integration is no longer needed.
242
+ subscription.remove();
243
+ ```
244
+
245
+ `getSessions()` only reads state. The first `onEvent` or `onSessionsChanged`
246
+ subscription starts native event delivery after installing the runtime's
247
+ listener. This can synchronously deliver queued events to the new subscriber.
248
+ Subscriptions do not replay the current snapshot; subscribe first, then read
249
+ `getSessions()` to initialize an observer without missing changes.
250
+
251
+ The runtime retains one native subscription for its lifetime, even while all
252
+ consumer subscriptions are removed. Later observers and React hosts can read
253
+ the current sessions without reconstructing past events. Native session events
254
+ update the snapshot before either kind of observer is notified, independently
255
+ of React commits and app disposal. A failing observer is reported without
256
+ preventing other observers from receiving the event.
257
+
258
+ Snapshots and their session objects are frozen. A real change creates a new
259
+ array while preserving unchanged session objects. Duplicate opens, unknown
260
+ session IDs, and repeated visibility values retain the same snapshot and do not
261
+ notify `onSessionsChanged` subscribers.
226
262
 
227
263
  The host can provide `onLifecycleEvent` when creating the runtime for logging
228
264
  and other observability integrations. Each event contains `session.id`,
@@ -291,9 +327,11 @@ function SessionRoot({ session }: { readonly session: MicroFrontendSessionState
291
327
  }
292
328
  ```
293
329
 
294
- `useMicroFrontendSessions(runtime)` owns the fixed React subscription and folds
295
- native open, close, and visibility events into session descriptors. The host
296
- continues to own module selection and the rendered Portal tree.
330
+ `useMicroFrontendSessions(runtime)` subscribes to the runtime's session snapshot
331
+ with `useSyncExternalStore`. It reads the same state as non-React observers,
332
+ including sessions opened before the hook mounted. It continues to report
333
+ committed mount/unmount lifecycle events and run app disposal. The host owns
334
+ module selection and the rendered Portal tree.
297
335
 
298
336
  The provider exposes the native session identity and combines
299
337
  `presentationVisibility` with Granite's existing `VisibilityChangedProvider`.
@@ -535,16 +573,33 @@ Import public APIs from `GraniteMicroFrontendRuntime`.
535
573
 
536
574
  ### Portal destination APIs
537
575
 
538
- | API | Lifetime / behavior |
539
- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
540
- | `-initWithFrame:` | Create and immediately activate a UIKit-owned Portal destination after React has booted. |
541
- | `-initWithFrame:deferredActivation:` | Create before React boot without reading React feature flags. |
542
- | `-setName:` | Register/unregister the destination name. Use `sessionId`. |
543
- | `-activateIfNeeded` | Create the Fabric host, attach its touch handler, and apply the pending name. Main thread only, after React boot. |
544
- | `isActivated` | Whether the underlying Fabric host exists. |
545
- | `hasAttachedContent` | Whether teleported content is currently attached. This is readiness, not presentation visibility. |
546
- | `onContentDidAttach` / `onContentDidDetach` | Main-thread readiness callbacks for the first attach and last detach. |
547
- | `-invalidate` | Unregister the host and clear callbacks during teardown. |
576
+ | API | Lifetime / behavior |
577
+ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
578
+ | `-initWithFrame:` | Create and immediately activate a UIKit-owned Portal destination after React has booted. |
579
+ | `-initWithFrame:deferredActivation:` | Create before React boot without reading React feature flags. |
580
+ | `-setName:` | Register/unregister the destination name. Use `sessionId`. |
581
+ | `-activateIfNeeded` | Create the Fabric host under an `RCTRootComponentView` anchor, attach the touch handler, and apply the pending name. Main thread only, after React boot. |
582
+ | `isActivated` | Whether the underlying Fabric host exists. |
583
+ | `hasAttachedContent` | Whether teleported content is currently attached. This is readiness, not presentation visibility. |
584
+ | `onContentDidAttach` / `onContentDidDetach` | Main-thread readiness callbacks for the first attach and last detach. |
585
+ | `-invalidate` | Unregister the host and clear callbacks during teardown. |
586
+
587
+ #### Touch handling under a Portal host
588
+
589
+ `-activateIfNeeded` mounts the Fabric host under an `RCTRootComponentView`
590
+ anchor:
591
+
592
+ ```text
593
+ PortalHostContainerView <- RCTSurfaceTouchHandler for all hosted content
594
+ └── RCTRootComponentView <- anchor, never registered with Fabric
595
+ └── PortalHostView <- teleported content attaches here
596
+ ```
597
+
598
+ react-native-screens attaches its own touch handler to any screen without an
599
+ `RCTRootComponentView` above it. Without the anchor every hosted screen would add
600
+ a second handler beneath the container's, and one tap would reach JS twice. The
601
+ anchor keeps a single handler and page coordinates relative to the container,
602
+ like content under a regular React Native root.
548
603
 
549
604
  ### UIViewController example
550
605
 
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- import { createRoute as createGraniteRoute, getSchemeUri } from '@granite-js/react-native';
3
+ import { createRoute as createGraniteRoute } from '@granite-js/react-native';
4
4
  import { registerPendingHostComponentRoute } from "./host/pendingHostComponentStore.js";
5
5
  export function createRoute(path, options) {
6
6
  const {
@@ -27,10 +27,10 @@ function getCurrentGraniteApp() {
27
27
  }
28
28
  const host = granite['app']['host'];
29
29
  const name = granite['app']['name'];
30
- if (typeof host !== 'string' || typeof name !== 'string') {
30
+ const scheme = granite['app']['scheme'];
31
+ if (typeof host !== 'string' || typeof name !== 'string' || typeof scheme !== 'string') {
31
32
  return null;
32
33
  }
33
- const scheme = new URL(getSchemeUri()).protocol.replace(/:$/g, '');
34
34
  return {
35
35
  name,
36
36
  scheme,
@@ -1 +1 @@
1
- {"version":3,"names":["createRoute","createGraniteRoute","getSchemeUri","registerPendingHostComponentRoute","path","options","hostPendingComponent","routeOptions","app","getCurrentGraniteApp","String","component","parserParams","validateParams","granite","Reflect","get","globalThis","isPropertyMap","host","name","scheme","URL","protocol","replace","value"],"sourceRoot":"../../src","sources":["createRoute.ts"],"mappings":";;AAAA,SACEA,WAAW,IAAIC,kBAAkB,EACjCC,YAAY,QAIP,0BAA0B;AAEjC,SAASC,iCAAiC,QAAQ,qCAAkC;AA+BpF,OAAO,SAASH,WAAWA,CACzBI,IAA+B,EAC/BC,OAA2C,EAC3C;EACA,MAAM;IAAEC,oBAAoB;IAAE,GAAGC;EAAa,CAAC,GAAGF,OAAO;EAEzD,IAAIC,oBAAoB,IAAI,IAAI,EAAE;IAChC,MAAME,GAAG,GAAGC,oBAAoB,CAAC,CAAC;IAClC,IAAID,GAAG,IAAI,IAAI,EAAE;MACfL,iCAAiC,CAACO,MAAM,CAACN,IAAI,CAAC,EAAE;QAC9CI,GAAG;QACHG,SAAS,EAAEL,oBAAoB;QAC/BM,YAAY,EAAEL,YAAY,CAACK,YAAY;QACvCC,cAAc,EAAEN,YAAY,CAACM;MAC/B,CAAC,CAAC;IACJ;EACF;EAEA,OAAOZ,kBAAkB,CAACG,IAAI,EAAEG,YAAY,CAAC;AAC/C;AAEA,SAASE,oBAAoBA,CAAA,EAAyC;EACpE,MAAMK,OAAgB,GAAGC,OAAO,CAACC,GAAG,CAACC,UAAU,EAAE,WAAW,CAAC;EAC7D,IAAI,CAACC,aAAa,CAACJ,OAAO,CAAC,IAAI,CAACI,aAAa,CAACJ,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE;IAC7D,OAAO,IAAI;EACb;EAEA,MAAMK,IAAI,GAAGL,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;EACnC,MAAMM,IAAI,GAAGN,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;EACnC,IAAI,OAAOK,IAAI,KAAK,QAAQ,IAAI,OAAOC,IAAI,KAAK,QAAQ,EAAE;IACxD,OAAO,IAAI;EACb;EAEA,MAAMC,MAAM,GAAG,IAAIC,GAAG,CAACpB,YAAY,CAAC,CAAC,CAAC,CAACqB,QAAQ,CAACC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;EAElE,OAAO;IACLJ,IAAI;IACJC,MAAM;IACNF;EACF,CAAC;AACH;AAEA,SAASD,aAAaA,CAACO,KAAc,EAAmD;EACtF,OAAO,OAAOA,KAAK,KAAK,QAAQ,IAAIA,KAAK,KAAK,IAAI;AACpD","ignoreList":[]}
1
+ {"version":3,"names":["createRoute","createGraniteRoute","registerPendingHostComponentRoute","path","options","hostPendingComponent","routeOptions","app","getCurrentGraniteApp","String","component","parserParams","validateParams","granite","Reflect","get","globalThis","isPropertyMap","host","name","scheme","value"],"sourceRoot":"../../src","sources":["createRoute.ts"],"mappings":";;AAAA,SACEA,WAAW,IAAIC,kBAAkB,QAI5B,0BAA0B;AAEjC,SAASC,iCAAiC,QAAQ,qCAAkC;AA+BpF,OAAO,SAASF,WAAWA,CACzBG,IAA+B,EAC/BC,OAA2C,EAC3C;EACA,MAAM;IAAEC,oBAAoB;IAAE,GAAGC;EAAa,CAAC,GAAGF,OAAO;EAEzD,IAAIC,oBAAoB,IAAI,IAAI,EAAE;IAChC,MAAME,GAAG,GAAGC,oBAAoB,CAAC,CAAC;IAClC,IAAID,GAAG,IAAI,IAAI,EAAE;MACfL,iCAAiC,CAACO,MAAM,CAACN,IAAI,CAAC,EAAE;QAC9CI,GAAG;QACHG,SAAS,EAAEL,oBAAoB;QAC/BM,YAAY,EAAEL,YAAY,CAACK,YAAY;QACvCC,cAAc,EAAEN,YAAY,CAACM;MAC/B,CAAC,CAAC;IACJ;EACF;EAEA,OAAOX,kBAAkB,CAACE,IAAI,EAAEG,YAAY,CAAC;AAC/C;AAEA,SAASE,oBAAoBA,CAAA,EAAyC;EACpE,MAAMK,OAAgB,GAAGC,OAAO,CAACC,GAAG,CAACC,UAAU,EAAE,WAAW,CAAC;EAC7D,IAAI,CAACC,aAAa,CAACJ,OAAO,CAAC,IAAI,CAACI,aAAa,CAACJ,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE;IAC7D,OAAO,IAAI;EACb;EAEA,MAAMK,IAAI,GAAGL,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;EACnC,MAAMM,IAAI,GAAGN,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;EACnC,MAAMO,MAAM,GAAGP,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC;EACvC,IAAI,OAAOK,IAAI,KAAK,QAAQ,IAAI,OAAOC,IAAI,KAAK,QAAQ,IAAI,OAAOC,MAAM,KAAK,QAAQ,EAAE;IACtF,OAAO,IAAI;EACb;EAEA,OAAO;IACLD,IAAI;IACJC,MAAM;IACNF;EACF,CAAC;AACH;AAEA,SAASD,aAAaA,CAACI,KAAc,EAAmD;EACtF,OAAO,OAAOA,KAAK,KAAK,QAAQ,IAAIA,KAAK,KAAK,IAAI;AACpD","ignoreList":[]}
@@ -5,9 +5,41 @@ import { getMicroFrontendGlobalContext } from "./globalContext.js";
5
5
  import { setMicroFrontendLifecycleCallback } from "./lifecycle.js";
6
6
  import { installNativeComponentRegistryCompatibility } from "./nativeComponentRegistryCompatibility.js";
7
7
  import { parseAppRequest } from "./parseAppRequest.js";
8
+ import { createSessionStore } from "./sessionStore.js";
8
9
  export function createMicroFrontendRuntimeWithDependencies(dependencies) {
9
10
  // A fulfilled promise is the evaluated-state cache. Rejected evaluations remove themselves.
10
11
  const appEvaluations = new Map();
12
+ const sessionStore = createSessionStore();
13
+ const sessionListeners = new Set();
14
+ const eventListeners = new Set();
15
+ let nativeSubscription;
16
+ function startEventDelivery() {
17
+ if (nativeSubscription != null) {
18
+ return;
19
+ }
20
+ nativeSubscription = dependencies.nativeRuntime.onEvent(event => {
21
+ const parsedEvent = dependencies.parseEvent(event);
22
+ switch (parsedEvent.name) {
23
+ case 'preloadApp':
24
+ void preloadApp(parsedEvent.params.appName).catch(dependencies.onPreloadError);
25
+ return;
26
+ case 'openApp':
27
+ case 'closeApp':
28
+ case 'sessionVisibilityChanged':
29
+ if (sessionStore.applyEvent(parsedEvent)) {
30
+ notifyListeners(sessionListeners, sessionStore.getSessions());
31
+ }
32
+ notifyListeners(eventListeners, parsedEvent);
33
+ return;
34
+ default:
35
+ {
36
+ const exhaustiveEvent = parsedEvent;
37
+ return exhaustiveEvent;
38
+ }
39
+ }
40
+ });
41
+ dependencies.nativeRuntime.startEventDelivery();
42
+ }
11
43
  function resetFailedEvaluation(appName) {
12
44
  appEvaluations.delete(appName);
13
45
  dependencies.registry.removeContainer(appName);
@@ -58,30 +90,34 @@ export function createMicroFrontendRuntimeWithDependencies(dependencies) {
58
90
  await preloadApp(appName);
59
91
  return dependencies.registry.importModule(request);
60
92
  },
93
+ getSessions: sessionStore.getSessions,
94
+ onSessionsChanged(listener) {
95
+ const callback = sessions => listener(sessions);
96
+ sessionListeners.add(callback);
97
+ startEventDelivery();
98
+ return {
99
+ remove: () => sessionListeners.delete(callback)
100
+ };
101
+ },
61
102
  onEvent(listener) {
62
- const subscription = dependencies.nativeRuntime.onEvent(event => {
63
- const parsedEvent = dependencies.parseEvent(event);
64
- switch (parsedEvent.name) {
65
- case 'preloadApp':
66
- void preloadApp(parsedEvent.params.appName).catch(dependencies.onPreloadError);
67
- return;
68
- case 'openApp':
69
- case 'closeApp':
70
- case 'sessionVisibilityChanged':
71
- listener(parsedEvent);
72
- return;
73
- default:
74
- {
75
- const exhaustiveEvent = parsedEvent;
76
- return exhaustiveEvent;
77
- }
78
- }
79
- });
80
- dependencies.nativeRuntime.startEventDelivery();
81
- return subscription;
103
+ const callback = event => listener(event);
104
+ eventListeners.add(callback);
105
+ startEventDelivery();
106
+ return {
107
+ remove: () => eventListeners.delete(callback)
108
+ };
82
109
  }
83
110
  };
84
111
  setMicroFrontendLifecycleCallback(runtime, dependencies.onLifecycleEvent);
85
112
  return runtime;
86
113
  }
114
+ function notifyListeners(listeners, event) {
115
+ listeners.forEach(listener => {
116
+ try {
117
+ listener(event);
118
+ } catch (error) {
119
+ console.error('Failed to run a micro-frontend runtime listener', error);
120
+ }
121
+ });
122
+ }
87
123
  //# sourceMappingURL=createMicroFrontendRuntime.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["AppContainerNotFoundError","InvalidAppNameError","getMicroFrontendGlobalContext","setMicroFrontendLifecycleCallback","installNativeComponentRegistryCompatibility","parseAppRequest","createMicroFrontendRuntimeWithDependencies","dependencies","appEvaluations","Map","resetFailedEvaluation","appName","delete","registry","removeContainer","removePendingHostComponentRoutes","preloadApp","length","Promise","reject","existingEvaluation","get","evaluation","evaluateApp","set","evaluateScript","filePath","nativeRuntime","adapter","loadBundle","hasContainer","error","runtime","importApp","request","importModule","onEvent","listener","subscription","event","parsedEvent","parseEvent","name","params","catch","onPreloadError","exhaustiveEvent","startEventDelivery","onLifecycleEvent"],"sourceRoot":"../../../src","sources":["runtime/createMicroFrontendRuntime.ts"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["AppContainerNotFoundError","InvalidAppNameError","getMicroFrontendGlobalContext","setMicroFrontendLifecycleCallback","installNativeComponentRegistryCompatibility","parseAppRequest","createSessionStore","createMicroFrontendRuntimeWithDependencies","dependencies","appEvaluations","Map","sessionStore","sessionListeners","Set","eventListeners","nativeSubscription","startEventDelivery","nativeRuntime","onEvent","event","parsedEvent","parseEvent","name","preloadApp","params","appName","catch","onPreloadError","applyEvent","notifyListeners","getSessions","exhaustiveEvent","resetFailedEvaluation","delete","registry","removeContainer","removePendingHostComponentRoutes","length","Promise","reject","existingEvaluation","get","evaluation","evaluateApp","set","evaluateScript","filePath","adapter","loadBundle","hasContainer","error","runtime","importApp","request","importModule","onSessionsChanged","listener","callback","sessions","add","remove","onLifecycleEvent","listeners","forEach","console"],"sourceRoot":"../../../src","sources":["runtime/createMicroFrontendRuntime.ts"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+
3
+ export function createSessionStore() {
4
+ let sessions = Object.freeze([]);
5
+ return {
6
+ getSessions: () => sessions,
7
+ applyEvent(event) {
8
+ const nextSessions = reduceSessions(sessions, event);
9
+ if (nextSessions === sessions) {
10
+ return false;
11
+ }
12
+ sessions = Object.freeze(nextSessions);
13
+ return true;
14
+ }
15
+ };
16
+ }
17
+ function reduceSessions(sessions, event) {
18
+ const session = sessions.find(({
19
+ sessionId
20
+ }) => sessionId === event.params.sessionId);
21
+ switch (event.name) {
22
+ case 'openApp':
23
+ return session == null ? [...sessions, Object.freeze({
24
+ ...event.params,
25
+ isVisible: false
26
+ })] : sessions;
27
+ case 'closeApp':
28
+ return session == null ? sessions : sessions.filter(({
29
+ sessionId
30
+ }) => sessionId !== event.params.sessionId);
31
+ case 'sessionVisibilityChanged':
32
+ return session == null || session.isVisible === event.params.isVisible ? sessions : sessions.map(current => current === session ? Object.freeze({
33
+ ...current,
34
+ isVisible: event.params.isVisible
35
+ }) : current);
36
+ default:
37
+ {
38
+ const exhaustiveEvent = event;
39
+ return exhaustiveEvent;
40
+ }
41
+ }
42
+ }
43
+ //# sourceMappingURL=sessionStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["createSessionStore","sessions","Object","freeze","getSessions","applyEvent","event","nextSessions","reduceSessions","session","find","sessionId","params","name","isVisible","filter","map","current","exhaustiveEvent"],"sourceRoot":"../../../src","sources":["runtime/sessionStore.ts"],"mappings":";;AAEA,OAAO,SAASA,kBAAkBA,CAAA,EAAG;EACnC,IAAIC,QAA8C,GAAGC,MAAM,CAACC,MAAM,CAAC,EAAE,CAAC;EAEtE,OAAO;IACLC,WAAW,EAAEA,CAAA,KAAMH,QAAQ;IAC3BI,UAAUA,CAACC,KAAgC,EAAW;MACpD,MAAMC,YAAY,GAAGC,cAAc,CAACP,QAAQ,EAAEK,KAAK,CAAC;MACpD,IAAIC,YAAY,KAAKN,QAAQ,EAAE;QAC7B,OAAO,KAAK;MACd;MACAA,QAAQ,GAAGC,MAAM,CAACC,MAAM,CAACI,YAAY,CAAC;MACtC,OAAO,IAAI;IACb;EACF,CAAC;AACH;AAEA,SAASC,cAAcA,CACrBP,QAA8C,EAC9CK,KAAgC,EACM;EACtC,MAAMG,OAAO,GAAGR,QAAQ,CAACS,IAAI,CAAC,CAAC;IAAEC;EAAU,CAAC,KAAKA,SAAS,KAAKL,KAAK,CAACM,MAAM,CAACD,SAAS,CAAC;EACtF,QAAQL,KAAK,CAACO,IAAI;IAChB,KAAK,SAAS;MACZ,OAAOJ,OAAO,IAAI,IAAI,GAAG,CAAC,GAAGR,QAAQ,EAAEC,MAAM,CAACC,MAAM,CAAC;QAAE,GAAGG,KAAK,CAACM,MAAM;QAAEE,SAAS,EAAE;MAAM,CAAC,CAAC,CAAC,GAAGb,QAAQ;IACzG,KAAK,UAAU;MACb,OAAOQ,OAAO,IAAI,IAAI,GAAGR,QAAQ,GAAGA,QAAQ,CAACc,MAAM,CAAC,CAAC;QAAEJ;MAAU,CAAC,KAAKA,SAAS,KAAKL,KAAK,CAACM,MAAM,CAACD,SAAS,CAAC;IAC9G,KAAK,0BAA0B;MAC7B,OAAOF,OAAO,IAAI,IAAI,IAAIA,OAAO,CAACK,SAAS,KAAKR,KAAK,CAACM,MAAM,CAACE,SAAS,GAClEb,QAAQ,GACRA,QAAQ,CAACe,GAAG,CAAEC,OAAO,IACnBA,OAAO,KAAKR,OAAO,GAAGP,MAAM,CAACC,MAAM,CAAC;QAAE,GAAGc,OAAO;QAAEH,SAAS,EAAER,KAAK,CAACM,MAAM,CAACE;MAAU,CAAC,CAAC,GAAGG,OAC3F,CAAC;IACP;MAAS;QACP,MAAMC,eAAsB,GAAGZ,KAAK;QACpC,OAAOY,eAAe;MACxB;EACF;AACF","ignoreList":[]}
@@ -1,40 +1,14 @@
1
1
  "use strict";
2
2
 
3
- import { useEffect, useLayoutEffect, useReducer, useRef } from 'react';
3
+ import { useCallback, useEffect, useLayoutEffect, useRef, useSyncExternalStore } from 'react';
4
4
  import { installPendingHostComponentBridge, resetPendingHostComponent } from "../host/pendingHostComponentStore.js";
5
5
  import { emitMicroFrontendLifecycleEvent } from "../runtime/lifecycle.js";
6
6
  import { disposeAppResources } from "../runtime/registry.js";
7
7
  const INITIAL_SESSIONS = [];
8
- function reduceMicroFrontendSessions(sessions, event) {
9
- switch (event.name) {
10
- case 'openApp':
11
- return sessions.some(({
12
- sessionId
13
- }) => sessionId === event.params.sessionId) ? sessions : [...sessions, {
14
- appName: event.params.appName,
15
- sessionId: event.params.sessionId,
16
- scheme: event.params.scheme,
17
- isVisible: false
18
- }];
19
- case 'closeApp':
20
- return sessions.filter(({
21
- sessionId
22
- }) => sessionId !== event.params.sessionId);
23
- case 'sessionVisibilityChanged':
24
- return sessions.map(session => session.sessionId === event.params.sessionId ? {
25
- ...session,
26
- isVisible: event.params.isVisible
27
- } : session);
28
- default:
29
- {
30
- const exhaustiveEvent = event;
31
- return exhaustiveEvent;
32
- }
33
- }
34
- }
35
8
  export function useMicroFrontendSessions(runtime) {
36
- const [sessions, dispatch] = useReducer(reduceMicroFrontendSessions, INITIAL_SESSIONS);
37
- const previousSessionsRef = useRef(sessions);
9
+ const subscribe = useCallback(listener => runtime.onSessionsChanged(listener).remove, [runtime]);
10
+ const sessions = useSyncExternalStore(subscribe, runtime.getSessions);
11
+ const previousSessionsRef = useRef(INITIAL_SESSIONS);
38
12
  useEffect(() => {
39
13
  const previousSessions = previousSessionsRef.current;
40
14
  previousSessionsRef.current = sessions;
@@ -76,7 +50,6 @@ export function useMicroFrontendSessions(runtime) {
76
50
  if (event.name === 'openApp') {
77
51
  resetPendingHostComponent();
78
52
  }
79
- dispatch(event);
80
53
  });
81
54
  return () => subscription.remove();
82
55
  }, [runtime]);
@@ -1 +1 @@
1
- {"version":3,"names":["useEffect","useLayoutEffect","useReducer","useRef","installPendingHostComponentBridge","resetPendingHostComponent","emitMicroFrontendLifecycleEvent","disposeAppResources","INITIAL_SESSIONS","reduceMicroFrontendSessions","sessions","event","name","some","sessionId","params","appName","scheme","isVisible","filter","map","session","exhaustiveEvent","useMicroFrontendSessions","runtime","dispatch","previousSessionsRef","previousSessions","current","activeSessionIds","Set","activeAppNames","mountedSessions","previousSession","inactiveSessions","has","inactiveAppNames","forEach","createLifecycleEvent","appDisposals","Map","Array","from","catch","error","console","appDisposal","get","then","subscription","onEvent","remove","phase","activeSessions","toLifecycleSession","id"],"sourceRoot":"../../../src","sources":["session/useMicroFrontendSessions.ts"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["useCallback","useEffect","useLayoutEffect","useRef","useSyncExternalStore","installPendingHostComponentBridge","resetPendingHostComponent","emitMicroFrontendLifecycleEvent","disposeAppResources","INITIAL_SESSIONS","useMicroFrontendSessions","runtime","subscribe","listener","onSessionsChanged","remove","sessions","getSessions","previousSessionsRef","previousSessions","current","activeSessionIds","Set","map","sessionId","activeAppNames","appName","mountedSessions","filter","some","previousSession","inactiveSessions","has","inactiveAppNames","forEach","session","createLifecycleEvent","appDisposals","Map","Array","from","catch","error","console","appDisposal","get","then","subscription","onEvent","event","name","phase","activeSessions","toLifecycleSession","id"],"sourceRoot":"../../../src","sources":["session/useMicroFrontendSessions.ts"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,5 @@
1
+ import type { MicroFrontendSessionEvent, MicroFrontendSessionState } from '../types';
2
+ export declare function createSessionStore(): {
3
+ getSessions: () => readonly MicroFrontendSessionState[];
4
+ applyEvent(event: MicroFrontendSessionEvent): boolean;
5
+ };
@@ -1,8 +1,3 @@
1
- import type { MicroFrontendRuntimeApi } from '../types';
2
- export interface MicroFrontendSessionState {
3
- readonly appName: string;
4
- readonly sessionId: string;
5
- readonly scheme: string;
6
- readonly isVisible: boolean;
7
- }
8
- export declare function useMicroFrontendSessions(runtime: Pick<MicroFrontendRuntimeApi, 'onEvent'>): readonly MicroFrontendSessionState[];
1
+ import type { MicroFrontendRuntimeApi, MicroFrontendSessionState } from '../types';
2
+ export type { MicroFrontendSessionState } from '../types';
3
+ export declare function useMicroFrontendSessions(runtime: Pick<MicroFrontendRuntimeApi, 'onEvent' | 'getSessions' | 'onSessionsChanged'>): readonly MicroFrontendSessionState[];
@@ -42,6 +42,12 @@ export type MicroFrontendSessionEvent = Exclude<MicroFrontendRuntimeEvent, {
42
42
  export interface MicroFrontendRuntimeEventSubscription {
43
43
  readonly remove: () => void;
44
44
  }
45
+ export interface MicroFrontendSessionState {
46
+ readonly appName: string;
47
+ readonly sessionId: string;
48
+ readonly scheme: string;
49
+ readonly isVisible: boolean;
50
+ }
45
51
  export interface MicroFrontendLifecycleSession {
46
52
  readonly appName: string;
47
53
  readonly id: string;
@@ -57,5 +63,9 @@ export interface MicroFrontendRuntimeApi {
57
63
  readonly evaluateScript: (filePath: string) => Promise<void>;
58
64
  readonly preloadApp: (appName: string) => Promise<void>;
59
65
  readonly importApp: <TModule>(request: AppRequest) => Promise<TModule>;
66
+ /** Read the current snapshot without starting event delivery. */
67
+ readonly getSessions: () => readonly MicroFrontendSessionState[];
68
+ /** Subscribe to changes and start native delivery if needed; read getSessions() for the initial snapshot. */
69
+ readonly onSessionsChanged: (listener: (sessions: readonly MicroFrontendSessionState[]) => void) => MicroFrontendRuntimeEventSubscription;
60
70
  readonly onEvent: (listener: (event: MicroFrontendSessionEvent) => void) => MicroFrontendRuntimeEventSubscription;
61
71
  }
@@ -28,9 +28,16 @@ NS_ASSUME_NONNULL_BEGIN
28
28
  /// when the host view is created. Pass nil to unregister.
29
29
  - (void)setName:(nullable NSString *)name;
30
30
 
31
- /// Creates the underlying Fabric host view, attaches the touch handler, and
32
- /// applies any pending name. No-op when already activated. Must be called on
33
- /// the main thread after the React runtime has booted.
31
+ /// Creates the underlying Fabric host view under an `RCTRootComponentView`
32
+ /// anchor, attaches the touch handler, and applies any pending name. No-op when
33
+ /// already activated. Must be called on the main thread after the React runtime
34
+ /// has booted.
35
+ ///
36
+ /// The anchor makes hosted screens count as mounted under a React root for
37
+ /// react-native-screens, so they do not attach a second touch handler beneath
38
+ /// the container's. Hosted content therefore gets one handler and page
39
+ /// coordinates relative to this container, like content under a regular React
40
+ /// Native root.
34
41
  - (void)activateIfNeeded;
35
42
 
36
43
  /// Whether the underlying Fabric host view has been created.
@@ -1,9 +1,183 @@
1
1
  #import "PortalHostContainerView.h"
2
2
 
3
+ #import <React/RCTRootComponentView.h>
3
4
  #import <React/RCTSurfaceTouchHandler.h>
4
5
  #import "PortalHostView.h"
5
6
 
7
+ @interface GranitePortalSurfaceTouchHandler : RCTSurfaceTouchHandler
8
+ @end
9
+
10
+ static BOOL GraniteIsSurfaceTouchHandler(UIGestureRecognizer *recognizer)
11
+ {
12
+ return [recognizer isKindOfClass:[RCTSurfaceTouchHandler class]];
13
+ }
14
+
15
+ static BOOL GraniteIsPortalSurfaceTouchHandler(UIGestureRecognizer *recognizer)
16
+ {
17
+ return [recognizer isKindOfClass:[GranitePortalSurfaceTouchHandler class]];
18
+ }
19
+
20
+ /// What a single view contributes to touch ownership. A view installs at most
21
+ /// one meaningful Fabric touch root: none, a Portal one, or a foreign one that
22
+ /// belongs to a plain React Native root and outranks any Portal handler.
23
+ typedef NS_ENUM(NSUInteger, GraniteTouchRootKind) {
24
+ GraniteTouchRootKindNone = 0,
25
+ GraniteTouchRootKindPortal,
26
+ GraniteTouchRootKindForeign,
27
+ };
28
+
29
+ /// Classifies the Fabric touch root installed on a single view of the touch path.
30
+ ///
31
+ /// Takes `view.gestureRecognizers` and ignores everything that is not an enabled
32
+ /// `RCTSurfaceTouchHandler`: disabled handlers never dispatch, and unrelated
33
+ /// recognizers (scroll, pan, and friends) do not own Fabric touch dispatch.
34
+ ///
35
+ /// A foreign handler wins over a Portal handler on the same view. A screen mounted
36
+ /// outside a React root can install its own handler beneath the Portal container,
37
+ /// and yielding to it is what stops both handlers from forwarding the same tap.
38
+ ///
39
+ /// On `GraniteTouchRootKindPortal` the first matching handler is written to
40
+ /// `outHandler`, which is left untouched for the other kinds.
41
+ static GraniteTouchRootKind GraniteTouchRootOnView(
42
+ UIView *view,
43
+ GranitePortalSurfaceTouchHandler *__strong *outHandler)
44
+ {
45
+ __block GraniteTouchRootKind kind = GraniteTouchRootKindNone;
46
+ __block GranitePortalSurfaceTouchHandler *portalHandler = nil;
47
+
48
+ [view.gestureRecognizers enumerateObjectsUsingBlock:^(
49
+ UIGestureRecognizer *recognizer,
50
+ __unused NSUInteger index,
51
+ BOOL *stop) {
52
+ if (!recognizer.isEnabled || !GraniteIsSurfaceTouchHandler(recognizer)) {
53
+ return;
54
+ }
55
+ if (!GraniteIsPortalSurfaceTouchHandler(recognizer)) {
56
+ kind = GraniteTouchRootKindForeign;
57
+ *stop = YES;
58
+ return;
59
+ }
60
+ if (kind == GraniteTouchRootKindNone) {
61
+ kind = GraniteTouchRootKindPortal;
62
+ portalHandler = (GranitePortalSurfaceTouchHandler *)recognizer;
63
+ }
64
+ }];
65
+
66
+ if (kind == GraniteTouchRootKindPortal) {
67
+ *outHandler = portalHandler;
68
+ }
69
+
70
+ return kind;
71
+ }
72
+
73
+ /// Walks the touch path upwards from `view` and reports whether it stays Portal-owned.
74
+ ///
75
+ /// Recurses through `view.superview`, classifying each view with
76
+ /// `GraniteTouchRootOnView`. Returns NO as soon as a foreign React Native root
77
+ /// claims the path, and YES once the walk runs past the top without meeting one.
78
+ ///
79
+ /// `outOwningPortalHandler` receives the Portal handler whose container hosts the
80
+ /// touched view. It accumulates across the recursion and must start as nil: the
81
+ /// walk runs bottom-up, so the first Portal handler met is the hosting one.
82
+ static BOOL GranitePortalOwnsTouchPath(
83
+ UIView *view,
84
+ GranitePortalSurfaceTouchHandler *__strong *outOwningPortalHandler)
85
+ {
86
+ if (view == nil) {
87
+ return YES;
88
+ }
89
+
90
+ GranitePortalSurfaceTouchHandler *handlerOnView = nil;
91
+
92
+ switch (GraniteTouchRootOnView(view, &handlerOnView)) {
93
+ case GraniteTouchRootKindForeign:
94
+ // A React Native root owns this touch path; let it dispatch alone.
95
+ return NO;
96
+ case GraniteTouchRootKindPortal:
97
+ if (*outOwningPortalHandler == nil) {
98
+ *outOwningPortalHandler = handlerOnView;
99
+ }
100
+ break;
101
+ case GraniteTouchRootKindNone:
102
+ break;
103
+ }
104
+
105
+ return GranitePortalOwnsTouchPath(view.superview, outOwningPortalHandler);
106
+ }
107
+
108
+ /// Decides whether `touchHandler` is the single handler allowed to dispatch `touch`.
109
+ ///
110
+ /// Takes the handler's own view and the touched view, then hands the actual touch
111
+ /// path — the touched view up through its superviews — to `GranitePortalOwnsTouchPath`.
112
+ /// The handler dispatches only when it is the Portal handler owning that path.
113
+ ///
114
+ /// Why this leaves exactly one handler, however many Portals exist:
115
+ ///
116
+ /// Sibling Portals never see each other's touches.
117
+ ///
118
+ /// screen
119
+ /// ├ portalA <- handlerA
120
+ /// │ └ leafA
121
+ /// └ portalB <- handlerB
122
+ /// └ leafB
123
+ ///
124
+ /// UIKit only asks `shouldReceiveTouch:` of recognizers on the touched view and its
125
+ /// ancestors, so handlerB is never asked about `leafA`. Even if it were, the
126
+ /// `isDescendantOfView:` guard below rejects a touch outside the handler's own subtree.
127
+ ///
128
+ /// Nested Portals are all asked, and all resolve to the same owner.
129
+ ///
130
+ /// portalA <- handlerA
131
+ /// └ portalB <- handlerB
132
+ /// └ portalC <- handlerC
133
+ /// └ leaf
134
+ ///
135
+ /// A, B and C are all ancestors of `leaf`, so UIKit asks all three. Each one walks
136
+ /// the same path from `leaf` upwards, and the walk direction is fixed, so each one
137
+ /// meets C first. `owningPortalHandler == touchHandler` therefore holds for C alone;
138
+ /// A and B yield. Any nesting depth leaves exactly one handler.
139
+ ///
140
+ /// C is also the right owner, not merely the unique one. Portal content is always
141
+ /// mounted inside its own container's subtree, so the first container met is the one
142
+ /// whose surface rendered `leaf`. Fabric resolves the event receiver from the touched
143
+ /// component view regardless of which handler fires, but it computes page coordinates
144
+ /// relative to the firing handler's view — and `leaf` belongs to C's coordinate space.
145
+ ///
146
+ /// Plain hosted content installs no touch root of its own and so resolves to the
147
+ /// container hosting it.
148
+ static BOOL GranitePortalSurfaceTouchHandlerShouldReceiveTouch(
149
+ GranitePortalSurfaceTouchHandler *touchHandler,
150
+ UITouch *touch)
151
+ {
152
+ UIView *handlerView = touchHandler.view;
153
+ UIView *touchView = touch.view;
154
+
155
+ // A touch that never entered this container is none of its business.
156
+ if (handlerView == nil || touchView == nil || ![touchView isDescendantOfView:handlerView]) {
157
+ return NO;
158
+ }
159
+
160
+ GranitePortalSurfaceTouchHandler *owningPortalHandler = nil;
161
+ if (!GranitePortalOwnsTouchPath(touchView, &owningPortalHandler)) {
162
+ return NO;
163
+ }
164
+
165
+ // No Portal handler on the path means this one detached mid-touch: stay
166
+ // permissive rather than dropping the touch entirely.
167
+ return owningPortalHandler == nil || owningPortalHandler == touchHandler;
168
+ }
169
+
170
+ @implementation GranitePortalSurfaceTouchHandler
171
+
172
+ - (BOOL)gestureRecognizer:(__unused UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
173
+ {
174
+ return GranitePortalSurfaceTouchHandlerShouldReceiveTouch(self, touch);
175
+ }
176
+
177
+ @end
178
+
6
179
  @implementation PortalHostContainerView {
180
+ RCTRootComponentView *_reactRootAnchor;
7
181
  PortalHostView *_portalHostView;
8
182
  RCTSurfaceTouchHandler *_touchHandler;
9
183
  NSString *_pendingName;
@@ -49,16 +223,37 @@
49
223
  return;
50
224
  }
51
225
 
52
- _portalHostView = [[PortalHostView alloc] initWithFrame:self.bounds];
226
+ // Hosted content mounts under an RCTRootComponentView anchor so react-native-screens treats
227
+ // hosted screens as root-mounted. RNSScreenView walks its superview chain for an
228
+ // RCTRootComponentView (or another RNSScreenView) in -isMountedUnderScreenOrReactRoot and, when
229
+ // it finds none, attaches its own RCTSurfaceTouchHandler in -didMoveToWindow. Under a Portal
230
+ // host that second handler would sit beneath this container's handler, every tap would reach JS
231
+ // twice, and on the second touchStart an ancestor PanResponder could steal the responder from
232
+ // the tapped Pressable. With the anchor, hosted screens see the same tree shape as content under
233
+ // a regular RCTSurfaceView: one handler, page coordinates relative to this container.
234
+ //
235
+ // The anchor is never registered with Fabric (tag 0, no event emitter, no shadow node); it is
236
+ // only a superview. The host view joins it through -addSubview:, not
237
+ // -mountChildComponentView:index:, so the anchor never posts RCTContentDidAppearNotification.
238
+ // It is created here rather than in -init for the same reason the host view is deferred: an
239
+ // RCTViewComponentView reads ReactNativeFeatureFlags while it settles into the view hierarchy,
240
+ // which must not happen before the host's boot-time override (see the header on deferred
241
+ // activation).
242
+ _reactRootAnchor = [[RCTRootComponentView alloc] initWithFrame:self.bounds];
243
+ _reactRootAnchor.autoresizingMask =
244
+ UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
245
+ [self addSubview:_reactRootAnchor];
246
+
247
+ _portalHostView = [[PortalHostView alloc] initWithFrame:_reactRootAnchor.bounds];
53
248
  _portalHostView.autoresizingMask =
54
249
  UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
55
250
  __weak PortalHostContainerView *weakSelf = self;
56
251
  _portalHostView.onSubviewCountChanged = ^{
57
252
  [weakSelf handleSubviewCountChanged];
58
253
  };
59
- [self addSubview:_portalHostView];
254
+ [_reactRootAnchor addSubview:_portalHostView];
60
255
 
61
- _touchHandler = [RCTSurfaceTouchHandler new];
256
+ _touchHandler = [GranitePortalSurfaceTouchHandler new];
62
257
  [_touchHandler attachToView:self];
63
258
 
64
259
  if (_pendingName) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@granite-js/micro-frontend",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "description": "Micro-frontend build, runtime, session, and Portal contracts for Granite brownfield applications",
5
5
  "main": "./dist/module/index.js",
6
6
  "types": "./dist/typescript/index.d.ts",
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "@babel/core": "7.28.5",
59
- "@granite-js/utils": "2.3.1",
59
+ "@granite-js/utils": "2.4.0",
60
60
  "@standard-schema/spec": "^1.0.0"
61
61
  },
62
62
  "peerDependencies": {
@@ -72,8 +72,8 @@
72
72
  },
73
73
  "devDependencies": {
74
74
  "@babel/types": "7.28.5",
75
- "@granite-js/plugin-core": "2.3.1",
76
- "@granite-js/react-native": "2.3.1",
75
+ "@granite-js/plugin-core": "2.4.0",
76
+ "@granite-js/react-native": "2.4.0",
77
77
  "@types/babel__core": "^7.20.5",
78
78
  "@types/node": "24.10.12",
79
79
  "@types/react": "19.2.0",
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  createRoute as createGraniteRoute,
3
- getSchemeUri,
4
3
  type RegisterScreenInput,
5
4
  type RouteOptions,
6
5
  useNavigation,
@@ -66,12 +65,11 @@ function getCurrentGraniteApp(): PendingHostComponentAppConfig | null {
66
65
 
67
66
  const host = granite['app']['host'];
68
67
  const name = granite['app']['name'];
69
- if (typeof host !== 'string' || typeof name !== 'string') {
68
+ const scheme = granite['app']['scheme'];
69
+ if (typeof host !== 'string' || typeof name !== 'string' || typeof scheme !== 'string') {
70
70
  return null;
71
71
  }
72
72
 
73
- const scheme = new URL(getSchemeUri()).protocol.replace(/:$/g, '');
74
-
75
73
  return {
76
74
  name,
77
75
  scheme,
@@ -5,12 +5,15 @@ import type {
5
5
  MicroFrontendRuntimeApi,
6
6
  MicroFrontendRuntimeEvent,
7
7
  MicroFrontendRuntimeEventSubscription,
8
+ MicroFrontendSessionEvent,
9
+ MicroFrontendSessionState,
8
10
  } from '../types';
9
11
  import { AppContainerNotFoundError, InvalidAppNameError } from './errors';
10
12
  import { getMicroFrontendGlobalContext } from './globalContext';
11
13
  import { setMicroFrontendLifecycleCallback } from './lifecycle';
12
14
  import { installNativeComponentRegistryCompatibility } from './nativeComponentRegistryCompatibility';
13
15
  import { parseAppRequest } from './parseAppRequest';
16
+ import { createSessionStore } from './sessionStore';
14
17
 
15
18
  export interface NativeMicroFrontendRuntimeEvent {
16
19
  readonly name: string;
@@ -51,6 +54,37 @@ export function createMicroFrontendRuntimeWithDependencies(
51
54
  ): MicroFrontendRuntimeApi {
52
55
  // A fulfilled promise is the evaluated-state cache. Rejected evaluations remove themselves.
53
56
  const appEvaluations = new Map<string, Promise<void>>();
57
+ const sessionStore = createSessionStore();
58
+ const sessionListeners = new Set<(sessions: readonly MicroFrontendSessionState[]) => void>();
59
+ const eventListeners = new Set<(event: MicroFrontendSessionEvent) => void>();
60
+ let nativeSubscription: MicroFrontendRuntimeEventSubscription | undefined;
61
+
62
+ function startEventDelivery() {
63
+ if (nativeSubscription != null) {
64
+ return;
65
+ }
66
+ nativeSubscription = dependencies.nativeRuntime.onEvent((event) => {
67
+ const parsedEvent = dependencies.parseEvent(event);
68
+ switch (parsedEvent.name) {
69
+ case 'preloadApp':
70
+ void preloadApp(parsedEvent.params.appName).catch(dependencies.onPreloadError);
71
+ return;
72
+ case 'openApp':
73
+ case 'closeApp':
74
+ case 'sessionVisibilityChanged':
75
+ if (sessionStore.applyEvent(parsedEvent)) {
76
+ notifyListeners(sessionListeners, sessionStore.getSessions());
77
+ }
78
+ notifyListeners(eventListeners, parsedEvent);
79
+ return;
80
+ default: {
81
+ const exhaustiveEvent: never = parsedEvent;
82
+ return exhaustiveEvent;
83
+ }
84
+ }
85
+ });
86
+ dependencies.nativeRuntime.startEventDelivery();
87
+ }
54
88
 
55
89
  function resetFailedEvaluation(appName: string) {
56
90
  appEvaluations.delete(appName);
@@ -101,26 +135,18 @@ export function createMicroFrontendRuntimeWithDependencies(
101
135
  await preloadApp(appName);
102
136
  return dependencies.registry.importModule<TModule>(request);
103
137
  },
138
+ getSessions: sessionStore.getSessions,
139
+ onSessionsChanged(listener) {
140
+ const callback = (sessions: readonly MicroFrontendSessionState[]) => listener(sessions);
141
+ sessionListeners.add(callback);
142
+ startEventDelivery();
143
+ return { remove: () => sessionListeners.delete(callback) };
144
+ },
104
145
  onEvent(listener) {
105
- const subscription = dependencies.nativeRuntime.onEvent((event) => {
106
- const parsedEvent = dependencies.parseEvent(event);
107
- switch (parsedEvent.name) {
108
- case 'preloadApp':
109
- void preloadApp(parsedEvent.params.appName).catch(dependencies.onPreloadError);
110
- return;
111
- case 'openApp':
112
- case 'closeApp':
113
- case 'sessionVisibilityChanged':
114
- listener(parsedEvent);
115
- return;
116
- default: {
117
- const exhaustiveEvent: never = parsedEvent;
118
- return exhaustiveEvent;
119
- }
120
- }
121
- });
122
- dependencies.nativeRuntime.startEventDelivery();
123
- return subscription;
146
+ const callback = (event: MicroFrontendSessionEvent) => listener(event);
147
+ eventListeners.add(callback);
148
+ startEventDelivery();
149
+ return { remove: () => eventListeners.delete(callback) };
124
150
  },
125
151
  };
126
152
 
@@ -128,3 +154,13 @@ export function createMicroFrontendRuntimeWithDependencies(
128
154
 
129
155
  return runtime;
130
156
  }
157
+
158
+ function notifyListeners<TEvent>(listeners: ReadonlySet<(event: TEvent) => void>, event: TEvent): void {
159
+ listeners.forEach((listener) => {
160
+ try {
161
+ listener(event);
162
+ } catch (error) {
163
+ console.error('Failed to run a micro-frontend runtime listener', error);
164
+ }
165
+ });
166
+ }
@@ -0,0 +1,40 @@
1
+ import type { MicroFrontendSessionEvent, MicroFrontendSessionState } from '../types';
2
+
3
+ export function createSessionStore() {
4
+ let sessions: readonly MicroFrontendSessionState[] = Object.freeze([]);
5
+
6
+ return {
7
+ getSessions: () => sessions,
8
+ applyEvent(event: MicroFrontendSessionEvent): boolean {
9
+ const nextSessions = reduceSessions(sessions, event);
10
+ if (nextSessions === sessions) {
11
+ return false;
12
+ }
13
+ sessions = Object.freeze(nextSessions);
14
+ return true;
15
+ },
16
+ };
17
+ }
18
+
19
+ function reduceSessions(
20
+ sessions: readonly MicroFrontendSessionState[],
21
+ event: MicroFrontendSessionEvent
22
+ ): readonly MicroFrontendSessionState[] {
23
+ const session = sessions.find(({ sessionId }) => sessionId === event.params.sessionId);
24
+ switch (event.name) {
25
+ case 'openApp':
26
+ return session == null ? [...sessions, Object.freeze({ ...event.params, isVisible: false })] : sessions;
27
+ case 'closeApp':
28
+ return session == null ? sessions : sessions.filter(({ sessionId }) => sessionId !== event.params.sessionId);
29
+ case 'sessionVisibilityChanged':
30
+ return session == null || session.isVisible === event.params.isVisible
31
+ ? sessions
32
+ : sessions.map((current) =>
33
+ current === session ? Object.freeze({ ...current, isVisible: event.params.isVisible }) : current
34
+ );
35
+ default: {
36
+ const exhaustiveEvent: never = event;
37
+ return exhaustiveEvent;
38
+ }
39
+ }
40
+ }
@@ -1,4 +1,4 @@
1
- import { useEffect, useLayoutEffect, useReducer, useRef } from 'react';
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useSyncExternalStore } from 'react';
2
2
  import { installPendingHostComponentBridge, resetPendingHostComponent } from '../host/pendingHostComponentStore';
3
3
  import { emitMicroFrontendLifecycleEvent } from '../runtime/lifecycle';
4
4
  import { disposeAppResources } from '../runtime/registry';
@@ -6,53 +6,19 @@ import type {
6
6
  MicroFrontendLifecycleEvent,
7
7
  MicroFrontendLifecycleSession,
8
8
  MicroFrontendRuntimeApi,
9
- MicroFrontendSessionEvent,
9
+ MicroFrontendSessionState,
10
10
  } from '../types';
11
11
 
12
- export interface MicroFrontendSessionState {
13
- readonly appName: string;
14
- readonly sessionId: string;
15
- readonly scheme: string;
16
- readonly isVisible: boolean;
17
- }
12
+ export type { MicroFrontendSessionState } from '../types';
18
13
 
19
14
  const INITIAL_SESSIONS: readonly MicroFrontendSessionState[] = [];
20
15
 
21
- function reduceMicroFrontendSessions(
22
- sessions: readonly MicroFrontendSessionState[],
23
- event: MicroFrontendSessionEvent
24
- ): readonly MicroFrontendSessionState[] {
25
- switch (event.name) {
26
- case 'openApp':
27
- return sessions.some(({ sessionId }) => sessionId === event.params.sessionId)
28
- ? sessions
29
- : [
30
- ...sessions,
31
- {
32
- appName: event.params.appName,
33
- sessionId: event.params.sessionId,
34
- scheme: event.params.scheme,
35
- isVisible: false,
36
- },
37
- ];
38
- case 'closeApp':
39
- return sessions.filter(({ sessionId }) => sessionId !== event.params.sessionId);
40
- case 'sessionVisibilityChanged':
41
- return sessions.map((session) =>
42
- session.sessionId === event.params.sessionId ? { ...session, isVisible: event.params.isVisible } : session
43
- );
44
- default: {
45
- const exhaustiveEvent: never = event;
46
- return exhaustiveEvent;
47
- }
48
- }
49
- }
50
-
51
16
  export function useMicroFrontendSessions(
52
- runtime: Pick<MicroFrontendRuntimeApi, 'onEvent'>
17
+ runtime: Pick<MicroFrontendRuntimeApi, 'onEvent' | 'getSessions' | 'onSessionsChanged'>
53
18
  ): readonly MicroFrontendSessionState[] {
54
- const [sessions, dispatch] = useReducer(reduceMicroFrontendSessions, INITIAL_SESSIONS);
55
- const previousSessionsRef = useRef(sessions);
19
+ const subscribe = useCallback((listener: () => void) => runtime.onSessionsChanged(listener).remove, [runtime]);
20
+ const sessions = useSyncExternalStore(subscribe, runtime.getSessions);
21
+ const previousSessionsRef = useRef(INITIAL_SESSIONS);
56
22
 
57
23
  useEffect(() => {
58
24
  const previousSessions = previousSessionsRef.current;
@@ -106,7 +72,6 @@ export function useMicroFrontendSessions(
106
72
  if (event.name === 'openApp') {
107
73
  resetPendingHostComponent();
108
74
  }
109
- dispatch(event);
110
75
  });
111
76
 
112
77
  return () => subscription.remove();
package/src/types.ts CHANGED
@@ -48,6 +48,13 @@ export interface MicroFrontendRuntimeEventSubscription {
48
48
  readonly remove: () => void;
49
49
  }
50
50
 
51
+ export interface MicroFrontendSessionState {
52
+ readonly appName: string;
53
+ readonly sessionId: string;
54
+ readonly scheme: string;
55
+ readonly isVisible: boolean;
56
+ }
57
+
51
58
  export interface MicroFrontendLifecycleSession {
52
59
  readonly appName: string;
53
60
  readonly id: string;
@@ -66,5 +73,11 @@ export interface MicroFrontendRuntimeApi {
66
73
  readonly evaluateScript: (filePath: string) => Promise<void>;
67
74
  readonly preloadApp: (appName: string) => Promise<void>;
68
75
  readonly importApp: <TModule>(request: AppRequest) => Promise<TModule>;
76
+ /** Read the current snapshot without starting event delivery. */
77
+ readonly getSessions: () => readonly MicroFrontendSessionState[];
78
+ /** Subscribe to changes and start native delivery if needed; read getSessions() for the initial snapshot. */
79
+ readonly onSessionsChanged: (
80
+ listener: (sessions: readonly MicroFrontendSessionState[]) => void
81
+ ) => MicroFrontendRuntimeEventSubscription;
69
82
  readonly onEvent: (listener: (event: MicroFrontendSessionEvent) => void) => MicroFrontendRuntimeEventSubscription;
70
83
  }