everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
187 lines (185 loc) • 7.33 kB
JavaScript
const require_config = require('./config.cjs');
const require_process_registry = require('./process-registry.cjs');
let node_fs = require("node:fs");
let node_path = require("node:path");
let effect = require("effect");
let node_net = require("node:net");
//#region src/app.ts
const PROBE_TIMEOUT_MS = 250;
const MAX_PORT_SCAN_STEPS = 1e3;
const PARALLEL_PROBE_WINDOW = 8;
const BLOCK_STEP = 100;
var PortAllocationError = class extends effect.Data.TaggedError("PortAllocationError") {};
var PortAllocator = class extends effect.Context.Service()("PortAllocator") {};
function detectLocalPackages(bosConfig, runtimeConfig, root = process.cwd()) {
const packages = [];
const configDir = root;
const uiLocalPath = runtimeConfig?.ui.localPath ?? require_config.resolveLocalDevelopmentPath(bosConfig?.app.ui.development, configDir);
if (uiLocalPath && (0, node_fs.existsSync)((0, node_path.join)(uiLocalPath, "package.json"))) packages.push({
name: "ui",
dir: uiLocalPath
});
const apiLocalPath = runtimeConfig?.api.localPath ?? require_config.resolveLocalDevelopmentPath(bosConfig?.app.api.development, configDir);
if (apiLocalPath && (0, node_fs.existsSync)((0, node_path.join)(apiLocalPath, "package.json"))) packages.push({
name: "api",
dir: apiLocalPath
});
const hostLocalPath = runtimeConfig?.host?.localPath ?? require_config.resolveLocalDevelopmentPath(bosConfig?.app.host.development, configDir);
if (hostLocalPath && (0, node_fs.existsSync)((0, node_path.join)(hostLocalPath, "package.json"))) packages.push({
name: "host",
dir: hostLocalPath
});
else if ((0, node_fs.existsSync)((0, node_path.join)(configDir, "host", "package.json"))) packages.push({
name: "host",
dir: (0, node_path.join)(configDir, "host")
});
for (const [pluginId, pluginConfig] of Object.entries(runtimeConfig?.plugins ?? {})) {
if (pluginConfig.localPath && (0, node_fs.existsSync)((0, node_path.join)(pluginConfig.localPath, "package.json"))) packages.push({
name: `plugin:${pluginId}`,
dir: pluginConfig.localPath
});
if (pluginConfig.ui?.localPath && (0, node_fs.existsSync)((0, node_path.join)(pluginConfig.ui.localPath, "package.json"))) packages.push({
name: `plugin-ui:${pluginId}`,
dir: pluginConfig.ui.localPath
});
}
const authLocalPath = runtimeConfig?.auth?.localPath ?? require_config.resolveLocalDevelopmentPath(bosConfig?.app.auth?.development, configDir);
if (authLocalPath && (0, node_fs.existsSync)((0, node_path.join)(authLocalPath, "package.json"))) packages.push({
name: "auth",
dir: authLocalPath
});
return packages;
}
const scheduleTimeout = (ms, fn) => setTimeout(fn, ms);
function probePortBindable(port) {
return effect.Effect.callback((resume) => {
const server = (0, node_net.createServer)();
server.once("listening", () => {
server.close(() => {
resume(effect.Effect.succeed(true));
});
});
server.once("error", () => {
server.removeAllListeners();
resume(effect.Effect.succeed(false));
});
server.listen(port, "127.0.0.1");
const timer = scheduleTimeout(PROBE_TIMEOUT_MS, () => {
server.removeAllListeners();
try {
server.close();
} catch {}
resume(effect.Effect.succeed(false));
});
server.once("listening", () => clearTimeout(timer));
server.once("error", () => clearTimeout(timer));
});
}
function pickAvailablePort(preferred, usedPorts, budget) {
return effect.Effect.gen(function* () {
const within = (candidate) => !budget || candidate >= budget.min && candidate <= budget.max;
let port = preferred;
if (!within(port)) port = budget ? budget.min : port;
const ceiling = budget ? budget.max + 1 : Number.MAX_SAFE_INTEGER;
let steps = 0;
const fail = () => effect.Effect.fail(new PortAllocationError({
preferred,
budget,
cause: budget ? `No free port in budget [${budget.min}, ${budget.max}] starting from ${preferred}` : `No free port found starting from ${preferred} within ${MAX_PORT_SCAN_STEPS} steps`
}));
while (true) {
if (port >= ceiling || steps > MAX_PORT_SCAN_STEPS) return yield* fail();
const candidates = [];
for (let i = 0; i < PARALLEL_PROBE_WINDOW && port + i < ceiling; i++) {
const candidate = port + i;
if (!usedPorts.has(candidate)) candidates.push(candidate);
}
if (candidates.length === 0) {
port += PARALLEL_PROBE_WINDOW;
steps += PARALLEL_PROBE_WINDOW;
continue;
}
const firstFree = (yield* effect.Effect.forEach(candidates, (c) => probePortBindable(c).pipe(effect.Effect.map((free) => ({
port: c,
free
}))), { concurrency: "unbounded" })).find((r) => r.free);
if (firstFree) {
usedPorts.add(firstFree.port);
return firstFree.port;
}
port += PARALLEL_PROBE_WINDOW;
steps += PARALLEL_PROBE_WINDOW;
}
});
}
const PortAllocatorLive = effect.Layer.sync(PortAllocator, () => {
const usedPorts = require_process_registry.claimedPorts();
return {
pickAvailable: (preferred, budget) => pickAvailablePort(preferred, usedPorts, budget),
acquireBlock: (request) => acquireBlockPort(request, usedPorts)
};
});
function acquireBlockPort(request, usedPorts) {
const step = request.step ?? BLOCK_STEP;
const fail = (cause) => effect.Effect.fail(new PortAllocationError({
preferred: request.base,
cause
}));
return effect.Effect.gen(function* () {
let base = request.base;
let steps = 0;
let firstConflicts = [];
while (steps <= MAX_PORT_SCAN_STEPS) {
const shift = base - request.base;
const candidates = request.entries.map((entry) => ({
...entry,
port: entry.pinned ? entry.preferred : entry.preferred + shift
}));
let pinnedHeldByListener;
for (const candidate of candidates.filter((c) => c.pinned)) {
if (!(yield* probePortBindable(candidate.port))) {
pinnedHeldByListener = candidate;
break;
}
usedPorts.delete(candidate.port);
}
if (pinnedHeldByListener) return yield* fail(`explicitly-requested port ${pinnedHeldByListener.port} (${pinnedHeldByListener.key}) is occupied by a listener`);
const unpinned = candidates.filter((c) => !c.pinned);
const claimConflicts = unpinned.filter((c) => usedPorts.has(c.port)).map((c) => ({
key: c.key,
port: c.port,
claimed: true
}));
if (shift === 0) firstConflicts = claimConflicts;
if (claimConflicts.length === 0) {
const busy = (yield* effect.Effect.forEach(unpinned, (c) => probePortBindable(c.port).pipe(effect.Effect.map((free) => ({
...c,
free
}))), { concurrency: "unbounded" })).filter((r) => !r.free);
if (shift === 0) firstConflicts = busy.map((b) => ({
key: b.key,
port: b.port,
claimed: false
}));
if (busy.length === 0) {
for (const candidate of candidates) usedPorts.add(candidate.port);
return {
base,
ports: Object.fromEntries(candidates.map((c) => [c.key, c.port])),
conflicts: firstConflicts
};
}
}
base += step;
steps += 1;
}
return yield* fail(`no free port block within ${MAX_PORT_SCAN_STEPS} steps of base ${request.base} (step ${step})`);
});
}
//#endregion
exports.PortAllocationError = PortAllocationError;
exports.PortAllocator = PortAllocator;
exports.PortAllocatorLive = PortAllocatorLive;
exports.detectLocalPackages = detectLocalPackages;
exports.probePortBindable = probePortBindable;
//# sourceMappingURL=app.cjs.map