@arki/http 0.0.1

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/dist/bundle.d.ts +121 -0
  4. package/dist/bundle.d.ts.map +1 -0
  5. package/dist/bundle.js +98 -0
  6. package/dist/bundle.js.map +1 -0
  7. package/dist/context.d.ts +29 -0
  8. package/dist/context.d.ts.map +1 -0
  9. package/dist/context.js +34 -0
  10. package/dist/context.js.map +1 -0
  11. package/dist/contract.d.ts +120 -0
  12. package/dist/contract.d.ts.map +1 -0
  13. package/dist/contract.js +49 -0
  14. package/dist/contract.js.map +1 -0
  15. package/dist/derive.d.ts +58 -0
  16. package/dist/derive.d.ts.map +1 -0
  17. package/dist/derive.js +28 -0
  18. package/dist/derive.js.map +1 -0
  19. package/dist/dot.d.ts +106 -0
  20. package/dist/dot.d.ts.map +1 -0
  21. package/dist/dot.js +210 -0
  22. package/dist/dot.js.map +1 -0
  23. package/dist/engine.d.ts +36 -0
  24. package/dist/engine.d.ts.map +1 -0
  25. package/dist/engine.js +173 -0
  26. package/dist/engine.js.map +1 -0
  27. package/dist/error.d.ts +107 -0
  28. package/dist/error.d.ts.map +1 -0
  29. package/dist/error.js +122 -0
  30. package/dist/error.js.map +1 -0
  31. package/dist/index.d.ts +28 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +21 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/listen.d.ts +23 -0
  36. package/dist/listen.d.ts.map +1 -0
  37. package/dist/listen.js +71 -0
  38. package/dist/listen.js.map +1 -0
  39. package/dist/openapi.d.ts +45 -0
  40. package/dist/openapi.d.ts.map +1 -0
  41. package/dist/openapi.js +140 -0
  42. package/dist/openapi.js.map +1 -0
  43. package/dist/sse.d.ts +12 -0
  44. package/dist/sse.d.ts.map +1 -0
  45. package/dist/sse.js +86 -0
  46. package/dist/sse.js.map +1 -0
  47. package/package.json +73 -0
  48. package/src/bundle.ts +221 -0
  49. package/src/context.ts +39 -0
  50. package/src/contract.ts +152 -0
  51. package/src/derive.ts +67 -0
  52. package/src/dot.ts +282 -0
  53. package/src/engine.ts +239 -0
  54. package/src/error.ts +158 -0
  55. package/src/index.ts +53 -0
  56. package/src/listen.ts +100 -0
  57. package/src/openapi.ts +168 -0
  58. package/src/sse.ts +92 -0
package/src/engine.ts ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * The request engine — builds one fetch handler from a list of bundles.
3
+ *
4
+ * Hono does the routing internally; no Hono type appears in the public
5
+ * API, so the engine stays a swappable implementation detail. The route
6
+ * table order is bundle order then binding order — declaration order,
7
+ * deterministically, per DOT principle 3.
8
+ */
9
+
10
+ import { Hono } from 'hono';
11
+ import { z } from 'zod';
12
+
13
+ import type { MountBinding, RouteBinding, RouteBundle } from './bundle.js';
14
+ import type { ContractLike } from './contract.js';
15
+ import { runWithRequestContext } from './context.js';
16
+ import type { BaseRequestCtx } from './derive.js';
17
+ import { describeInternalError, errorResponse, HTTP_ERROR_CODES, HttpConfigError, HttpError, toErrorResponse } from './error.js';
18
+ import { sseResponse } from './sse.js';
19
+
20
+ /** A built engine: the composed fetch handler plus its route inventory. */
21
+ export type Engine = {
22
+ readonly fetch: (req: Request) => Promise<Response>;
23
+ /** Contract ids + mount ids, in registration order. */
24
+ readonly routeIds: readonly string[];
25
+ };
26
+
27
+ /**
28
+ * Compose bundles into a single fetch handler. Throws
29
+ * `ARKI_HTTP_E002`/`E006` on route collisions and malformed mounts —
30
+ * callers (the `http()` pip's `boot`) let these bubble as boot failures.
31
+ */
32
+ export function buildEngine(bundles: readonly RouteBundle[]): Engine {
33
+ const app = new Hono();
34
+ const claimed = new Map<string, string>();
35
+ const routeIds: string[] = [];
36
+
37
+ for (const bundle of bundles) {
38
+ for (const mount of bundle.mounts) {
39
+ registerMount(app, claimed, mount);
40
+ routeIds.push(mount.id);
41
+ }
42
+ for (const binding of bundle.bindings) {
43
+ registerBinding(app, claimed, bundle, binding);
44
+ routeIds.push(binding.contract.id);
45
+ }
46
+ }
47
+
48
+ app.notFound(c =>
49
+ errorResponse(404, HTTP_ERROR_CODES.notFound, `no route for ${c.req.method} ${new URL(c.req.url).pathname}`),
50
+ );
51
+ app.onError((error, _c) => toErrorResponse(error));
52
+
53
+ return {
54
+ fetch: req => Promise.resolve(app.fetch(req)),
55
+ routeIds,
56
+ };
57
+ }
58
+
59
+ function registerMount(app: Hono, claimed: Map<string, string>, mount: MountBinding): void {
60
+ const key = `MOUNT ${mount.path}`;
61
+ const prior = claimed.get(key);
62
+ if (prior !== undefined) {
63
+ throw new HttpConfigError({
64
+ code: HTTP_ERROR_CODES.duplicateRoute,
65
+ message: `[http] duplicate mount at "${mount.path}": "${mount.id}" collides with "${prior}".`,
66
+ remediation: 'Give each mounted handler a distinct path prefix.',
67
+ });
68
+ }
69
+ claimed.set(key, mount.id);
70
+ app.mount(mount.path, mount.handler);
71
+ }
72
+
73
+ function registerBinding(app: Hono, claimed: Map<string, string>, bundle: RouteBundle, binding: RouteBinding): void {
74
+ const { contract } = binding;
75
+ const key = `${contract.method} ${contract.path}`;
76
+ const prior = claimed.get(key);
77
+ if (prior !== undefined) {
78
+ throw new HttpConfigError({
79
+ code: HTTP_ERROR_CODES.duplicateRoute,
80
+ message: `[http] duplicate route ${key}: "${contract.id}" collides with "${prior}".`,
81
+ remediation:
82
+ 'Two bindings (possibly from different bundles) claim the same method and path. Change one path, or drop the duplicate binding.',
83
+ });
84
+ }
85
+ claimed.set(key, contract.id);
86
+ app.on(contract.method, contract.path, c => executeBinding(bundle, binding, c.req.raw, c.req.param()));
87
+ }
88
+
89
+ async function executeBinding(
90
+ bundle: RouteBundle,
91
+ binding: RouteBinding,
92
+ req: Request,
93
+ params: Record<string, string>,
94
+ ): Promise<Response> {
95
+ const base: BaseRequestCtx = {
96
+ req,
97
+ url: new URL(req.url),
98
+ requestId: req.headers.get('x-request-id') ?? crypto.randomUUID(),
99
+ signal: req.signal,
100
+ };
101
+ return runWithRequestContext(base, async () => {
102
+ try {
103
+ let ctx: BaseRequestCtx & Record<string, unknown> = base;
104
+ for (const deriver of bundle.derivers) {
105
+ // Erasure boundary: derivers were stored with `never` context (see
106
+ // ErasedDeriver); the accumulated ctx is the value they were typed
107
+ // against at composition time.
108
+ const value = await deriver.derive(req, ctx as never);
109
+ ctx = { ...ctx, [deriver.key]: value };
110
+ }
111
+
112
+ const input = await validateInput(binding.contract, params, base.url, req);
113
+
114
+ if (binding.contract.kind === 'sse') {
115
+ // Erasure boundary, same as above — input/ctx match the handler's
116
+ // composition-time types.
117
+ const events = (binding.handler as (input: never, ctx: never) => AsyncIterable<unknown>)(
118
+ input as never,
119
+ ctx as never,
120
+ );
121
+ return sseResponse(events, binding.contract, base.signal);
122
+ }
123
+
124
+ const result = await (binding.handler as (input: never, ctx: never) => unknown)(input as never, ctx as never);
125
+ if (result instanceof Response) return result;
126
+ return serializeOutput(binding.contract, result);
127
+ } catch (error) {
128
+ return toErrorResponse(error);
129
+ }
130
+ });
131
+ }
132
+
133
+ type ValidatedInput = {
134
+ readonly params: Record<string, string>;
135
+ readonly query: unknown;
136
+ readonly body: unknown;
137
+ };
138
+
139
+ async function validateInput(
140
+ contract: ContractLike,
141
+ params: Record<string, string>,
142
+ url: URL,
143
+ req: Request,
144
+ ): Promise<ValidatedInput> {
145
+ let query: unknown;
146
+ if (contract.query !== undefined) {
147
+ // Multi-valued query params collapse to their last value here; declare
148
+ // arrays via a transform on the contract schema if you need them.
149
+ query = parseWith(contract.query, Object.fromEntries(url.searchParams), 'query');
150
+ }
151
+
152
+ let body: unknown;
153
+ if (contract.body !== undefined) {
154
+ let raw: unknown;
155
+ try {
156
+ raw = await req.json();
157
+ } catch {
158
+ throw new HttpError(400, HTTP_ERROR_CODES.badRequest, 'request body is not valid JSON', {
159
+ remediation: 'Send a JSON body with content-type: application/json.',
160
+ });
161
+ }
162
+ body = parseWith(contract.body, raw, 'body');
163
+ }
164
+
165
+ return { params, query, body };
166
+ }
167
+
168
+ function parseWith(schema: { parse(input: unknown): unknown }, value: unknown, where: 'query' | 'body'): unknown {
169
+ try {
170
+ return schema.parse(value);
171
+ } catch (error) {
172
+ throw new HttpError(400, HTTP_ERROR_CODES.badRequest, `${where} validation failed: ${zodMessage(error)}`, {
173
+ cause: error,
174
+ });
175
+ }
176
+ }
177
+
178
+ function zodMessage(error: unknown): string {
179
+ if (error instanceof z.ZodError) {
180
+ return error.issues.map(issue => `${issue.path.join('.') || '(root)'}: ${issue.message}`).join('; ');
181
+ }
182
+ return describeInternalError(error);
183
+ }
184
+
185
+ function serializeOutput(contract: ContractLike, result: unknown): Response {
186
+ if (contract.output === undefined) {
187
+ throw new HttpError(
188
+ 500,
189
+ HTTP_ERROR_CODES.internal,
190
+ `handler for "${contract.id}" returned a value but the contract declares no output schema`,
191
+ { remediation: 'Return a Response, or declare an output schema on the contract.' },
192
+ );
193
+ }
194
+ let validated: unknown;
195
+ try {
196
+ validated = contract.output.parse(result);
197
+ } catch (error) {
198
+ throw new HttpError(
199
+ 500,
200
+ HTTP_ERROR_CODES.internal,
201
+ `handler output for "${contract.id}" violates the contract's output schema: ${zodMessage(error)}`,
202
+ { cause: error },
203
+ );
204
+ }
205
+ return Response.json(validated);
206
+ }
207
+
208
+ /**
209
+ * App-wide middleware: a fetch-shaped wrapper. The first middleware in a
210
+ * list runs outermost. Use for cross-cutting concerns that must see or
211
+ * touch the raw request/response — CORS, request logging, compression.
212
+ * For per-request *values* (auth principals, tenant handles), use
213
+ * derivers — they are typed into handler contexts; middleware is not.
214
+ */
215
+ export type HttpMiddleware = (req: Request, next: (req: Request) => Promise<Response>) => Response | Promise<Response>;
216
+
217
+ /**
218
+ * Wrap a fetch handler in middleware (first = outermost). A middleware
219
+ * that throws resolves to the coded error envelope — `HttpError` keeps
220
+ * its status, anything else is a 500.
221
+ */
222
+ export function composeMiddleware(
223
+ fetch: (req: Request) => Promise<Response>,
224
+ middleware: readonly HttpMiddleware[],
225
+ ): (req: Request) => Promise<Response> {
226
+ let composed = fetch;
227
+ for (const layer of middleware.toReversed()) {
228
+ const next = composed;
229
+ composed = async req => await layer(req, next);
230
+ }
231
+ const outermost = composed;
232
+ return async req => {
233
+ try {
234
+ return await outermost(req);
235
+ } catch (error) {
236
+ return toErrorResponse(error);
237
+ }
238
+ };
239
+ }
package/src/error.ts ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Error surface for `@arki/http`.
3
+ *
4
+ * Two families, mirroring the DOT diagnostics doctrine:
5
+ *
6
+ * - {@link HttpConfigError} — composition/lifecycle failures (duplicate
7
+ * routes, listen failures, drain timeouts). Thrown from `boot`/`start`/
8
+ * `stop`; the kernel wraps them and their `code`/`remediation`/`docsUrl`
9
+ * propagate into `app.diagnostics.issues`.
10
+ * - {@link HttpError} — per-request failures. Thrown from handlers,
11
+ * derivers, or validation; serialized onto the wire as the coded
12
+ * envelope `{ error: { code, message, remediation?, docsUrl? } }`.
13
+ *
14
+ * Every code is stable API. Match on codes, never parse messages.
15
+ */
16
+
17
+ /**
18
+ * Stable error codes for `@arki/http`. `E0NN` codes surface through the
19
+ * DOT lifecycle (boot/start/stop failures); `E4xx`/`E5xx` codes surface
20
+ * on the wire in the error envelope.
21
+ */
22
+ export const HTTP_ERROR_CODES = {
23
+ /** `start` could not bind the port. */
24
+ listenFailed: 'ARKI_HTTP_E001',
25
+ /** Two bindings claim the same method + path. */
26
+ duplicateRoute: 'ARKI_HTTP_E002',
27
+ /** `stop` exceeded `drainTimeoutMs` with requests still in flight. */
28
+ drainTimeout: 'ARKI_HTTP_E005',
29
+ /** A mount path is malformed. */
30
+ invalidMount: 'ARKI_HTTP_E006',
31
+ /** A deriver key duplicates an earlier deriver or a base context key. */
32
+ duplicateDeriverKey: 'ARKI_HTTP_E007',
33
+ /** Request validation failed (query/body) or the body is not valid JSON. */
34
+ badRequest: 'ARKI_HTTP_E400',
35
+ /** A deriver rejected the request's credentials. */
36
+ unauthorized: 'ARKI_HTTP_E401',
37
+ /** A deriver rejected the request's permissions. */
38
+ forbidden: 'ARKI_HTTP_E403',
39
+ /** No route matches the request. */
40
+ notFound: 'ARKI_HTTP_E404',
41
+ /** Handler crash or output-schema violation. */
42
+ internal: 'ARKI_HTTP_E500',
43
+ } as const;
44
+
45
+ const docsUrlFor = (code: string): string => `https://arki.dev/http/errors/${code.toLowerCase().replaceAll('_', '-')}`;
46
+
47
+ /**
48
+ * A per-request failure with an HTTP status and a stable code. Throw from
49
+ * a handler or deriver to short-circuit into the wire envelope:
50
+ *
51
+ * ```ts
52
+ * throw new HttpError(401, HTTP_ERROR_CODES.unauthorized, 'missing or invalid credentials');
53
+ * ```
54
+ */
55
+ export class HttpError extends Error {
56
+ readonly status: number;
57
+ readonly code: string;
58
+ readonly remediation: string | undefined;
59
+ readonly docsUrl: string | undefined;
60
+ /** Extra response headers — e.g. `www-authenticate` on a 401, `retry-after` on a 429. */
61
+ readonly headers: Readonly<Record<string, string>> | undefined;
62
+
63
+ constructor(
64
+ status: number,
65
+ code: string,
66
+ message: string,
67
+ options: {
68
+ readonly remediation?: string;
69
+ readonly docsUrl?: string;
70
+ readonly headers?: Readonly<Record<string, string>>;
71
+ readonly cause?: unknown;
72
+ } = {},
73
+ ) {
74
+ super(message, options.cause === undefined ? {} : { cause: options.cause });
75
+ this.name = 'HttpError';
76
+ this.status = status;
77
+ this.code = code;
78
+ this.remediation = options.remediation;
79
+ this.docsUrl = options.docsUrl ?? docsUrlFor(code);
80
+ this.headers = options.headers;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * A composition or lifecycle failure. Carries the same code/remediation/
86
+ * docsUrl fields the DOT kernel propagates into diagnostics when a hook
87
+ * throws.
88
+ */
89
+ export class HttpConfigError extends Error {
90
+ readonly code: string;
91
+ readonly remediation: string;
92
+ readonly docsUrl: string;
93
+
94
+ constructor(fields: { readonly code: string; readonly message: string; readonly remediation: string; readonly docsUrl?: string }) {
95
+ super(fields.message);
96
+ this.name = 'HttpConfigError';
97
+ this.code = fields.code;
98
+ this.remediation = fields.remediation;
99
+ this.docsUrl = fields.docsUrl ?? docsUrlFor(fields.code);
100
+ }
101
+ }
102
+
103
+ /** The wire error envelope. Stable shape — agents parse this. */
104
+ export type HttpErrorEnvelope = {
105
+ readonly error: {
106
+ readonly code: string;
107
+ readonly message: string;
108
+ readonly remediation?: string;
109
+ readonly docsUrl?: string;
110
+ };
111
+ };
112
+
113
+ /**
114
+ * Message for an unexpected (non-{@link HttpError}) failure: full detail
115
+ * in development, redacted in production — internals never leak onto the
116
+ * wire of a deployed app.
117
+ */
118
+ export function describeInternalError(error: unknown): string {
119
+ if (process.env.NODE_ENV === 'production') return 'internal error';
120
+ return error instanceof Error ? error.message : String(error);
121
+ }
122
+
123
+ /**
124
+ * Map any thrown value to its wire envelope: {@link HttpError} keeps its
125
+ * status and fields; everything else is a redacted-in-production 500.
126
+ */
127
+ export function toErrorResponse(error: unknown): Response {
128
+ if (error instanceof HttpError) {
129
+ return errorResponse(error.status, error.code, error.message, {
130
+ ...(error.remediation === undefined ? {} : { remediation: error.remediation }),
131
+ ...(error.docsUrl === undefined ? {} : { docsUrl: error.docsUrl }),
132
+ ...(error.headers === undefined ? {} : { headers: error.headers }),
133
+ });
134
+ }
135
+ return errorResponse(500, HTTP_ERROR_CODES.internal, describeInternalError(error));
136
+ }
137
+
138
+ /** Build the envelope `Response` for a coded failure. */
139
+ export function errorResponse(
140
+ status: number,
141
+ code: string,
142
+ message: string,
143
+ options: {
144
+ readonly remediation?: string;
145
+ readonly docsUrl?: string;
146
+ readonly headers?: Readonly<Record<string, string>>;
147
+ } = {},
148
+ ): Response {
149
+ const envelope: HttpErrorEnvelope = {
150
+ error: {
151
+ code,
152
+ message,
153
+ ...(options.remediation === undefined ? {} : { remediation: options.remediation }),
154
+ ...(options.docsUrl === undefined ? {} : { docsUrl: options.docsUrl }),
155
+ },
156
+ };
157
+ return Response.json(envelope, { status, ...(options.headers === undefined ? {} : { headers: options.headers }) });
158
+ }
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `@arki/http` — typed HTTP for ARKI.
3
+ *
4
+ * Route contracts as data ({@link route}), zod-validated handlers bound in
5
+ * a typed bundle builder ({@link routes}), request-scope derivers
6
+ * ({@link derive}), SSE streaming, and deterministic OpenAPI generation
7
+ * ({@link toOpenApi}).
8
+ *
9
+ * The framework-agnostic core lives here — {@link buildEngine} +
10
+ * {@link listen} run a server without any framework. The DOT adapter (the
11
+ * `http()` pip) lives at `@arki/http/dot`; the ambient request context —
12
+ * a deliberate last resort — at `@arki/http/context`.
13
+ */
14
+
15
+ export { route } from './contract.js';
16
+ export type {
17
+ ContractLike,
18
+ HttpMethod,
19
+ ParamsOf,
20
+ PathParams,
21
+ RouteContract,
22
+ SchemaLike,
23
+ SseContract,
24
+ } from './contract.js';
25
+
26
+ export { routes } from './bundle.js';
27
+ export type {
28
+ HandlerResult,
29
+ MountBinding,
30
+ MountMeta,
31
+ MountTransport,
32
+ RouteBinding,
33
+ RouteBundle,
34
+ RouteBundleBuilder,
35
+ RouteHandler,
36
+ RouteInput,
37
+ SseHandler,
38
+ } from './bundle.js';
39
+
40
+ export { BASE_CTX_KEYS, derive } from './derive.js';
41
+ export type { BaseRequestCtx, Deriver, ErasedDeriver } from './derive.js';
42
+
43
+ export { describeInternalError, errorResponse, HTTP_ERROR_CODES, HttpConfigError, HttpError, toErrorResponse } from './error.js';
44
+ export type { HttpErrorEnvelope } from './error.js';
45
+
46
+ export { contractMeta, toOpenApi } from './openapi.js';
47
+ export type { OpenApiInfo, RouteMeta } from './openapi.js';
48
+
49
+ export { buildEngine, composeMiddleware } from './engine.js';
50
+ export type { Engine, HttpMiddleware } from './engine.js';
51
+
52
+ export { listen } from './listen.js';
53
+ export type { ListenHandle, ListenOptions } from './listen.js';
package/src/listen.ts ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The socket layer — runtime-adaptive listening.
3
+ *
4
+ * `Bun.serve` when the Bun global is present, `@hono/node-server`
5
+ * otherwise. Neither runtime's types leak into the public surface; the
6
+ * handle exposes exactly what the `http()` pip's lifecycle needs.
7
+ */
8
+
9
+ import { HTTP_ERROR_CODES, HttpConfigError } from './error.js';
10
+
11
+ export type ListenOptions = {
12
+ readonly port: number;
13
+ readonly hostname?: string;
14
+ };
15
+
16
+ export type ListenHandle = {
17
+ readonly port: number;
18
+ readonly hostname: string;
19
+ /** Stop accepting new connections; in-flight requests continue. */
20
+ stopAccepting(): void;
21
+ /** Sever remaining connections and release the port. */
22
+ forceClose(): void;
23
+ };
24
+
25
+ type FetchLike = (req: Request) => Response | Promise<Response>;
26
+
27
+ type BunServerLike = {
28
+ readonly port: number;
29
+ readonly hostname: string;
30
+ stop(closeActiveConnections?: boolean): void;
31
+ };
32
+
33
+ type BunGlobalLike = {
34
+ serve(options: { readonly port: number; readonly hostname?: string; readonly fetch: FetchLike }): BunServerLike;
35
+ };
36
+
37
+ /** Narrow structural view of the Bun global — `@types/node` has no Bun. */
38
+ function bunGlobal(): BunGlobalLike | undefined {
39
+ return (globalThis as { Bun?: BunGlobalLike }).Bun;
40
+ }
41
+
42
+ function listenFailure(port: number, cause: unknown): HttpConfigError {
43
+ return new HttpConfigError({
44
+ code: HTTP_ERROR_CODES.listenFailed,
45
+ message: `[http] failed to listen on port ${port}: ${cause instanceof Error ? cause.message : String(cause)}`,
46
+ remediation: 'Is the port already in use? Change options.port on http(...) or stop the other process.',
47
+ });
48
+ }
49
+
50
+ export async function listen(fetch: FetchLike, options: ListenOptions): Promise<ListenHandle> {
51
+ const bun = bunGlobal();
52
+ if (bun !== undefined) return listenBun(bun, fetch, options);
53
+ return listenNode(fetch, options);
54
+ }
55
+
56
+ function listenBun(bun: BunGlobalLike, fetch: FetchLike, options: ListenOptions): ListenHandle {
57
+ let server: BunServerLike;
58
+ try {
59
+ server = bun.serve({
60
+ port: options.port,
61
+ fetch,
62
+ ...(options.hostname === undefined ? {} : { hostname: options.hostname }),
63
+ });
64
+ } catch (error) {
65
+ throw listenFailure(options.port, error);
66
+ }
67
+ return {
68
+ port: server.port,
69
+ hostname: server.hostname,
70
+ stopAccepting: () => {
71
+ server.stop(false);
72
+ },
73
+ forceClose: () => {
74
+ server.stop(true);
75
+ },
76
+ };
77
+ }
78
+
79
+ async function listenNode(fetch: FetchLike, options: ListenOptions): Promise<ListenHandle> {
80
+ const { serve } = await import('@hono/node-server');
81
+ return await new Promise<ListenHandle>((resolve, reject) => {
82
+ const server = serve({ fetch, port: options.port, hostname: options.hostname ?? '0.0.0.0' }, info => {
83
+ resolve({
84
+ port: info.port,
85
+ hostname: info.address,
86
+ stopAccepting: () => {
87
+ server.close();
88
+ },
89
+ forceClose: () => {
90
+ const closable = server as { close(): unknown; closeAllConnections?(): void };
91
+ closable.closeAllConnections?.();
92
+ closable.close();
93
+ },
94
+ });
95
+ });
96
+ server.on('error', (error: unknown) => {
97
+ reject(listenFailure(options.port, error));
98
+ });
99
+ });
100
+ }