UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

304 lines (302 loc) • 14.3 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const require_runtime = require('../../_virtual/_rolldown/runtime.cjs'); const require_http_client = require('../../utils/http-client.cjs'); const require_load = require('./load.cjs'); const require_services_registry_bundle_cache = require('../registry/bundle-cache.cjs'); let node_fs = require("node:fs"); let node_path = require("node:path"); node_path = require_runtime.__toESM(node_path, 1); let effect = require("effect"); let every_plugin_build_artifact_names = require("every-plugin/build/artifact-names"); let node_fs_promises = require("node:fs/promises"); //#region src/services/resolution/resolver.ts var BundleReadError = class extends effect.Schema.TaggedError()("BundleReadError", { path: effect.Schema.String, cause: effect.Schema.optional(effect.Schema.Unknown) }) {}; const MIME_TYPES = { ".js": "text/javascript", ".mjs": "text/javascript", ".cjs": "text/javascript", ".css": "text/css", ".json": "application/json", ".map": "application/json", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".avif": "image/avif", ".ico": "image/x-icon", ".html": "text/html", ".htm": "text/html", ".webmanifest": "application/manifest+json", ".md": "text/markdown", ".ts": "text/plain", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".otf": "font/otf", ".eot": "application/vnd.ms-opentype", ".wasm": "application/wasm", ".txt": "text/xml", ".xml": "application/xml" }; /** * Maps a bundle URL onto the staged namespace layout. Bundle URLs carry their * namespace in the path (`…/bundles/<account>/<gateway>/<workspace>/…`); the * staged layout mirrors it under `<bundleDir>/<account>/<gateway>/…`. The * namespace is taken from the URL itself — this answers "which staged * directory do these bytes live in?" rather than "is this my namespace?" — * and the result is contained within the account/gateway directory. Returns * null for foreign shapes. */ function bundleUrlToStagedPath(url, bundleDir) { let parsed; try { parsed = new URL(url); } catch { return null; } if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null; const segments = parsed.pathname.split("/").filter(Boolean); if (segments[0] !== "bundles" || segments.length < 3) return null; let decoded; try { decoded = segments.slice(1).map((segment) => decodeURIComponent(segment)); } catch { return null; } const [account, gateway, ...rest] = decoded; if (!account || !gateway || rest.length === 0) return null; const nsDir = node_path.default.resolve(bundleDir, account, gateway); const filePath = node_path.default.resolve(nsDir, ...rest); if (filePath === nsDir || !filePath.startsWith(nsDir + node_path.default.sep)) return null; return filePath; } /** Resolve a relative config-supplied path inside `dir`, or null on escape. */ function resolveContained(dir, relative) { const resolved = node_path.default.resolve(dir, ...relative.split("/").filter(Boolean)); if (resolved === dir || !resolved.startsWith(dir + node_path.default.sep)) return null; return resolved; } /** * Own-namespace path for a bundle URL: the shared URL→staged-path mapping, * narrowed to the runtime identity — a URL whose encoded namespace differs * from `namespace.account`/`namespace.gateway` must fall to the network, * never serve a sibling namespace's staged bytes. */ function bundleUrlToLocalPath(url, namespace) { const filePath = bundleUrlToStagedPath(url, namespace.bundleDir); if (!filePath) return null; const nsDir = node_path.default.resolve(namespace.bundleDir, namespace.account, namespace.gateway); if (filePath !== nsDir && !filePath.startsWith(nsDir + node_path.default.sep)) return null; return filePath; } var BundleResolver = class BundleResolver extends effect.Context.Service()("everything-dev/resolver/BundleResolver") { static layer(namespace) { if (!namespace) return effect.Layer.succeed(BundleResolver, BundleResolver.of({ lookup: () => effect.Effect.succeed(null) })); const respond = effect.Effect.fn("BundleResolver.respond")(function* (url) { const filePath = bundleUrlToLocalPath(url, namespace); if (!filePath) return null; const bytes = yield* effect.Effect.tryPromise({ try: () => (0, node_fs_promises.readFile)(filePath), catch: (cause) => new BundleReadError({ path: filePath, cause }) }).pipe(effect.Effect.catchTag("BundleReadError", () => effect.Effect.succeed(null))); if (bytes === null) return null; const name = node_path.default.basename(filePath); const contentType = MIME_TYPES[node_path.default.extname(name).toLowerCase()] ?? "application/octet-stream"; return new Response(new Uint8Array(bytes), { headers: { "content-type": contentType, "cache-control": (0, every_plugin_build_artifact_names.cacheControlOf)(name), etag: `"${Buffer.from(bytes.subarray(0, 4096)).toString("base64").slice(0, 24)}"` } }); }); return effect.Layer.effect(BundleResolver, effect.Effect.succeed(BundleResolver.of({ lookup: (input) => { const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; return respond(url); } }))); } }; const staleResponse = (bytes, url) => { const name = url.split("?")[0]?.split("/").pop() ?? ""; const contentType = MIME_TYPES[node_path.default.extname(name).toLowerCase()] ?? "application/octet-stream"; return new Response(bytes.slice().buffer, { status: 200, headers: { "content-type": contentType, "x-bundle-cache": "stale" } }); }; const DEFAULT_ATTEMPT_TIMEOUT = "30 seconds"; const RETRY_BASE_DELAY = "250 millis"; const RETRY_CAP = "5 seconds"; const RETRY_TIMES = 4; const isTransientStatus = (status) => status === 429 || status >= 500; function requestMethod(input, init) { if (init?.method) return init.method.toUpperCase(); if (typeof input === "string" || input instanceof URL) return "GET"; return input.method.toUpperCase(); } const asError = (cause) => cause instanceof Error ? cause : new Error(String(cause)); /** A 429/5xx response — carried as a failure so the Schedule can retry it, * with the Response kept for the exhausted-retries fallthrough. */ var TransientBundleResponse = class extends effect.Data.TaggedError("TransientBundleResponse") {}; /** * One network attempt through the `HttpFetch` policy: the fetch runs under * the fiber's interruption signal, and `Effect.timeout` interrupts at the * deadline — aborting the in-flight request. The response is returned * regardless of status; transient statuses are lifted by the caller. */ const fetchAttempt = effect.Effect.fn("resolver.attempt")(function* (http, input, init, timeout) { return yield* http.attempt(input, init).pipe(effect.Effect.timeoutOrElse({ duration: effect.Duration.toMillis(timeout), orElse: () => effect.Effect.fail(new effect.Cause.TimeoutError()) }), effect.Effect.filterOrFail((response) => !isTransientStatus(response.status), (response) => new TransientBundleResponse({ response }))); }); /** * The resilience policy for idempotent GETs crossing the interceptor * (ADR 0021 amendment — the CDN-only image): a cold boot has no staged * fallback, so one edge hiccup or a hung connection during the load window * must not hard-fail the host or a plugin. Transient statuses and network * errors (including the deadline's TimeoutError) retry on a capped * exponential schedule before propagating; the last transient response is * returned so an explicit stale-if-error cache still sees it. Non-GETs pass * through untouched; a caller-initiated cancellation is never spent on the * schedule. */ const resilientFetch = effect.Effect.fn("resolver.fetch")(function* (http, input, init, timeout) { if (requestMethod(input, init) !== "GET") return yield* http.attempt(input, init).pipe(effect.Effect.mapError((cause) => asError(cause.cause))); return yield* fetchAttempt(http, input, init, timeout).pipe(effect.Effect.retry({ schedule: effect.Schedule.exponential(RETRY_BASE_DELAY).pipe(effect.Schedule.upTo({ duration: RETRY_CAP, times: RETRY_TIMES })), while: (error) => !((error instanceof require_http_client.FetchNetworkError || error instanceof effect.Cause.TimeoutError) && init?.signal?.aborted) }), effect.Effect.catchTags({ TransientBundleResponse: (error) => effect.Effect.succeed(error.response), FetchNetworkError: (error) => effect.Effect.fail(asError(error.cause)) })); }); /** * Installs the resolver as a global fetch interceptor — the one seam every * boot-time consumer shares (config manifest discovery, contract-type * fetches, orchestrator host loading, MF remoteEntry/identity probes) and * the SSR serving path. One layered resolution policy, as a single Effect * program on the managed runtime: staged disk → network → (explicitly * opted-in) stale-if-error. Own-namespace URLs resolve from the staged * directory and fall through to the network on a miss; a miss never touches * the write-through cache — for the own namespace the staged disk IS the * resilience artifact. Inert when neither a staged namespace nor an * explicit cache is configured. */ function installGlobalBundleFetch(options) { const namespace = options.namespace; const cacheDir = options.cacheDir; const timeout = effect.Duration.millis(options.timeoutMs ?? effect.Duration.toMillis(DEFAULT_ATTEMPT_TIMEOUT)); const original = globalThis.fetch; const runtime = effect.ManagedRuntime.make(effect.Layer.mergeAll(BundleResolver.layer(namespace), require_http_client.HttpFetch.layerWith({ fetchImpl: original }))); const program = effect.Effect.fn("resolver.program")(function* (input, init) { const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; const http = yield* require_http_client.HttpFetch; if (namespace && bundleUrlToLocalPath(url, namespace)) { const local = yield* (yield* BundleResolver).lookup(input); if (local !== null) return local; return yield* resilientFetch(http, input, init, timeout); } if (cacheDir !== void 0 && require_services_registry_bundle_cache.bundleCachePath(url, { cacheDir })) { const cachedRead = effect.Effect.tryPromise(() => require_services_registry_bundle_cache.readBundleCache(url, { cacheDir })); return yield* effect.Effect.gen(function* () { const response = yield* resilientFetch(http, input, init, timeout); if (response.ok) yield* effect.Effect.tryPromise(async () => { const bytes = new Uint8Array(await response.clone().arrayBuffer()); await require_services_registry_bundle_cache.writeBundleCache(url, bytes, { cacheDir }); }).pipe(effect.Effect.ignore); else { const cached = yield* cachedRead; if (cached) return staleResponse(cached, url); } return response; }).pipe(effect.Effect.catch((error) => effect.Effect.gen(function* () { const cached = yield* cachedRead; if (cached) return staleResponse(cached, url); return yield* effect.Effect.fail(error); }))); } return yield* resilientFetch(http, input, init, timeout); }); const patched = ((input, init) => runtime.runPromise(program(input, init))); const originalPreconnect = original.preconnect; if (originalPreconnect) patched.preconnect = originalPreconnect.bind(original); globalThis.fetch = patched; return { uninstall: async () => { if (globalThis.fetch === patched) globalThis.fetch = original; await runtime.dispose(); } }; } /** * A deployment env that accidentally carries an empty or quoted-empty * `BOS_BUNDLE_DIR` (e.g. the literal two characters `""` from a quoting * mistake) normalizes to unset — silently, no warning. */ const normalizeBundleDir = (raw) => { const trimmed = raw?.trim(); if (!trimmed) return void 0; const unquoted = trimmed.length >= 2 && (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) ? trimmed.slice(1, -1).trim() : trimmed; return unquoted.length > 0 ? unquoted : void 0; }; /** * Boot-time installer for the CLI and the host server: derives the own * namespace from `BOS_BUNDLE_DIR` plus the runtime identity * (`BOS_ACCOUNT`/`BOS_GATEWAY` registry env taking precedence over the * bos.config.json fields), and the stale-if-error cache from an explicit * `BOS_BUNDLE_CACHE_DIR` — never implicit. Tier auto-detection (ADR 0021): * an identity whose namespace is not actually staged under the bundle dir * drops to the registry tier — plain resilient network fetch — instead of * deterministically 404ing own-namespace URLs against a foreign image's * staged bytes. Never throws — a missing or malformed identity leaves the * default fetch in place. */ function installBundleFetchFromEnv(input) { const bundleDir = normalizeBundleDir(process.env.BOS_BUNDLE_DIR); const cacheDir = process.env.BOS_BUNDLE_CACHE_DIR; if (!bundleDir && cacheDir === void 0) return null; let configAccount; let configGateway; const configPath = input.configPath; if (configPath && !require_load.isAppDescriptorPath(configPath)) try { const parsed = JSON.parse((0, node_fs.readFileSync)(configPath, "utf8")); configAccount = parsed.account; configGateway = parsed.domain; } catch {} const account = process.env.BOS_ACCOUNT ?? configAccount; const gateway = process.env.BOS_GATEWAY ?? configGateway; if (bundleDir && (!account || !gateway)) return null; let namespace; if (bundleDir && account && gateway) { if ((0, node_fs.existsSync)(node_path.default.join(bundleDir, account, gateway))) namespace = { bundleDir, account, gateway }; else console.warn(`[bundles] staged namespace ${account}/${gateway} not found under ${JSON.stringify(bundleDir)} — registry tier (network fetch)`); } return installGlobalBundleFetch({ namespace, cacheDir }); } //#endregion exports.BundleReadError = BundleReadError; exports.BundleResolver = BundleResolver; exports.bundleUrlToLocalPath = bundleUrlToLocalPath; exports.bundleUrlToStagedPath = bundleUrlToStagedPath; exports.installBundleFetchFromEnv = installBundleFetchFromEnv; exports.installGlobalBundleFetch = installGlobalBundleFetch; exports.resolveContained = resolveContained; //# sourceMappingURL=resolver.cjs.map