diff --git a/server/package.json b/server/package.json index 5e551f3..02d6221 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@ty-ras/server-node", - "version": "2.1.1", + "version": "2.2.0", "author": { "name": "Stanislav Muhametsin", "email": "346799+stazz@users.noreply.github.com", @@ -31,7 +31,7 @@ } }, "dependencies": { - "@ty-ras/server": "^2.1.1" + "@ty-ras/server": "^2.2.0" }, "devDependencies": { "@ava/get-port": "2.0.0", diff --git a/server/src/__test__/listen.spec.ts b/server/src/__test__/listen.spec.ts index 6a8837c..2cd25b1 100644 --- a/server/src/__test__/listen.spec.ts +++ b/server/src/__test__/listen.spec.ts @@ -8,14 +8,25 @@ import getPort from "@ava/get-port"; import * as spec from "../listen"; import * as server from "../server"; -test("Validate that listening to server does not crash", async (c) => { - // Not much else we can do since CORS callbacks are just call-thru to @ty-ras/server functionality +test("Verify that listen overload for host and port as in starter template, works", async (c) => { c.plan(1); - await c.notThrowsAsync(async () => { - await spec.listenAsync( - server.createServer({ endpoints: [] }), - "localhost", - await getPort(), - ); - }); + await c.notThrowsAsync( + async () => + await spec.listenAsync( + server.createServer({ endpoints: [] }), + "localhost", + await getPort(), + ), + ); +}); + +test("Verify that listen overload for listen options works", async (c) => { + c.plan(1); + await c.notThrowsAsync( + async () => + await spec.listenAsync(server.createServer({ endpoints: [] }), { + host: "localhost", + port: await getPort(), + }), + ); }); diff --git a/server/src/__test__/server.spec.ts b/server/src/__test__/server.spec.ts index b2b0f6a..5f20b6f 100644 --- a/server/src/__test__/server.spec.ts +++ b/server/src/__test__/server.spec.ts @@ -85,7 +85,7 @@ testSupport.registerTests(test, createServer, { const getCreateState = ( info: testSupport.ServerTestAdditionalInfo[0], ): Pick< - spec.ServerCreationOptions, + spec.ServerCreationOptions, "createState" > => info == 500 diff --git a/server/src/index.ts b/server/src/index.ts index 23178d1..eab54c6 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -3,6 +3,9 @@ */ export type * from "./context.types"; -export * from "./server"; export * from "./cors"; +export * from "./middleware"; +export * from "./server"; export * from "./listen"; + +// Don't export anything from ./internal.ts. diff --git a/server/src/middleware.ts b/server/src/middleware.ts new file mode 100644 index 0000000..026e09a --- /dev/null +++ b/server/src/middleware.ts @@ -0,0 +1,62 @@ +/** + * @file This file contains helper function to create Node server callback. + */ + +import type * as ep from "@ty-ras/endpoint"; +import * as server from "@ty-ras/server"; +import type * as context from "./context.types"; +import * as internal from "./internal"; + +import type * as http from "node:http"; +import type * as http2 from "node:http2"; + +/** + * Creates a new {@link koa.Middleware} to serve the given TyRAS {@link ep.AppEndpoint}s. + * @param endpoints The TyRAS {@link ep.AppEndpoint}s to serve through this Koa middleware. + * @param createState The optional callback to create state for the endpoints. + * @param events The optional {@link server.ServerEventHandler} callback to observe server events. + * @returns The Koa middleware which will serve the given endpoints. + */ +export const createMiddleware = ( + endpoints: ReadonlyArray>, + createState?: context.CreateState, + events?: server.ServerEventHandler< + server.GetContext, + TState + >, +): server.HTTP1Handler | server.HTTP2Handler => { + const flow = server.createTypicalServerFlow< + context.ServerContext, + TStateInfo, + TState + >( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + endpoints as any, + { + ...internal.staticCallbacks, + getState: async ({ req }, stateInfo) => + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any + await createState?.({ context: req as any, stateInfo }), + }, + events, + ); + return async ( + req: http.IncomingMessage | http2.Http2ServerRequest, + res: http.ServerResponse | http2.Http2ServerResponse, + ) => { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const ctx: context.ServerContext = { + req, + res, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + // Perform flow (typicalServerFlow is no-throw (as much as there can be one in JS) function) + await flow(ctx); + } finally { + if (!res.writableEnded) { + res.end(); + } + } + }; +}; diff --git a/server/src/server.ts b/server/src/server.ts index 48c3716..eace256 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -6,12 +6,11 @@ import * as ep from "@ty-ras/endpoint"; import * as server from "@ty-ras/server"; import type * as ctx from "./context.types"; -import * as internal from "./internal"; +import * as middleware from "./middleware"; import * as http from "node:http"; import * as https from "node:https"; import * as http2 from "node:http2"; -import type * as tls from "node:tls"; /** * Creates new non-secure HTTP1 {@link http.Server} serving given TyRAS {@link ep.AppEndpoint}s with additional configuration via {@link ServerCreationOptions}. @@ -19,14 +18,8 @@ import type * as tls from "node:tls"; * @returns A new non-secure HTTP1 {@link http.Server}. */ export function createServer( - opts: ServerCreationOptions< - ctx.HTTP1ServerContext, - TStateInfo, - TState, - http.ServerOptions, - false - > & - HTTP1ServerOptions, + opts: ServerCreationOptions & + server.NodeServerOptions1, ): http.Server; /** @@ -35,14 +28,8 @@ export function createServer( * @returns A new secure HTTP1 {@link https.Server}. */ export function createServer( - opts: ServerCreationOptions< - ctx.HTTP1ServerContext, - TStateInfo, - TState, - https.ServerOptions, - true - > & - HTTP1ServerOptions, + opts: ServerCreationOptions & + server.NodeServerOptions1, ): https.Server; /** @@ -52,14 +39,8 @@ export function createServer( * @returns A new non-secure HTTP2 {@link http2.Http2Server}. */ export function createServer( - opts: ServerCreationOptions< - ctx.HTTP2ServerContext, - TStateInfo, - TState, - http2.ServerOptions, - false - > & - HTTP2ServerOptions, + opts: ServerCreationOptions & + server.NodeServerOptions2, ): http2.Http2Server; /** @@ -69,114 +50,41 @@ export function createServer( * @returns A new secure HTTP2 {@link http2.Http2SecureServer}. */ export function createServer( - opts: ServerCreationOptions< - ctx.HTTP2ServerContext, - TStateInfo, - TState, - http2.SecureServerOptions, - true - > & - HTTP2ServerOptions, + opts: ServerCreationOptions & + server.NodeServerOptions2, ): http2.Http2SecureServer; /** * Creates new secure or non-secure HTTP1 or HTTP2 Node server serving given TyRAS {@link ep.AppEndpoint}s with additional configuration via {@link ServerCreationOptions}. * Please set `httpVersion` value of `opts` to `2` to enable HTTP2 protocol, otherwise HTTP1 server will be returned. * @param opts The {@link ServerCreationOptions} to use when creating server. + * @param opts.endpoints Privately deconstructed variable. + * @param opts.createState Privately deconstructed variable. + * @param opts.events Privately deconstructed variable. + * @param opts.options Privately deconstructed variable. * @returns Secure or non-secure HTTP1 or HTTP2 Node server */ -export function createServer( - opts: - | (ServerCreationOptions< - ctx.HTTP1ServerContext, - TStateInfo, - TState, - http.ServerOptions, - false - > & - HTTP1ServerOptions) - | (ServerCreationOptions< - ctx.HTTP1ServerContext, - TStateInfo, - TState, - https.ServerOptions, - true - > & - HTTP1ServerOptions) - | (ServerCreationOptions< - ctx.HTTP2ServerContext, - TStateInfo, - TState, - http2.ServerOptions, - false - > & - HTTP2ServerOptions) - | (ServerCreationOptions< - ctx.HTTP2ServerContext, - TStateInfo, - TState, - http2.SecureServerOptions, - true - > & - HTTP2ServerOptions), -) { - let retVal; - if ("httpVersion" in opts && opts.httpVersion === 2) { - const { options, secure, ...handlerOptions } = opts; - const httpHandler = asyncToVoid( - createHandleHttpRequest< - TStateInfo, - TState, - http2.Http2ServerRequest, - http2.Http2ServerResponse - >(handlerOptions), - ); - if (isSecure(secure, options, 2)) { - retVal = http2.createSecureServer(options ?? {}, httpHandler); - } else { - retVal = http2.createServer(options ?? {}, httpHandler); - } - } else { - const { options, secure, ...handlerOptions } = opts; - const httpHandler = asyncToVoid( - createHandleHttpRequest< - TStateInfo, - TState, - http.IncomingMessage, - http.ServerResponse - >(handlerOptions), - ); - if (isSecure(secure, options, 1)) { - retVal = https.createServer(options ?? {}, httpHandler); - } else { - retVal = http.createServer(options ?? {}, httpHandler); - } - } - return retVal; +export function createServer({ + endpoints, + createState, + events, + ...options +}: + | (ServerCreationOptions & + server.NodeServerOptions1) + | (ServerCreationOptions & + server.NodeServerOptions1) + | (ServerCreationOptions & + server.NodeServerOptions2) + | (ServerCreationOptions & + server.NodeServerOptions2)): HttpServer { + return server.createNodeServerGeneric( + options, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + middleware.createMiddleware(endpoints as any, createState, events), + ); } -/** - * This type is used to make it possible to explicitly specify using HTTP protocol version 1 for server if given to {@link createServer}. - */ -export type HTTP1ServerOptions = { - /** - * Optional property which should be set to `1` if needed to explicitly use HTTP protocol version 1 for server. - * The default protocol version is 1, so this is optional. - */ - httpVersion?: 1; -}; - -/** - * This type is used to make it possible to specify {@link createServer} to use HTTP protocol version 2, as opposed to default 1. - */ -export type HTTP2ServerOptions = { - /** - * Property which should be set to `2` if needed to use HTTP protocol version 2 for server. - * The default protocol version is 1, so to override that, this property must be specified. - */ - httpVersion: 2; -}; - /** * This interface contains options common for both HTTP 1 and 2 servers when creating them via {@link createServer}. */ @@ -184,8 +92,6 @@ export interface ServerCreationOptions< TServerContext extends { req: unknown }, TStateInfo, TState, - TOPtions, - TSecure extends boolean, > { /** * The TyRAS {@link ep.AppEndpoint}s to server via returned HTTP server. @@ -203,127 +109,8 @@ export interface ServerCreationOptions< events?: | server.ServerEventHandler, TState> | undefined; - - /** - * The further options for the HTTP server. - */ - options?: TOPtions | undefined; - - /** - * Set this to `true` explicitly if automatic detection of server being secure by {@link createServer} fails. - */ - secure?: TSecure | undefined; } -const secureHttp1OptionKeys: ReadonlyArray = [ - "key", - "cert", - "pfx", - "passphrase", - "rejectUnauthorized", - "ciphers", - "ca", - "requestCert", - "secureContext", - "secureOptions", - "secureProtocol", - "sigalgs", - "ticketKeys", - "crl", - "clientCertEngine", - "dhparam", - "ecdhCurve", - "allowHalfOpen", - "handshakeTimeout", - "honorCipherOrder", - "keepAlive", - "keepAliveInitialDelay", - "maxVersion", - "minVersion", - "noDelay", - "pauseOnConnect", - "privateKeyEngine", - "privateKeyIdentifier", - "pskCallback", - "pskIdentityHint", - "sessionIdContext", - "sessionTimeout", - "ALPNProtocols", - "SNICallback", -]; - -const secureHttp2OptionKeys: ReadonlyArray< - "allowHTTP1" | "origins" | keyof tls.TlsOptions -> = ["allowHTTP1", "origins", ...secureHttp1OptionKeys]; - -const createHandleHttpRequest = < - TStateInfo, - TState, - TRequest extends http.IncomingMessage | http2.Http2ServerRequest, - TResponse extends http.ServerResponse | http2.Http2ServerResponse, ->({ - endpoints, - createState, - events, -}: Pick< - ServerCreationOptions< - ctx.ServerContextGeneric, - TStateInfo, - TState, - never, - never - >, - "endpoints" | "createState" | "events" ->): HTTP1Or2Handler => { - const flow = server.createTypicalServerFlow( - endpoints, - { - ...internal.staticCallbacks, - getState: async ({ req }, stateInfo) => - await createState?.({ context: req, stateInfo }), - }, - events, - ); - return async (req: TRequest, res: TResponse) => { - try { - const ctx: ctx.ServerContextGeneric = { - req, - res, - }; - // Perform flow (typicalServerFlow is no-throw (as much as there can be one in JS) function) - await flow(ctx); - } finally { - if (!res.writableEnded) { - res.end(); - } - } - }; -}; -type HTTP1Or2Handler = ( - req: TRequest, - res: TResponse, -) => Promise; - -const asyncToVoid = - ( - asyncCallback: HTTP1Or2Handler, - ): ((...args: Parameters) => void) => - (...args) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - void asyncCallback(...args); - }; - -const isSecure = ( - secure: boolean | undefined, - options: object | undefined, - version: 1 | 2, -) => - secure || - (options && - (version === 1 ? secureHttp1OptionKeys : secureHttp2OptionKeys).some( - (propKey) => propKey in options, - )); - /** * This type contains all the HTTP server types that can be created with TyRAS backend for Node servers. */ @@ -332,3 +119,12 @@ export type HttpServer = | https.Server | http2.Http2Server | http2.Http2SecureServer; + +/** + * Helper method to optionalize {@link server.NodeServerOptions1} and {@link server.NodeServerOptions2}. + */ +export type OptionalizeOptions< + TType extends + | server.NodeServerOptions1 + | server.NodeServerOptions2, +> = Omit & { options?: TType["options"] }; diff --git a/server/yarn.lock b/server/yarn.lock index 805e4db..b7540bf 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -445,10 +445,10 @@ dependencies: "@ty-ras/endpoint" "^2.0.0" -"@ty-ras/server@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@ty-ras/server/-/server-2.1.1.tgz#ea13ddbe58f3603d6eab396277c1cf97c2171fe7" - integrity sha512-KAiuSEYwXKQIfMSIDCaGjuX/hy42oes/JnyG0blumMNwR0kSA6PYonDfg1ROtBPO9dGQZ/mWiWS3JKbZfMFKoQ== +"@ty-ras/server@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ty-ras/server/-/server-2.2.0.tgz#6a1dc54767241355be5202006127afea3a2ff602" + integrity sha512-rJwZj20uiO2tcS9Ln+5TfapdILDPcWxYbeyIDUjmbOmkv+FvMma+pVgKhozNP3iQj1mnKN2CmqgIhaxKw+bRBg== dependencies: "@ty-ras/endpoint" "^2.0.0"