@module-federation/electron 0.1.0 → 0.2.0
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/CHANGELOG.md +23 -0
- package/README.md +177 -2
- package/dist/index.cjs +104 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -1
- package/dist/index.js.map +1 -0
- package/dist/remote.cjs +287 -0
- package/dist/remote.cjs.map +1 -0
- package/dist/remote.d.cts +35 -0
- package/dist/remote.d.cts.map +1 -0
- package/dist/remote.d.ts +35 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +227 -0
- package/dist/remote.js.map +1 -0
- package/dist/resolve.cjs +60 -0
- package/dist/resolve.cjs.map +1 -0
- package/dist/resolve.d.cts +7 -0
- package/dist/resolve.d.cts.map +1 -0
- package/dist/resolve.d.ts +7 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +12 -0
- package/dist/resolve.js.map +1 -0
- package/dist/rsbuild.cjs +89 -0
- package/dist/rsbuild.cjs.map +1 -0
- package/dist/rsbuild.d.cts +31 -0
- package/dist/rsbuild.d.cts.map +1 -0
- package/dist/rsbuild.d.ts +31 -0
- package/dist/rsbuild.d.ts.map +1 -0
- package/dist/rsbuild.js +30 -0
- package/dist/rsbuild.js.map +1 -0
- package/dist/url.cjs +67 -0
- package/dist/url.cjs.map +1 -0
- package/dist/url.d.cts +22 -0
- package/dist/url.d.cts.map +1 -0
- package/dist/url.d.ts +22 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +17 -0
- package/dist/url.js.map +1 -0
- package/dist/vite.cjs +84 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.cts +29 -0
- package/dist/vite.d.cts.map +1 -0
- package/dist/vite.d.ts +29 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +25 -0
- package/dist/vite.js.map +1 -0
- package/package.json +82 -23
package/dist/remote.cjs
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.n = (module)=>{
|
|
5
|
+
var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
|
|
6
|
+
__webpack_require__.d(getter, {
|
|
7
|
+
a: getter
|
|
8
|
+
});
|
|
9
|
+
return getter;
|
|
10
|
+
};
|
|
11
|
+
})();
|
|
12
|
+
(()=>{
|
|
13
|
+
__webpack_require__.d = (exports1, getters, values)=>{
|
|
14
|
+
var define = (defs, kind)=>{
|
|
15
|
+
for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
[kind]: defs[key]
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
define(getters, "get");
|
|
21
|
+
define(values, "value");
|
|
22
|
+
};
|
|
23
|
+
})();
|
|
24
|
+
(()=>{
|
|
25
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
26
|
+
})();
|
|
27
|
+
(()=>{
|
|
28
|
+
__webpack_require__.r = (exports1)=>{
|
|
29
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
30
|
+
value: 'Module'
|
|
31
|
+
});
|
|
32
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
33
|
+
value: true
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
var __webpack_exports__ = {};
|
|
38
|
+
__webpack_require__.r(__webpack_exports__);
|
|
39
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
40
|
+
CACHE_HEADER: ()=>CACHE_HEADER,
|
|
41
|
+
UNREACHABLE_FOR_MS: ()=>UNREACHABLE_FOR_MS,
|
|
42
|
+
createRemoteHandler: ()=>createRemoteHandler,
|
|
43
|
+
isRemoteLocation: ()=>isRemoteLocation,
|
|
44
|
+
resolveAppUrl: ()=>resolveAppUrl
|
|
45
|
+
});
|
|
46
|
+
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
47
|
+
const promises_namespaceObject = require("node:fs/promises");
|
|
48
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
49
|
+
var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
|
|
50
|
+
const CACHE_HEADER = 'x-mf-cache';
|
|
51
|
+
const UNREACHABLE_FOR_MS = 30000;
|
|
52
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
53
|
+
const DROPPED_HEADERS = new Set([
|
|
54
|
+
'connection',
|
|
55
|
+
'keep-alive',
|
|
56
|
+
'transfer-encoding',
|
|
57
|
+
'content-encoding',
|
|
58
|
+
'content-length',
|
|
59
|
+
'set-cookie'
|
|
60
|
+
]);
|
|
61
|
+
const FORWARDED_HEADERS = [
|
|
62
|
+
'accept',
|
|
63
|
+
'accept-language',
|
|
64
|
+
'content-type'
|
|
65
|
+
];
|
|
66
|
+
const NULL_BODY_STATUSES = new Set([
|
|
67
|
+
101,
|
|
68
|
+
103,
|
|
69
|
+
204,
|
|
70
|
+
205,
|
|
71
|
+
304
|
|
72
|
+
]);
|
|
73
|
+
function isRemoteLocation(location) {
|
|
74
|
+
return /^https?:\/\//i.test(location);
|
|
75
|
+
}
|
|
76
|
+
function resolveAppUrl(requestUrl, bases) {
|
|
77
|
+
const [, appId, ...rest] = requestUrl.pathname.split('/');
|
|
78
|
+
if (!appId || !Object.hasOwn(bases, appId)) return;
|
|
79
|
+
const base = new URL(bases[appId]);
|
|
80
|
+
base.search = '';
|
|
81
|
+
base.hash = '';
|
|
82
|
+
if (!base.pathname.endsWith('/')) base.pathname += '/';
|
|
83
|
+
const target = new URL(rest.join('/') + requestUrl.search, base);
|
|
84
|
+
if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) return;
|
|
85
|
+
return target;
|
|
86
|
+
}
|
|
87
|
+
class FetchFailure extends Error {
|
|
88
|
+
timedOut;
|
|
89
|
+
constructor(message, timedOut){
|
|
90
|
+
super(message), this.timedOut = timedOut;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function createRemoteHandler(options) {
|
|
94
|
+
const { fetch, cache } = options;
|
|
95
|
+
const log = options.log ?? ((message)=>console.warn(`[module-federation] ${message}`));
|
|
96
|
+
const now = options.now ?? Date.now;
|
|
97
|
+
if (!cache) return (request, upstream)=>fetch(upstream.href, upstreamInit(request));
|
|
98
|
+
const connectTimeout = cache.connectTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
99
|
+
const readTimeout = cache.readTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
100
|
+
const unreachableSince = new Map();
|
|
101
|
+
const isUnreachable = (origin)=>{
|
|
102
|
+
const since = unreachableSince.get(origin);
|
|
103
|
+
return void 0 !== since && now() - since < UNREACHABLE_FOR_MS;
|
|
104
|
+
};
|
|
105
|
+
return async (request, upstream)=>{
|
|
106
|
+
const cacheable = 'GET' === request.method;
|
|
107
|
+
const file = cacheFile(cache.dir, upstream);
|
|
108
|
+
const stale = async (reason)=>{
|
|
109
|
+
if (!cacheable) return;
|
|
110
|
+
const cached = await readCached(file);
|
|
111
|
+
if (cached) log(`${upstream.href} ${reason}, serving the cached copy`);
|
|
112
|
+
return cached && toResponse(cached, 'stale');
|
|
113
|
+
};
|
|
114
|
+
if (cacheable && isUnreachable(upstream.origin)) {
|
|
115
|
+
const cached = await readCached(file);
|
|
116
|
+
if (cached) return toResponse(cached, 'stale');
|
|
117
|
+
}
|
|
118
|
+
let fetched;
|
|
119
|
+
try {
|
|
120
|
+
fetched = await fetchFully(fetch, upstream, request, connectTimeout, readTimeout);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
unreachableSince.set(upstream.origin, now());
|
|
123
|
+
const failure = error instanceof FetchFailure ? error : new FetchFailure(String(error), false);
|
|
124
|
+
return await stale(`failed (${failure.message})`) ?? remote_text(failure.timedOut ? 504 : 502, `${upstream.href} failed (${failure.message}) and is not cached`);
|
|
125
|
+
}
|
|
126
|
+
unreachableSince.delete(upstream.origin);
|
|
127
|
+
if (fetched.status >= 200 && fetched.status < 300) {
|
|
128
|
+
if (cacheable) await writeCached(file, fetched).catch((error)=>log(`failed to cache ${upstream.href}: ${String(error)}`));
|
|
129
|
+
return toResponse(fetched);
|
|
130
|
+
}
|
|
131
|
+
return await stale(`answered ${fetched.status}`) ?? toResponse(fetched);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function upstreamInit(request) {
|
|
135
|
+
const headers = new Headers();
|
|
136
|
+
for (const name of FORWARDED_HEADERS){
|
|
137
|
+
const value = request.headers.get(name);
|
|
138
|
+
if (null !== value) headers.set(name, value);
|
|
139
|
+
}
|
|
140
|
+
const init = {
|
|
141
|
+
method: request.method,
|
|
142
|
+
headers
|
|
143
|
+
};
|
|
144
|
+
if (request.body && 'GET' !== request.method && 'HEAD' !== request.method) {
|
|
145
|
+
init.body = request.body;
|
|
146
|
+
init.duplex = 'half';
|
|
147
|
+
}
|
|
148
|
+
return init;
|
|
149
|
+
}
|
|
150
|
+
async function fetchFully(fetch, upstream, request, connectTimeout, readTimeout) {
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const response = await withTimeout(fetch(upstream.href, {
|
|
153
|
+
...upstreamInit(request),
|
|
154
|
+
signal: controller.signal
|
|
155
|
+
}), connectTimeout, controller, 'no response');
|
|
156
|
+
const chunks = [];
|
|
157
|
+
const reader = response.body?.getReader();
|
|
158
|
+
if (reader) try {
|
|
159
|
+
for(;;){
|
|
160
|
+
const { done, value } = await withTimeout(reader.read(), readTimeout, controller, 'body stalled');
|
|
161
|
+
if (done) break;
|
|
162
|
+
chunks.push(value);
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
reader.cancel().catch(()=>{});
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
status: response.status,
|
|
170
|
+
headers: [
|
|
171
|
+
...response.headers
|
|
172
|
+
].filter(([name])=>!DROPPED_HEADERS.has(name.toLowerCase())),
|
|
173
|
+
body: concat(chunks)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function withTimeout(promise, ms, controller, what) {
|
|
177
|
+
let timer;
|
|
178
|
+
const timeout = new Promise((_, reject)=>{
|
|
179
|
+
timer = setTimeout(()=>{
|
|
180
|
+
reject(new FetchFailure(`${what} after ${ms} ms`, true));
|
|
181
|
+
controller.abort();
|
|
182
|
+
}, ms);
|
|
183
|
+
});
|
|
184
|
+
return Promise.race([
|
|
185
|
+
promise,
|
|
186
|
+
timeout
|
|
187
|
+
]).catch((error)=>{
|
|
188
|
+
throw error instanceof FetchFailure ? error : new FetchFailure(errorMessage(error), false);
|
|
189
|
+
}).finally(()=>clearTimeout(timer));
|
|
190
|
+
}
|
|
191
|
+
function errorMessage(error) {
|
|
192
|
+
if (!(error instanceof Error)) return String(error);
|
|
193
|
+
return error.cause instanceof Error ? `${error.message}: ${error.cause.message}` : error.message;
|
|
194
|
+
}
|
|
195
|
+
function concat(chunks) {
|
|
196
|
+
const body = new Uint8Array(chunks.reduce((size, chunk)=>size + chunk.byteLength, 0));
|
|
197
|
+
let offset = 0;
|
|
198
|
+
for (const chunk of chunks){
|
|
199
|
+
body.set(chunk, offset);
|
|
200
|
+
offset += chunk.byteLength;
|
|
201
|
+
}
|
|
202
|
+
return body;
|
|
203
|
+
}
|
|
204
|
+
function sha1(value) {
|
|
205
|
+
return (0, external_node_crypto_namespaceObject.createHash)('sha1').update(value).digest('hex');
|
|
206
|
+
}
|
|
207
|
+
function cacheFile(dir, url) {
|
|
208
|
+
return external_node_path_default().join(dir, sha1(url.origin), sha1(url.pathname));
|
|
209
|
+
}
|
|
210
|
+
async function readCached(file) {
|
|
211
|
+
let data;
|
|
212
|
+
try {
|
|
213
|
+
data = await (0, promises_namespaceObject.readFile)(file);
|
|
214
|
+
} catch {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const newline = data.indexOf(0x0a);
|
|
218
|
+
if (newline < 0) return;
|
|
219
|
+
try {
|
|
220
|
+
const meta = JSON.parse(data.subarray(0, newline).toString('utf8'));
|
|
221
|
+
return {
|
|
222
|
+
status: meta.status,
|
|
223
|
+
headers: meta.headers,
|
|
224
|
+
body: data.subarray(newline + 1)
|
|
225
|
+
};
|
|
226
|
+
} catch {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
let tmpCounter = 0;
|
|
231
|
+
async function writeCached(file, fetched) {
|
|
232
|
+
await (0, promises_namespaceObject.mkdir)(external_node_path_default().dirname(file), {
|
|
233
|
+
recursive: true
|
|
234
|
+
});
|
|
235
|
+
const meta = JSON.stringify({
|
|
236
|
+
status: fetched.status,
|
|
237
|
+
headers: fetched.headers
|
|
238
|
+
});
|
|
239
|
+
const tmp = `${file}.tmp-${process.pid}-${tmpCounter++}`;
|
|
240
|
+
try {
|
|
241
|
+
await (0, promises_namespaceObject.writeFile)(tmp, Buffer.concat([
|
|
242
|
+
Buffer.from(`${meta}\n`),
|
|
243
|
+
fetched.body
|
|
244
|
+
]));
|
|
245
|
+
await (0, promises_namespaceObject.rename)(tmp, file);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
await (0, promises_namespaceObject.rm)(tmp, {
|
|
248
|
+
force: true
|
|
249
|
+
});
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function toResponse(fetched, fromCache) {
|
|
254
|
+
const headers = new Headers(fetched.headers);
|
|
255
|
+
if (fromCache) headers.set(CACHE_HEADER, fromCache);
|
|
256
|
+
const body = NULL_BODY_STATUSES.has(fetched.status) ? null : fetched.body;
|
|
257
|
+
return new Response(body, {
|
|
258
|
+
status: fetched.status,
|
|
259
|
+
headers
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function remote_text(status, message) {
|
|
263
|
+
return new Response(message, {
|
|
264
|
+
status,
|
|
265
|
+
headers: {
|
|
266
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
267
|
+
'cache-control': 'no-store'
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
exports.CACHE_HEADER = __webpack_exports__.CACHE_HEADER;
|
|
272
|
+
exports.UNREACHABLE_FOR_MS = __webpack_exports__.UNREACHABLE_FOR_MS;
|
|
273
|
+
exports.createRemoteHandler = __webpack_exports__.createRemoteHandler;
|
|
274
|
+
exports.isRemoteLocation = __webpack_exports__.isRemoteLocation;
|
|
275
|
+
exports.resolveAppUrl = __webpack_exports__.resolveAppUrl;
|
|
276
|
+
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
277
|
+
"CACHE_HEADER",
|
|
278
|
+
"UNREACHABLE_FOR_MS",
|
|
279
|
+
"createRemoteHandler",
|
|
280
|
+
"isRemoteLocation",
|
|
281
|
+
"resolveAppUrl"
|
|
282
|
+
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
283
|
+
Object.defineProperty(exports, '__esModule', {
|
|
284
|
+
value: true
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
//# sourceMappingURL=remote.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.cjs","sources":["webpack://webpack/runtime/compat_get_default_export","webpack://webpack/runtime/define_property_getters","webpack://webpack/runtime/has_own_property","webpack://webpack/runtime/make_namespace_object","../src/remote.ts"],"sourcesContent":["// getDefaultExport function for compatibility with non-ESM modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};\n","__webpack_require__.d = (exports, getters, values) => {\n\tvar define = (defs, kind) => {\n\t\tfor(var key in defs) {\n\t\t\tif(__webpack_require__.o(defs, key) && !__webpack_require__.o(exports, key)) {\n\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, [kind]: defs[key] });\n\t\t\t}\n\t\t}\n\t};\n\tdefine(getters, \"get\");\n\tdefine(values, \"value\");\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","/**\n * Serves apps deployed to an http(s) URL, with an on-disk cache that stands in for\n * the network when it fails. Must not import `electron`: the fetch function is\n * passed in, so this runs (and is tested) in plain Node.\n *\n * Cache layout, relative to the cache directory:\n *\n * ```text\n * <sha1(origin)>/<sha1(pathname)> one JSON line with status and headers, then the body\n * ```\n */\nimport { createHash } from 'node:crypto';\nimport { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\n\n/** Header added to responses served from the cache. */\nexport const CACHE_HEADER = 'x-mf-cache';\n\n/** How long an origin is skipped after a network failure, for requests that have a cached copy. */\nexport const UNREACHABLE_FOR_MS = 30_000;\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * Headers not replayed from the upstream response: they describe the original connection,\n * an encoding the fetch already undid, or cookies that do not belong to the `mf://` origin.\n */\nconst DROPPED_HEADERS = new Set([\n 'connection',\n 'keep-alive',\n 'transfer-encoding',\n 'content-encoding',\n 'content-length',\n 'set-cookie',\n]);\n\n/** Request headers sent upstream. Conditional headers are left out: a 304 has no body to cache. */\nconst FORWARDED_HEADERS = ['accept', 'accept-language', 'content-type'];\n\nconst NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);\n\nexport type FetchFunction = (url: string, init: RequestInit) => Promise<Response>;\n\nexport interface RemoteCacheOptions {\n /** Directory for cached responses. */\n dir: string;\n /** Milliseconds to wait for response headers. Default 10 000. */\n connectTimeout?: number;\n /** Milliseconds the body may stall between chunks. Not a limit on the total download. Default 10 000. */\n readTimeout?: number;\n}\n\nexport interface RemoteHandlerOptions {\n /** Performs the network request. In Electron, `net.fetch`. */\n fetch: FetchFunction;\n /** Offline cache settings, or `false` to proxy without caching or timeouts. */\n cache: RemoteCacheOptions | false;\n /** Receives fallbacks and cache write failures. Default `console.warn`. */\n log?: (message: string) => void;\n /** Clock, for tests. */\n now?: () => number;\n}\n\n/** Fetches `upstream` on behalf of `request`, which is the `mf://` request it stands for. */\nexport type RemoteHandler = (request: Request, upstream: URL) => Promise<Response>;\n\n/** Whether an `apps` entry is a deployment URL rather than a directory. */\nexport function isRemoteLocation(location: string): boolean {\n return /^https?:\\/\\//i.test(location);\n}\n\n/**\n * Maps a request URL (`<scheme>://<host>/<appId>/<path>?<query>`) to the matching URL\n * under the app's base URL. Returns `undefined` for unknown apps or for paths that would\n * leave the base URL.\n */\nexport function resolveAppUrl(\n requestUrl: URL,\n bases: Readonly<Record<string, string>>,\n): URL | undefined {\n const [, appId, ...rest] = requestUrl.pathname.split('/');\n if (!appId || !Object.hasOwn(bases, appId)) return undefined;\n\n const base = new URL(bases[appId] as string);\n base.search = '';\n base.hash = '';\n if (!base.pathname.endsWith('/')) base.pathname += '/';\n\n const target = new URL(rest.join('/') + requestUrl.search, base);\n if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) return undefined;\n return target;\n}\n\ninterface Fetched {\n status: number;\n headers: [string, string][];\n body: Uint8Array;\n}\n\nclass FetchFailure extends Error {\n constructor(\n message: string,\n readonly timedOut: boolean,\n ) {\n super(message);\n }\n}\n\nexport function createRemoteHandler(options: RemoteHandlerOptions): RemoteHandler {\n const { fetch, cache } = options;\n const log = options.log ?? ((message: string) => console.warn(`[module-federation] ${message}`));\n const now = options.now ?? Date.now;\n\n if (!cache) {\n return (request, upstream) => fetch(upstream.href, upstreamInit(request));\n }\n\n const connectTimeout = cache.connectTimeout ?? DEFAULT_TIMEOUT_MS;\n const readTimeout = cache.readTimeout ?? DEFAULT_TIMEOUT_MS;\n const unreachableSince = new Map<string, number>();\n const isUnreachable = (origin: string) => {\n const since = unreachableSince.get(origin);\n return since !== undefined && now() - since < UNREACHABLE_FOR_MS;\n };\n\n return async (request, upstream) => {\n const cacheable = request.method === 'GET';\n const file = cacheFile(cache.dir, upstream);\n const stale = async (reason: string) => {\n if (!cacheable) return undefined;\n const cached = await readCached(file);\n if (cached) log(`${upstream.href} ${reason}, serving the cached copy`);\n return cached && toResponse(cached, 'stale');\n };\n\n if (cacheable && isUnreachable(upstream.origin)) {\n const cached = await readCached(file);\n if (cached) return toResponse(cached, 'stale');\n }\n\n let fetched: Fetched;\n try {\n fetched = await fetchFully(fetch, upstream, request, connectTimeout, readTimeout);\n } catch (error) {\n unreachableSince.set(upstream.origin, now());\n const failure =\n error instanceof FetchFailure ? error : new FetchFailure(String(error), false);\n return (\n (await stale(`failed (${failure.message})`)) ??\n text(\n failure.timedOut ? 504 : 502,\n `${upstream.href} failed (${failure.message}) and is not cached`,\n )\n );\n }\n\n unreachableSince.delete(upstream.origin);\n if (fetched.status >= 200 && fetched.status < 300) {\n if (cacheable) {\n await writeCached(file, fetched).catch((error: unknown) =>\n log(`failed to cache ${upstream.href}: ${String(error)}`),\n );\n }\n return toResponse(fetched);\n }\n return (await stale(`answered ${fetched.status}`)) ?? toResponse(fetched);\n };\n}\n\nfunction upstreamInit(request: Request): RequestInit {\n const headers = new Headers();\n for (const name of FORWARDED_HEADERS) {\n const value = request.headers.get(name);\n if (value !== null) headers.set(name, value);\n }\n const init: RequestInit & { duplex?: 'half' } = { method: request.method, headers };\n if (request.body && request.method !== 'GET' && request.method !== 'HEAD') {\n init.body = request.body;\n init.duplex = 'half';\n }\n return init;\n}\n\n/**\n * Fetches the whole response. Throws a {@link FetchFailure} when the request fails, the\n * headers take longer than `connectTimeout`, the body stalls for `readTimeout`, or the\n * body fails halfway: a truncated response must not be cached as a success.\n */\nasync function fetchFully(\n fetch: FetchFunction,\n upstream: URL,\n request: Request,\n connectTimeout: number,\n readTimeout: number,\n): Promise<Fetched> {\n const controller = new AbortController();\n const response = await withTimeout(\n fetch(upstream.href, { ...upstreamInit(request), signal: controller.signal }),\n connectTimeout,\n controller,\n 'no response',\n );\n\n const chunks: Uint8Array[] = [];\n const reader = response.body?.getReader();\n if (reader) {\n try {\n for (;;) {\n const { done, value } = await withTimeout(\n reader.read(),\n readTimeout,\n controller,\n 'body stalled',\n );\n if (done) break;\n chunks.push(value);\n }\n } catch (error) {\n reader.cancel().catch(() => {});\n throw error;\n }\n }\n\n return {\n status: response.status,\n headers: [...response.headers].filter(([name]) => !DROPPED_HEADERS.has(name.toLowerCase())),\n body: concat(chunks),\n };\n}\n\nfunction withTimeout<T>(\n promise: Promise<T>,\n ms: number,\n controller: AbortController,\n what: string,\n): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n // Reject before aborting: the abort also rejects `promise`, and the race must\n // settle with the timeout.\n reject(new FetchFailure(`${what} after ${ms} ms`, true));\n controller.abort();\n }, ms);\n });\n return Promise.race([promise, timeout])\n .catch((error: unknown) => {\n throw error instanceof FetchFailure ? error : new FetchFailure(errorMessage(error), false);\n })\n .finally(() => clearTimeout(timer));\n}\n\nfunction errorMessage(error: unknown): string {\n if (!(error instanceof Error)) return String(error);\n // Node's fetch reports \"fetch failed\" and puts the reason in `cause`.\n return error.cause instanceof Error ? `${error.message}: ${error.cause.message}` : error.message;\n}\n\nfunction concat(chunks: Uint8Array[]): Uint8Array {\n const body = new Uint8Array(chunks.reduce((size, chunk) => size + chunk.byteLength, 0));\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return body;\n}\n\nfunction sha1(value: string): string {\n return createHash('sha1').update(value).digest('hex');\n}\n\n/** The query string is not part of the key, so cache-busting parameters still hit the cache. */\nfunction cacheFile(dir: string, url: URL): string {\n return path.join(dir, sha1(url.origin), sha1(url.pathname));\n}\n\nasync function readCached(file: string): Promise<Fetched | undefined> {\n let data: Buffer;\n try {\n data = await readFile(file);\n } catch {\n return undefined;\n }\n const newline = data.indexOf(0x0a);\n if (newline < 0) return undefined;\n try {\n const meta = JSON.parse(data.subarray(0, newline).toString('utf8')) as Omit<Fetched, 'body'>;\n return { status: meta.status, headers: meta.headers, body: data.subarray(newline + 1) };\n } catch {\n return undefined;\n }\n}\n\nlet tmpCounter = 0;\n\n/** Writes headers and body as one file through a rename, so a crash never leaves a mismatched pair. */\nasync function writeCached(file: string, fetched: Fetched): Promise<void> {\n await mkdir(path.dirname(file), { recursive: true });\n const meta = JSON.stringify({ status: fetched.status, headers: fetched.headers });\n const tmp = `${file}.tmp-${process.pid}-${tmpCounter++}`;\n try {\n await writeFile(tmp, Buffer.concat([Buffer.from(`${meta}\\n`), fetched.body]));\n await rename(tmp, file);\n } catch (error) {\n await rm(tmp, { force: true });\n throw error;\n }\n}\n\nfunction toResponse(fetched: Fetched, fromCache?: 'stale'): Response {\n const headers = new Headers(fetched.headers);\n if (fromCache) headers.set(CACHE_HEADER, fromCache);\n const body = NULL_BODY_STATUSES.has(fetched.status) ? null : fetched.body;\n return new Response(body, { status: fetched.status, headers });\n}\n\nfunction text(status: number, message: string): Response {\n return new Response(message, {\n status,\n headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' },\n });\n}\n"],"names":["__webpack_require__","module","getter","getters","values","define","defs","kind","key","Object","obj","prop","Symbol","CACHE_HEADER","UNREACHABLE_FOR_MS","DEFAULT_TIMEOUT_MS","DROPPED_HEADERS","Set","FORWARDED_HEADERS","NULL_BODY_STATUSES","isRemoteLocation","location","resolveAppUrl","requestUrl","bases","appId","rest","base","URL","target","FetchFailure","Error","message","timedOut","createRemoteHandler","options","fetch","cache","log","console","now","Date","request","upstream","upstreamInit","connectTimeout","readTimeout","unreachableSince","Map","isUnreachable","origin","since","undefined","cacheable","file","cacheFile","stale","reason","cached","readCached","toResponse","fetched","fetchFully","error","failure","String","text","writeCached","headers","Headers","name","value","init","controller","AbortController","response","withTimeout","chunks","reader","done","concat","promise","ms","what","timer","timeout","Promise","_","reject","setTimeout","errorMessage","clearTimeout","body","Uint8Array","size","chunk","offset","sha1","createHash","dir","url","path","data","readFile","newline","meta","JSON","tmpCounter","mkdir","tmp","process","writeFile","Buffer","rename","rm","fromCache","Response","status"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Header added to responses served from the cache. */
|
|
2
|
+
export declare const CACHE_HEADER = "x-mf-cache";
|
|
3
|
+
/** How long an origin is skipped after a network failure, for requests that have a cached copy. */
|
|
4
|
+
export declare const UNREACHABLE_FOR_MS = 30000;
|
|
5
|
+
export type FetchFunction = (url: string, init: RequestInit) => Promise<Response>;
|
|
6
|
+
export interface RemoteCacheOptions {
|
|
7
|
+
/** Directory for cached responses. */
|
|
8
|
+
dir: string;
|
|
9
|
+
/** Milliseconds to wait for response headers. Default 10 000. */
|
|
10
|
+
connectTimeout?: number;
|
|
11
|
+
/** Milliseconds the body may stall between chunks. Not a limit on the total download. Default 10 000. */
|
|
12
|
+
readTimeout?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RemoteHandlerOptions {
|
|
15
|
+
/** Performs the network request. In Electron, `net.fetch`. */
|
|
16
|
+
fetch: FetchFunction;
|
|
17
|
+
/** Offline cache settings, or `false` to proxy without caching or timeouts. */
|
|
18
|
+
cache: RemoteCacheOptions | false;
|
|
19
|
+
/** Receives fallbacks and cache write failures. Default `console.warn`. */
|
|
20
|
+
log?: (message: string) => void;
|
|
21
|
+
/** Clock, for tests. */
|
|
22
|
+
now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
/** Fetches `upstream` on behalf of `request`, which is the `mf://` request it stands for. */
|
|
25
|
+
export type RemoteHandler = (request: Request, upstream: URL) => Promise<Response>;
|
|
26
|
+
/** Whether an `apps` entry is a deployment URL rather than a directory. */
|
|
27
|
+
export declare function isRemoteLocation(location: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Maps a request URL (`<scheme>://<host>/<appId>/<path>?<query>`) to the matching URL
|
|
30
|
+
* under the app's base URL. Returns `undefined` for unknown apps or for paths that would
|
|
31
|
+
* leave the base URL.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveAppUrl(requestUrl: URL, bases: Readonly<Record<string, string>>): URL | undefined;
|
|
34
|
+
export declare function createRemoteHandler(options: RemoteHandlerOptions): RemoteHandler;
|
|
35
|
+
//# sourceMappingURL=remote.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.d.cts","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"AAeA,uDAAuD;AACvD,eAAO,MAAM,YAAY,eAAe,CAAC;AAEzC,mGAAmG;AACnG,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAsBzC,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElF,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yGAAyG;IACzG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,8DAA8D;IAC9D,KAAK,EAAE,aAAa,CAAC;IACrB,+EAA+E;IAC/E,KAAK,EAAE,kBAAkB,GAAG,KAAK,CAAC;IAClC,2EAA2E;IAC3E,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,6FAA6F;AAC7F,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEnF,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,GAAG,EACf,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACtC,GAAG,GAAG,SAAS,CAYjB;AAiBD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CA2DhF"}
|
package/dist/remote.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Header added to responses served from the cache. */
|
|
2
|
+
export declare const CACHE_HEADER = "x-mf-cache";
|
|
3
|
+
/** How long an origin is skipped after a network failure, for requests that have a cached copy. */
|
|
4
|
+
export declare const UNREACHABLE_FOR_MS = 30000;
|
|
5
|
+
export type FetchFunction = (url: string, init: RequestInit) => Promise<Response>;
|
|
6
|
+
export interface RemoteCacheOptions {
|
|
7
|
+
/** Directory for cached responses. */
|
|
8
|
+
dir: string;
|
|
9
|
+
/** Milliseconds to wait for response headers. Default 10 000. */
|
|
10
|
+
connectTimeout?: number;
|
|
11
|
+
/** Milliseconds the body may stall between chunks. Not a limit on the total download. Default 10 000. */
|
|
12
|
+
readTimeout?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RemoteHandlerOptions {
|
|
15
|
+
/** Performs the network request. In Electron, `net.fetch`. */
|
|
16
|
+
fetch: FetchFunction;
|
|
17
|
+
/** Offline cache settings, or `false` to proxy without caching or timeouts. */
|
|
18
|
+
cache: RemoteCacheOptions | false;
|
|
19
|
+
/** Receives fallbacks and cache write failures. Default `console.warn`. */
|
|
20
|
+
log?: (message: string) => void;
|
|
21
|
+
/** Clock, for tests. */
|
|
22
|
+
now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
/** Fetches `upstream` on behalf of `request`, which is the `mf://` request it stands for. */
|
|
25
|
+
export type RemoteHandler = (request: Request, upstream: URL) => Promise<Response>;
|
|
26
|
+
/** Whether an `apps` entry is a deployment URL rather than a directory. */
|
|
27
|
+
export declare function isRemoteLocation(location: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Maps a request URL (`<scheme>://<host>/<appId>/<path>?<query>`) to the matching URL
|
|
30
|
+
* under the app's base URL. Returns `undefined` for unknown apps or for paths that would
|
|
31
|
+
* leave the base URL.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveAppUrl(requestUrl: URL, bases: Readonly<Record<string, string>>): URL | undefined;
|
|
34
|
+
export declare function createRemoteHandler(options: RemoteHandlerOptions): RemoteHandler;
|
|
35
|
+
//# sourceMappingURL=remote.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.d.ts","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"AAeA,uDAAuD;AACvD,eAAO,MAAM,YAAY,eAAe,CAAC;AAEzC,mGAAmG;AACnG,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAsBzC,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElF,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yGAAyG;IACzG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,8DAA8D;IAC9D,KAAK,EAAE,aAAa,CAAC;IACrB,+EAA+E;IAC/E,KAAK,EAAE,kBAAkB,GAAG,KAAK,CAAC;IAClC,2EAA2E;IAC3E,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,6FAA6F;AAC7F,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEnF,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,GAAG,EACf,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACtC,GAAG,GAAG,SAAS,CAYjB;AAiBD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CA2DhF"}
|
package/dist/remote.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import node_path from "node:path";
|
|
4
|
+
const CACHE_HEADER = 'x-mf-cache';
|
|
5
|
+
const UNREACHABLE_FOR_MS = 30000;
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
7
|
+
const DROPPED_HEADERS = new Set([
|
|
8
|
+
'connection',
|
|
9
|
+
'keep-alive',
|
|
10
|
+
'transfer-encoding',
|
|
11
|
+
'content-encoding',
|
|
12
|
+
'content-length',
|
|
13
|
+
'set-cookie'
|
|
14
|
+
]);
|
|
15
|
+
const FORWARDED_HEADERS = [
|
|
16
|
+
'accept',
|
|
17
|
+
'accept-language',
|
|
18
|
+
'content-type'
|
|
19
|
+
];
|
|
20
|
+
const NULL_BODY_STATUSES = new Set([
|
|
21
|
+
101,
|
|
22
|
+
103,
|
|
23
|
+
204,
|
|
24
|
+
205,
|
|
25
|
+
304
|
|
26
|
+
]);
|
|
27
|
+
function isRemoteLocation(location) {
|
|
28
|
+
return /^https?:\/\//i.test(location);
|
|
29
|
+
}
|
|
30
|
+
function resolveAppUrl(requestUrl, bases) {
|
|
31
|
+
const [, appId, ...rest] = requestUrl.pathname.split('/');
|
|
32
|
+
if (!appId || !Object.hasOwn(bases, appId)) return;
|
|
33
|
+
const base = new URL(bases[appId]);
|
|
34
|
+
base.search = '';
|
|
35
|
+
base.hash = '';
|
|
36
|
+
if (!base.pathname.endsWith('/')) base.pathname += '/';
|
|
37
|
+
const target = new URL(rest.join('/') + requestUrl.search, base);
|
|
38
|
+
if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) return;
|
|
39
|
+
return target;
|
|
40
|
+
}
|
|
41
|
+
class FetchFailure extends Error {
|
|
42
|
+
timedOut;
|
|
43
|
+
constructor(message, timedOut){
|
|
44
|
+
super(message), this.timedOut = timedOut;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function createRemoteHandler(options) {
|
|
48
|
+
const { fetch, cache } = options;
|
|
49
|
+
const log = options.log ?? ((message)=>console.warn(`[module-federation] ${message}`));
|
|
50
|
+
const now = options.now ?? Date.now;
|
|
51
|
+
if (!cache) return (request, upstream)=>fetch(upstream.href, upstreamInit(request));
|
|
52
|
+
const connectTimeout = cache.connectTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
53
|
+
const readTimeout = cache.readTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
54
|
+
const unreachableSince = new Map();
|
|
55
|
+
const isUnreachable = (origin)=>{
|
|
56
|
+
const since = unreachableSince.get(origin);
|
|
57
|
+
return void 0 !== since && now() - since < UNREACHABLE_FOR_MS;
|
|
58
|
+
};
|
|
59
|
+
return async (request, upstream)=>{
|
|
60
|
+
const cacheable = 'GET' === request.method;
|
|
61
|
+
const file = cacheFile(cache.dir, upstream);
|
|
62
|
+
const stale = async (reason)=>{
|
|
63
|
+
if (!cacheable) return;
|
|
64
|
+
const cached = await readCached(file);
|
|
65
|
+
if (cached) log(`${upstream.href} ${reason}, serving the cached copy`);
|
|
66
|
+
return cached && toResponse(cached, 'stale');
|
|
67
|
+
};
|
|
68
|
+
if (cacheable && isUnreachable(upstream.origin)) {
|
|
69
|
+
const cached = await readCached(file);
|
|
70
|
+
if (cached) return toResponse(cached, 'stale');
|
|
71
|
+
}
|
|
72
|
+
let fetched;
|
|
73
|
+
try {
|
|
74
|
+
fetched = await fetchFully(fetch, upstream, request, connectTimeout, readTimeout);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
unreachableSince.set(upstream.origin, now());
|
|
77
|
+
const failure = error instanceof FetchFailure ? error : new FetchFailure(String(error), false);
|
|
78
|
+
return await stale(`failed (${failure.message})`) ?? remote_text(failure.timedOut ? 504 : 502, `${upstream.href} failed (${failure.message}) and is not cached`);
|
|
79
|
+
}
|
|
80
|
+
unreachableSince.delete(upstream.origin);
|
|
81
|
+
if (fetched.status >= 200 && fetched.status < 300) {
|
|
82
|
+
if (cacheable) await writeCached(file, fetched).catch((error)=>log(`failed to cache ${upstream.href}: ${String(error)}`));
|
|
83
|
+
return toResponse(fetched);
|
|
84
|
+
}
|
|
85
|
+
return await stale(`answered ${fetched.status}`) ?? toResponse(fetched);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function upstreamInit(request) {
|
|
89
|
+
const headers = new Headers();
|
|
90
|
+
for (const name of FORWARDED_HEADERS){
|
|
91
|
+
const value = request.headers.get(name);
|
|
92
|
+
if (null !== value) headers.set(name, value);
|
|
93
|
+
}
|
|
94
|
+
const init = {
|
|
95
|
+
method: request.method,
|
|
96
|
+
headers
|
|
97
|
+
};
|
|
98
|
+
if (request.body && 'GET' !== request.method && 'HEAD' !== request.method) {
|
|
99
|
+
init.body = request.body;
|
|
100
|
+
init.duplex = 'half';
|
|
101
|
+
}
|
|
102
|
+
return init;
|
|
103
|
+
}
|
|
104
|
+
async function fetchFully(fetch, upstream, request, connectTimeout, readTimeout) {
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const response = await withTimeout(fetch(upstream.href, {
|
|
107
|
+
...upstreamInit(request),
|
|
108
|
+
signal: controller.signal
|
|
109
|
+
}), connectTimeout, controller, 'no response');
|
|
110
|
+
const chunks = [];
|
|
111
|
+
const reader = response.body?.getReader();
|
|
112
|
+
if (reader) try {
|
|
113
|
+
for(;;){
|
|
114
|
+
const { done, value } = await withTimeout(reader.read(), readTimeout, controller, 'body stalled');
|
|
115
|
+
if (done) break;
|
|
116
|
+
chunks.push(value);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
reader.cancel().catch(()=>{});
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
status: response.status,
|
|
124
|
+
headers: [
|
|
125
|
+
...response.headers
|
|
126
|
+
].filter(([name])=>!DROPPED_HEADERS.has(name.toLowerCase())),
|
|
127
|
+
body: concat(chunks)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function withTimeout(promise, ms, controller, what) {
|
|
131
|
+
let timer;
|
|
132
|
+
const timeout = new Promise((_, reject)=>{
|
|
133
|
+
timer = setTimeout(()=>{
|
|
134
|
+
reject(new FetchFailure(`${what} after ${ms} ms`, true));
|
|
135
|
+
controller.abort();
|
|
136
|
+
}, ms);
|
|
137
|
+
});
|
|
138
|
+
return Promise.race([
|
|
139
|
+
promise,
|
|
140
|
+
timeout
|
|
141
|
+
]).catch((error)=>{
|
|
142
|
+
throw error instanceof FetchFailure ? error : new FetchFailure(errorMessage(error), false);
|
|
143
|
+
}).finally(()=>clearTimeout(timer));
|
|
144
|
+
}
|
|
145
|
+
function errorMessage(error) {
|
|
146
|
+
if (!(error instanceof Error)) return String(error);
|
|
147
|
+
return error.cause instanceof Error ? `${error.message}: ${error.cause.message}` : error.message;
|
|
148
|
+
}
|
|
149
|
+
function concat(chunks) {
|
|
150
|
+
const body = new Uint8Array(chunks.reduce((size, chunk)=>size + chunk.byteLength, 0));
|
|
151
|
+
let offset = 0;
|
|
152
|
+
for (const chunk of chunks){
|
|
153
|
+
body.set(chunk, offset);
|
|
154
|
+
offset += chunk.byteLength;
|
|
155
|
+
}
|
|
156
|
+
return body;
|
|
157
|
+
}
|
|
158
|
+
function sha1(value) {
|
|
159
|
+
return createHash('sha1').update(value).digest('hex');
|
|
160
|
+
}
|
|
161
|
+
function cacheFile(dir, url) {
|
|
162
|
+
return node_path.join(dir, sha1(url.origin), sha1(url.pathname));
|
|
163
|
+
}
|
|
164
|
+
async function readCached(file) {
|
|
165
|
+
let data;
|
|
166
|
+
try {
|
|
167
|
+
data = await readFile(file);
|
|
168
|
+
} catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const newline = data.indexOf(0x0a);
|
|
172
|
+
if (newline < 0) return;
|
|
173
|
+
try {
|
|
174
|
+
const meta = JSON.parse(data.subarray(0, newline).toString('utf8'));
|
|
175
|
+
return {
|
|
176
|
+
status: meta.status,
|
|
177
|
+
headers: meta.headers,
|
|
178
|
+
body: data.subarray(newline + 1)
|
|
179
|
+
};
|
|
180
|
+
} catch {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let tmpCounter = 0;
|
|
185
|
+
async function writeCached(file, fetched) {
|
|
186
|
+
await mkdir(node_path.dirname(file), {
|
|
187
|
+
recursive: true
|
|
188
|
+
});
|
|
189
|
+
const meta = JSON.stringify({
|
|
190
|
+
status: fetched.status,
|
|
191
|
+
headers: fetched.headers
|
|
192
|
+
});
|
|
193
|
+
const tmp = `${file}.tmp-${process.pid}-${tmpCounter++}`;
|
|
194
|
+
try {
|
|
195
|
+
await writeFile(tmp, Buffer.concat([
|
|
196
|
+
Buffer.from(`${meta}\n`),
|
|
197
|
+
fetched.body
|
|
198
|
+
]));
|
|
199
|
+
await rename(tmp, file);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
await rm(tmp, {
|
|
202
|
+
force: true
|
|
203
|
+
});
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function toResponse(fetched, fromCache) {
|
|
208
|
+
const headers = new Headers(fetched.headers);
|
|
209
|
+
if (fromCache) headers.set(CACHE_HEADER, fromCache);
|
|
210
|
+
const body = NULL_BODY_STATUSES.has(fetched.status) ? null : fetched.body;
|
|
211
|
+
return new Response(body, {
|
|
212
|
+
status: fetched.status,
|
|
213
|
+
headers
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function remote_text(status, message) {
|
|
217
|
+
return new Response(message, {
|
|
218
|
+
status,
|
|
219
|
+
headers: {
|
|
220
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
221
|
+
'cache-control': 'no-store'
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
export { CACHE_HEADER, UNREACHABLE_FOR_MS, createRemoteHandler, isRemoteLocation, resolveAppUrl };
|
|
226
|
+
|
|
227
|
+
//# sourceMappingURL=remote.js.map
|