@bleedingdev/modern-js-renderer-core 3.9.3-ultramodern.7 → 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/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,227 @@
|
|
|
1
|
+
import { identityCacheKey, readRendererIdentity } from "../identity.mjs";
|
|
2
|
+
const WORKER_FEDERATION_METADATA_KEY = 'ultramodernWorkerFederation';
|
|
3
|
+
const WORKER_FEDERATION_SCHEMA = 'ultramodern.worker-federation';
|
|
4
|
+
const WORKER_FEDERATION_VERSION = 1;
|
|
5
|
+
function workerFederationRecord(value, label) {
|
|
6
|
+
if (!value || 'object' != typeof value || ![
|
|
7
|
+
Object.prototype,
|
|
8
|
+
null
|
|
9
|
+
].includes(Object.getPrototypeOf(value))) throw new Error(`Worker federation ${label} must be a plain record.`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function exactFields(value, fields, label) {
|
|
13
|
+
if (Object.keys(value).length !== fields.length || Object.keys(value).some((key)=>!fields.includes(key))) throw new Error(`Worker federation ${label} has unsupported fields.`);
|
|
14
|
+
}
|
|
15
|
+
function requiredString(value, label) {
|
|
16
|
+
if ('string' != typeof value || 0 === value.length || value.trim() !== value || /[\u0000-\u001f\u007f]/u.test(value)) throw new Error(`Worker federation requires ${label}.`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function readWorkerFederationPath(value) {
|
|
20
|
+
const path = requiredString(value, 'an endpoint path');
|
|
21
|
+
let decoded;
|
|
22
|
+
try {
|
|
23
|
+
decoded = decodeURIComponent(path);
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error('Worker federation endpoint path has invalid encoding.');
|
|
26
|
+
}
|
|
27
|
+
if (!path.startsWith('/') || path.startsWith('//') || /[\\?#\s\u0000-\u001f\u007f]/u.test(decoded) || /%[0-9a-f]{2}/iu.test(decoded) || decoded.split('/').some((segment)=>'.' === segment || '..' === segment) || new URL(path, 'https://worker.invalid').pathname !== path) throw new Error('Worker federation endpoint must be an exact root path.');
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
function readWorkerFederationManifestUrl(value) {
|
|
31
|
+
const url = new URL(value);
|
|
32
|
+
if (![
|
|
33
|
+
'http:',
|
|
34
|
+
'https:'
|
|
35
|
+
].includes(url.protocol) || url.username || url.password || url.hash) throw new Error('Worker federation manifest requires an HTTP(S) publication URL.');
|
|
36
|
+
return url;
|
|
37
|
+
}
|
|
38
|
+
function readWorkerFederationPublication(value, identities) {
|
|
39
|
+
const record = workerFederationRecord(value, 'publication');
|
|
40
|
+
exactFields(record, [
|
|
41
|
+
'schema',
|
|
42
|
+
'schemaVersion',
|
|
43
|
+
'remote',
|
|
44
|
+
'exposes'
|
|
45
|
+
], 'publication');
|
|
46
|
+
if (record.schema !== WORKER_FEDERATION_SCHEMA || record.schemaVersion !== WORKER_FEDERATION_VERSION) throw new Error('Unsupported Worker federation publication protocol.');
|
|
47
|
+
const remote = requiredString(record.remote, 'a remote');
|
|
48
|
+
const entries = workerFederationRecord(record.exposes, 'exposes');
|
|
49
|
+
if (0 === Object.keys(entries).length) throw new Error('Worker federation publication has no exposes.');
|
|
50
|
+
const paths = new Set();
|
|
51
|
+
const exposes = Object.fromEntries(Object.entries(entries).map(([expose, raw])=>{
|
|
52
|
+
if (!/^\.\/[A-Za-z0-9_$@.-]+(?:\/[A-Za-z0-9_$@.-]+)*$/u.test(expose) || expose.slice(2).split('/').some((segment)=>'.' === segment || '..' === segment)) throw new Error(`Unsupported Worker federation expose ${expose}.`);
|
|
53
|
+
const coordinate = workerFederationRecord(raw, 'expose coordinate');
|
|
54
|
+
exactFields(coordinate, [
|
|
55
|
+
'entryName',
|
|
56
|
+
'boundaryId',
|
|
57
|
+
'paths',
|
|
58
|
+
'native'
|
|
59
|
+
], 'expose coordinate');
|
|
60
|
+
const entryName = requiredString(coordinate.entryName, 'an entry name');
|
|
61
|
+
if (!Object.hasOwn(identities, entryName)) throw new Error(`Worker federation expose ${expose} has no built entry.`);
|
|
62
|
+
const identity = identities[entryName];
|
|
63
|
+
identityCacheKey(identity);
|
|
64
|
+
if (identity.entryName !== entryName) throw new Error('Worker federation entry identity conflicts with its key.');
|
|
65
|
+
const rawPaths = workerFederationRecord(coordinate.paths, 'locale paths');
|
|
66
|
+
const native = workerFederationRecord(coordinate.native, 'native authority');
|
|
67
|
+
exactFields(native, [
|
|
68
|
+
'publisherBuildId',
|
|
69
|
+
'runtimeABI'
|
|
70
|
+
], 'native authority');
|
|
71
|
+
if (1 !== native.runtimeABI) throw new Error('Unsupported native Worker federation runtime ABI.');
|
|
72
|
+
if (0 === Object.keys(rawPaths).length) throw new Error('Worker federation expose has no locale endpoints.');
|
|
73
|
+
const locales = Object.keys(rawPaths);
|
|
74
|
+
if (new Set(locales.map((locale)=>locale.toLowerCase())).size !== locales.length || locales.includes('default') && 1 !== locales.length) throw new Error('Worker federation requires unique locales or only default.');
|
|
75
|
+
const localePaths = Object.fromEntries(Object.entries(rawPaths).map(([locale, rawPath])=>{
|
|
76
|
+
if (!/^[a-zA-Z0-9]+(?:[-_][a-zA-Z0-9]+)*$/u.test(locale)) throw new Error('Worker federation endpoint has an invalid locale.');
|
|
77
|
+
const path = readWorkerFederationPath(rawPath);
|
|
78
|
+
if (paths.has(path)) throw new Error('Worker federation exposes have conflicting endpoint paths.');
|
|
79
|
+
paths.add(path);
|
|
80
|
+
return [
|
|
81
|
+
locale,
|
|
82
|
+
path
|
|
83
|
+
];
|
|
84
|
+
}));
|
|
85
|
+
return [
|
|
86
|
+
expose,
|
|
87
|
+
Object.freeze({
|
|
88
|
+
entryName,
|
|
89
|
+
boundaryId: requiredString(coordinate.boundaryId, 'a boundary ID'),
|
|
90
|
+
paths: Object.freeze(localePaths),
|
|
91
|
+
native: Object.freeze({
|
|
92
|
+
publisherBuildId: requiredString(native.publisherBuildId, 'a native publisher build ID'),
|
|
93
|
+
runtimeABI: 1
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
];
|
|
97
|
+
}));
|
|
98
|
+
return Object.freeze({
|
|
99
|
+
schema: WORKER_FEDERATION_SCHEMA,
|
|
100
|
+
schemaVersion: WORKER_FEDERATION_VERSION,
|
|
101
|
+
remote,
|
|
102
|
+
exposes: Object.freeze(exposes)
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function workerFederationWireJson(value) {
|
|
106
|
+
const encode = (input)=>{
|
|
107
|
+
if (null === input || 'string' == typeof input || 'boolean' == typeof input) return input;
|
|
108
|
+
if ('number' == typeof input && Number.isFinite(input)) return input;
|
|
109
|
+
if (Array.isArray(input)) return input.map(encode);
|
|
110
|
+
const record = workerFederationRecord(input, 'wire value');
|
|
111
|
+
return Object.fromEntries(Object.keys(record).toSorted().map((key)=>[
|
|
112
|
+
key,
|
|
113
|
+
encode(record[key])
|
|
114
|
+
]));
|
|
115
|
+
};
|
|
116
|
+
return JSON.stringify(encode(value));
|
|
117
|
+
}
|
|
118
|
+
function freezeWireValue(value) {
|
|
119
|
+
const copy = JSON.parse(workerFederationWireJson(value));
|
|
120
|
+
const freeze = (input)=>{
|
|
121
|
+
if (input && 'object' == typeof input) {
|
|
122
|
+
Object.values(input).forEach(freeze);
|
|
123
|
+
Object.freeze(input);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
freeze(copy);
|
|
127
|
+
return copy;
|
|
128
|
+
}
|
|
129
|
+
function readWorkerFederationStyles(options) {
|
|
130
|
+
const manifest = workerFederationRecord(options.manifest, 'manifest');
|
|
131
|
+
const metadata = workerFederationRecord(manifest.metaData, 'manifest metadata');
|
|
132
|
+
const manifestUrl = readWorkerFederationManifestUrl(options.manifestUrl);
|
|
133
|
+
if (!Array.isArray(manifest.exposes)) throw new Error('Worker federation manifest exposes must be an array.');
|
|
134
|
+
const selected = manifest.exposes.map((value)=>workerFederationRecord(value, 'manifest expose')).filter((value)=>value.path === options.expose);
|
|
135
|
+
if (1 !== selected.length) throw new Error('Worker federation styles require one exact manifest expose.');
|
|
136
|
+
if (void 0 === selected[0].assets) return Object.freeze([]);
|
|
137
|
+
const assets = workerFederationRecord(selected[0].assets, 'expose assets');
|
|
138
|
+
if (void 0 === assets.css) return Object.freeze([]);
|
|
139
|
+
const css = workerFederationRecord(assets.css, 'expose CSS assets');
|
|
140
|
+
const files = [
|
|
141
|
+
'sync',
|
|
142
|
+
'async'
|
|
143
|
+
].flatMap((kind)=>{
|
|
144
|
+
const values = css[kind];
|
|
145
|
+
if (void 0 === values) return [];
|
|
146
|
+
if (!Array.isArray(values)) throw new Error('Worker federation CSS assets must be string arrays.');
|
|
147
|
+
return values.map((value)=>requiredString(value, 'a CSS asset URL'));
|
|
148
|
+
});
|
|
149
|
+
if (0 === files.length) return Object.freeze([]);
|
|
150
|
+
if (void 0 === metadata.publicPath || 'auto' === metadata.publicPath) throw new Error('Worker federation CSS requires a completed static publicPath; auto and getPublicPath cannot resolve styles in a Worker.');
|
|
151
|
+
const publicPath = requiredString(metadata.publicPath, 'a completed public path');
|
|
152
|
+
const explicitScheme = /^[a-z][a-z0-9+.-]*:/iu;
|
|
153
|
+
const isHttpScheme = /^https?:/iu;
|
|
154
|
+
const assertScheme = (value)=>{
|
|
155
|
+
if (explicitScheme.test(value) && !isHttpScheme.test(value)) throw new Error('Worker federation styles require an HTTP(S) publication URL.');
|
|
156
|
+
};
|
|
157
|
+
assertScheme(publicPath);
|
|
158
|
+
const publicUrl = new URL(publicPath, manifestUrl);
|
|
159
|
+
if (![
|
|
160
|
+
'http:',
|
|
161
|
+
'https:'
|
|
162
|
+
].includes(publicUrl.protocol) || publicUrl.username || publicUrl.password || publicUrl.hash) throw new Error('Worker federation styles require an HTTP(S) publication URL.');
|
|
163
|
+
const styles = files.map((file)=>{
|
|
164
|
+
assertScheme(file);
|
|
165
|
+
const resolved = /^(?:https?:)?\/\//iu.test(file) ? file : `${publicPath.replace(/\/$/u, '')}/${file.replace(/^\//u, '')}`;
|
|
166
|
+
const url = new URL(resolved, manifestUrl);
|
|
167
|
+
if (![
|
|
168
|
+
'http:',
|
|
169
|
+
'https:'
|
|
170
|
+
].includes(url.protocol) || url.username || url.password || url.hash) throw new Error('Worker federation styles require an HTTP(S) publication URL.');
|
|
171
|
+
return resolved;
|
|
172
|
+
});
|
|
173
|
+
return Object.freeze([
|
|
174
|
+
...new Set(styles)
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
function admitWorkerFederationExpose(options) {
|
|
178
|
+
const url = readWorkerFederationManifestUrl(options.manifestUrl);
|
|
179
|
+
const manifest = workerFederationRecord(options.manifest, 'manifest');
|
|
180
|
+
const metadata = workerFederationRecord(manifest.metaData, 'manifest metadata');
|
|
181
|
+
const renderer = freezeWireValue(options.admitRendererContract(metadata.ultramodernRenderer));
|
|
182
|
+
const worker = readWorkerFederationPublication(metadata[WORKER_FEDERATION_METADATA_KEY], renderer.identities);
|
|
183
|
+
if (worker.remote !== options.remote) throw new Error('Worker federation publication names another remote.');
|
|
184
|
+
if (!Object.hasOwn(worker.exposes, options.expose)) throw new Error(`Worker federation expose ${options.expose} is not published.`);
|
|
185
|
+
const coordinate = worker.exposes[options.expose];
|
|
186
|
+
if (!Object.hasOwn(coordinate.paths, options.locale)) throw new Error('Worker federation locale has no published endpoint.');
|
|
187
|
+
const path = coordinate.paths[options.locale];
|
|
188
|
+
const identity = readRendererIdentity(renderer.identities[coordinate.entryName], renderer.identities[coordinate.entryName]);
|
|
189
|
+
return Object.freeze({
|
|
190
|
+
authority: Object.freeze({
|
|
191
|
+
origin: url.origin,
|
|
192
|
+
renderer,
|
|
193
|
+
worker
|
|
194
|
+
}),
|
|
195
|
+
expose: options.expose,
|
|
196
|
+
locale: options.locale,
|
|
197
|
+
coordinate,
|
|
198
|
+
path,
|
|
199
|
+
identity,
|
|
200
|
+
endpoint: new URL(path, url.origin).href,
|
|
201
|
+
styles: Object.freeze([])
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function workerFederationEnvelope(kind, admitted, payload) {
|
|
205
|
+
return {
|
|
206
|
+
schema: `ultramodern.worker-federation-${kind}`,
|
|
207
|
+
schemaVersion: 1,
|
|
208
|
+
authority: admitted.authority,
|
|
209
|
+
expose: admitted.expose,
|
|
210
|
+
locale: admitted.locale,
|
|
211
|
+
payload
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function readWorkerFederationEnvelope(value, kind, admitted) {
|
|
215
|
+
const record = workerFederationRecord(value, `${kind} envelope`);
|
|
216
|
+
exactFields(record, [
|
|
217
|
+
'schema',
|
|
218
|
+
'schemaVersion',
|
|
219
|
+
'authority',
|
|
220
|
+
'expose',
|
|
221
|
+
'locale',
|
|
222
|
+
'payload'
|
|
223
|
+
], `${kind} envelope`);
|
|
224
|
+
if (record.schema !== `ultramodern.worker-federation-${kind}` || 1 !== record.schemaVersion || record.expose !== admitted.expose || record.locale !== admitted.locale || workerFederationWireJson(record.authority) !== workerFederationWireJson(admitted.authority)) throw new Error(`Worker federation ${kind} conflicts with the admitted publication.`);
|
|
225
|
+
return record.payload;
|
|
226
|
+
}
|
|
227
|
+
export { WORKER_FEDERATION_METADATA_KEY, WORKER_FEDERATION_SCHEMA, WORKER_FEDERATION_VERSION, admitWorkerFederationExpose, readWorkerFederationEnvelope, readWorkerFederationManifestUrl, readWorkerFederationPath, readWorkerFederationPublication, readWorkerFederationStyles, workerFederationEnvelope, workerFederationRecord, workerFederationWireJson };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { admitWorkerFederationExpose, readWorkerFederationEnvelope, readWorkerFederationPublication, workerFederationEnvelope, workerFederationRecord, workerFederationWireJson } from "./contract.mjs";
|
|
2
|
+
import { WorkerFederationBodyTooLargeError, assertWorkerFederationBodySize, checkWorkerFederationDeadline, readWorkerFederationJsonBody, workerAbortReason, workerAbortable, workerFederationErrorMessage } from "./lease.mjs";
|
|
3
|
+
function createWorkerFederationHandler(options) {
|
|
4
|
+
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.');
|
|
5
|
+
const timeout = options.requestTimeout ?? 3000;
|
|
6
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('Worker federation requires a positive request deadline.');
|
|
7
|
+
const renderer = options.admitRendererContract(options.publication.renderer);
|
|
8
|
+
const worker = readWorkerFederationPublication(options.publication.worker, renderer.identities);
|
|
9
|
+
const manifest = JSON.parse(workerFederationWireJson({
|
|
10
|
+
metaData: {
|
|
11
|
+
ultramodernRenderer: renderer,
|
|
12
|
+
ultramodernWorkerFederation: worker
|
|
13
|
+
}
|
|
14
|
+
}));
|
|
15
|
+
const loaders = workerFederationRecord(options.exposes, 'compiled exposes');
|
|
16
|
+
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.');
|
|
17
|
+
const pinnedLoaders = {
|
|
18
|
+
...options.exposes
|
|
19
|
+
};
|
|
20
|
+
const endpoints = new Map();
|
|
21
|
+
for (const [expose, coordinate] of Object.entries(worker.exposes))for (const locale of Object.keys(coordinate.paths)){
|
|
22
|
+
const admitted = admitWorkerFederationExpose({
|
|
23
|
+
manifest,
|
|
24
|
+
manifestUrl: options.manifestUrl,
|
|
25
|
+
remote: worker.remote,
|
|
26
|
+
expose,
|
|
27
|
+
locale,
|
|
28
|
+
admitRendererContract: options.admitRendererContract
|
|
29
|
+
});
|
|
30
|
+
endpoints.set(admitted.path, admitted);
|
|
31
|
+
}
|
|
32
|
+
const admitNativeRequest = options.admitNativeRequest;
|
|
33
|
+
const releaseNativeRequest = options.releaseNativeRequest;
|
|
34
|
+
const renderNativeSnapshot = options.renderNativeSnapshot;
|
|
35
|
+
return async (request)=>{
|
|
36
|
+
const deadlineAt = performance.now() + timeout;
|
|
37
|
+
const url = new URL(request.url);
|
|
38
|
+
const admitted = endpoints.get(url.pathname);
|
|
39
|
+
if (!admitted) return;
|
|
40
|
+
if ('POST' !== request.method || url.href !== admitted.endpoint || request.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json') {
|
|
41
|
+
request.body?.cancel().catch(()=>void 0);
|
|
42
|
+
return new Response('Worker federation endpoint coordinate conflicts with its publication.', {
|
|
43
|
+
status: 400
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (request.signal.aborted) {
|
|
47
|
+
request.body?.cancel().catch(()=>void 0);
|
|
48
|
+
throw workerAbortReason(request.signal);
|
|
49
|
+
}
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
const signal = AbortSignal.any([
|
|
52
|
+
request.signal,
|
|
53
|
+
controller.signal
|
|
54
|
+
]);
|
|
55
|
+
const checkDeadline = ()=>checkWorkerFederationDeadline(deadlineAt, signal, controller);
|
|
56
|
+
const execution = Object.freeze({
|
|
57
|
+
admitted,
|
|
58
|
+
signal
|
|
59
|
+
});
|
|
60
|
+
const deadline = setTimeout(()=>{
|
|
61
|
+
controller.abort(new Error('Worker federation request deadline exceeded.'));
|
|
62
|
+
}, Math.max(0, deadlineAt - performance.now()));
|
|
63
|
+
let executing = false;
|
|
64
|
+
let admittedRequest;
|
|
65
|
+
let release;
|
|
66
|
+
let response;
|
|
67
|
+
let failedRequest;
|
|
68
|
+
const releaseOnce = (value)=>{
|
|
69
|
+
if (!release) try {
|
|
70
|
+
release = Promise.resolve(releaseNativeRequest(value));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
release = Promise.reject(error);
|
|
73
|
+
}
|
|
74
|
+
return release ?? Promise.resolve();
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
if (!request.body) throw new Error('Worker federation request has no native payload.');
|
|
78
|
+
const envelope = await readWorkerFederationJsonBody(request.body, signal, request.headers.get('content-length'), checkDeadline);
|
|
79
|
+
checkDeadline();
|
|
80
|
+
const payload = readWorkerFederationEnvelope(envelope, 'request', admitted);
|
|
81
|
+
checkDeadline();
|
|
82
|
+
const preparing = Promise.resolve().then(()=>{
|
|
83
|
+
checkDeadline();
|
|
84
|
+
return admitNativeRequest(payload, execution);
|
|
85
|
+
});
|
|
86
|
+
preparing.then((value)=>{
|
|
87
|
+
if (signal.aborted) releaseOnce(value).catch(()=>void 0);
|
|
88
|
+
}, ()=>void 0);
|
|
89
|
+
const nativeRequest = await workerAbortable(preparing, signal);
|
|
90
|
+
admittedRequest = {
|
|
91
|
+
value: nativeRequest
|
|
92
|
+
};
|
|
93
|
+
checkDeadline();
|
|
94
|
+
executing = true;
|
|
95
|
+
const module = await workerAbortable(pinnedLoaders[admitted.expose](), signal);
|
|
96
|
+
checkDeadline();
|
|
97
|
+
const snapshot = await workerAbortable(Promise.resolve(renderNativeSnapshot(module, nativeRequest, execution)), signal);
|
|
98
|
+
checkDeadline();
|
|
99
|
+
if ('string' == typeof snapshot) assertWorkerFederationBodySize(snapshot);
|
|
100
|
+
const body = assertWorkerFederationBodySize(workerFederationWireJson(workerFederationEnvelope('response', admitted, snapshot)));
|
|
101
|
+
checkDeadline();
|
|
102
|
+
response = new Response(body, {
|
|
103
|
+
headers: {
|
|
104
|
+
'content-type': 'application/json'
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
let failure = error;
|
|
109
|
+
try {
|
|
110
|
+
checkDeadline();
|
|
111
|
+
} catch (deadlineError) {
|
|
112
|
+
failure = deadlineError;
|
|
113
|
+
}
|
|
114
|
+
failedRequest = {
|
|
115
|
+
error: failure
|
|
116
|
+
};
|
|
117
|
+
response = new Response(workerFederationErrorMessage(failure, 'Worker federation request failed.'), {
|
|
118
|
+
status: signal.aborted ? 504 : executing ? 502 : failure instanceof WorkerFederationBodyTooLargeError ? 413 : 400
|
|
119
|
+
});
|
|
120
|
+
} finally{
|
|
121
|
+
try {
|
|
122
|
+
if (admittedRequest) {
|
|
123
|
+
const releasing = releaseOnce(admittedRequest.value);
|
|
124
|
+
if (signal.aborted) releasing.catch(()=>void 0);
|
|
125
|
+
else try {
|
|
126
|
+
await workerAbortable(releasing, signal);
|
|
127
|
+
checkDeadline();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
response?.body?.cancel().catch(()=>void 0);
|
|
130
|
+
let releaseFailure = error;
|
|
131
|
+
try {
|
|
132
|
+
checkDeadline();
|
|
133
|
+
} catch (deadlineError) {
|
|
134
|
+
releaseFailure = new AggregateError([
|
|
135
|
+
error,
|
|
136
|
+
deadlineError
|
|
137
|
+
], `${workerFederationErrorMessage(error, 'Native request release failed.', 384)}; ${workerFederationErrorMessage(deadlineError, 'Worker federation deadline exceeded.', 384)}`);
|
|
138
|
+
}
|
|
139
|
+
const failure = failedRequest ? new AggregateError([
|
|
140
|
+
failedRequest.error,
|
|
141
|
+
releaseFailure
|
|
142
|
+
], `${workerFederationErrorMessage(failedRequest.error, 'Worker federation request failed.', 384)}; native request release failed: ${workerFederationErrorMessage(releaseFailure, 'Native request release failed.', 384)}`) : releaseFailure;
|
|
143
|
+
response = new Response(workerFederationErrorMessage(failure, 'Worker federation native request release failed.'), {
|
|
144
|
+
status: signal.aborted ? 504 : 502
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} finally{
|
|
149
|
+
clearTimeout(deadline);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (request.signal.aborted) {
|
|
153
|
+
response?.body?.cancel().catch(()=>void 0);
|
|
154
|
+
throw workerAbortReason(request.signal);
|
|
155
|
+
}
|
|
156
|
+
return response;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export { createWorkerFederationHandler };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { WORKER_FEDERATION_METADATA_KEY, WORKER_FEDERATION_SCHEMA, WORKER_FEDERATION_VERSION, readWorkerFederationManifestUrl, readWorkerFederationPath, readWorkerFederationPublication } from "./contract.mjs";
|
|
2
|
+
export { createWorkerFederationHandler } from "./handler.mjs";
|
|
3
|
+
export { createWorkerFederationTransport } from "./transport.mjs";
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
function checkWorkerFederationDeadline(deadlineAt, signal, controller) {
|
|
2
|
+
if (signal?.aborted) throw workerAbortReason(signal);
|
|
3
|
+
if (performance.now() >= deadlineAt) {
|
|
4
|
+
const error = new Error('Worker federation request deadline exceeded.');
|
|
5
|
+
controller?.abort(error);
|
|
6
|
+
throw error;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function workerFederationErrorMessage(error, fallback, maxLength = 1024) {
|
|
10
|
+
return error instanceof Error && error.message.length <= maxLength ? error.message : fallback;
|
|
11
|
+
}
|
|
12
|
+
function workerAbortReason(signal) {
|
|
13
|
+
return signal.reason ?? new DOMException('Worker federation request aborted.', 'AbortError');
|
|
14
|
+
}
|
|
15
|
+
function workerAbortable(operation, signal) {
|
|
16
|
+
if (signal.aborted) {
|
|
17
|
+
operation.catch(()=>void 0);
|
|
18
|
+
return Promise.reject(workerAbortReason(signal));
|
|
19
|
+
}
|
|
20
|
+
return new Promise((resolve, reject)=>{
|
|
21
|
+
const onAbort = ()=>reject(workerAbortReason(signal));
|
|
22
|
+
signal.addEventListener('abort', onAbort, {
|
|
23
|
+
once: true
|
|
24
|
+
});
|
|
25
|
+
operation.then(resolve, reject).finally(()=>signal.removeEventListener('abort', onAbort));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function readWorkerFederationJsonBody(body, signal, contentLength, checkDeadline) {
|
|
29
|
+
if (signal.aborted) {
|
|
30
|
+
body.cancel().catch(()=>void 0);
|
|
31
|
+
throw workerAbortReason(signal);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
checkDeadline?.();
|
|
35
|
+
if (null != contentLength) {
|
|
36
|
+
if (!/^\d+$/u.test(contentLength)) throw new Error('Worker federation body has an invalid Content-Length.');
|
|
37
|
+
if (Number(contentLength) > WORKER_FEDERATION_MAX_BODY_BYTES) throw new WorkerFederationBodyTooLargeError();
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
body.cancel(error).catch(()=>void 0);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
const reader = body.getReader();
|
|
44
|
+
const onAbort = ()=>{
|
|
45
|
+
reader.cancel(workerAbortReason(signal)).catch(()=>void 0);
|
|
46
|
+
};
|
|
47
|
+
signal.addEventListener('abort', onAbort, {
|
|
48
|
+
once: true
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
const chunks = [];
|
|
52
|
+
let length = 0;
|
|
53
|
+
while(true){
|
|
54
|
+
checkDeadline?.();
|
|
55
|
+
const chunk = await workerAbortable(reader.read(), signal);
|
|
56
|
+
checkDeadline?.();
|
|
57
|
+
if (chunk.done) break;
|
|
58
|
+
if (chunk.value.byteLength > WORKER_FEDERATION_MAX_BODY_BYTES - length) throw new WorkerFederationBodyTooLargeError();
|
|
59
|
+
chunks.push(new Uint8Array(chunk.value));
|
|
60
|
+
length += chunk.value.byteLength;
|
|
61
|
+
}
|
|
62
|
+
checkDeadline?.();
|
|
63
|
+
const bytes = new Uint8Array(length);
|
|
64
|
+
let offset = 0;
|
|
65
|
+
for (const chunk of chunks){
|
|
66
|
+
bytes.set(chunk, offset);
|
|
67
|
+
offset += chunk.byteLength;
|
|
68
|
+
}
|
|
69
|
+
if (signal.aborted) throw workerAbortReason(signal);
|
|
70
|
+
const decoded = new TextDecoder('utf-8', {
|
|
71
|
+
fatal: true
|
|
72
|
+
}).decode(bytes);
|
|
73
|
+
checkDeadline?.();
|
|
74
|
+
const result = JSON.parse(decoded);
|
|
75
|
+
checkDeadline?.();
|
|
76
|
+
return result;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
reader.cancel(error).catch(()=>void 0);
|
|
79
|
+
throw error;
|
|
80
|
+
} finally{
|
|
81
|
+
signal.removeEventListener('abort', onAbort);
|
|
82
|
+
reader.releaseLock();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const WORKER_FEDERATION_MAX_BODY_BYTES = 8388608;
|
|
86
|
+
class WorkerFederationBodyTooLargeError extends Error {
|
|
87
|
+
constructor(){
|
|
88
|
+
super('Worker federation body exceeds 8 MiB.');
|
|
89
|
+
this.name = 'WorkerFederationBodyTooLargeError';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function assertWorkerFederationBodySize(value) {
|
|
93
|
+
if (value.length > WORKER_FEDERATION_MAX_BODY_BYTES) throw new WorkerFederationBodyTooLargeError();
|
|
94
|
+
let bytes = 0;
|
|
95
|
+
for(let index = 0; index < value.length; index++){
|
|
96
|
+
const code = value.charCodeAt(index);
|
|
97
|
+
if (code <= 0x7f) bytes++;
|
|
98
|
+
else if (code <= 0x7ff) bytes += 2;
|
|
99
|
+
else if (code >= 0xd800 && code <= 0xdbff && value.charCodeAt(index + 1) >= 0xdc00 && value.charCodeAt(index + 1) <= 0xdfff) {
|
|
100
|
+
bytes += 4;
|
|
101
|
+
index++;
|
|
102
|
+
} else bytes += 3;
|
|
103
|
+
if (bytes > WORKER_FEDERATION_MAX_BODY_BYTES) throw new WorkerFederationBodyTooLargeError();
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
export { WORKER_FEDERATION_MAX_BODY_BYTES, WorkerFederationBodyTooLargeError, assertWorkerFederationBodySize, checkWorkerFederationDeadline, readWorkerFederationJsonBody, workerAbortReason, workerAbortable, workerFederationErrorMessage };
|