@granite-js/micro-frontend 2.3.2 → 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 +14 -0
- package/README.md +74 -19
- package/dist/module/runtime/createMicroFrontendRuntime.js +56 -20
- package/dist/module/runtime/createMicroFrontendRuntime.js.map +1 -1
- package/dist/module/runtime/sessionStore.js +43 -0
- package/dist/module/runtime/sessionStore.js.map +1 -0
- package/dist/module/session/useMicroFrontendSessions.js +4 -31
- package/dist/module/session/useMicroFrontendSessions.js.map +1 -1
- package/dist/typescript/runtime/sessionStore.d.ts +5 -0
- package/dist/typescript/session/useMicroFrontendSessions.d.ts +3 -8
- package/dist/typescript/types.d.ts +10 -0
- package/ios/PortalHostContainerView.h +10 -3
- package/ios/PortalHostContainerView.mm +198 -3
- package/package.json +4 -4
- package/src/runtime/createMicroFrontendRuntime.ts +55 -19
- package/src/runtime/sessionStore.ts +40 -0
- package/src/session/useMicroFrontendSessions.ts +7 -42
- package/src/types.ts +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
3
17
|
## 2.3.2
|
|
4
18
|
|
|
5
19
|
### 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
|
|
221
|
-
|
|
|
222
|
-
| `preloadApp(appName)`
|
|
223
|
-
| `importApp(request)`
|
|
224
|
-
| `evaluateScript(filePath)`
|
|
225
|
-
| `onEvent(listener)`
|
|
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)`
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
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
|
|
|
@@ -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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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","
|
|
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,
|
|
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
|
|
37
|
-
const
|
|
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","
|
|
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":[]}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import type { MicroFrontendRuntimeApi } from '../types';
|
|
2
|
-
export
|
|
3
|
-
|
|
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
|
|
32
|
-
/// applies any pending name. No-op when
|
|
33
|
-
/// the main thread after the React runtime
|
|
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
|
-
|
|
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
|
-
[
|
|
254
|
+
[_reactRootAnchor addSubview:_portalHostView];
|
|
60
255
|
|
|
61
|
-
_touchHandler = [
|
|
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
|
+
"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.
|
|
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.
|
|
76
|
-
"@granite-js/react-native": "2.
|
|
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",
|
|
@@ -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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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,
|
|
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
|
-
|
|
9
|
+
MicroFrontendSessionState,
|
|
10
10
|
} from '../types';
|
|
11
11
|
|
|
12
|
-
export
|
|
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
|
|
55
|
-
const
|
|
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
|
}
|