UNPKG

@cordisjs/plugin-server

Version:
204 lines (200 loc) 6 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/index.ts import { Context, Service } from "cordis"; import { makeArray, remove, trimSlash } from "cosmokit"; import { createServer } from "node:http"; import { pathToRegexp } from "path-to-regexp"; import { koaBody } from "koa-body"; import parseUrl from "parseurl"; import { WebSocketServer } from "ws"; import Schema from "schemastery"; import KoaRouter from "@koa/router"; import Koa from "koa"; // src/listen.ts import net from "net"; function listen({ host, port, maxPort = port }) { const server = net.createServer(); return new Promise((resolve, reject) => { function onListen() { server.off("error", onError); server.close((err) => { err ? reject(err) : resolve(port); }); } __name(onListen, "onListen"); function onError(err) { server.off("listening", onListen); if (!(err.code === "EADDRINUSE" || err.code === "EACCES")) { return reject(err); } port++; if (port > maxPort) { return reject(new Error("No open ports available")); } testPort(); } __name(onError, "onError"); function testPort() { server.once("error", onError); server.once("listening", onListen); server.listen(port, host); } __name(testPort, "testPort"); testPort(); }); } __name(listen, "listen"); // src/index.ts var WebSocketLayer = class { constructor(server, path, callback) { this.server = server; this.callback = callback; this.regexp = pathToRegexp(path); } static { __name(this, "WebSocketLayer"); } clients = /* @__PURE__ */ new Set(); regexp; accept(socket, request) { if (!this.regexp.test(parseUrl(request).pathname)) return; this.clients.add(socket); socket.addEventListener("close", () => { this.clients.delete(socket); }); this.callback?.(socket, request); return true; } close() { remove(this.server.wsStack, this); for (const socket of this.clients) { socket.close(); } } }; var Server = class extends KoaRouter { constructor(ctx, config) { super(); this.ctx = ctx; this.config = config; ctx.runtime.name = "server"; ctx.provide("server"); ctx.alias("server", ["router"]); const body = koaBody({ multipart: true, jsonLimit: "10mb", formLimit: "10mb", textLimit: "10mb", includeUnparsed: true }); this._body = async (c, next) => { if (c[/* @__PURE__ */ Symbol.for("isBodyParsed")]) return next(); c[/* @__PURE__ */ Symbol.for("isBodyParsed")] = true; await body(c, next); }; this._koa.use(this.routes()); this._koa.use(this.allowedMethods()); this._http = createServer(this._koa.callback()); this._ws = new WebSocketServer({ server: this._http }); this._ws.on("connection", (socket, request) => { for (const manager of this.wsStack) { if (manager.accept(socket, request)) return; } socket.close(); }); ctx.decline(["selfUrl", "host", "port", "maxPort"]); if (config.selfUrl) { config.selfUrl = trimSlash(config.selfUrl); } ctx.on("ready", async () => { const { host = "127.0.0.1", port } = config; if (!port) return; this.host = host; this.port = await listen(config); this._http.listen(this.port, host); this.ctx.logger.info("server listening at %c", `http://${host}:${this.port}`); ctx.emit(this, "server/ready"); }, true); ctx.on("dispose", () => { if (config.port) { this.ctx.logger.info("server closing"); } this._ws?.close(); this._http?.close(); }); const self = Context.associate(this, "server"); ctx.set("server", self); ctx.on("internal/listener", function(name, listener) { if (name !== "server/ready" || !self[Context.filter](this) || !self.port) return; this.scope.ensure(async () => listener()); return () => false; }); return self; } static { __name(this, "Server"); } [Service.tracker] = { associate: "server", property: "ctx" }; _http; _ws; wsStack = []; _koa = new Koa(); _body; host; port; [Context.filter](ctx) { return ctx[Context.isolate].server === this.ctx[Context.isolate].server; } get selfUrl() { const wildcard = ["0.0.0.0", "::"]; const host = wildcard.includes(this.host) ? "127.0.0.1" : this.host; if (this.port === 80) { return `http://${host}`; } else if (this.port === 443) { return `https://${host}`; } else { return `http://${host}:${this.port}`; } } /** * hack into router methods to make sure that koa middlewares are disposable */ register(...args) { args[2] = makeArray(args[2]); if (!args[2][0][/* @__PURE__ */ Symbol.for("noParseBody")]) { args[2].unshift(this._body); } const layer = super.register(...args); this.ctx.scope.disposables.push(() => { remove(this.stack, layer); }); return layer; } ws(path, callback) { const layer = new WebSocketLayer(this, path, callback); this.wsStack.push(layer); this.ctx.scope.disposables.push(() => layer.close()); return layer; } }; ((Server2) => { Server2.Config = Schema.object({ host: Schema.string().default("127.0.0.1").description("要监听的 IP 地址。如果将此设置为 `0.0.0.0` 将监听所有地址,包括局域网和公网地址。"), port: Schema.natural().max(65535).description("要监听的初始端口号。"), maxPort: Schema.natural().max(65535).description("允许监听的最大端口号。"), selfUrl: Schema.string().role("link").description("应用暴露在公网的地址。") }); })(Server || (Server = {})); var index_default = Server; export { Server, WebSocketLayer, index_default as default }; //# sourceMappingURL=index.mjs.map