@bleedingdev/modern-js-federation-runtime 3.9.0-ultramodern.11 → 3.9.0-ultramodern.13
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/cjs/module-federation/consume-surface.js +6 -16
- package/dist/cjs/module-federation/index.js +16 -202
- package/dist/cjs/module-federation/telemetry.js +214 -0
- package/dist/esm/module-federation/consume-surface.mjs +3 -13
- package/dist/esm/module-federation/index.mjs +1 -109
- package/dist/esm/module-federation/telemetry.mjs +142 -0
- package/dist/esm-node/module-federation/consume-surface.mjs +3 -13
- package/dist/esm-node/module-federation/index.mjs +1 -109
- package/dist/esm-node/module-federation/telemetry.mjs +143 -0
- package/dist/types/module-federation/consume-surface.d.ts +5 -5
- package/dist/types/module-federation/index.d.ts +1 -65
- package/dist/types/module-federation/telemetry.d.ts +71 -0
- package/package.json +12 -12
- package/src/module-federation/consume-surface.ts +18 -33
- package/src/module-federation/index.ts +1 -262
- package/src/module-federation/telemetry.ts +297 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// @effect-diagnostics asyncFunction:off extendsNativeError:off globalFetch:off strictBooleanExpressions:off
|
|
2
|
+
|
|
3
|
+
export const MODULE_FEDERATION_FALLBACK_SIGNAL_EVENT =
|
|
4
|
+
'modernjs:mf-runtime-fallback';
|
|
5
|
+
|
|
6
|
+
export const MODULE_FEDERATION_RECOVERY_SIGNAL_EVENT =
|
|
7
|
+
'modernjs:mf-runtime-recovery';
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_RUNTIME_FALLBACK_SIGNAL_ENDPOINT =
|
|
10
|
+
'/_modern/contract-gates/runtime-fallback';
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_RUNTIME_FALLBACK_SIGNAL_AUTH_HEADER =
|
|
13
|
+
'x-modernjs-runtime-signal-token';
|
|
14
|
+
|
|
15
|
+
export type ModuleFederationFallbackClassification =
|
|
16
|
+
| 'timeout'
|
|
17
|
+
| 'network'
|
|
18
|
+
| 'contract'
|
|
19
|
+
| 'version-skew'
|
|
20
|
+
| 'remote-unavailable';
|
|
21
|
+
|
|
22
|
+
export type ModuleFederationFallbackStatus =
|
|
23
|
+
| 'degraded'
|
|
24
|
+
| 'failed'
|
|
25
|
+
| 'recovered';
|
|
26
|
+
|
|
27
|
+
export type ModuleFederationFallbackTelemetryInput = {
|
|
28
|
+
appName: string;
|
|
29
|
+
classification: ModuleFederationFallbackClassification;
|
|
30
|
+
entry?: string;
|
|
31
|
+
error?: unknown;
|
|
32
|
+
eventName?: string;
|
|
33
|
+
exportName?: string;
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
35
|
+
phase: 'discovery' | 'load' | 'mount' | 'hydrate' | 'recover';
|
|
36
|
+
remote: string;
|
|
37
|
+
runtimeDigest?: string;
|
|
38
|
+
status?: ModuleFederationFallbackStatus;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ModuleFederationFallbackTelemetryPayload = {
|
|
42
|
+
appName: string;
|
|
43
|
+
entry?: string;
|
|
44
|
+
eventName: string;
|
|
45
|
+
phase: ModuleFederationFallbackTelemetryInput['phase'];
|
|
46
|
+
reason: ModuleFederationFallbackClassification;
|
|
47
|
+
runtimeDigest?: string;
|
|
48
|
+
schemaVersion: 1;
|
|
49
|
+
metadata: {
|
|
50
|
+
classification: ModuleFederationFallbackClassification;
|
|
51
|
+
errorMessage?: string;
|
|
52
|
+
errorName?: string;
|
|
53
|
+
exportName?: string;
|
|
54
|
+
remote: string;
|
|
55
|
+
runtimeDigest?: string;
|
|
56
|
+
status: ModuleFederationFallbackStatus;
|
|
57
|
+
} & Record<string, unknown>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type ModuleFederationFallbackTelemetryEmitOptions = {
|
|
61
|
+
authHeaderName?: string;
|
|
62
|
+
authToken?: string;
|
|
63
|
+
endpoint?: string;
|
|
64
|
+
fetchImpl?: typeof fetch;
|
|
65
|
+
postSignal?: boolean;
|
|
66
|
+
/** Positive reporting deadline; defaults to one second. */
|
|
67
|
+
timeoutMs?: number;
|
|
68
|
+
/** Observe the exact payload and await delivery explicitly in tests. */
|
|
69
|
+
observe?: (
|
|
70
|
+
payload: ModuleFederationFallbackTelemetryPayload,
|
|
71
|
+
delivery: Promise<ModuleFederationFallbackTelemetryEmitResult>,
|
|
72
|
+
) => void;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type ModuleFederationFallbackTelemetryEmitResult = {
|
|
76
|
+
dispatched: boolean;
|
|
77
|
+
posted: boolean;
|
|
78
|
+
postStatus?: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export class ModuleFederationRemoteLoadTimeoutError extends Error {
|
|
82
|
+
constructor(remote: string, timeoutMs: number) {
|
|
83
|
+
super(`Loading remote "${remote}" timed out after ${timeoutMs}ms`);
|
|
84
|
+
this.name = 'ModuleFederationRemoteLoadTimeoutError';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class ModuleFederationRemoteLoadError extends Error {
|
|
89
|
+
readonly remote: string;
|
|
90
|
+
readonly attempts: number;
|
|
91
|
+
readonly causeError: Error;
|
|
92
|
+
|
|
93
|
+
constructor(remote: string, attempts: number, causeError: Error) {
|
|
94
|
+
super(
|
|
95
|
+
`Unable to load remote "${remote}" after ${attempts} attempt${
|
|
96
|
+
attempts > 1 ? 's' : ''
|
|
97
|
+
}: ${causeError.message}`,
|
|
98
|
+
);
|
|
99
|
+
this.name = 'ModuleFederationRemoteLoadError';
|
|
100
|
+
this.remote = remote;
|
|
101
|
+
this.attempts = attempts;
|
|
102
|
+
this.causeError = causeError;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class ModuleFederationRemoteComponentContractError extends Error {
|
|
107
|
+
constructor(remote: string, exportName: string) {
|
|
108
|
+
super(
|
|
109
|
+
`Remote "${remote}" export "${exportName}" is not a valid React component`,
|
|
110
|
+
);
|
|
111
|
+
this.name = 'ModuleFederationRemoteComponentContractError';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function toError(error: unknown) {
|
|
116
|
+
if (error instanceof Error) {
|
|
117
|
+
return error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return new Error(
|
|
121
|
+
typeof error === 'string' ? error : 'Unknown remote load error',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function classifyModuleFederationFallback(
|
|
126
|
+
error: unknown,
|
|
127
|
+
): ModuleFederationFallbackClassification {
|
|
128
|
+
const normalizedError = toError(error);
|
|
129
|
+
if (normalizedError instanceof ModuleFederationRemoteLoadError) {
|
|
130
|
+
return classifyModuleFederationFallback(normalizedError.causeError);
|
|
131
|
+
}
|
|
132
|
+
if (normalizedError instanceof ModuleFederationRemoteLoadTimeoutError) {
|
|
133
|
+
return 'timeout';
|
|
134
|
+
}
|
|
135
|
+
if (normalizedError instanceof ModuleFederationRemoteComponentContractError) {
|
|
136
|
+
return 'contract';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const message = normalizedError.message;
|
|
140
|
+
if (
|
|
141
|
+
/version|requiredVersion|singleton|share scope|shared module/i.test(message)
|
|
142
|
+
) {
|
|
143
|
+
return 'version-skew';
|
|
144
|
+
}
|
|
145
|
+
if (/network|fetch|script|timeout|chunk|loading/i.test(message)) {
|
|
146
|
+
return 'network';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return 'remote-unavailable';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function createModuleFederationFallbackTelemetry(
|
|
153
|
+
input: ModuleFederationFallbackTelemetryInput,
|
|
154
|
+
): ModuleFederationFallbackTelemetryPayload {
|
|
155
|
+
const error = input.error !== undefined ? toError(input.error) : undefined;
|
|
156
|
+
const status = input.status ?? 'degraded';
|
|
157
|
+
const eventName =
|
|
158
|
+
input.eventName ??
|
|
159
|
+
(status === 'recovered'
|
|
160
|
+
? MODULE_FEDERATION_RECOVERY_SIGNAL_EVENT
|
|
161
|
+
: MODULE_FEDERATION_FALLBACK_SIGNAL_EVENT);
|
|
162
|
+
const metadata: ModuleFederationFallbackTelemetryPayload['metadata'] = {
|
|
163
|
+
...(input.metadata ?? {}),
|
|
164
|
+
classification: input.classification,
|
|
165
|
+
remote: input.remote,
|
|
166
|
+
status,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (input.exportName !== undefined) {
|
|
170
|
+
metadata.exportName = input.exportName;
|
|
171
|
+
}
|
|
172
|
+
if (input.runtimeDigest !== undefined) {
|
|
173
|
+
metadata.runtimeDigest = input.runtimeDigest;
|
|
174
|
+
}
|
|
175
|
+
if (error !== undefined) {
|
|
176
|
+
metadata.errorName = error.name;
|
|
177
|
+
metadata.errorMessage = error.message;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const payload: ModuleFederationFallbackTelemetryPayload = {
|
|
181
|
+
appName: input.appName,
|
|
182
|
+
eventName,
|
|
183
|
+
metadata,
|
|
184
|
+
phase: input.phase,
|
|
185
|
+
reason: input.classification,
|
|
186
|
+
schemaVersion: 1,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (input.entry !== undefined) {
|
|
190
|
+
payload.entry = input.entry;
|
|
191
|
+
}
|
|
192
|
+
if (input.runtimeDigest !== undefined) {
|
|
193
|
+
payload.runtimeDigest = input.runtimeDigest;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return payload;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function toModuleFederationFallbackAttributes(
|
|
200
|
+
payload: ModuleFederationFallbackTelemetryPayload,
|
|
201
|
+
): Record<string, string> {
|
|
202
|
+
return {
|
|
203
|
+
'data-mf-fallback-app': payload.appName,
|
|
204
|
+
'data-mf-fallback-classification': payload.reason,
|
|
205
|
+
'data-mf-fallback-phase': payload.phase,
|
|
206
|
+
'data-mf-fallback-remote': String(payload.metadata.remote),
|
|
207
|
+
'data-mf-fallback-status': String(payload.metadata.status),
|
|
208
|
+
'data-mf-telemetry-event': payload.eventName,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function emitModuleFederationFallbackTelemetry(
|
|
213
|
+
input: ModuleFederationFallbackTelemetryInput,
|
|
214
|
+
options: ModuleFederationFallbackTelemetryEmitOptions = {},
|
|
215
|
+
): Promise<ModuleFederationFallbackTelemetryEmitResult> {
|
|
216
|
+
return emitModuleFederationFallbackPayload(
|
|
217
|
+
createModuleFederationFallbackTelemetry(input),
|
|
218
|
+
options,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Dispatch once locally; remote delivery is bounded and best effort. */
|
|
223
|
+
export function emitModuleFederationFallbackPayload(
|
|
224
|
+
payload: ModuleFederationFallbackTelemetryPayload,
|
|
225
|
+
options: ModuleFederationFallbackTelemetryEmitOptions = {},
|
|
226
|
+
): Promise<ModuleFederationFallbackTelemetryEmitResult> {
|
|
227
|
+
const delivery = deliver(payload, options);
|
|
228
|
+
try {
|
|
229
|
+
options.observe?.(payload, delivery);
|
|
230
|
+
} catch {
|
|
231
|
+
/* Observers cannot break fallback. */
|
|
232
|
+
}
|
|
233
|
+
return delivery;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function deliver(
|
|
237
|
+
payload: ModuleFederationFallbackTelemetryPayload,
|
|
238
|
+
options: ModuleFederationFallbackTelemetryEmitOptions,
|
|
239
|
+
): Promise<ModuleFederationFallbackTelemetryEmitResult> {
|
|
240
|
+
let dispatched = false;
|
|
241
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
242
|
+
const controller = new AbortController();
|
|
243
|
+
try {
|
|
244
|
+
if (
|
|
245
|
+
typeof window !== 'undefined' &&
|
|
246
|
+
typeof window.dispatchEvent === 'function' &&
|
|
247
|
+
typeof CustomEvent !== 'undefined'
|
|
248
|
+
) {
|
|
249
|
+
window.dispatchEvent(
|
|
250
|
+
new CustomEvent(payload.eventName, { detail: payload }),
|
|
251
|
+
);
|
|
252
|
+
dispatched = true;
|
|
253
|
+
}
|
|
254
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
255
|
+
if (
|
|
256
|
+
!(options.postSignal === true || options.endpoint) ||
|
|
257
|
+
typeof fetchImpl !== 'function'
|
|
258
|
+
) {
|
|
259
|
+
return { dispatched, posted: false };
|
|
260
|
+
}
|
|
261
|
+
const headers = new Headers({ 'content-type': 'application/json' });
|
|
262
|
+
if (options.authToken)
|
|
263
|
+
headers.set(
|
|
264
|
+
options.authHeaderName ?? DEFAULT_RUNTIME_FALLBACK_SIGNAL_AUTH_HEADER,
|
|
265
|
+
options.authToken,
|
|
266
|
+
);
|
|
267
|
+
const timeoutMs =
|
|
268
|
+
Number.isSafeInteger(options.timeoutMs) && (options.timeoutMs ?? 0) > 0
|
|
269
|
+
? options.timeoutMs!
|
|
270
|
+
: 1_000;
|
|
271
|
+
// Race even custom transports that ignore AbortSignal. Promise.race also
|
|
272
|
+
// observes a late rejection after the timeout wins.
|
|
273
|
+
const expired = new Promise<undefined>(resolve => {
|
|
274
|
+
timer = setTimeout(() => {
|
|
275
|
+
controller.abort();
|
|
276
|
+
resolve(undefined);
|
|
277
|
+
}, timeoutMs);
|
|
278
|
+
});
|
|
279
|
+
const response = await Promise.race([
|
|
280
|
+
fetchImpl(options.endpoint ?? DEFAULT_RUNTIME_FALLBACK_SIGNAL_ENDPOINT, {
|
|
281
|
+
body: JSON.stringify(payload),
|
|
282
|
+
headers,
|
|
283
|
+
method: 'POST',
|
|
284
|
+
keepalive: true,
|
|
285
|
+
signal: controller.signal,
|
|
286
|
+
}),
|
|
287
|
+
expired,
|
|
288
|
+
]);
|
|
289
|
+
return response
|
|
290
|
+
? { dispatched, posted: true, postStatus: response.status }
|
|
291
|
+
: { dispatched, posted: false };
|
|
292
|
+
} catch {
|
|
293
|
+
return { dispatched, posted: false };
|
|
294
|
+
} finally {
|
|
295
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
296
|
+
}
|
|
297
|
+
}
|