@mfeflow/runtime 0.5.0 → 0.7.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/dist/beacon.d.ts +45 -0
- package/dist/beacon.js +169 -0
- package/dist/report.d.ts +2 -0
- package/dist/report.js +2 -1
- package/package.json +7 -2
package/dist/beacon.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type RegistrySources } from './registry.js';
|
|
2
|
+
import { type RuntimeConfigValues } from './runtime-config.js';
|
|
3
|
+
/** The registry server's endpoint for these index URLs, and the env they name. */
|
|
4
|
+
export interface BeaconTarget {
|
|
5
|
+
url: string;
|
|
6
|
+
env: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const CLIENT_ID_KEY = "mfeflow:client";
|
|
9
|
+
/** The first index URL that is a registry server's `/v1/index/<env>`. A static host has no beacon. */
|
|
10
|
+
export declare function beaconTarget(urls: readonly string[]): BeaconTarget | undefined;
|
|
11
|
+
/** A stable id for this browser, so the server keeps giving it the same rollout side. Not tied to a person. */
|
|
12
|
+
export declare function clientId(): string;
|
|
13
|
+
/** The server index URLs get `?u=<client id>`; any other URL is returned as it came. */
|
|
14
|
+
export declare function withClientId(url: string, id?: string): string;
|
|
15
|
+
export type Send = (url: string, body: string) => boolean;
|
|
16
|
+
export interface BeaconReporter {
|
|
17
|
+
load(remote: string, version: string): void;
|
|
18
|
+
error(remote: string, version: string): void;
|
|
19
|
+
/** Sends what has been counted since the last flush as one request. Nothing counted, nothing sent. */
|
|
20
|
+
flush(): void;
|
|
21
|
+
stop(): void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Counts, per remote@version, whether this page loaded it and whether it failed, and sends the
|
|
25
|
+
* page's counts as one request: `{ env, u, entries }`. A page is one load and at most one error
|
|
26
|
+
* for a remote, so the error rate reads as "page views that hit an error". What was sent is
|
|
27
|
+
* remembered, so a flush on `visibilitychange` followed by more loads never reports the same load
|
|
28
|
+
* again. An error that comes after a clean report is sent once more.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createBeaconReporter(target: BeaconTarget, send: Send, client?: string): BeaconReporter;
|
|
31
|
+
/** Stops the running reporter so the next start is a fresh one. For tests. */
|
|
32
|
+
export declare function resetRegistryBeaconForTests(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Starts the reporter when the runtime config has `registryBeacon: true` and one of the index URLs
|
|
35
|
+
* is a registry server's. Otherwise does nothing. Calling it again returns the reporter already
|
|
36
|
+
* running, so the Module Federation plugin is registered once.
|
|
37
|
+
*/
|
|
38
|
+
export declare function startRegistryBeacon(config: RuntimeConfigValues, urls: readonly string[], send?: Send): BeaconReporter | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* `registrySources()` for a shell that wants the beacon. With `registryBeacon: true` in the runtime
|
|
41
|
+
* config it adds this browser's id to the registry server's index URLs and starts the reporter;
|
|
42
|
+
* without it, the sources come back untouched. Import it on demand
|
|
43
|
+
* (`await import('@mfeflow/runtime/beacon')`) so a shell with the option off never downloads it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function registrySourcesWithBeacon(waitMs?: number): Promise<RegistrySources>;
|
package/dist/beacon.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { registerPlugins } from "@module-federation/enhanced/runtime";
|
|
2
|
+
import { REMOTE_ERROR_EVENT, remoteVersion } from "./report.js";
|
|
3
|
+
import { registrySources } from "./registry.js";
|
|
4
|
+
import { RUNTIME_CONFIG_KEY } from "./runtime-config.js";
|
|
5
|
+
const SERVER_INDEX = /\/v1\/index\/([a-z][a-z0-9-]*)$/;
|
|
6
|
+
const PARSE_BASE = 'https://page.invalid';
|
|
7
|
+
const MAX_ENTRIES = 20;
|
|
8
|
+
const MAX_BYTES = 4096;
|
|
9
|
+
const MAX_PENDING = 200;
|
|
10
|
+
const CLIENT_ID_KEY = 'mfeflow:client';
|
|
11
|
+
function beaconTarget(urls) {
|
|
12
|
+
for (const raw of urls){
|
|
13
|
+
if (!URL.canParse(raw, PARSE_BASE)) continue;
|
|
14
|
+
const url = new URL(raw, PARSE_BASE);
|
|
15
|
+
const env = SERVER_INDEX.exec(url.pathname)?.[1];
|
|
16
|
+
if (void 0 === env) continue;
|
|
17
|
+
const beacon = new URL(raw, "u" < typeof location ? PARSE_BASE : location.href);
|
|
18
|
+
beacon.pathname = beacon.pathname.replace(SERVER_INDEX, '/v1/beacon');
|
|
19
|
+
beacon.search = '';
|
|
20
|
+
beacon.hash = '';
|
|
21
|
+
return {
|
|
22
|
+
url: beacon.href,
|
|
23
|
+
env
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const randomId = ()=>Array.from(crypto.getRandomValues(new Uint8Array(16)), (b)=>b.toString(16).padStart(2, '0')).join('');
|
|
28
|
+
let memoryId;
|
|
29
|
+
function clientId() {
|
|
30
|
+
try {
|
|
31
|
+
const stored = localStorage.getItem(CLIENT_ID_KEY);
|
|
32
|
+
if (null !== stored && /^[\w.:-]{1,128}$/.test(stored)) return stored;
|
|
33
|
+
const fresh = randomId();
|
|
34
|
+
localStorage.setItem(CLIENT_ID_KEY, fresh);
|
|
35
|
+
return fresh;
|
|
36
|
+
} catch {
|
|
37
|
+
memoryId ??= randomId();
|
|
38
|
+
return memoryId;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function withClientId(url, id = clientId()) {
|
|
42
|
+
if (!URL.canParse(url, PARSE_BASE)) return url;
|
|
43
|
+
const parsed = new URL(url, PARSE_BASE);
|
|
44
|
+
if (!SERVER_INDEX.test(parsed.pathname)) return url;
|
|
45
|
+
const absolute = URL.canParse(url);
|
|
46
|
+
const target = absolute ? new URL(url) : parsed;
|
|
47
|
+
target.searchParams.set('u', id);
|
|
48
|
+
return absolute ? target.href : `${target.pathname}${target.search}${target.hash}`;
|
|
49
|
+
}
|
|
50
|
+
const byteLength = (text)=>new TextEncoder().encode(text).length;
|
|
51
|
+
function createBeaconReporter(target, send, client = clientId()) {
|
|
52
|
+
const pending = new Map();
|
|
53
|
+
const sent = new Map();
|
|
54
|
+
const note = (remote, version, failed)=>{
|
|
55
|
+
const key = `${remote}@${version}`;
|
|
56
|
+
const before = sent.get(key);
|
|
57
|
+
if (void 0 !== before && (before || !failed)) return;
|
|
58
|
+
const known = pending.get(key);
|
|
59
|
+
if (void 0 !== known) known.failed ||= failed;
|
|
60
|
+
else if (pending.size < MAX_PENDING) pending.set(key, {
|
|
61
|
+
remote,
|
|
62
|
+
version,
|
|
63
|
+
failed
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const flush = ()=>{
|
|
67
|
+
if (0 === pending.size) return;
|
|
68
|
+
const batch = [
|
|
69
|
+
...pending.entries()
|
|
70
|
+
];
|
|
71
|
+
pending.clear();
|
|
72
|
+
for (const [key, { failed }] of batch)sent.set(key, failed);
|
|
73
|
+
const head = JSON.stringify({
|
|
74
|
+
env: target.env,
|
|
75
|
+
u: client,
|
|
76
|
+
entries: []
|
|
77
|
+
});
|
|
78
|
+
let chunk = [];
|
|
79
|
+
let size = head.length;
|
|
80
|
+
const emit = ()=>{
|
|
81
|
+
if (chunk.length > 0) send(target.url, JSON.stringify({
|
|
82
|
+
env: target.env,
|
|
83
|
+
u: client,
|
|
84
|
+
entries: chunk
|
|
85
|
+
}));
|
|
86
|
+
chunk = [];
|
|
87
|
+
size = head.length;
|
|
88
|
+
};
|
|
89
|
+
for (const [, { remote, version, failed }] of batch){
|
|
90
|
+
const entry = {
|
|
91
|
+
remote,
|
|
92
|
+
version,
|
|
93
|
+
loads: 1,
|
|
94
|
+
errors: failed ? 1 : 0
|
|
95
|
+
};
|
|
96
|
+
const extra = byteLength(JSON.stringify(entry)) + 1;
|
|
97
|
+
if (chunk.length === MAX_ENTRIES || size + extra > MAX_BYTES) emit();
|
|
98
|
+
chunk.push(entry);
|
|
99
|
+
size += extra;
|
|
100
|
+
}
|
|
101
|
+
emit();
|
|
102
|
+
};
|
|
103
|
+
const onError = (event)=>{
|
|
104
|
+
const detail = event instanceof CustomEvent ? event.detail : void 0;
|
|
105
|
+
const mfe = 'object' == typeof detail && null !== detail ? Reflect.get(detail, 'mfe') : void 0;
|
|
106
|
+
const version = 'object' == typeof detail && null !== detail ? Reflect.get(detail, 'version') : void 0;
|
|
107
|
+
if ('string' == typeof mfe && 'string' == typeof version) note(mfe, version, true);
|
|
108
|
+
};
|
|
109
|
+
const onHide = ()=>{
|
|
110
|
+
if ("u" < typeof document || 'hidden' === document.visibilityState) flush();
|
|
111
|
+
};
|
|
112
|
+
window.addEventListener(REMOTE_ERROR_EVENT, onError);
|
|
113
|
+
window.addEventListener('pagehide', flush);
|
|
114
|
+
document.addEventListener('visibilitychange', onHide);
|
|
115
|
+
return {
|
|
116
|
+
load: (remote, version)=>note(remote, version, false),
|
|
117
|
+
error: (remote, version)=>note(remote, version, true),
|
|
118
|
+
flush,
|
|
119
|
+
stop () {
|
|
120
|
+
window.removeEventListener(REMOTE_ERROR_EVENT, onError);
|
|
121
|
+
window.removeEventListener('pagehide', flush);
|
|
122
|
+
document.removeEventListener('visibilitychange', onHide);
|
|
123
|
+
pending.clear();
|
|
124
|
+
sent.clear();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const sendWithBeacon = (url, body)=>"u" > typeof navigator && 'function' == typeof navigator.sendBeacon ? navigator.sendBeacon(url, body) : false;
|
|
129
|
+
let active;
|
|
130
|
+
function resetRegistryBeaconForTests() {
|
|
131
|
+
active?.stop();
|
|
132
|
+
active = void 0;
|
|
133
|
+
}
|
|
134
|
+
function startRegistryBeacon(config, urls, send = sendWithBeacon) {
|
|
135
|
+
if (true !== config.registryBeacon) return;
|
|
136
|
+
if (void 0 !== active) return active;
|
|
137
|
+
const target = beaconTarget(urls);
|
|
138
|
+
if (void 0 === target) return;
|
|
139
|
+
const reporter = createBeaconReporter(target, send);
|
|
140
|
+
active = reporter;
|
|
141
|
+
registerPlugins([
|
|
142
|
+
{
|
|
143
|
+
name: 'mfeflow-beacon',
|
|
144
|
+
onLoad (args) {
|
|
145
|
+
const remote = args.id.split('/')[0] ?? args.id;
|
|
146
|
+
const version = remoteVersion(remote);
|
|
147
|
+
if (void 0 !== version) reporter.load(remote, version);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
]);
|
|
151
|
+
return reporter;
|
|
152
|
+
}
|
|
153
|
+
const isConfig = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
154
|
+
async function registrySourcesWithBeacon(waitMs) {
|
|
155
|
+
const sources = await registrySources(waitMs);
|
|
156
|
+
const config = Reflect.get(globalThis, RUNTIME_CONFIG_KEY);
|
|
157
|
+
if (!isConfig(config) || true !== config.registryBeacon) return sources;
|
|
158
|
+
try {
|
|
159
|
+
const urls = sources.urls.map((url)=>withClientId(url));
|
|
160
|
+
startRegistryBeacon(config, urls);
|
|
161
|
+
return {
|
|
162
|
+
...sources,
|
|
163
|
+
urls
|
|
164
|
+
};
|
|
165
|
+
} catch {
|
|
166
|
+
return sources;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export { CLIENT_ID_KEY, beaconTarget, clientId, createBeaconReporter, registrySourcesWithBeacon, resetRegistryBeaconForTests, startRegistryBeacon, withClientId };
|
package/dist/report.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface RemoteErrorReport {
|
|
|
8
8
|
/** The registered version of the remote, when the registry gave one. */
|
|
9
9
|
version?: string;
|
|
10
10
|
}
|
|
11
|
+
/** The version the registry gave this remote on this page, if any. */
|
|
12
|
+
export declare const remoteVersion: (mfe: string) => string | undefined;
|
|
11
13
|
/** Remembers each remote's version so its reports say `cart@1.2.3`. Replaces what was remembered before. */
|
|
12
14
|
export declare function rememberRemoteVersions(remotes: readonly {
|
|
13
15
|
name: string;
|
package/dist/report.js
CHANGED
|
@@ -7,6 +7,7 @@ function versionMap() {
|
|
|
7
7
|
Reflect.set(globalThis, VERSIONS, created);
|
|
8
8
|
return created;
|
|
9
9
|
}
|
|
10
|
+
const remoteVersion = (mfe)=>versionMap().get(mfe);
|
|
10
11
|
function rememberRemoteVersions(remotes) {
|
|
11
12
|
const versions = versionMap();
|
|
12
13
|
versions.clear();
|
|
@@ -31,4 +32,4 @@ function reportRemoteError(mfe, where, error) {
|
|
|
31
32
|
console.error(`[mfeflow] ${void 0 === version ? mfe : `${mfe}@${version}`} ${where}: ${report.message}`);
|
|
32
33
|
return report;
|
|
33
34
|
}
|
|
34
|
-
export { REMOTE_ERROR_EVENT, rememberRemoteVersions, reportRemoteError };
|
|
35
|
+
export { REMOTE_ERROR_EVENT, rememberRemoteVersions, remoteVersion, reportRemoteError };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mfeflow/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Runtime for mfeflow micro-frontends: remotes from a registry index, RemoteBoundary with timeout and fallback, error reporting, singleton checks, composeProviders.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"micro-frontend",
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
"import": "./dist/providers.js",
|
|
34
34
|
"default": "./dist/providers.js"
|
|
35
35
|
},
|
|
36
|
+
"./beacon": {
|
|
37
|
+
"types": "./dist/beacon.d.ts",
|
|
38
|
+
"import": "./dist/beacon.js",
|
|
39
|
+
"default": "./dist/beacon.js"
|
|
40
|
+
},
|
|
36
41
|
"./plugins/report": {
|
|
37
42
|
"types": "./dist/plugins/report.d.ts",
|
|
38
43
|
"import": "./dist/plugins/report.js",
|
|
@@ -55,7 +60,7 @@
|
|
|
55
60
|
"provenance": true
|
|
56
61
|
},
|
|
57
62
|
"dependencies": {
|
|
58
|
-
"@mfeflow/contracts": "0.
|
|
63
|
+
"@mfeflow/contracts": "0.7.0"
|
|
59
64
|
},
|
|
60
65
|
"peerDependencies": {
|
|
61
66
|
"@module-federation/enhanced": "^2.9.0",
|