@bleedingdev/modern-js-federation-runtime 0.0.0 → 3.9.0-ultramodern.6
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/LICENSE +21 -0
- package/dist/cjs/module-federation/consume-surface.js +199 -0
- package/dist/cjs/module-federation/distributed-ssr.js +135 -0
- package/dist/cjs/module-federation/index.js +316 -0
- package/dist/cjs/module-federation/last-known-good.js +157 -0
- package/dist/cjs/module-federation/manifest-recovery-runtime-plugin.js +130 -0
- package/dist/cjs/module-federation/surface-resolution-types.js +61 -0
- package/dist/esm/module-federation/consume-surface.mjs +158 -0
- package/dist/esm/module-federation/distributed-ssr.mjs +82 -0
- package/dist/esm/module-federation/index.mjs +115 -0
- package/dist/esm/module-federation/last-known-good.mjs +113 -0
- package/dist/esm/module-federation/manifest-recovery-runtime-plugin.mjs +90 -0
- package/dist/esm/module-federation/surface-resolution-types.mjs +8 -0
- package/dist/esm-node/module-federation/consume-surface.mjs +159 -0
- package/dist/esm-node/module-federation/distributed-ssr.mjs +83 -0
- package/dist/esm-node/module-federation/index.mjs +116 -0
- package/dist/esm-node/module-federation/last-known-good.mjs +114 -0
- package/dist/esm-node/module-federation/manifest-recovery-runtime-plugin.mjs +91 -0
- package/dist/esm-node/module-federation/surface-resolution-types.mjs +9 -0
- package/dist/types/module-federation/consume-surface.d.ts +104 -0
- package/dist/types/module-federation/distributed-ssr.d.ts +61 -0
- package/dist/types/module-federation/index.d.ts +71 -0
- package/dist/types/module-federation/last-known-good.d.ts +52 -0
- package/dist/types/module-federation/manifest-recovery-runtime-plugin.d.ts +25 -0
- package/dist/types/module-federation/surface-resolution-types.d.ts +23 -0
- package/package.json +86 -4
- package/src/module-federation/consume-surface.ts +369 -0
- package/src/module-federation/distributed-ssr.tsx +240 -0
- package/src/module-federation/index.ts +274 -0
- package/src/module-federation/last-known-good.ts +292 -0
- package/src/module-federation/manifest-recovery-runtime-plugin.ts +215 -0
- package/src/module-federation/surface-resolution-types.ts +60 -0
- package/README.md +0 -3
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// @effect-diagnostics asyncFunction:off strictBooleanExpressions:off
|
|
2
|
+
/**
|
|
3
|
+
* G24a/b — last-known-good (LKG) provider wrapper.
|
|
4
|
+
*
|
|
5
|
+
* RESOLUTION-0001 §2.2: the LKG cache "wraps any provider; serves the last
|
|
6
|
+
* complete record on provider failure, marked `compatibility: 'degraded'`". It
|
|
7
|
+
* is universal — no DOM/fetch/framework coupling — so the same wrapper works in
|
|
8
|
+
* the browser, Node, and Cloudflare.
|
|
9
|
+
*
|
|
10
|
+
* Rollback semantics (ADR-0019 / CONTEXT.md Rollback): the delivery UNIT is
|
|
11
|
+
* the unit of rollback. The cache stores exactly ONE whole
|
|
12
|
+
* {@link ResolvedDeliveryUnit} per (unitId, env) key — never per (ref, env) —
|
|
13
|
+
* so every surface of a unit shares one snapshot and a fresh success swaps that
|
|
14
|
+
* snapshot atomically for all of them (two surfaces can never be served from
|
|
15
|
+
* different cached build markers). It never stores or serves a partial record,
|
|
16
|
+
* and it never mixes locations across build markers — a served degraded record
|
|
17
|
+
* is a byte-for-byte prior success with only its compatibility verdict flipped
|
|
18
|
+
* to `degraded`. Only a compatible record is promoted to last-known-good;
|
|
19
|
+
* degraded and incompatible provider results remain live responses without
|
|
20
|
+
* replacing the recovery snapshot. The ref's external-major participates in
|
|
21
|
+
* lookup validation but not in the storage key.
|
|
22
|
+
*/
|
|
23
|
+
import { selectResolvedSurface } from '@modern-js/surface-resolution';
|
|
24
|
+
import {
|
|
25
|
+
createDiscoveryError,
|
|
26
|
+
type DiscoveryResult,
|
|
27
|
+
deliveryUnitResolutionKey,
|
|
28
|
+
type EnvironmentId,
|
|
29
|
+
type ParsedSurfaceRef,
|
|
30
|
+
type ResolvedDeliveryUnit,
|
|
31
|
+
type SurfaceResolutionProvider,
|
|
32
|
+
} from './surface-resolution-types';
|
|
33
|
+
|
|
34
|
+
export const LAST_KNOWN_GOOD_PROVIDER_NAME = 'last-known-good';
|
|
35
|
+
|
|
36
|
+
/** One cached complete record plus the time it was stored. */
|
|
37
|
+
export type LkgRecord = {
|
|
38
|
+
resolved: ResolvedDeliveryUnit;
|
|
39
|
+
storedAt: number;
|
|
40
|
+
/**
|
|
41
|
+
* External-major selector the record was resolved against. Participates in
|
|
42
|
+
* lookup validation — a cached record is only served for a matching major —
|
|
43
|
+
* but never in the storage key, which is (unitId, env) so all surfaces of a
|
|
44
|
+
* unit share one atomically-swapped record.
|
|
45
|
+
*/
|
|
46
|
+
major?: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Pluggable storage hook. The default is an in-memory Map; callers may supply a
|
|
51
|
+
* durable store (KV, disk, etc). Reads/writes may be async. A store only ever
|
|
52
|
+
* receives whole records.
|
|
53
|
+
*/
|
|
54
|
+
export type LkgStorage = {
|
|
55
|
+
read(key: string): (LkgRecord | undefined) | Promise<LkgRecord | undefined>;
|
|
56
|
+
write(key: string, record: LkgRecord): void | Promise<void>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type LkgFreshnessPolicy = {
|
|
60
|
+
/**
|
|
61
|
+
* Maximum age, in ms, a stored record may be served after a provider
|
|
62
|
+
* failure. When omitted, a served record never expires by age. A record
|
|
63
|
+
* older than this yields a typed `stale-record` discovery error.
|
|
64
|
+
*/
|
|
65
|
+
maxStaleMs?: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type LastKnownGoodOptions = {
|
|
69
|
+
/** The wrapped provider (env/static, Zephyr, or any other). */
|
|
70
|
+
provider: SurfaceResolutionProvider;
|
|
71
|
+
/** Pluggable storage; defaults to a process-local in-memory Map. */
|
|
72
|
+
storage?: LkgStorage;
|
|
73
|
+
freshness?: LkgFreshnessPolicy;
|
|
74
|
+
/** Clock injection for deterministic freshness tests. */
|
|
75
|
+
now?: () => number;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Default process-local in-memory storage. */
|
|
79
|
+
export function createInMemoryLkgStorage(): LkgStorage {
|
|
80
|
+
const map = new Map<string, LkgRecord>();
|
|
81
|
+
return {
|
|
82
|
+
read(key) {
|
|
83
|
+
return map.get(key);
|
|
84
|
+
},
|
|
85
|
+
write(key, record) {
|
|
86
|
+
map.set(key, record);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Flip a resolved record's verdict to degraded without mutating the input. */
|
|
92
|
+
function markDegraded(
|
|
93
|
+
resolved: ResolvedDeliveryUnit,
|
|
94
|
+
reason: string,
|
|
95
|
+
): ResolvedDeliveryUnit {
|
|
96
|
+
return {
|
|
97
|
+
...resolved,
|
|
98
|
+
compatibility: {
|
|
99
|
+
...resolved.compatibility,
|
|
100
|
+
status: 'degraded',
|
|
101
|
+
reason,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function staleRecordResult(
|
|
107
|
+
ref: ParsedSurfaceRef,
|
|
108
|
+
env: EnvironmentId,
|
|
109
|
+
ageMs: number,
|
|
110
|
+
maxStaleMs: number,
|
|
111
|
+
): DiscoveryResult {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
error: createDiscoveryError(
|
|
115
|
+
'stale-record',
|
|
116
|
+
ref,
|
|
117
|
+
`Last-known-good record for surface is stale: age ${ageMs}ms exceeds maxStaleMs ${maxStaleMs}ms`,
|
|
118
|
+
{ env, ageMs, maxStaleMs },
|
|
119
|
+
),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resolutionAnswersRef(
|
|
124
|
+
resolved: ResolvedDeliveryUnit,
|
|
125
|
+
ref: ParsedSurfaceRef,
|
|
126
|
+
): boolean {
|
|
127
|
+
const selected = selectResolvedSurface(resolved, ref);
|
|
128
|
+
return selected.ok && selected.surface.servedMajor === ref.major;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Wrap a provider with a last-known-good cache. On success the whole record is
|
|
133
|
+
* cached (atomic swap). On provider failure (typed discovery error or a thrown
|
|
134
|
+
* error) the last complete record is served, marked degraded — unless it has
|
|
135
|
+
* expired per {@link LkgFreshnessPolicy}, in which case a typed `stale-record`
|
|
136
|
+
* error is returned. If there is no cached record, the original failure is
|
|
137
|
+
* passed through (a thrown error becomes a `provider-unavailable` discovery
|
|
138
|
+
* error).
|
|
139
|
+
*/
|
|
140
|
+
export function createLastKnownGoodProvider(
|
|
141
|
+
options: LastKnownGoodOptions,
|
|
142
|
+
): SurfaceResolutionProvider {
|
|
143
|
+
const storage = options.storage ?? createInMemoryLkgStorage();
|
|
144
|
+
const now =
|
|
145
|
+
options.now ??
|
|
146
|
+
(() => Math.floor(performance.timeOrigin + performance.now()));
|
|
147
|
+
const maxStaleMs = options.freshness?.maxStaleMs;
|
|
148
|
+
let nextResolutionGeneration = 0;
|
|
149
|
+
const latestCacheableGenerationByKey = new Map<string, number>();
|
|
150
|
+
const writeTailByKey = new Map<string, Promise<void>>();
|
|
151
|
+
|
|
152
|
+
async function cacheCompatibleResolution(
|
|
153
|
+
key: string,
|
|
154
|
+
generation: number,
|
|
155
|
+
record: LkgRecord,
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
latestCacheableGenerationByKey.set(
|
|
158
|
+
key,
|
|
159
|
+
Math.max(latestCacheableGenerationByKey.get(key) ?? 0, generation),
|
|
160
|
+
);
|
|
161
|
+
// This orders writes made by this wrapper instance. LkgStorage has no
|
|
162
|
+
// revisioned CAS contract, so it intentionally makes no cross-process claim.
|
|
163
|
+
const previous = writeTailByKey.get(key) ?? Promise.resolve();
|
|
164
|
+
const write = previous
|
|
165
|
+
.catch(() => undefined)
|
|
166
|
+
.then(async () => {
|
|
167
|
+
if (latestCacheableGenerationByKey.get(key) !== generation) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await storage.write(key, record);
|
|
171
|
+
});
|
|
172
|
+
writeTailByKey.set(key, write);
|
|
173
|
+
try {
|
|
174
|
+
await write;
|
|
175
|
+
} finally {
|
|
176
|
+
if (writeTailByKey.get(key) === write) {
|
|
177
|
+
writeTailByKey.delete(key);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function serveFromCache(
|
|
183
|
+
ref: ParsedSurfaceRef,
|
|
184
|
+
env: EnvironmentId,
|
|
185
|
+
fallback: () => DiscoveryResult,
|
|
186
|
+
): Promise<DiscoveryResult> {
|
|
187
|
+
const key = deliveryUnitResolutionKey(ref.unitId, env);
|
|
188
|
+
const cached = await storage.read(key);
|
|
189
|
+
if (!cached) {
|
|
190
|
+
return fallback();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// The external-major participates in lookup validation: a record resolved
|
|
194
|
+
// against a different major is not a valid last-known-good for this ref.
|
|
195
|
+
if (cached.major !== ref.major) {
|
|
196
|
+
return fallback();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// A unit-level cache entry is only usable when it contains the exact
|
|
200
|
+
// requested surface materialization. The storage key intentionally lets
|
|
201
|
+
// sibling surfaces share one atomic snapshot, but it must not turn one
|
|
202
|
+
// cached surface into evidence that every possible surface exists.
|
|
203
|
+
if (!resolutionAnswersRef(cached.resolved, ref)) {
|
|
204
|
+
return fallback();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Only a previously compatible record is a last-known-good snapshot.
|
|
208
|
+
// Defend against pluggable stores that contain degraded or incompatible
|
|
209
|
+
// records even though the write path below rejects both.
|
|
210
|
+
if (cached.resolved.compatibility.status !== 'compatible') {
|
|
211
|
+
return fallback();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const ageMs = now() - cached.storedAt;
|
|
215
|
+
if (maxStaleMs !== undefined && ageMs > maxStaleMs) {
|
|
216
|
+
return staleRecordResult(ref, env, ageMs, maxStaleMs);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
unit: markDegraded(
|
|
222
|
+
cached.resolved,
|
|
223
|
+
`served last-known-good record (age ${ageMs}ms) after provider failure`,
|
|
224
|
+
),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
name: LAST_KNOWN_GOOD_PROVIDER_NAME,
|
|
230
|
+
async resolve(
|
|
231
|
+
ref: ParsedSurfaceRef,
|
|
232
|
+
env: EnvironmentId,
|
|
233
|
+
): Promise<DiscoveryResult> {
|
|
234
|
+
const key = deliveryUnitResolutionKey(ref.unitId, env);
|
|
235
|
+
const generation = ++nextResolutionGeneration;
|
|
236
|
+
|
|
237
|
+
let resolution: DiscoveryResult;
|
|
238
|
+
try {
|
|
239
|
+
resolution = await options.provider.resolve(ref, env);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
return serveFromCache(ref, env, () => ({
|
|
242
|
+
ok: false,
|
|
243
|
+
error: createDiscoveryError(
|
|
244
|
+
'provider-unavailable',
|
|
245
|
+
ref,
|
|
246
|
+
error instanceof Error
|
|
247
|
+
? error.message
|
|
248
|
+
: 'provider threw a non-error value',
|
|
249
|
+
{ env, provider: options.provider.name },
|
|
250
|
+
),
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!resolution.ok) {
|
|
255
|
+
// Preserve the resolver's own typed failure when no LKG record exists.
|
|
256
|
+
const failure = resolution;
|
|
257
|
+
return serveFromCache(ref, env, () => failure);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!resolutionAnswersRef(resolution.unit, ref)) {
|
|
261
|
+
return serveFromCache(ref, env, () => ({
|
|
262
|
+
ok: false,
|
|
263
|
+
error: createDiscoveryError(
|
|
264
|
+
'identity-mismatch',
|
|
265
|
+
ref,
|
|
266
|
+
`Provider ${options.provider.name} returned a delivery unit that does not answer the requested surface.`,
|
|
267
|
+
{
|
|
268
|
+
env,
|
|
269
|
+
provider: options.provider.name,
|
|
270
|
+
recordUnitId: resolution.unit.unitId,
|
|
271
|
+
availableSurfaces: resolution.unit.surfaces.map(
|
|
272
|
+
surface => surface.surfaceId,
|
|
273
|
+
),
|
|
274
|
+
},
|
|
275
|
+
),
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Fresh compatible success: swap the whole record atomically. Degraded
|
|
280
|
+
// and incompatible refreshes are still returned live, but neither can
|
|
281
|
+
// replace the prior known-good snapshot used for recovery.
|
|
282
|
+
if (resolution.unit.compatibility.status === 'compatible') {
|
|
283
|
+
await cacheCompatibleResolution(key, generation, {
|
|
284
|
+
resolved: resolution.unit,
|
|
285
|
+
storedAt: now(),
|
|
286
|
+
major: ref.major,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return resolution;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
const DEFAULT_ATTEMPTS = 3;
|
|
2
|
+
const DEFAULT_RETRY_DELAY_MS = 100;
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 1_000;
|
|
4
|
+
const MAX_ATTEMPTS = 5;
|
|
5
|
+
const MAX_RETRY_DELAY_MS = 1_000;
|
|
6
|
+
const MAX_TIMEOUT_MS = 5_000;
|
|
7
|
+
|
|
8
|
+
const RETRYABLE_HTTP_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
9
|
+
|
|
10
|
+
const NON_RETRYABLE_ERROR_PATTERNS = [
|
|
11
|
+
/RUNTIME-013/i,
|
|
12
|
+
/invalid manifest/i,
|
|
13
|
+
/manifest (?:schema|contract|version|identity)/i,
|
|
14
|
+
/(?:incompatible|mismatch).*(?:manifest|remote)/i,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
type ErrorLoadRemoteArgs = {
|
|
18
|
+
id?: string;
|
|
19
|
+
error?: unknown;
|
|
20
|
+
lifecycle?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ModuleFederationManifestRecoveryPluginOptions = {
|
|
24
|
+
attempts?: number;
|
|
25
|
+
retryDelayMs?: number;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
fetchImpl?: typeof fetch;
|
|
28
|
+
waitImpl?: (milliseconds: number) => Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const clampInteger = (
|
|
32
|
+
value: number | undefined,
|
|
33
|
+
fallback: number,
|
|
34
|
+
minimum: number,
|
|
35
|
+
maximum: number,
|
|
36
|
+
) => {
|
|
37
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getErrorMessage = (error: unknown) => {
|
|
45
|
+
if (error instanceof Error) {
|
|
46
|
+
return error.message;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return typeof error === 'string' ? error : '';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getErrorStatus = (error: unknown) => {
|
|
53
|
+
if (
|
|
54
|
+
typeof error === 'object' &&
|
|
55
|
+
error !== null &&
|
|
56
|
+
'status' in error &&
|
|
57
|
+
typeof error.status === 'number'
|
|
58
|
+
) {
|
|
59
|
+
return error.status;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const isRetryableError = (error: unknown) => {
|
|
66
|
+
if (error instanceof SyntaxError) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const message = getErrorMessage(error);
|
|
71
|
+
|
|
72
|
+
if (NON_RETRYABLE_ERROR_PATTERNS.some(pattern => pattern.test(message))) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const status = getErrorStatus(error);
|
|
77
|
+
if (status !== undefined) {
|
|
78
|
+
return RETRYABLE_HTTP_STATUSES.has(status);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
error instanceof Error &&
|
|
83
|
+
['AbortError', 'TimeoutError'].includes(error.name)
|
|
84
|
+
) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
error instanceof TypeError ||
|
|
90
|
+
/(?:fetch failed|network|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN)/i.test(
|
|
91
|
+
message,
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const isHttpManifestUrl = (id: string | undefined): id is string => {
|
|
97
|
+
if (id === undefined || id.length === 0) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const url = new URL(id);
|
|
103
|
+
return (
|
|
104
|
+
(url.protocol === 'http:' || url.protocol === 'https:') &&
|
|
105
|
+
url.pathname.endsWith('.json')
|
|
106
|
+
);
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const defaultWait = (milliseconds: number): Promise<void> =>
|
|
113
|
+
new Promise<void>(resolve => {
|
|
114
|
+
setTimeout(resolve, milliseconds);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const fetchWithTimeout = (
|
|
118
|
+
fetchImpl: typeof fetch,
|
|
119
|
+
url: string,
|
|
120
|
+
timeoutMs: number,
|
|
121
|
+
) =>
|
|
122
|
+
fetchImpl(url, {
|
|
123
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Gives server-side manifest loading a small, bounded recovery window.
|
|
128
|
+
*
|
|
129
|
+
* Returning `undefined` deliberately hands control back to Module Federation:
|
|
130
|
+
* its runtime preserves the original typed RUNTIME-003 error and invalidates
|
|
131
|
+
* the rejected manifest promise. Parsed manifests are returned without schema
|
|
132
|
+
* interpretation so runtime-core remains the authority for RUNTIME-013.
|
|
133
|
+
*/
|
|
134
|
+
export const createModuleFederationManifestRecoveryPlugin = (
|
|
135
|
+
options: ModuleFederationManifestRecoveryPluginOptions = {},
|
|
136
|
+
) => {
|
|
137
|
+
const attempts = clampInteger(
|
|
138
|
+
options.attempts,
|
|
139
|
+
DEFAULT_ATTEMPTS,
|
|
140
|
+
1,
|
|
141
|
+
MAX_ATTEMPTS,
|
|
142
|
+
);
|
|
143
|
+
const retryDelayMs = clampInteger(
|
|
144
|
+
options.retryDelayMs,
|
|
145
|
+
DEFAULT_RETRY_DELAY_MS,
|
|
146
|
+
0,
|
|
147
|
+
MAX_RETRY_DELAY_MS,
|
|
148
|
+
);
|
|
149
|
+
const timeoutMs = clampInteger(
|
|
150
|
+
options.timeoutMs,
|
|
151
|
+
DEFAULT_TIMEOUT_MS,
|
|
152
|
+
1,
|
|
153
|
+
MAX_TIMEOUT_MS,
|
|
154
|
+
);
|
|
155
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
156
|
+
const waitImpl = options.waitImpl ?? defaultWait;
|
|
157
|
+
|
|
158
|
+
const recoverManifest = (url: string, attempt: number): Promise<unknown> => {
|
|
159
|
+
const fetchManifest = () =>
|
|
160
|
+
Promise.resolve()
|
|
161
|
+
.then(() => fetchWithTimeout(fetchImpl, url, timeoutMs))
|
|
162
|
+
.then(
|
|
163
|
+
response => {
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
if (
|
|
166
|
+
!RETRYABLE_HTTP_STATUSES.has(response.status) ||
|
|
167
|
+
attempt === attempts - 1
|
|
168
|
+
) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return recoverManifest(url, attempt + 1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return Promise.resolve()
|
|
176
|
+
.then(() => response.json())
|
|
177
|
+
.then(
|
|
178
|
+
manifest => manifest,
|
|
179
|
+
() => undefined,
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
error => {
|
|
183
|
+
if (!isRetryableError(error) || attempt === attempts - 1) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return recoverManifest(url, attempt + 1);
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (attempt > 0 && retryDelayMs > 0) {
|
|
192
|
+
return waitImpl(retryDelayMs).then(fetchManifest);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return fetchManifest();
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
name: 'modern-js-manifest-recovery-plugin',
|
|
200
|
+
errorLoadRemote(args: ErrorLoadRemoteArgs) {
|
|
201
|
+
if (
|
|
202
|
+
args.lifecycle !== 'afterResolve' ||
|
|
203
|
+
!isHttpManifestUrl(args.id) ||
|
|
204
|
+
!isRetryableError(args.error) ||
|
|
205
|
+
typeof fetchImpl !== 'function'
|
|
206
|
+
) {
|
|
207
|
+
return Promise.resolve(undefined);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return recoverManifest(args.id, 0);
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export default createModuleFederationManifestRecoveryPlugin;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface-resolution types for the module-federation runtime.
|
|
3
|
+
*
|
|
4
|
+
* Thin re-export of the canonical module in `@modern-js/surface-resolution`
|
|
5
|
+
* (`packages/toolkit/surface-resolution/src/surface-resolution/`,
|
|
6
|
+
* RESOLUTION-0001). Kept as a file so existing `./surface-resolution-types`
|
|
7
|
+
* imports stay stable. Only helpers the canonical module deliberately does not
|
|
8
|
+
* ship (the (ref, env) cache key) live here, implemented against canonical
|
|
9
|
+
* types.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
type EnvironmentId,
|
|
13
|
+
formatSurfaceRef,
|
|
14
|
+
type ParsedSurfaceRef,
|
|
15
|
+
} from '@modern-js/surface-resolution';
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
type CompatibilityStatus,
|
|
19
|
+
type CompatibilityVerdict,
|
|
20
|
+
createDiscoveryError,
|
|
21
|
+
type DiscoveryError,
|
|
22
|
+
type DiscoveryErrorCode,
|
|
23
|
+
type DiscoveryResult,
|
|
24
|
+
type EnvironmentId,
|
|
25
|
+
formatSurfaceRef,
|
|
26
|
+
type ParsedSurfaceRef,
|
|
27
|
+
parseSurfaceRef,
|
|
28
|
+
type ResolvedDeliveryUnit,
|
|
29
|
+
type ResolvedSurface,
|
|
30
|
+
type ResolvedSurfaceKind,
|
|
31
|
+
type ResolvedSurfaceLocation,
|
|
32
|
+
type ResolvedSurfaceLocationPlatform,
|
|
33
|
+
type SurfaceRefParseError,
|
|
34
|
+
type SurfaceRefParseResult,
|
|
35
|
+
type SurfaceResolutionProvider,
|
|
36
|
+
validateSurfaceRef,
|
|
37
|
+
} from '@modern-js/surface-resolution';
|
|
38
|
+
|
|
39
|
+
/** Stable cache/identity key for a (ref, env) pair. */
|
|
40
|
+
export function surfaceResolutionKey(
|
|
41
|
+
ref: ParsedSurfaceRef,
|
|
42
|
+
env: EnvironmentId,
|
|
43
|
+
): string {
|
|
44
|
+
return `${env}::${formatSurfaceRef(ref)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Stable cache key for a (deliveryUnit, env) pair — ONE record per unit per
|
|
49
|
+
* env (ADR-0019 atomicity). All surfaces of a unit share this key so a refresh
|
|
50
|
+
* replaces the whole unit snapshot atomically for every surface, and two
|
|
51
|
+
* surfaces of the same unit can never be served from different cached
|
|
52
|
+
* snapshots. The ref's external-major participates in lookup validation (see
|
|
53
|
+
* the LKG provider), not in this key.
|
|
54
|
+
*/
|
|
55
|
+
export function deliveryUnitResolutionKey(
|
|
56
|
+
unitId: string,
|
|
57
|
+
env: EnvironmentId,
|
|
58
|
+
): string {
|
|
59
|
+
return `${env}::${unitId}`;
|
|
60
|
+
}
|
package/README.md
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
# @bleedingdev/modern-js-federation-runtime
|
|
2
|
-
|
|
3
|
-
This 0.0.0 version only registers the package name so npm trusted publishing can be configured. It contains no runtime implementation. Use the subsequent UltraModern.js release, published from the repository workflow with provenance.
|