@mfeflow/runtime 0.5.0 → 0.6.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.
@@ -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.5.0",
3
+ "version": "0.6.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.5.0"
63
+ "@mfeflow/contracts": "0.6.0"
59
64
  },
60
65
  "peerDependencies": {
61
66
  "@module-federation/enhanced": "^2.9.0",