@bleedingdev/modern-js-renderer-core 3.9.3-ultramodern.8 → 3.9.3-ultramodern.9
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/adapter.js +1 -1
- package/dist/cjs/entry-client.js +5 -0
- package/dist/cjs/federation.js +14 -5
- package/dist/cjs/server/worker.js +2 -1
- package/dist/cjs/worker-federation/contract.js +298 -0
- package/dist/cjs/worker-federation/handler.js +197 -0
- package/dist/cjs/worker-federation/index.js +63 -0
- package/dist/cjs/worker-federation/lease.js +167 -0
- package/dist/cjs/worker-federation/transport.js +272 -0
- package/dist/esm/adapter.mjs +1 -1
- package/dist/esm/entry-client.mjs +5 -0
- package/dist/esm/federation.mjs +14 -5
- package/dist/esm/server/worker.mjs +2 -1
- package/dist/esm/worker-federation/contract.mjs +227 -0
- package/dist/esm/worker-federation/handler.mjs +159 -0
- package/dist/esm/worker-federation/index.mjs +3 -0
- package/dist/esm/worker-federation/lease.mjs +107 -0
- package/dist/esm/worker-federation/transport.mjs +234 -0
- package/dist/esm-node/adapter.mjs +1 -1
- package/dist/esm-node/entry-client.mjs +5 -0
- package/dist/esm-node/federation.mjs +14 -5
- package/dist/esm-node/server/worker.mjs +2 -1
- package/dist/esm-node/worker-federation/contract.mjs +228 -0
- package/dist/esm-node/worker-federation/handler.mjs +160 -0
- package/dist/esm-node/worker-federation/index.mjs +4 -0
- package/dist/esm-node/worker-federation/lease.mjs +108 -0
- package/dist/esm-node/worker-federation/transport.mjs +235 -0
- package/dist/types/entry-client.d.ts +2 -0
- package/dist/types/federation.d.ts +15 -2
- package/dist/types/server/types.d.ts +3 -0
- package/dist/types/server/worker.d.ts +19 -0
- package/dist/types/worker-federation/contract.d.ts +70 -0
- package/dist/types/worker-federation/handler.d.ts +21 -0
- package/dist/types/worker-federation/index.d.ts +6 -0
- package/dist/types/worker-federation/lease.d.ts +11 -0
- package/dist/types/worker-federation/transport.d.ts +47 -0
- package/package.json +12 -2
- package/src/adapter.ts +3 -1
- package/src/entry-client.ts +5 -0
- package/src/federation.ts +36 -8
- package/src/server/types.ts +3 -0
- package/src/server/worker.ts +23 -0
- package/src/worker-federation/contract.ts +449 -0
- package/src/worker-federation/handler.ts +274 -0
- package/src/worker-federation/index.ts +23 -0
- package/src/worker-federation/lease.ts +143 -0
- package/src/worker-federation/transport.ts +430 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AdmittedWorkerFederationExpose,
|
|
3
|
+
type AdmitWorkerRendererContract,
|
|
4
|
+
admitWorkerFederationExpose,
|
|
5
|
+
readWorkerFederationEnvelope,
|
|
6
|
+
readWorkerFederationManifestUrl,
|
|
7
|
+
readWorkerFederationPath,
|
|
8
|
+
readWorkerFederationStyles,
|
|
9
|
+
workerFederationEnvelope,
|
|
10
|
+
workerFederationWireJson,
|
|
11
|
+
} from './contract';
|
|
12
|
+
import {
|
|
13
|
+
assertWorkerFederationBodySize,
|
|
14
|
+
checkWorkerFederationDeadline,
|
|
15
|
+
readWorkerFederationJsonBody,
|
|
16
|
+
workerAbortable,
|
|
17
|
+
workerAbortReason,
|
|
18
|
+
} from './lease';
|
|
19
|
+
|
|
20
|
+
export interface WorkerFederationService {
|
|
21
|
+
readonly remote: string;
|
|
22
|
+
/** Existing MF import alias, mapped to the pinned topology publisher. */
|
|
23
|
+
readonly alias?: string;
|
|
24
|
+
readonly manifestUrl: string;
|
|
25
|
+
readonly fragments: readonly {
|
|
26
|
+
readonly expose: string;
|
|
27
|
+
readonly boundaryId: string;
|
|
28
|
+
readonly paths: Readonly<Record<string, string>>;
|
|
29
|
+
}[];
|
|
30
|
+
/** A configured Workers service binding, never a caller-selected fetch target. */
|
|
31
|
+
fetch(request: Request): Promise<Response>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface WorkerFederationSnapshot {
|
|
35
|
+
readonly admitted: AdmittedWorkerFederationExpose;
|
|
36
|
+
readonly payload: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WorkerFederationTransport {
|
|
40
|
+
/** Retire the owning document, its authorities and outstanding requests. */
|
|
41
|
+
dispose(reason?: unknown): void;
|
|
42
|
+
/** Admit lazily before constructing the native snapshot boundary. */
|
|
43
|
+
admit(options: {
|
|
44
|
+
remote: string;
|
|
45
|
+
expose: string;
|
|
46
|
+
locale: string;
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
}): Promise<AdmittedWorkerFederationExpose>;
|
|
49
|
+
/** Deduplicate active requests only; the native boundary owns settled snapshots. */
|
|
50
|
+
request(options: {
|
|
51
|
+
remote: string;
|
|
52
|
+
expose: string;
|
|
53
|
+
locale: string;
|
|
54
|
+
/** Already encoded by the renderer's native wire codec. */
|
|
55
|
+
payload: unknown;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
}): Promise<WorkerFederationSnapshot>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readResponseJson(
|
|
61
|
+
response: Response,
|
|
62
|
+
expectedUrl: string,
|
|
63
|
+
signal: AbortSignal,
|
|
64
|
+
checkDeadline: () => void,
|
|
65
|
+
): Promise<unknown> {
|
|
66
|
+
const cancelBody = () => {
|
|
67
|
+
void response.body?.cancel().catch(() => undefined);
|
|
68
|
+
};
|
|
69
|
+
if (signal.aborted) {
|
|
70
|
+
cancelBody();
|
|
71
|
+
throw workerAbortReason(signal);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
checkDeadline();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
cancelBody();
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
// Workers support manual redirects, not redirect: 'error'. A terminal 3xx
|
|
80
|
+
// response is not OK: reject and cancel it here, never follow its Location.
|
|
81
|
+
// Also reject a binding that ignored manual mode and followed a redirect.
|
|
82
|
+
if (
|
|
83
|
+
!response.ok ||
|
|
84
|
+
response.redirected ||
|
|
85
|
+
(response.url && response.url !== expectedUrl)
|
|
86
|
+
) {
|
|
87
|
+
cancelBody();
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Worker federation publication request failed (${response.status}).`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (!response.body)
|
|
93
|
+
throw new Error('Worker federation response has no body.');
|
|
94
|
+
return readWorkerFederationJsonBody(
|
|
95
|
+
response.body,
|
|
96
|
+
signal,
|
|
97
|
+
response.headers.get('content-length'),
|
|
98
|
+
checkDeadline,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function fetchJson(
|
|
103
|
+
fetchResponse: (request: Request) => Promise<Response>,
|
|
104
|
+
request: Request,
|
|
105
|
+
signal: AbortSignal,
|
|
106
|
+
checkDeadline: () => void,
|
|
107
|
+
) {
|
|
108
|
+
checkDeadline();
|
|
109
|
+
if (signal.aborted) throw workerAbortReason(signal);
|
|
110
|
+
const pending = fetchResponse(request);
|
|
111
|
+
// A service binding may ignore abort. Dispose a response arriving after its lease.
|
|
112
|
+
void pending.then(
|
|
113
|
+
response => {
|
|
114
|
+
if (signal.aborted) void response.body?.cancel().catch(() => undefined);
|
|
115
|
+
},
|
|
116
|
+
() => undefined,
|
|
117
|
+
);
|
|
118
|
+
return readResponseJson(
|
|
119
|
+
await workerAbortable(pending, signal),
|
|
120
|
+
request.url,
|
|
121
|
+
signal,
|
|
122
|
+
checkDeadline,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface PendingValue<Value> {
|
|
127
|
+
readonly controller: AbortController;
|
|
128
|
+
readonly promise: Promise<Value>;
|
|
129
|
+
waiters: number;
|
|
130
|
+
settled: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function createWorkerFederationTransport(options: {
|
|
134
|
+
services: readonly WorkerFederationService[];
|
|
135
|
+
admitRendererContract: AdmitWorkerRendererContract;
|
|
136
|
+
fetchManifest?: (request: Request) => Promise<Response>;
|
|
137
|
+
/** One lease covers manifest headers/body and snapshot headers/body. */
|
|
138
|
+
requestTimeout?: number;
|
|
139
|
+
/** The document handle owns this transport's complete lifetime. */
|
|
140
|
+
signal?: AbortSignal;
|
|
141
|
+
}): WorkerFederationTransport {
|
|
142
|
+
const timeout = options.requestTimeout ?? 3000;
|
|
143
|
+
const admitRendererContract = options.admitRendererContract;
|
|
144
|
+
if (typeof admitRendererContract !== 'function')
|
|
145
|
+
throw new Error('Worker federation requires canonical renderer admission.');
|
|
146
|
+
if (!Number.isFinite(timeout) || timeout <= 0)
|
|
147
|
+
throw new Error('Worker federation requires a positive request deadline.');
|
|
148
|
+
const services = new Map<string, WorkerFederationService>();
|
|
149
|
+
for (const service of options.services) {
|
|
150
|
+
if (!service.remote || services.has(service.remote))
|
|
151
|
+
throw new Error('Worker federation service remotes must be unique.');
|
|
152
|
+
const exposes = new Set<string>();
|
|
153
|
+
for (const fragment of service.fragments) {
|
|
154
|
+
Object.values(fragment.paths).forEach(readWorkerFederationPath);
|
|
155
|
+
if (!fragment.boundaryId || exposes.has(fragment.expose))
|
|
156
|
+
throw new Error(
|
|
157
|
+
'Worker federation service exposes must be explicit and unique.',
|
|
158
|
+
);
|
|
159
|
+
exposes.add(fragment.expose);
|
|
160
|
+
}
|
|
161
|
+
const captured = {
|
|
162
|
+
...service,
|
|
163
|
+
manifestUrl: readWorkerFederationManifestUrl(service.manifestUrl).href,
|
|
164
|
+
fetch: service.fetch.bind(service),
|
|
165
|
+
fragments: service.fragments.map(fragment =>
|
|
166
|
+
Object.freeze({
|
|
167
|
+
...fragment,
|
|
168
|
+
paths: Object.freeze({ ...fragment.paths }),
|
|
169
|
+
}),
|
|
170
|
+
),
|
|
171
|
+
};
|
|
172
|
+
services.set(service.remote, captured);
|
|
173
|
+
if (service.alias && service.alias !== service.remote) {
|
|
174
|
+
if (services.has(service.alias))
|
|
175
|
+
throw new Error('Worker federation service aliases must be unique.');
|
|
176
|
+
services.set(service.alias, captured);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const snapshots = new Map<string, PendingValue<WorkerFederationSnapshot>>();
|
|
180
|
+
const admissions = new Map<
|
|
181
|
+
string,
|
|
182
|
+
PendingValue<AdmittedWorkerFederationExpose>
|
|
183
|
+
>();
|
|
184
|
+
const fetchManifest = options.fetchManifest;
|
|
185
|
+
let retired: { reason: unknown } | undefined;
|
|
186
|
+
const onOwnerAbort = () => dispose(workerAbortReason(options.signal!));
|
|
187
|
+
|
|
188
|
+
// Read the live closure state at each use, including after awaited work.
|
|
189
|
+
function checkDocumentLiveness(): void {
|
|
190
|
+
if (retired) throw retired.reason;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function dispose(
|
|
194
|
+
reason: unknown = new DOMException(
|
|
195
|
+
'The Worker federation document was disposed',
|
|
196
|
+
'AbortError',
|
|
197
|
+
),
|
|
198
|
+
): void {
|
|
199
|
+
if (retired) return;
|
|
200
|
+
retired = { reason };
|
|
201
|
+
options.signal?.removeEventListener('abort', onOwnerAbort);
|
|
202
|
+
for (const pending of [...snapshots.values(), ...admissions.values()])
|
|
203
|
+
pending.controller.abort(reason);
|
|
204
|
+
snapshots.clear();
|
|
205
|
+
admissions.clear();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (options.signal?.aborted) onOwnerAbort();
|
|
209
|
+
else options.signal?.addEventListener('abort', onOwnerAbort, { once: true });
|
|
210
|
+
|
|
211
|
+
function serviceCoordinate(remote: string, expose: string, locale: string) {
|
|
212
|
+
const service = services.get(remote);
|
|
213
|
+
const fragment = service?.fragments.find(value => value.expose === expose);
|
|
214
|
+
if (!service || !fragment || !Object.hasOwn(fragment.paths, locale))
|
|
215
|
+
throw new Error(
|
|
216
|
+
'Worker federation remote/expose has no configured service binding.',
|
|
217
|
+
);
|
|
218
|
+
return { service, fragment };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function shared<Value>(
|
|
222
|
+
cache: Map<string, PendingValue<Value>>,
|
|
223
|
+
key: string,
|
|
224
|
+
operation: (
|
|
225
|
+
signal: AbortSignal,
|
|
226
|
+
checkDeadline: () => void,
|
|
227
|
+
) => Promise<Value>,
|
|
228
|
+
signal?: AbortSignal,
|
|
229
|
+
deadlineAt = performance.now() + timeout,
|
|
230
|
+
retainFulfilled = true,
|
|
231
|
+
): Promise<Value> {
|
|
232
|
+
checkDocumentLiveness();
|
|
233
|
+
if (signal?.aborted) throw workerAbortReason(signal);
|
|
234
|
+
let pending = cache.get(key);
|
|
235
|
+
if (!pending) {
|
|
236
|
+
const controller = new AbortController();
|
|
237
|
+
const checkDeadline = () =>
|
|
238
|
+
checkWorkerFederationDeadline(
|
|
239
|
+
deadlineAt,
|
|
240
|
+
controller.signal,
|
|
241
|
+
controller,
|
|
242
|
+
);
|
|
243
|
+
const deadline = setTimeout(
|
|
244
|
+
() =>
|
|
245
|
+
controller.abort(
|
|
246
|
+
new Error('Worker federation request deadline exceeded.'),
|
|
247
|
+
),
|
|
248
|
+
Math.max(0, deadlineAt - performance.now()),
|
|
249
|
+
);
|
|
250
|
+
const promise = operation(controller.signal, checkDeadline);
|
|
251
|
+
pending = { controller, promise, waiters: 0, settled: false };
|
|
252
|
+
const captured = pending;
|
|
253
|
+
cache.set(key, captured);
|
|
254
|
+
void promise.then(
|
|
255
|
+
() => {
|
|
256
|
+
captured.settled = true;
|
|
257
|
+
clearTimeout(deadline);
|
|
258
|
+
if (!retainFulfilled && cache.get(key) === captured)
|
|
259
|
+
cache.delete(key);
|
|
260
|
+
},
|
|
261
|
+
() => {
|
|
262
|
+
captured.settled = true;
|
|
263
|
+
clearTimeout(deadline);
|
|
264
|
+
if (cache.get(key) === captured) cache.delete(key);
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const captured = pending;
|
|
269
|
+
captured.waiters++;
|
|
270
|
+
try {
|
|
271
|
+
const value = await (signal
|
|
272
|
+
? workerAbortable(captured.promise, signal)
|
|
273
|
+
: captured.promise);
|
|
274
|
+
checkDocumentLiveness();
|
|
275
|
+
// A fulfilled document cache still belongs to this caller's lease.
|
|
276
|
+
checkWorkerFederationDeadline(deadlineAt, signal);
|
|
277
|
+
return value;
|
|
278
|
+
} finally {
|
|
279
|
+
captured.waiters--;
|
|
280
|
+
if (!captured.settled && captured.waiters === 0) {
|
|
281
|
+
captured.controller.abort(
|
|
282
|
+
new Error('Worker federation request has no active consumers.'),
|
|
283
|
+
);
|
|
284
|
+
if (cache.get(key) === captured) cache.delete(key);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function admitWithin(
|
|
290
|
+
{
|
|
291
|
+
remote,
|
|
292
|
+
expose,
|
|
293
|
+
locale,
|
|
294
|
+
signal,
|
|
295
|
+
}: {
|
|
296
|
+
remote: string;
|
|
297
|
+
expose: string;
|
|
298
|
+
locale: string;
|
|
299
|
+
signal?: AbortSignal;
|
|
300
|
+
},
|
|
301
|
+
deadlineAt = performance.now() + timeout,
|
|
302
|
+
) {
|
|
303
|
+
const { service, fragment } = serviceCoordinate(remote, expose, locale);
|
|
304
|
+
const key = workerFederationWireJson([service.remote, expose, locale]);
|
|
305
|
+
const promise = shared(
|
|
306
|
+
admissions,
|
|
307
|
+
key,
|
|
308
|
+
async (signal, checkDeadline) => {
|
|
309
|
+
checkDeadline();
|
|
310
|
+
const request = new Request(service.manifestUrl, {
|
|
311
|
+
redirect: 'manual',
|
|
312
|
+
signal,
|
|
313
|
+
});
|
|
314
|
+
const manifest = await fetchJson(
|
|
315
|
+
fetchManifest ?? service.fetch,
|
|
316
|
+
request,
|
|
317
|
+
signal,
|
|
318
|
+
checkDeadline,
|
|
319
|
+
);
|
|
320
|
+
// Capture browser resources from this same completed manifest before
|
|
321
|
+
// calling renderer admission, with no intervening await or callback.
|
|
322
|
+
const styles = readWorkerFederationStyles({
|
|
323
|
+
manifest,
|
|
324
|
+
manifestUrl: request.url,
|
|
325
|
+
expose,
|
|
326
|
+
});
|
|
327
|
+
const admitted = admitWorkerFederationExpose({
|
|
328
|
+
manifest,
|
|
329
|
+
manifestUrl: request.url,
|
|
330
|
+
remote: service.remote,
|
|
331
|
+
expose,
|
|
332
|
+
locale,
|
|
333
|
+
admitRendererContract,
|
|
334
|
+
});
|
|
335
|
+
checkDeadline();
|
|
336
|
+
if (
|
|
337
|
+
admitted.coordinate.boundaryId !== fragment.boundaryId ||
|
|
338
|
+
admitted.path !== fragment.paths[locale]
|
|
339
|
+
)
|
|
340
|
+
throw new Error(
|
|
341
|
+
'Worker federation publication conflicts with its service coordinate.',
|
|
342
|
+
);
|
|
343
|
+
return Object.freeze({ ...admitted, styles });
|
|
344
|
+
},
|
|
345
|
+
signal,
|
|
346
|
+
deadlineAt,
|
|
347
|
+
);
|
|
348
|
+
// Capture synchronously: a later waiter may already be using a replacement
|
|
349
|
+
// authority by the time this admission's fulfilled promise resumes.
|
|
350
|
+
return { promise, key, pending: admissions.get(key) };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
dispose,
|
|
355
|
+
admit: options => admitWithin(options).promise,
|
|
356
|
+
async request({ remote, expose, locale, payload, signal }) {
|
|
357
|
+
const deadlineAt = performance.now() + timeout;
|
|
358
|
+
checkDocumentLiveness();
|
|
359
|
+
if (signal?.aborted) throw workerAbortReason(signal);
|
|
360
|
+
const { service } = serviceCoordinate(remote, expose, locale);
|
|
361
|
+
if (typeof payload === 'string') assertWorkerFederationBodySize(payload);
|
|
362
|
+
const encodedPayload = assertWorkerFederationBodySize(
|
|
363
|
+
workerFederationWireJson(payload),
|
|
364
|
+
);
|
|
365
|
+
const key = workerFederationWireJson([
|
|
366
|
+
service.remote,
|
|
367
|
+
expose,
|
|
368
|
+
locale,
|
|
369
|
+
encodedPayload,
|
|
370
|
+
]);
|
|
371
|
+
checkWorkerFederationDeadline(deadlineAt, signal);
|
|
372
|
+
return shared(
|
|
373
|
+
snapshots,
|
|
374
|
+
key,
|
|
375
|
+
async (signal, checkDeadline) => {
|
|
376
|
+
checkDeadline();
|
|
377
|
+
const admission = admitWithin(
|
|
378
|
+
{ remote, expose, locale, signal },
|
|
379
|
+
deadlineAt,
|
|
380
|
+
);
|
|
381
|
+
const admitted = await admission.promise;
|
|
382
|
+
checkDeadline();
|
|
383
|
+
try {
|
|
384
|
+
const body = assertWorkerFederationBodySize(
|
|
385
|
+
workerFederationWireJson(
|
|
386
|
+
workerFederationEnvelope(
|
|
387
|
+
'request',
|
|
388
|
+
admitted,
|
|
389
|
+
JSON.parse(encodedPayload),
|
|
390
|
+
),
|
|
391
|
+
),
|
|
392
|
+
);
|
|
393
|
+
checkDeadline();
|
|
394
|
+
const response = await fetchJson(
|
|
395
|
+
service.fetch,
|
|
396
|
+
new Request(admitted.endpoint, {
|
|
397
|
+
method: 'POST',
|
|
398
|
+
headers: { 'content-type': 'application/json' },
|
|
399
|
+
body,
|
|
400
|
+
redirect: 'manual',
|
|
401
|
+
signal,
|
|
402
|
+
}),
|
|
403
|
+
signal,
|
|
404
|
+
checkDeadline,
|
|
405
|
+
);
|
|
406
|
+
checkDeadline();
|
|
407
|
+
const decodedPayload = readWorkerFederationEnvelope(
|
|
408
|
+
response,
|
|
409
|
+
'response',
|
|
410
|
+
admitted,
|
|
411
|
+
);
|
|
412
|
+
checkDeadline();
|
|
413
|
+
return { admitted, payload: decodedPayload };
|
|
414
|
+
} catch (error) {
|
|
415
|
+
// Consumers keep their captured authority; retry may admit a new publisher.
|
|
416
|
+
if (
|
|
417
|
+
!signal.aborted &&
|
|
418
|
+
admissions.get(admission.key) === admission.pending
|
|
419
|
+
)
|
|
420
|
+
admissions.delete(admission.key);
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
signal,
|
|
425
|
+
deadlineAt,
|
|
426
|
+
false,
|
|
427
|
+
);
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
}
|