@znight/http 0.2.2 → 0.4.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.
@@ -1,4 +1,4 @@
1
- import { s as HttpAdapter } from "./adapter-J0UXFnNM.mjs";
1
+ import { s as HttpAdapter } from "./adapter-DmhQGqAR.mjs";
2
2
  import { Server } from "node:http";
3
3
  import { Express, Router } from "express";
4
4
  /** Options of {@link expressAdapter}. */
@@ -1,4 +1,4 @@
1
- import { s as HttpAdapter } from "./adapter-J0UXFnNM.mjs";
1
+ import { s as HttpAdapter } from "./adapter-DmhQGqAR.mjs";
2
2
  import { FastifyInstance, FastifyServerOptions } from "fastify";
3
3
  /** Options of {@link fastifyAdapter}. */
4
4
  export interface FastifyAdapterOptions {
package/dist/hono.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { s as HttpAdapter } from "./adapter-J0UXFnNM.mjs";
1
+ import { s as HttpAdapter } from "./adapter-DmhQGqAR.mjs";
2
2
  import { Hono } from "hono";
3
3
  /** Options of {@link honoAdapter}. */
4
4
  export interface HonoAdapterOptions {
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as PayloadTooLargeException, A as UploadedFileOptions, B as BadRequestException, C as HttpUploadDefinition, Ct as CookieOptions, D as RequestAbortedError, Dt as parseSetCookie, E as RequestAbortKind, Et as parseCookieHeader, F as formatByteSize, G as HeaderValue, H as ForbiddenException, I as parseByteSize, J as HttpExceptionOptions, K as HttpErrorBody, L as QueryRecord, M as Uploads, N as uploads, O as RequestId, Ot as serializeClearCookie, P as ByteSize, Q as NotImplementedException, R as parseQuery, S as HttpSseDefinition, St as CookieDefaults, T as HttpRouteInfo, Tt as SetCookieInfo, U as GatewayTimeoutException, V as ConflictException, W as GoneException, X as MethodNotAllowedException, Y as InternalServerErrorException, Z as NotFoundException, _ as HttpMethod, _t as StandardSchemaV1Result, a as AdapterRoute, at as ValidationException, b as HttpRouteDefinition, bt as isStandardSchema, c as HttpServerAddress, ct as toIssueDetail, d as InjectStreamOptions, dt as StandardSchemaV1, et as ServiceUnavailableException, f as InjectStreamRequest, ft as StandardSchemaV1FailureResult, g as HttpMetadata, gt as StandardSchemaV1Props, h as HttpControllerDefinition, ht as StandardSchemaV1PathSegment, i as AdapterRequest, it as UnsupportedMediaTypeException, j as UploadedFilesOptions, k as UploadLimits, kt as serializeCookie, l as InjectRequest, lt as InferSchemaInput, m as ReplyBody, mt as StandardSchemaV1Options, n as AdapterHandler, nt as UnauthorizedException, o as FallbackHandler, ot as ValidationIssueDetail, p as InjectStreamResponse, pt as StandardSchemaV1Issue, q as HttpException, r as AdapterReply, rt as UnprocessableEntityException, s as HttpAdapter, st as isHttpException, t as AdapterCloseOptions, tt as TooManyRequestsException, u as InjectResponse, ut as InferSchemaOutput, v as HttpParamDefinition, vt as StandardSchemaV1SuccessResult, w as HttpContext, wt as ResponseCookie, x as HttpRouteGroupDefinition, xt as ClearCookieOptions, y as HttpParamSource, yt as StandardSchemaV1Types, z as BadGatewayException } from "./adapter-J0UXFnNM.mjs";
1
+ import { $ as PayloadTooLargeException, A as UploadedFileOptions, B as BadRequestException, C as HttpUploadDefinition, Ct as CookieOptions, D as RequestAbortedError, Dt as parseSetCookie, E as RequestAbortKind, Et as parseCookieHeader, F as formatByteSize, G as HeaderValue, H as ForbiddenException, I as parseByteSize, J as HttpExceptionOptions, K as HttpErrorBody, L as QueryRecord, M as Uploads, N as uploads, O as RequestId, Ot as serializeClearCookie, P as ByteSize, Q as NotImplementedException, R as parseQuery, S as HttpSseDefinition, St as CookieDefaults, T as HttpRouteInfo, Tt as SetCookieInfo, U as GatewayTimeoutException, V as ConflictException, W as GoneException, X as MethodNotAllowedException, Y as InternalServerErrorException, Z as NotFoundException, _ as HttpMethod, _t as StandardSchemaV1Result, a as AdapterRoute, at as ValidationException, b as HttpRouteDefinition, bt as isStandardSchema, c as HttpServerAddress, ct as toIssueDetail, d as InjectStreamOptions, dt as StandardSchemaV1, et as ServiceUnavailableException, f as InjectStreamRequest, ft as StandardSchemaV1FailureResult, g as HttpMetadata, gt as StandardSchemaV1Props, h as HttpControllerDefinition, ht as StandardSchemaV1PathSegment, i as AdapterRequest, it as UnsupportedMediaTypeException, j as UploadedFilesOptions, k as UploadLimits, kt as serializeCookie, l as InjectRequest, lt as InferSchemaInput, m as ReplyBody, mt as StandardSchemaV1Options, n as AdapterHandler, nt as UnauthorizedException, o as FallbackHandler, ot as ValidationIssueDetail, p as InjectStreamResponse, pt as StandardSchemaV1Issue, q as HttpException, r as AdapterReply, rt as UnprocessableEntityException, s as HttpAdapter, st as isHttpException, t as AdapterCloseOptions, tt as TooManyRequestsException, u as InjectResponse, ut as InferSchemaOutput, v as HttpParamDefinition, vt as StandardSchemaV1SuccessResult, w as HttpContext, wt as ResponseCookie, x as HttpRouteGroupDefinition, xt as ClearCookieOptions, y as HttpParamSource, yt as StandardSchemaV1Types, z as BadGatewayException } from "./adapter-DmhQGqAR.mjs";
2
2
  import { NodeAdapter, NodeAdapterOptions, nodeAdapter } from "./node.mjs";
3
3
  import { ClassMarker, Container, MethodMarker, ParameterMarker, Plugin, PluginContext, PluginOptions, ProviderKey, Scope, TypedToken } from "@znight/core";
4
4
  /**
@@ -1162,6 +1162,14 @@ interface PluginHttpRoutesContext {
1162
1162
  */
1163
1163
  interface PluginHttpRoutes {
1164
1164
  httpRoutes(context: PluginHttpRoutesContext): readonly PluginRoute[];
1165
+ /**
1166
+ * Called once when `HttpPlugin` starts draining (its `onStop`, whatever the plugin order), after
1167
+ * it stopped accepting connections and started closing its event streams: end the long-lived
1168
+ * responses `HttpPlugin` does not know of (a stream another server produced), so that the drain
1169
+ * does not wait for them until `drainTimeoutMs`. Requests still in flight are served. The drain
1170
+ * waits for a returned promise too (within `drainTimeoutMs`); a failure is logged.
1171
+ */
1172
+ onHttpDrain?(): void | Promise<void>;
1165
1173
  }
1166
1174
  /** A parsed `content-type` header. */
1167
1175
  interface MediaType {
package/dist/index.mjs CHANGED
@@ -3967,7 +3967,7 @@ var Pipeline = class {
3967
3967
  let result;
3968
3968
  let reply;
3969
3969
  try {
3970
- result = plan.definition.invoke(this.#resolve(plan.controller), ctx);
3970
+ result = plan.invoke(this.#resolve(plan.controller), ctx);
3971
3971
  if (isThenable(result)) return this.#fastLater(route, request, ctx, result, startTime);
3972
3972
  reply = this.#fastReply(route, ctx, result);
3973
3973
  } catch (error) {
@@ -4003,7 +4003,7 @@ var Pipeline = class {
4003
4003
  if (result === NO_RESULT) {
4004
4004
  const controller = this.#resolve(plan.controller);
4005
4005
  await ctx.parseBody();
4006
- result = plan.definition.invoke(controller, ctx);
4006
+ result = plan.invoke(controller, ctx);
4007
4007
  }
4008
4008
  if (isThenable(result)) result = await result;
4009
4009
  reply = this.#fastReply(route, ctx, result);
@@ -4091,7 +4091,7 @@ var Pipeline = class {
4091
4091
  if (plan.parseBody) return this.#fastOperationLater(x, route, NO_RESULT);
4092
4092
  let result;
4093
4093
  try {
4094
- result = plan.definition.invoke(this.#resolve(plan.controller), ctx);
4094
+ result = plan.invoke(this.#resolve(plan.controller), ctx);
4095
4095
  } catch (error) {
4096
4096
  return this.#fastChecked(x, this.#failed(error, ctx, x.event));
4097
4097
  }
@@ -4107,7 +4107,7 @@ var Pipeline = class {
4107
4107
  if (result === NO_RESULT) {
4108
4108
  const controller = this.#resolve(plan.controller);
4109
4109
  await ctx.parseBody();
4110
- result = plan.definition.invoke(controller, ctx);
4110
+ result = plan.invoke(controller, ctx);
4111
4111
  }
4112
4112
  if (isThenable(result)) result = await result;
4113
4113
  } catch (error) {
@@ -4634,12 +4634,11 @@ var Pipeline = class {
4634
4634
  const controller = this.#resolve(plan.controller);
4635
4635
  for (const guard of plan.guards) if (await this.#resolve(guard).canActivate(ctx) !== true) throw new ForbiddenException("Forbidden resource");
4636
4636
  if (plan.parseBody) await ctx.parseBody();
4637
- const definition = plan.definition;
4638
4637
  const interceptors = plan.interceptors;
4639
- if (interceptors.length === 0) result = definition.invoke(controller, ctx);
4638
+ if (interceptors.length === 0) result = plan.invoke(controller, ctx);
4640
4639
  else {
4641
4640
  const chain = async (index) => {
4642
- if (index === interceptors.length) return await definition.invoke(controller, ctx);
4641
+ if (index === interceptors.length) return await plan.invoke(controller, ctx);
4643
4642
  return await this.#resolve(interceptors[index]).intercept(ctx, () => chain(index + 1));
4644
4643
  };
4645
4644
  result = chain(0);
@@ -5135,6 +5134,28 @@ function collectPluginRoutes(plugins, self, context) {
5135
5134
  }
5136
5135
  return collected;
5137
5136
  }
5137
+ /**
5138
+ * Calls `onHttpDrain()` on every other plugin implementing it, in plugin order: the promises the
5139
+ * drain waits for (they never reject: a failure is logged).
5140
+ */
5141
+ function drainPlugins(plugins, self, logger) {
5142
+ const pending = [];
5143
+ for (const plugin of plugins) {
5144
+ if (plugin === self) continue;
5145
+ const drain = plugin.onHttpDrain;
5146
+ if (typeof drain !== "function") continue;
5147
+ const failed = (error) => {
5148
+ logger.error(`HttpPlugin: onHttpDrain() of the plugin "${String(plugin.name)}" failed`, { err: error });
5149
+ };
5150
+ try {
5151
+ const result = drain.call(plugin);
5152
+ if (typeof result?.then === "function") pending.push(Promise.resolve(result).then(void 0, failed));
5153
+ } catch (error) {
5154
+ failed(error);
5155
+ }
5156
+ }
5157
+ return pending;
5158
+ }
5138
5159
  function validateRoute(instance, plugin, value, prefix) {
5139
5160
  if (value === null || typeof value !== "object") throw invalid(plugin, `invalid route ${describe(value)}`);
5140
5161
  const route = value;
@@ -5827,13 +5848,18 @@ var HttpPlugin = class HttpPlugin {
5827
5848
  if (runtime === void 0) return;
5828
5849
  runtime.pipeline.draining = true;
5829
5850
  runtime.pipeline.closeStreams("shutdown");
5851
+ const drains = drainPlugins(ctx.app.plugins, this, ctx.logger);
5830
5852
  const timeoutMs = runtime.options.drainTimeoutMs;
5831
5853
  let timer;
5832
5854
  const timeout = new Promise((resolve) => {
5833
5855
  if (timeoutMs !== Number.POSITIVE_INFINITY) timer = setTimeout(() => resolve("timeout"), Math.min(timeoutMs, MAX_TIMER_MS));
5834
5856
  });
5835
5857
  try {
5836
- const drained = Promise.all([runtime.adapter.close(), runtime.pipeline.whenIdle()]).then(() => "drained");
5858
+ const drained = Promise.all([
5859
+ runtime.adapter.close(),
5860
+ runtime.pipeline.whenIdle(),
5861
+ ...drains
5862
+ ]).then(() => "drained");
5837
5863
  if (await Promise.race([drained, timeout]) === "timeout") {
5838
5864
  ctx.logger.warn(`HTTP server: closing ${runtime.pipeline.inflight} in-flight request(s) after ${timeoutMs} ms`);
5839
5865
  await runtime.adapter.close({ force: true });
@@ -6102,6 +6128,7 @@ function buildRegistrations(ctx, options, raw, pluginRoutes) {
6102
6128
  const body = bodyPlan(route, options.bodyLimit, options.uploads);
6103
6129
  const parseBody = body.upload || route.params.some((param) => param.source === "body");
6104
6130
  const chain = guards.map((guard) => guard.resolvable);
6131
+ const invoke = routeInvoke(container, route, lookup, where);
6105
6132
  let meta;
6106
6133
  const effective = () => {
6107
6134
  meta ??= effectiveMeta(controllerMeta, routeMeta, groupOf);
@@ -6113,6 +6140,7 @@ function buildRegistrations(ctx, options, raw, pluginRoutes) {
6113
6140
  path,
6114
6141
  plan: (method) => ({
6115
6142
  definition: route,
6143
+ invoke,
6116
6144
  path,
6117
6145
  info: Object.freeze({
6118
6146
  method,
@@ -6166,6 +6194,7 @@ function buildRegistrations(ctx, options, raw, pluginRoutes) {
6166
6194
  path,
6167
6195
  plan: (method) => ({
6168
6196
  definition,
6197
+ invoke: definition.invoke,
6169
6198
  path,
6170
6199
  info: Object.freeze({
6171
6200
  method,
@@ -6320,6 +6349,7 @@ function assertRoute(route, where) {
6320
6349
  if (route.bodyLimit !== void 0 && !isSize(route.bodyLimit)) throw new ZnightError(ErrorCode.InvalidRoute, `Invalid route ${where}: bodyLimit must be a number of bytes (or Infinity), got ${describeValue(route.bodyLimit)}`);
6321
6350
  if (route.uploads !== void 0) assertUploads(route, where);
6322
6351
  if (route.sse !== void 0) assertSse(route, where);
6352
+ if (route.paramResolvers !== void 0 && !Array.isArray(route.paramResolvers)) throw new ZnightError(ErrorCode.InvalidRoute, `Invalid route ${where}: paramResolvers must be an array of tokens`);
6323
6353
  }
6324
6354
  function assertSse(route, where) {
6325
6355
  const sse = route.sse;
@@ -6359,6 +6389,22 @@ function describeValue(value) {
6359
6389
  }
6360
6390
  }
6361
6391
  /**
6392
+ * The `invoke` of a route plan: the generated `invoke` itself, unless the route has parameter
6393
+ * resolvers (RFC 0010), whose instances are passed as its third argument: one frozen array resolved
6394
+ * at bootstrap when they are all singletons, otherwise an array resolved in the request scope.
6395
+ */
6396
+ function routeInvoke(container, route, lookup, where) {
6397
+ const invoke = route.invoke;
6398
+ const tokens = route.paramResolvers;
6399
+ if (tokens === void 0 || tokens.length === 0) return invoke;
6400
+ const resolvers = tokens.map((token) => lookup(token, `parameter resolver of ${where}`));
6401
+ if (resolvers.every((resolver) => "instance" in resolver)) {
6402
+ const instances = Object.freeze(resolvers.map((resolver) => resolver.instance));
6403
+ return (controller, ctx) => invoke(controller, ctx, instances);
6404
+ }
6405
+ return (controller, ctx) => invoke(controller, ctx, resolvers.map((resolver) => "instance" in resolver ? resolver.instance : container.get(resolver.token)));
6406
+ }
6407
+ /**
6362
6408
  * Singletons (and registered values) are resolved once at bootstrap; request-scoped and transient
6363
6409
  * providers on every request.
6364
6410
  */