@bleedingdev/modern-js-renderer-core 3.9.3-ultramodern.1 → 3.9.3-ultramodern.11
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/data/codec.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/data/codec.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/data/codec.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/adapter.d.ts +1 -1
- package/dist/types/data/codec.d.ts +1 -1
- 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 +17 -7
- package/src/adapter.ts +4 -2
- package/src/data/codec.ts +1 -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,160 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { admitWorkerFederationExpose, readWorkerFederationEnvelope, readWorkerFederationPublication, workerFederationEnvelope, workerFederationRecord, workerFederationWireJson } from "./contract.mjs";
|
|
3
|
+
import { WorkerFederationBodyTooLargeError, assertWorkerFederationBodySize, checkWorkerFederationDeadline, readWorkerFederationJsonBody, workerAbortReason, workerAbortable, workerFederationErrorMessage } from "./lease.mjs";
|
|
4
|
+
function createWorkerFederationHandler(options) {
|
|
5
|
+
if ('function' != typeof options.admitRendererContract || 'function' != typeof options.admitNativeRequest || 'function' != typeof options.releaseNativeRequest || 'function' != typeof options.renderNativeSnapshot) throw new Error('Worker federation requires canonical and native admission callbacks.');
|
|
6
|
+
const timeout = options.requestTimeout ?? 3000;
|
|
7
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('Worker federation requires a positive request deadline.');
|
|
8
|
+
const renderer = options.admitRendererContract(options.publication.renderer);
|
|
9
|
+
const worker = readWorkerFederationPublication(options.publication.worker, renderer.identities);
|
|
10
|
+
const manifest = JSON.parse(workerFederationWireJson({
|
|
11
|
+
metaData: {
|
|
12
|
+
ultramodernRenderer: renderer,
|
|
13
|
+
ultramodernWorkerFederation: worker
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
const loaders = workerFederationRecord(options.exposes, 'compiled exposes');
|
|
17
|
+
if (Object.keys(loaders).length !== Object.keys(worker.exposes).length || Object.keys(loaders).some((expose)=>!Object.hasOwn(worker.exposes, expose) || 'function' != typeof loaders[expose])) throw new Error('Worker federation registry conflicts with its own publication.');
|
|
18
|
+
const pinnedLoaders = {
|
|
19
|
+
...options.exposes
|
|
20
|
+
};
|
|
21
|
+
const endpoints = new Map();
|
|
22
|
+
for (const [expose, coordinate] of Object.entries(worker.exposes))for (const locale of Object.keys(coordinate.paths)){
|
|
23
|
+
const admitted = admitWorkerFederationExpose({
|
|
24
|
+
manifest,
|
|
25
|
+
manifestUrl: options.manifestUrl,
|
|
26
|
+
remote: worker.remote,
|
|
27
|
+
expose,
|
|
28
|
+
locale,
|
|
29
|
+
admitRendererContract: options.admitRendererContract
|
|
30
|
+
});
|
|
31
|
+
endpoints.set(admitted.path, admitted);
|
|
32
|
+
}
|
|
33
|
+
const admitNativeRequest = options.admitNativeRequest;
|
|
34
|
+
const releaseNativeRequest = options.releaseNativeRequest;
|
|
35
|
+
const renderNativeSnapshot = options.renderNativeSnapshot;
|
|
36
|
+
return async (request)=>{
|
|
37
|
+
const deadlineAt = performance.now() + timeout;
|
|
38
|
+
const url = new URL(request.url);
|
|
39
|
+
const admitted = endpoints.get(url.pathname);
|
|
40
|
+
if (!admitted) return;
|
|
41
|
+
if ('POST' !== request.method || url.href !== admitted.endpoint || request.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json') {
|
|
42
|
+
request.body?.cancel().catch(()=>void 0);
|
|
43
|
+
return new Response('Worker federation endpoint coordinate conflicts with its publication.', {
|
|
44
|
+
status: 400
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (request.signal.aborted) {
|
|
48
|
+
request.body?.cancel().catch(()=>void 0);
|
|
49
|
+
throw workerAbortReason(request.signal);
|
|
50
|
+
}
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const signal = AbortSignal.any([
|
|
53
|
+
request.signal,
|
|
54
|
+
controller.signal
|
|
55
|
+
]);
|
|
56
|
+
const checkDeadline = ()=>checkWorkerFederationDeadline(deadlineAt, signal, controller);
|
|
57
|
+
const execution = Object.freeze({
|
|
58
|
+
admitted,
|
|
59
|
+
signal
|
|
60
|
+
});
|
|
61
|
+
const deadline = setTimeout(()=>{
|
|
62
|
+
controller.abort(new Error('Worker federation request deadline exceeded.'));
|
|
63
|
+
}, Math.max(0, deadlineAt - performance.now()));
|
|
64
|
+
let executing = false;
|
|
65
|
+
let admittedRequest;
|
|
66
|
+
let release;
|
|
67
|
+
let response;
|
|
68
|
+
let failedRequest;
|
|
69
|
+
const releaseOnce = (value)=>{
|
|
70
|
+
if (!release) try {
|
|
71
|
+
release = Promise.resolve(releaseNativeRequest(value));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
release = Promise.reject(error);
|
|
74
|
+
}
|
|
75
|
+
return release ?? Promise.resolve();
|
|
76
|
+
};
|
|
77
|
+
try {
|
|
78
|
+
if (!request.body) throw new Error('Worker federation request has no native payload.');
|
|
79
|
+
const envelope = await readWorkerFederationJsonBody(request.body, signal, request.headers.get('content-length'), checkDeadline);
|
|
80
|
+
checkDeadline();
|
|
81
|
+
const payload = readWorkerFederationEnvelope(envelope, 'request', admitted);
|
|
82
|
+
checkDeadline();
|
|
83
|
+
const preparing = Promise.resolve().then(()=>{
|
|
84
|
+
checkDeadline();
|
|
85
|
+
return admitNativeRequest(payload, execution);
|
|
86
|
+
});
|
|
87
|
+
preparing.then((value)=>{
|
|
88
|
+
if (signal.aborted) releaseOnce(value).catch(()=>void 0);
|
|
89
|
+
}, ()=>void 0);
|
|
90
|
+
const nativeRequest = await workerAbortable(preparing, signal);
|
|
91
|
+
admittedRequest = {
|
|
92
|
+
value: nativeRequest
|
|
93
|
+
};
|
|
94
|
+
checkDeadline();
|
|
95
|
+
executing = true;
|
|
96
|
+
const module = await workerAbortable(pinnedLoaders[admitted.expose](), signal);
|
|
97
|
+
checkDeadline();
|
|
98
|
+
const snapshot = await workerAbortable(Promise.resolve(renderNativeSnapshot(module, nativeRequest, execution)), signal);
|
|
99
|
+
checkDeadline();
|
|
100
|
+
if ('string' == typeof snapshot) assertWorkerFederationBodySize(snapshot);
|
|
101
|
+
const body = assertWorkerFederationBodySize(workerFederationWireJson(workerFederationEnvelope('response', admitted, snapshot)));
|
|
102
|
+
checkDeadline();
|
|
103
|
+
response = new Response(body, {
|
|
104
|
+
headers: {
|
|
105
|
+
'content-type': 'application/json'
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
let failure = error;
|
|
110
|
+
try {
|
|
111
|
+
checkDeadline();
|
|
112
|
+
} catch (deadlineError) {
|
|
113
|
+
failure = deadlineError;
|
|
114
|
+
}
|
|
115
|
+
failedRequest = {
|
|
116
|
+
error: failure
|
|
117
|
+
};
|
|
118
|
+
response = new Response(workerFederationErrorMessage(failure, 'Worker federation request failed.'), {
|
|
119
|
+
status: signal.aborted ? 504 : executing ? 502 : failure instanceof WorkerFederationBodyTooLargeError ? 413 : 400
|
|
120
|
+
});
|
|
121
|
+
} finally{
|
|
122
|
+
try {
|
|
123
|
+
if (admittedRequest) {
|
|
124
|
+
const releasing = releaseOnce(admittedRequest.value);
|
|
125
|
+
if (signal.aborted) releasing.catch(()=>void 0);
|
|
126
|
+
else try {
|
|
127
|
+
await workerAbortable(releasing, signal);
|
|
128
|
+
checkDeadline();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
response?.body?.cancel().catch(()=>void 0);
|
|
131
|
+
let releaseFailure = error;
|
|
132
|
+
try {
|
|
133
|
+
checkDeadline();
|
|
134
|
+
} catch (deadlineError) {
|
|
135
|
+
releaseFailure = new AggregateError([
|
|
136
|
+
error,
|
|
137
|
+
deadlineError
|
|
138
|
+
], `${workerFederationErrorMessage(error, 'Native request release failed.', 384)}; ${workerFederationErrorMessage(deadlineError, 'Worker federation deadline exceeded.', 384)}`);
|
|
139
|
+
}
|
|
140
|
+
const failure = failedRequest ? new AggregateError([
|
|
141
|
+
failedRequest.error,
|
|
142
|
+
releaseFailure
|
|
143
|
+
], `${workerFederationErrorMessage(failedRequest.error, 'Worker federation request failed.', 384)}; native request release failed: ${workerFederationErrorMessage(releaseFailure, 'Native request release failed.', 384)}`) : releaseFailure;
|
|
144
|
+
response = new Response(workerFederationErrorMessage(failure, 'Worker federation native request release failed.'), {
|
|
145
|
+
status: signal.aborted ? 504 : 502
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} finally{
|
|
150
|
+
clearTimeout(deadline);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (request.signal.aborted) {
|
|
154
|
+
response?.body?.cancel().catch(()=>void 0);
|
|
155
|
+
throw workerAbortReason(request.signal);
|
|
156
|
+
}
|
|
157
|
+
return response;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export { createWorkerFederationHandler };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
export { WORKER_FEDERATION_METADATA_KEY, WORKER_FEDERATION_SCHEMA, WORKER_FEDERATION_VERSION, readWorkerFederationManifestUrl, readWorkerFederationPath, readWorkerFederationPublication } from "./contract.mjs";
|
|
3
|
+
export { createWorkerFederationHandler } from "./handler.mjs";
|
|
4
|
+
export { createWorkerFederationTransport } from "./transport.mjs";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
function checkWorkerFederationDeadline(deadlineAt, signal, controller) {
|
|
3
|
+
if (signal?.aborted) throw workerAbortReason(signal);
|
|
4
|
+
if (performance.now() >= deadlineAt) {
|
|
5
|
+
const error = new Error('Worker federation request deadline exceeded.');
|
|
6
|
+
controller?.abort(error);
|
|
7
|
+
throw error;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function workerFederationErrorMessage(error, fallback, maxLength = 1024) {
|
|
11
|
+
return error instanceof Error && error.message.length <= maxLength ? error.message : fallback;
|
|
12
|
+
}
|
|
13
|
+
function workerAbortReason(signal) {
|
|
14
|
+
return signal.reason ?? new DOMException('Worker federation request aborted.', 'AbortError');
|
|
15
|
+
}
|
|
16
|
+
function workerAbortable(operation, signal) {
|
|
17
|
+
if (signal.aborted) {
|
|
18
|
+
operation.catch(()=>void 0);
|
|
19
|
+
return Promise.reject(workerAbortReason(signal));
|
|
20
|
+
}
|
|
21
|
+
return new Promise((resolve, reject)=>{
|
|
22
|
+
const onAbort = ()=>reject(workerAbortReason(signal));
|
|
23
|
+
signal.addEventListener('abort', onAbort, {
|
|
24
|
+
once: true
|
|
25
|
+
});
|
|
26
|
+
operation.then(resolve, reject).finally(()=>signal.removeEventListener('abort', onAbort));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function readWorkerFederationJsonBody(body, signal, contentLength, checkDeadline) {
|
|
30
|
+
if (signal.aborted) {
|
|
31
|
+
body.cancel().catch(()=>void 0);
|
|
32
|
+
throw workerAbortReason(signal);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
checkDeadline?.();
|
|
36
|
+
if (null != contentLength) {
|
|
37
|
+
if (!/^\d+$/u.test(contentLength)) throw new Error('Worker federation body has an invalid Content-Length.');
|
|
38
|
+
if (Number(contentLength) > WORKER_FEDERATION_MAX_BODY_BYTES) throw new WorkerFederationBodyTooLargeError();
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
body.cancel(error).catch(()=>void 0);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
const reader = body.getReader();
|
|
45
|
+
const onAbort = ()=>{
|
|
46
|
+
reader.cancel(workerAbortReason(signal)).catch(()=>void 0);
|
|
47
|
+
};
|
|
48
|
+
signal.addEventListener('abort', onAbort, {
|
|
49
|
+
once: true
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
const chunks = [];
|
|
53
|
+
let length = 0;
|
|
54
|
+
while(true){
|
|
55
|
+
checkDeadline?.();
|
|
56
|
+
const chunk = await workerAbortable(reader.read(), signal);
|
|
57
|
+
checkDeadline?.();
|
|
58
|
+
if (chunk.done) break;
|
|
59
|
+
if (chunk.value.byteLength > WORKER_FEDERATION_MAX_BODY_BYTES - length) throw new WorkerFederationBodyTooLargeError();
|
|
60
|
+
chunks.push(new Uint8Array(chunk.value));
|
|
61
|
+
length += chunk.value.byteLength;
|
|
62
|
+
}
|
|
63
|
+
checkDeadline?.();
|
|
64
|
+
const bytes = new Uint8Array(length);
|
|
65
|
+
let offset = 0;
|
|
66
|
+
for (const chunk of chunks){
|
|
67
|
+
bytes.set(chunk, offset);
|
|
68
|
+
offset += chunk.byteLength;
|
|
69
|
+
}
|
|
70
|
+
if (signal.aborted) throw workerAbortReason(signal);
|
|
71
|
+
const decoded = new TextDecoder('utf-8', {
|
|
72
|
+
fatal: true
|
|
73
|
+
}).decode(bytes);
|
|
74
|
+
checkDeadline?.();
|
|
75
|
+
const result = JSON.parse(decoded);
|
|
76
|
+
checkDeadline?.();
|
|
77
|
+
return result;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
reader.cancel(error).catch(()=>void 0);
|
|
80
|
+
throw error;
|
|
81
|
+
} finally{
|
|
82
|
+
signal.removeEventListener('abort', onAbort);
|
|
83
|
+
reader.releaseLock();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const WORKER_FEDERATION_MAX_BODY_BYTES = 8388608;
|
|
87
|
+
class WorkerFederationBodyTooLargeError extends Error {
|
|
88
|
+
constructor(){
|
|
89
|
+
super('Worker federation body exceeds 8 MiB.');
|
|
90
|
+
this.name = 'WorkerFederationBodyTooLargeError';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function assertWorkerFederationBodySize(value) {
|
|
94
|
+
if (value.length > WORKER_FEDERATION_MAX_BODY_BYTES) throw new WorkerFederationBodyTooLargeError();
|
|
95
|
+
let bytes = 0;
|
|
96
|
+
for(let index = 0; index < value.length; index++){
|
|
97
|
+
const code = value.charCodeAt(index);
|
|
98
|
+
if (code <= 0x7f) bytes++;
|
|
99
|
+
else if (code <= 0x7ff) bytes += 2;
|
|
100
|
+
else if (code >= 0xd800 && code <= 0xdbff && value.charCodeAt(index + 1) >= 0xdc00 && value.charCodeAt(index + 1) <= 0xdfff) {
|
|
101
|
+
bytes += 4;
|
|
102
|
+
index++;
|
|
103
|
+
} else bytes += 3;
|
|
104
|
+
if (bytes > WORKER_FEDERATION_MAX_BODY_BYTES) throw new WorkerFederationBodyTooLargeError();
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
export { WORKER_FEDERATION_MAX_BODY_BYTES, WorkerFederationBodyTooLargeError, assertWorkerFederationBodySize, checkWorkerFederationDeadline, readWorkerFederationJsonBody, workerAbortReason, workerAbortable, workerFederationErrorMessage };
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { admitWorkerFederationExpose, readWorkerFederationEnvelope, readWorkerFederationManifestUrl, readWorkerFederationPath, readWorkerFederationStyles, workerFederationEnvelope, workerFederationWireJson } from "./contract.mjs";
|
|
3
|
+
import { assertWorkerFederationBodySize, checkWorkerFederationDeadline, readWorkerFederationJsonBody, workerAbortReason, workerAbortable } from "./lease.mjs";
|
|
4
|
+
async function readResponseJson(response, expectedUrl, signal, checkDeadline) {
|
|
5
|
+
const cancelBody = ()=>{
|
|
6
|
+
response.body?.cancel().catch(()=>void 0);
|
|
7
|
+
};
|
|
8
|
+
if (signal.aborted) {
|
|
9
|
+
cancelBody();
|
|
10
|
+
throw workerAbortReason(signal);
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
checkDeadline();
|
|
14
|
+
} catch (error) {
|
|
15
|
+
cancelBody();
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
if (!response.ok || response.redirected || response.url && response.url !== expectedUrl) {
|
|
19
|
+
cancelBody();
|
|
20
|
+
throw new Error(`Worker federation publication request failed (${response.status}).`);
|
|
21
|
+
}
|
|
22
|
+
if (!response.body) throw new Error('Worker federation response has no body.');
|
|
23
|
+
return readWorkerFederationJsonBody(response.body, signal, response.headers.get('content-length'), checkDeadline);
|
|
24
|
+
}
|
|
25
|
+
async function fetchJson(fetchResponse, request, signal, checkDeadline) {
|
|
26
|
+
checkDeadline();
|
|
27
|
+
if (signal.aborted) throw workerAbortReason(signal);
|
|
28
|
+
const pending = fetchResponse(request);
|
|
29
|
+
pending.then((response)=>{
|
|
30
|
+
if (signal.aborted) response.body?.cancel().catch(()=>void 0);
|
|
31
|
+
}, ()=>void 0);
|
|
32
|
+
return readResponseJson(await workerAbortable(pending, signal), request.url, signal, checkDeadline);
|
|
33
|
+
}
|
|
34
|
+
function createWorkerFederationTransport(options) {
|
|
35
|
+
const timeout = options.requestTimeout ?? 3000;
|
|
36
|
+
const admitRendererContract = options.admitRendererContract;
|
|
37
|
+
if ('function' != typeof admitRendererContract) throw new Error('Worker federation requires canonical renderer admission.');
|
|
38
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('Worker federation requires a positive request deadline.');
|
|
39
|
+
const services = new Map();
|
|
40
|
+
for (const service of options.services){
|
|
41
|
+
if (!service.remote || services.has(service.remote)) throw new Error('Worker federation service remotes must be unique.');
|
|
42
|
+
const exposes = new Set();
|
|
43
|
+
for (const fragment of service.fragments){
|
|
44
|
+
Object.values(fragment.paths).forEach(readWorkerFederationPath);
|
|
45
|
+
if (!fragment.boundaryId || exposes.has(fragment.expose)) throw new Error('Worker federation service exposes must be explicit and unique.');
|
|
46
|
+
exposes.add(fragment.expose);
|
|
47
|
+
}
|
|
48
|
+
const captured = {
|
|
49
|
+
...service,
|
|
50
|
+
manifestUrl: readWorkerFederationManifestUrl(service.manifestUrl).href,
|
|
51
|
+
fetch: service.fetch.bind(service),
|
|
52
|
+
fragments: service.fragments.map((fragment)=>Object.freeze({
|
|
53
|
+
...fragment,
|
|
54
|
+
paths: Object.freeze({
|
|
55
|
+
...fragment.paths
|
|
56
|
+
})
|
|
57
|
+
}))
|
|
58
|
+
};
|
|
59
|
+
services.set(service.remote, captured);
|
|
60
|
+
if (service.alias && service.alias !== service.remote) {
|
|
61
|
+
if (services.has(service.alias)) throw new Error('Worker federation service aliases must be unique.');
|
|
62
|
+
services.set(service.alias, captured);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const snapshots = new Map();
|
|
66
|
+
const admissions = new Map();
|
|
67
|
+
const fetchManifest = options.fetchManifest;
|
|
68
|
+
let retired;
|
|
69
|
+
const onOwnerAbort = ()=>dispose(workerAbortReason(options.signal));
|
|
70
|
+
function checkDocumentLiveness() {
|
|
71
|
+
if (retired) throw retired.reason;
|
|
72
|
+
}
|
|
73
|
+
function dispose(reason = new DOMException('The Worker federation document was disposed', 'AbortError')) {
|
|
74
|
+
if (retired) return;
|
|
75
|
+
retired = {
|
|
76
|
+
reason
|
|
77
|
+
};
|
|
78
|
+
options.signal?.removeEventListener('abort', onOwnerAbort);
|
|
79
|
+
for (const pending of [
|
|
80
|
+
...snapshots.values(),
|
|
81
|
+
...admissions.values()
|
|
82
|
+
])pending.controller.abort(reason);
|
|
83
|
+
snapshots.clear();
|
|
84
|
+
admissions.clear();
|
|
85
|
+
}
|
|
86
|
+
if (options.signal?.aborted) onOwnerAbort();
|
|
87
|
+
else options.signal?.addEventListener('abort', onOwnerAbort, {
|
|
88
|
+
once: true
|
|
89
|
+
});
|
|
90
|
+
function serviceCoordinate(remote, expose, locale) {
|
|
91
|
+
const service = services.get(remote);
|
|
92
|
+
const fragment = service?.fragments.find((value)=>value.expose === expose);
|
|
93
|
+
if (!service || !fragment || !Object.hasOwn(fragment.paths, locale)) throw new Error('Worker federation remote/expose has no configured service binding.');
|
|
94
|
+
return {
|
|
95
|
+
service,
|
|
96
|
+
fragment
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function shared(cache, key, operation, signal, deadlineAt = performance.now() + timeout, retainFulfilled = true) {
|
|
100
|
+
checkDocumentLiveness();
|
|
101
|
+
if (signal?.aborted) throw workerAbortReason(signal);
|
|
102
|
+
let pending = cache.get(key);
|
|
103
|
+
if (!pending) {
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const checkDeadline = ()=>checkWorkerFederationDeadline(deadlineAt, controller.signal, controller);
|
|
106
|
+
const deadline = setTimeout(()=>controller.abort(new Error('Worker federation request deadline exceeded.')), Math.max(0, deadlineAt - performance.now()));
|
|
107
|
+
const promise = operation(controller.signal, checkDeadline);
|
|
108
|
+
pending = {
|
|
109
|
+
controller,
|
|
110
|
+
promise,
|
|
111
|
+
waiters: 0,
|
|
112
|
+
settled: false
|
|
113
|
+
};
|
|
114
|
+
const captured = pending;
|
|
115
|
+
cache.set(key, captured);
|
|
116
|
+
promise.then(()=>{
|
|
117
|
+
captured.settled = true;
|
|
118
|
+
clearTimeout(deadline);
|
|
119
|
+
if (!retainFulfilled && cache.get(key) === captured) cache.delete(key);
|
|
120
|
+
}, ()=>{
|
|
121
|
+
captured.settled = true;
|
|
122
|
+
clearTimeout(deadline);
|
|
123
|
+
if (cache.get(key) === captured) cache.delete(key);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const captured = pending;
|
|
127
|
+
captured.waiters++;
|
|
128
|
+
try {
|
|
129
|
+
const value = await (signal ? workerAbortable(captured.promise, signal) : captured.promise);
|
|
130
|
+
checkDocumentLiveness();
|
|
131
|
+
checkWorkerFederationDeadline(deadlineAt, signal);
|
|
132
|
+
return value;
|
|
133
|
+
} finally{
|
|
134
|
+
captured.waiters--;
|
|
135
|
+
if (!captured.settled && 0 === captured.waiters) {
|
|
136
|
+
captured.controller.abort(new Error('Worker federation request has no active consumers.'));
|
|
137
|
+
if (cache.get(key) === captured) cache.delete(key);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function admitWithin({ remote, expose, locale, signal }, deadlineAt = performance.now() + timeout) {
|
|
142
|
+
const { service, fragment } = serviceCoordinate(remote, expose, locale);
|
|
143
|
+
const key = workerFederationWireJson([
|
|
144
|
+
service.remote,
|
|
145
|
+
expose,
|
|
146
|
+
locale
|
|
147
|
+
]);
|
|
148
|
+
const promise = shared(admissions, key, async (signal, checkDeadline)=>{
|
|
149
|
+
checkDeadline();
|
|
150
|
+
const request = new Request(service.manifestUrl, {
|
|
151
|
+
redirect: 'manual',
|
|
152
|
+
signal
|
|
153
|
+
});
|
|
154
|
+
const manifest = await fetchJson(fetchManifest ?? service.fetch, request, signal, checkDeadline);
|
|
155
|
+
const styles = readWorkerFederationStyles({
|
|
156
|
+
manifest,
|
|
157
|
+
manifestUrl: request.url,
|
|
158
|
+
expose
|
|
159
|
+
});
|
|
160
|
+
const admitted = admitWorkerFederationExpose({
|
|
161
|
+
manifest,
|
|
162
|
+
manifestUrl: request.url,
|
|
163
|
+
remote: service.remote,
|
|
164
|
+
expose,
|
|
165
|
+
locale,
|
|
166
|
+
admitRendererContract
|
|
167
|
+
});
|
|
168
|
+
checkDeadline();
|
|
169
|
+
if (admitted.coordinate.boundaryId !== fragment.boundaryId || admitted.path !== fragment.paths[locale]) throw new Error('Worker federation publication conflicts with its service coordinate.');
|
|
170
|
+
return Object.freeze({
|
|
171
|
+
...admitted,
|
|
172
|
+
styles
|
|
173
|
+
});
|
|
174
|
+
}, signal, deadlineAt);
|
|
175
|
+
return {
|
|
176
|
+
promise,
|
|
177
|
+
key,
|
|
178
|
+
pending: admissions.get(key)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
dispose,
|
|
183
|
+
admit: (options)=>admitWithin(options).promise,
|
|
184
|
+
async request ({ remote, expose, locale, payload, signal }) {
|
|
185
|
+
const deadlineAt = performance.now() + timeout;
|
|
186
|
+
checkDocumentLiveness();
|
|
187
|
+
if (signal?.aborted) throw workerAbortReason(signal);
|
|
188
|
+
const { service } = serviceCoordinate(remote, expose, locale);
|
|
189
|
+
if ('string' == typeof payload) assertWorkerFederationBodySize(payload);
|
|
190
|
+
const encodedPayload = assertWorkerFederationBodySize(workerFederationWireJson(payload));
|
|
191
|
+
const key = workerFederationWireJson([
|
|
192
|
+
service.remote,
|
|
193
|
+
expose,
|
|
194
|
+
locale,
|
|
195
|
+
encodedPayload
|
|
196
|
+
]);
|
|
197
|
+
checkWorkerFederationDeadline(deadlineAt, signal);
|
|
198
|
+
return shared(snapshots, key, async (signal, checkDeadline)=>{
|
|
199
|
+
checkDeadline();
|
|
200
|
+
const admission = admitWithin({
|
|
201
|
+
remote,
|
|
202
|
+
expose,
|
|
203
|
+
locale,
|
|
204
|
+
signal
|
|
205
|
+
}, deadlineAt);
|
|
206
|
+
const admitted = await admission.promise;
|
|
207
|
+
checkDeadline();
|
|
208
|
+
try {
|
|
209
|
+
const body = assertWorkerFederationBodySize(workerFederationWireJson(workerFederationEnvelope('request', admitted, JSON.parse(encodedPayload))));
|
|
210
|
+
checkDeadline();
|
|
211
|
+
const response = await fetchJson(service.fetch, new Request(admitted.endpoint, {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
headers: {
|
|
214
|
+
'content-type': 'application/json'
|
|
215
|
+
},
|
|
216
|
+
body,
|
|
217
|
+
redirect: 'manual',
|
|
218
|
+
signal
|
|
219
|
+
}), signal, checkDeadline);
|
|
220
|
+
checkDeadline();
|
|
221
|
+
const decodedPayload = readWorkerFederationEnvelope(response, 'response', admitted);
|
|
222
|
+
checkDeadline();
|
|
223
|
+
return {
|
|
224
|
+
admitted,
|
|
225
|
+
payload: decodedPayload
|
|
226
|
+
};
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (!signal.aborted && admissions.get(admission.key) === admission.pending) admissions.delete(admission.key);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}, signal, deadlineAt, false);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
export { createWorkerFederationTransport };
|
package/dist/types/adapter.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface RendererBuildProfile<TRenderer extends Renderer = Renderer> {
|
|
|
10
10
|
renderer: TRenderer;
|
|
11
11
|
status: 'stable' | 'preview';
|
|
12
12
|
protocolVersion: 1;
|
|
13
|
-
minimumNode: '26.
|
|
13
|
+
minimumNode: '26.12.0';
|
|
14
14
|
hmr: {
|
|
15
15
|
editedBoundary: 'may-reset';
|
|
16
16
|
unaffectedComponents: 'preserved';
|
|
@@ -40,6 +40,8 @@ export interface NativeClientStart<Bootstrap extends DocumentBootstrap, Router,
|
|
|
40
40
|
export interface NativeClientAdapter<Bootstrap extends DocumentBootstrap, Router extends object, Instance extends NativeI18nInstance, LocalisedUrls> {
|
|
41
41
|
readBootstrap(document: Document, identity: Readonly<RendererIdentity>): Bootstrap;
|
|
42
42
|
createRouter(application: NativeRoutedApplication, options: NativeRouterOptions): Router;
|
|
43
|
+
/** Release the router and browser history created for this entry. */
|
|
44
|
+
disposeRouter(router: Router): void;
|
|
43
45
|
/** Load a fresh router, or adopt the server's matches when hydrating. */
|
|
44
46
|
prepareRouter(router: Router, hydrating: boolean, signal: AbortSignal): Promise<void>;
|
|
45
47
|
/** Mount or hydrate the root; resolves its disposal. */
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
/** The Node dev runtime captures assets before releasing a container load. */
|
|
2
|
+
declare const LOAD_WITH_CAPTURE: unique symbol;
|
|
1
3
|
/** The application compiler's native Module Federation runtime. */
|
|
2
4
|
export interface FederationInstance {
|
|
3
5
|
/** The application's federation name, unique among this realm's instances. */
|
|
4
6
|
readonly name?: string;
|
|
5
7
|
loadRemote<T>(id: string): Promise<T | null>;
|
|
8
|
+
[LOAD_WITH_CAPTURE]?: <T, Captured>(id: string, capture: (module: T | null, resolution?: LoadResolution) => Captured) => Promise<{
|
|
9
|
+
module: T | null;
|
|
10
|
+
captured: Captured | undefined;
|
|
11
|
+
}>;
|
|
6
12
|
readonly remoteHandler?: {
|
|
7
13
|
readonly idToRemoteMap?: Record<string, {
|
|
8
14
|
name: string;
|
|
@@ -34,6 +40,10 @@ interface RemoteSnapshot {
|
|
|
34
40
|
};
|
|
35
41
|
}[];
|
|
36
42
|
}
|
|
43
|
+
interface LoadResolution {
|
|
44
|
+
readonly remoteSnapshot: unknown;
|
|
45
|
+
readonly expose: string;
|
|
46
|
+
}
|
|
37
47
|
/** Generated entries retain their own compiler's host, including on the server. */
|
|
38
48
|
export interface NativeFederationBinding {
|
|
39
49
|
readonly instance: () => FederationInstance;
|
|
@@ -50,7 +60,10 @@ export interface FederatedAssets {
|
|
|
50
60
|
/** Capture the host before importing application modules or remote containers. */
|
|
51
61
|
export declare function getFederationHost(binding: NativeFederationBinding | undefined): FederationInstance | undefined;
|
|
52
62
|
/** Load through the selected application's runtime, never another app's host. */
|
|
53
|
-
export declare function loadFederatedModule<T>(instance: FederationInstance | undefined, id: string): Promise<
|
|
63
|
+
export declare function loadFederatedModule<T>(instance: FederationInstance | undefined, id: string): Promise<{
|
|
64
|
+
module: T;
|
|
65
|
+
assets: FederatedAssets | undefined;
|
|
66
|
+
}>;
|
|
54
67
|
/** Browser assets from the native snapshot that admitted and loaded this remote. */
|
|
55
|
-
export declare function remoteBrowserAssets(instance: FederationInstance, id: string): FederatedAssets | undefined;
|
|
68
|
+
export declare function remoteBrowserAssets(instance: FederationInstance, id: string, resolution?: LoadResolution): FederatedAssets | undefined;
|
|
56
69
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DocumentAsset } from '../document.js';
|
|
2
2
|
import type { RendererIdentity } from '../identity.js';
|
|
3
3
|
import type { RequestPlatform, RequestSession } from '../session/index.js';
|
|
4
|
+
import type { WorkerFederationTransport } from '../worker-federation/index.js';
|
|
4
5
|
/** Native route matching remains with the selected router adapter. */
|
|
5
6
|
export interface NativeServerConfig {
|
|
6
7
|
readonly ssr?: boolean | 'stream';
|
|
@@ -15,6 +16,8 @@ export interface NativeRequestContext<Bindings extends object = object> {
|
|
|
15
16
|
readonly nativeManifest?: unknown;
|
|
16
17
|
readonly serverConfig?: NativeServerConfig;
|
|
17
18
|
readonly entry: Readonly<RendererIdentity>;
|
|
19
|
+
/** Request-owned service transport for native Worker snapshot boundaries. */
|
|
20
|
+
readonly workerFederation?: WorkerFederationTransport;
|
|
18
21
|
}
|
|
19
22
|
export type NativeRequestHandler<Bindings extends object = object> = (request: Request, context: NativeRequestContext<Bindings>) => Response | Promise<Response>;
|
|
20
23
|
/** Identity is generated with the bundle, never inferred from its filename. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type DocumentAsset } from '../document.js';
|
|
2
2
|
import { type RendererIdentity } from '../identity.js';
|
|
3
|
+
import type { WorkerFederationPublication, WorkerFederationTransport } from '../worker-federation/index.js';
|
|
3
4
|
import type { NativeExecutionContext, NativeServerConfig } from './types.js';
|
|
4
5
|
/** Build-validated per-entry inputs a worker bundle cannot read from disk. */
|
|
5
6
|
export interface NativeWorkerEntryResources {
|
|
@@ -11,6 +12,22 @@ export interface NativeWorkerEntryResources {
|
|
|
11
12
|
/** Present only when the entry admits a CSR fallback request. */
|
|
12
13
|
readonly csrFallbackHeader?: string;
|
|
13
14
|
readonly nonce?: string;
|
|
15
|
+
readonly workerFederation?: {
|
|
16
|
+
readonly renderer: unknown;
|
|
17
|
+
readonly worker: WorkerFederationPublication;
|
|
18
|
+
};
|
|
19
|
+
readonly workerFederationManifestUrl?: string;
|
|
20
|
+
readonly workerFederationServices?: readonly {
|
|
21
|
+
readonly remote: string;
|
|
22
|
+
readonly alias?: string;
|
|
23
|
+
readonly manifestUrl: string;
|
|
24
|
+
readonly binding: string;
|
|
25
|
+
readonly fragments: readonly {
|
|
26
|
+
readonly expose: string;
|
|
27
|
+
readonly boundaryId: string;
|
|
28
|
+
readonly paths: Readonly<Record<string, string>>;
|
|
29
|
+
}[];
|
|
30
|
+
}[];
|
|
14
31
|
}
|
|
15
32
|
export interface NativeWorkerDispatchOptions<Bindings extends object> {
|
|
16
33
|
readonly identity: RendererIdentity;
|
|
@@ -20,6 +37,8 @@ export interface NativeWorkerDispatchOptions<Bindings extends object> {
|
|
|
20
37
|
/** The module worker `env`, forwarded unchanged as the platform binding. */
|
|
21
38
|
readonly bindings: Bindings | undefined;
|
|
22
39
|
readonly executionContext?: NativeExecutionContext;
|
|
40
|
+
/** Constructed by this compiled renderer entry for this document only. */
|
|
41
|
+
readonly workerFederation?: WorkerFederationTransport;
|
|
23
42
|
}
|
|
24
43
|
/** Worker Fetch dispatch over the same native handler the Node host loads. */
|
|
25
44
|
export declare function dispatchNativeWorkerRequest<Bindings extends object>(request: Request, options: NativeWorkerDispatchOptions<Bindings>): Promise<Response>;
|