@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.
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/bundle.d.ts +121 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +98 -0
- package/dist/bundle.js.map +1 -0
- package/dist/context.d.ts +29 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +34 -0
- package/dist/context.js.map +1 -0
- package/dist/contract.d.ts +120 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +49 -0
- package/dist/contract.js.map +1 -0
- package/dist/derive.d.ts +58 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/derive.js +28 -0
- package/dist/derive.js.map +1 -0
- package/dist/dot.d.ts +106 -0
- package/dist/dot.d.ts.map +1 -0
- package/dist/dot.js +210 -0
- package/dist/dot.js.map +1 -0
- package/dist/engine.d.ts +36 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +173 -0
- package/dist/engine.js.map +1 -0
- package/dist/error.d.ts +107 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +122 -0
- package/dist/error.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/listen.d.ts +23 -0
- package/dist/listen.d.ts.map +1 -0
- package/dist/listen.js +71 -0
- package/dist/listen.js.map +1 -0
- package/dist/openapi.d.ts +45 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +140 -0
- package/dist/openapi.js.map +1 -0
- package/dist/sse.d.ts +12 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +86 -0
- package/dist/sse.js.map +1 -0
- package/package.json +73 -0
- package/src/bundle.ts +221 -0
- package/src/context.ts +39 -0
- package/src/contract.ts +152 -0
- package/src/derive.ts +67 -0
- package/src/dot.ts +282 -0
- package/src/engine.ts +239 -0
- package/src/error.ts +158 -0
- package/src/index.ts +53 -0
- package/src/listen.ts +100 -0
- package/src/openapi.ts +168 -0
- package/src/sse.ts +92 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route contracts — the static half of an HTTP surface.
|
|
3
|
+
*
|
|
4
|
+
* A contract is pure data at module scope: method + path + zod schemas +
|
|
5
|
+
* a stable id. No handler, no services, no I/O. That split is what makes
|
|
6
|
+
* three things possible at once:
|
|
7
|
+
*
|
|
8
|
+
* - `configure` can register it in the DOT manifest (configure is sync);
|
|
9
|
+
* - OpenAPI generation is static — no boot required;
|
|
10
|
+
* - the handler signature is fully inferred when the contract is bound
|
|
11
|
+
* (see `routes().bind()` in `bundle.ts`).
|
|
12
|
+
*/
|
|
13
|
+
import type { z } from 'zod';
|
|
14
|
+
/** Methods a {@link RouteContract} can declare. */
|
|
15
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
16
|
+
/**
|
|
17
|
+
* Path-parameter names extracted from a path literal at the type level:
|
|
18
|
+
* `'/orders/:id/items/:itemId'` → `'id' | 'itemId'`.
|
|
19
|
+
*/
|
|
20
|
+
export type PathParams<TPath extends string> = TPath extends `${string}:${infer Rest}` ? Rest extends `${infer Param}/${infer Tail}` ? Param | PathParams<`/${Tail}`> : Rest : never;
|
|
21
|
+
/** Typed view of a path's parameters: every value is a string. */
|
|
22
|
+
export type ParamsOf<TPath extends string> = Readonly<Record<PathParams<TPath>, string>>;
|
|
23
|
+
/**
|
|
24
|
+
* The minimal schema surface the engine needs (`parse` only). Every zod
|
|
25
|
+
* schema satisfies it structurally, which lets mixed-generic contracts
|
|
26
|
+
* live in one array without variance fights. Values MUST be zod schemas —
|
|
27
|
+
* `toOpenApi` converts them via `z.toJSONSchema`.
|
|
28
|
+
*/
|
|
29
|
+
export type SchemaLike = {
|
|
30
|
+
parse(input: unknown): unknown;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* A JSON route contract. Create via {@link route}. The generics carry the
|
|
34
|
+
* validated input/output types into the bound handler's signature.
|
|
35
|
+
*/
|
|
36
|
+
export type RouteContract<TPath extends string = string, TQuery = undefined, TBody = undefined, TOutput = undefined> = {
|
|
37
|
+
readonly kind: 'http';
|
|
38
|
+
/** Stable identifier — lands in the manifest and OpenAPI `operationId`. */
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly method: HttpMethod;
|
|
41
|
+
readonly path: TPath;
|
|
42
|
+
readonly summary?: string;
|
|
43
|
+
readonly query?: z.ZodType<TQuery>;
|
|
44
|
+
readonly body?: z.ZodType<TBody>;
|
|
45
|
+
readonly output?: z.ZodType<TOutput>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* A typed server-sent-events contract. Create via {@link route.sse}. The
|
|
49
|
+
* bound handler is an async generator; every yielded value is validated
|
|
50
|
+
* against `event` and framed as an SSE `data:` message.
|
|
51
|
+
*/
|
|
52
|
+
export type SseContract<TPath extends string = string, TQuery = undefined, TEvent = unknown> = {
|
|
53
|
+
readonly kind: 'sse';
|
|
54
|
+
readonly id: string;
|
|
55
|
+
readonly method: 'GET';
|
|
56
|
+
readonly path: TPath;
|
|
57
|
+
readonly summary?: string;
|
|
58
|
+
readonly query?: z.ZodType<TQuery>;
|
|
59
|
+
/** Schema every yielded event is validated against. */
|
|
60
|
+
readonly event: z.ZodType<TEvent>;
|
|
61
|
+
/** Emit `:keepalive` comments at this interval. Off when omitted. */
|
|
62
|
+
readonly heartbeatMs?: number;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Generics-erased view of a contract — what bundles store and the engine,
|
|
66
|
+
* manifest registration, and OpenAPI generation consume. Both contract
|
|
67
|
+
* types are structurally assignable to it; no casts involved.
|
|
68
|
+
*/
|
|
69
|
+
export type ContractLike = {
|
|
70
|
+
readonly kind: 'http' | 'sse';
|
|
71
|
+
readonly id: string;
|
|
72
|
+
readonly method: HttpMethod;
|
|
73
|
+
readonly path: string;
|
|
74
|
+
readonly summary?: string;
|
|
75
|
+
readonly query?: SchemaLike;
|
|
76
|
+
readonly body?: SchemaLike;
|
|
77
|
+
readonly output?: SchemaLike;
|
|
78
|
+
readonly event?: SchemaLike;
|
|
79
|
+
readonly heartbeatMs?: number;
|
|
80
|
+
};
|
|
81
|
+
type RouteDef<TQuery, TBody, TOutput> = {
|
|
82
|
+
readonly id: string;
|
|
83
|
+
readonly summary?: string;
|
|
84
|
+
readonly query?: z.ZodType<TQuery>;
|
|
85
|
+
readonly body?: z.ZodType<TBody>;
|
|
86
|
+
readonly output?: z.ZodType<TOutput>;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Contract factories, one per method, plus {@link route.sse} for typed
|
|
90
|
+
* event streams.
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* export const listOrders = route.get('/orders', {
|
|
94
|
+
* id: 'orders.list',
|
|
95
|
+
* query: z.object({ status: z.enum(['open', 'shipped']).optional() }),
|
|
96
|
+
* output: z.array(Order),
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* export const progress = route.sse('/orders/:id/progress', {
|
|
100
|
+
* id: 'orders.progress',
|
|
101
|
+
* event: z.discriminatedUnion('type', [Queued, Shipped]),
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export declare const route: {
|
|
106
|
+
get: <TPath extends string, TQuery = undefined, TBody = undefined, TOutput = undefined>(path: TPath, def: RouteDef<TQuery, TBody, TOutput>) => RouteContract<TPath, TQuery, TBody, TOutput>;
|
|
107
|
+
post: <TPath extends string, TQuery = undefined, TBody = undefined, TOutput = undefined>(path: TPath, def: RouteDef<TQuery, TBody, TOutput>) => RouteContract<TPath, TQuery, TBody, TOutput>;
|
|
108
|
+
put: <TPath extends string, TQuery = undefined, TBody = undefined, TOutput = undefined>(path: TPath, def: RouteDef<TQuery, TBody, TOutput>) => RouteContract<TPath, TQuery, TBody, TOutput>;
|
|
109
|
+
patch: <TPath extends string, TQuery = undefined, TBody = undefined, TOutput = undefined>(path: TPath, def: RouteDef<TQuery, TBody, TOutput>) => RouteContract<TPath, TQuery, TBody, TOutput>;
|
|
110
|
+
delete: <TPath extends string, TQuery = undefined, TBody = undefined, TOutput = undefined>(path: TPath, def: RouteDef<TQuery, TBody, TOutput>) => RouteContract<TPath, TQuery, TBody, TOutput>;
|
|
111
|
+
sse: <TPath extends string, TEvent, TQuery = undefined>(path: TPath, def: {
|
|
112
|
+
readonly id: string;
|
|
113
|
+
readonly summary?: string;
|
|
114
|
+
readonly query?: z.ZodType<TQuery>;
|
|
115
|
+
readonly event: z.ZodType<TEvent>;
|
|
116
|
+
readonly heartbeatMs?: number;
|
|
117
|
+
}) => SseContract<TPath, TQuery, TEvent>;
|
|
118
|
+
};
|
|
119
|
+
export {};
|
|
120
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route contracts — the static half of an HTTP surface.
|
|
3
|
+
*
|
|
4
|
+
* A contract is pure data at module scope: method + path + zod schemas +
|
|
5
|
+
* a stable id. No handler, no services, no I/O. That split is what makes
|
|
6
|
+
* three things possible at once:
|
|
7
|
+
*
|
|
8
|
+
* - `configure` can register it in the DOT manifest (configure is sync);
|
|
9
|
+
* - OpenAPI generation is static — no boot required;
|
|
10
|
+
* - the handler signature is fully inferred when the contract is bound
|
|
11
|
+
* (see `routes().bind()` in `bundle.ts`).
|
|
12
|
+
*/
|
|
13
|
+
const makeRoute = (method) => (path, def) => ({
|
|
14
|
+
kind: 'http',
|
|
15
|
+
method,
|
|
16
|
+
path,
|
|
17
|
+
...def,
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Contract factories, one per method, plus {@link route.sse} for typed
|
|
21
|
+
* event streams.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* export const listOrders = route.get('/orders', {
|
|
25
|
+
* id: 'orders.list',
|
|
26
|
+
* query: z.object({ status: z.enum(['open', 'shipped']).optional() }),
|
|
27
|
+
* output: z.array(Order),
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* export const progress = route.sse('/orders/:id/progress', {
|
|
31
|
+
* id: 'orders.progress',
|
|
32
|
+
* event: z.discriminatedUnion('type', [Queued, Shipped]),
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const route = {
|
|
37
|
+
get: makeRoute('GET'),
|
|
38
|
+
post: makeRoute('POST'),
|
|
39
|
+
put: makeRoute('PUT'),
|
|
40
|
+
patch: makeRoute('PATCH'),
|
|
41
|
+
delete: makeRoute('DELETE'),
|
|
42
|
+
sse: (path, def) => ({
|
|
43
|
+
kind: 'sse',
|
|
44
|
+
method: 'GET',
|
|
45
|
+
path,
|
|
46
|
+
...def,
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA0FH,MAAM,SAAS,GACb,CAA6B,MAAe,EAAE,EAAE,CAChD,CACE,IAAW,EACX,GAAqC,EACS,EAAE,CAAC,CAAC;IAClD,IAAI,EAAE,MAAM;IACZ,MAAM;IACN,IAAI;IACJ,GAAG,GAAG;CACP,CAAC,CAAC;AAEL;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,GAAG,EAAE,SAAS,CAAC,KAAK,CAAC;IACrB,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC;IACvB,GAAG,EAAE,SAAS,CAAC,KAAK,CAAC;IACrB,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC;IACzB,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC;IAC3B,GAAG,EAAE,CACH,IAAW,EACX,GAMC,EACmC,EAAE,CAAC,CAAC;QACxC,IAAI,EAAE,KAAK;QACX,MAAM,EAAE,KAAK;QACb,IAAI;QACJ,GAAG,GAAG;KACP,CAAC;CACH,CAAC"}
|
package/dist/derive.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-scope derivers — "scope is data, not containers."
|
|
3
|
+
*
|
|
4
|
+
* A deriver is a named function from `(req, ctx)` to a value; each one
|
|
5
|
+
* adds a typed key to the per-request context handlers receive. Derivers
|
|
6
|
+
* run in declaration order, later derivers see earlier keys typed, and a
|
|
7
|
+
* deriver that throws {@link HttpError} short-circuits into the wire
|
|
8
|
+
* envelope — which is the entire "guard" story:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* const withPrincipal = derive('principal', async req => {
|
|
12
|
+
* const p = await auth.verify(req.headers.get('authorization'));
|
|
13
|
+
* if (!p) throw new HttpError(401, HTTP_ERROR_CODES.unauthorized, 'missing or invalid credentials');
|
|
14
|
+
* return p;
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Because bundles are assembled inside a pip's `boot`, derivers close over
|
|
19
|
+
* injected singletons — request scope is function application over the
|
|
20
|
+
* compile-time-wired service graph, not per-request container resolution.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The context every handler and deriver receives. Derivers extend it with
|
|
24
|
+
* their own typed keys; the base keys are reserved.
|
|
25
|
+
*/
|
|
26
|
+
export type BaseRequestCtx = {
|
|
27
|
+
readonly req: Request;
|
|
28
|
+
readonly url: URL;
|
|
29
|
+
/** Inbound `x-request-id` header, else a fresh UUID. */
|
|
30
|
+
readonly requestId: string;
|
|
31
|
+
/** Aborts when the client disconnects. */
|
|
32
|
+
readonly signal: AbortSignal;
|
|
33
|
+
};
|
|
34
|
+
/** Base context keys — derivers may not shadow these. */
|
|
35
|
+
export declare const BASE_CTX_KEYS: readonly ["req", "url", "requestId", "signal"];
|
|
36
|
+
/**
|
|
37
|
+
* A named, typed context deriver. `TIn` is the context the deriver needs
|
|
38
|
+
* to see — a deriver written against {@link BaseRequestCtx} composes into
|
|
39
|
+
* any bundle; one written against a richer context can only be added
|
|
40
|
+
* after the derivers that provide it.
|
|
41
|
+
*/
|
|
42
|
+
export type Deriver<K extends string = string, V = unknown, TIn extends BaseRequestCtx = BaseRequestCtx> = {
|
|
43
|
+
readonly key: K;
|
|
44
|
+
readonly derive: (req: Request, ctx: TIn) => V | Promise<V>;
|
|
45
|
+
};
|
|
46
|
+
/** Create a reusable {@link Deriver}. */
|
|
47
|
+
export declare function derive<K extends string, V, TIn extends BaseRequestCtx = BaseRequestCtx>(key: K, fn: (req: Request, ctx: TIn) => V | Promise<V>): Deriver<K, V, TIn>;
|
|
48
|
+
/**
|
|
49
|
+
* Generics-erased deriver view for bundle storage. Typed derivers are
|
|
50
|
+
* assignable via parameter contravariance from the bottom type; the
|
|
51
|
+
* engine crosses the erasure boundary at the call site (`ctx as never`),
|
|
52
|
+
* mirroring the DOT kernel's hook-storage pattern.
|
|
53
|
+
*/
|
|
54
|
+
export type ErasedDeriver = {
|
|
55
|
+
readonly key: string;
|
|
56
|
+
readonly derive: (req: Request, ctx: never) => unknown;
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=derive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive.d.ts","sourceRoot":"","sources":["../src/derive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB,wDAAwD;IACxD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B,CAAC;AAEF,yDAAyD;AACzD,eAAO,MAAM,aAAa,gDAAiD,CAAC;AAE5E;;;;;GAKG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,OAAO,EAAE,GAAG,SAAS,cAAc,GAAG,cAAc,IAAI;IACzG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC;IAChB,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC7D,CAAC;AAEF,yCAAyC;AACzC,wBAAgB,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,GAAG,SAAS,cAAc,GAAG,cAAc,EACrF,GAAG,EAAE,CAAC,EACN,EAAE,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAC7C,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAEpB;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,KAAK,OAAO,CAAC;CACxD,CAAC"}
|
package/dist/derive.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-scope derivers — "scope is data, not containers."
|
|
3
|
+
*
|
|
4
|
+
* A deriver is a named function from `(req, ctx)` to a value; each one
|
|
5
|
+
* adds a typed key to the per-request context handlers receive. Derivers
|
|
6
|
+
* run in declaration order, later derivers see earlier keys typed, and a
|
|
7
|
+
* deriver that throws {@link HttpError} short-circuits into the wire
|
|
8
|
+
* envelope — which is the entire "guard" story:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* const withPrincipal = derive('principal', async req => {
|
|
12
|
+
* const p = await auth.verify(req.headers.get('authorization'));
|
|
13
|
+
* if (!p) throw new HttpError(401, HTTP_ERROR_CODES.unauthorized, 'missing or invalid credentials');
|
|
14
|
+
* return p;
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Because bundles are assembled inside a pip's `boot`, derivers close over
|
|
19
|
+
* injected singletons — request scope is function application over the
|
|
20
|
+
* compile-time-wired service graph, not per-request container resolution.
|
|
21
|
+
*/
|
|
22
|
+
/** Base context keys — derivers may not shadow these. */
|
|
23
|
+
export const BASE_CTX_KEYS = ['req', 'url', 'requestId', 'signal'];
|
|
24
|
+
/** Create a reusable {@link Deriver}. */
|
|
25
|
+
export function derive(key, fn) {
|
|
26
|
+
return { key, derive: fn };
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=derive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive.js","sourceRoot":"","sources":["../src/derive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAeH,yDAAyD;AACzD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,CAAU,CAAC;AAa5E,yCAAyC;AACzC,MAAM,UAAU,MAAM,CACpB,GAAM,EACN,EAA8C;IAE9C,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AAC7B,CAAC"}
|
package/dist/dot.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOT adapter for `@arki/http` — the `http()` pip.
|
|
3
|
+
*
|
|
4
|
+
* Mount it LAST. Feature pips publish {@link RouteBundle}s as token
|
|
5
|
+
* services in their `boot`; `http()` derives its `needs` from the tokens
|
|
6
|
+
* you pass, so the app builder's wiring guard enforces — at compile time —
|
|
7
|
+
* that every listed bundle is provided by an earlier pip:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { defineApp } from '@arki/dot';
|
|
11
|
+
* import { http } from '@arki/http/dot';
|
|
12
|
+
*
|
|
13
|
+
* const app = await defineApp('shop-api')
|
|
14
|
+
* .use(db({ url }))
|
|
15
|
+
* .use(ordersPip) // publishes OrdersRoutes
|
|
16
|
+
* .use(billingPip) // publishes BillingRoutes
|
|
17
|
+
* .use(http({ port: 3000, bundles: [OrdersRoutes, BillingRoutes] }))
|
|
18
|
+
* .start();
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Lifecycle mapping — mounting last is what makes the drain order right:
|
|
22
|
+
*
|
|
23
|
+
* - `boot` — collect bundles, build the router, publish `httpServer`.
|
|
24
|
+
* NOT listening yet; all feature boots finish first.
|
|
25
|
+
* - `start` — listen (first among starts — the route table is final).
|
|
26
|
+
* - `stop` — stop accepting, drain in-flight handlers
|
|
27
|
+
* (`drainTimeoutMs`). Reverse order puts this FIRST:
|
|
28
|
+
* ingress stops before feature pips tear down.
|
|
29
|
+
* - `dispose` — sever remaining connections (open SSE streams end here)
|
|
30
|
+
* and release the port.
|
|
31
|
+
*
|
|
32
|
+
* `@arki/dot` is an OPTIONAL peer of `@arki/http`. Importing this adapter
|
|
33
|
+
* without `@arki/dot` installed fails at module load — intentional: the
|
|
34
|
+
* adapter only makes sense inside a DOT app.
|
|
35
|
+
*/
|
|
36
|
+
import type { DotConfigureContext, Pip, Token } from '@arki/dot/pip';
|
|
37
|
+
import type { RouteBundle } from './bundle.js';
|
|
38
|
+
import type { ContractLike } from './contract.js';
|
|
39
|
+
import type { HttpMiddleware } from './engine.js';
|
|
40
|
+
/** A token publishing a {@link RouteBundle} — what `options.bundles` lists. */
|
|
41
|
+
export type BundleToken = Token<RouteBundle, string>;
|
|
42
|
+
/**
|
|
43
|
+
* The service the `http()` pip publishes. `fetch` is the composed handler
|
|
44
|
+
* — also the unit-test seam: call the app without a socket. `port`/`url`
|
|
45
|
+
* are defined once the app has started.
|
|
46
|
+
*/
|
|
47
|
+
export type HttpServer = {
|
|
48
|
+
readonly fetch: (req: Request) => Promise<Response>;
|
|
49
|
+
port(): number | undefined;
|
|
50
|
+
url(): string | undefined;
|
|
51
|
+
/** Requests currently being handled (streams count until their handler returns). */
|
|
52
|
+
inflight(): number;
|
|
53
|
+
};
|
|
54
|
+
/** Services published by the http adapter. */
|
|
55
|
+
export type HttpServices = {
|
|
56
|
+
readonly httpServer: HttpServer;
|
|
57
|
+
};
|
|
58
|
+
export type HttpOptions<TBundles extends readonly BundleToken[]> = {
|
|
59
|
+
/** Port to listen on in `start`. `0` picks an ephemeral port. */
|
|
60
|
+
readonly port: number;
|
|
61
|
+
readonly hostname?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Bundle tokens to serve, in route-table order. Each becomes a `needs`
|
|
64
|
+
* entry — the wiring guard rejects the composition unless an earlier
|
|
65
|
+
* pip publishes it.
|
|
66
|
+
*/
|
|
67
|
+
readonly bundles: TBundles;
|
|
68
|
+
/**
|
|
69
|
+
* App-wide fetch-shaped middleware (first = outermost): CORS, request
|
|
70
|
+
* logging, compression. For per-request values use bundle derivers —
|
|
71
|
+
* they are typed into handler contexts; middleware is not.
|
|
72
|
+
*/
|
|
73
|
+
readonly middleware?: readonly HttpMiddleware[];
|
|
74
|
+
/** Milliseconds `stop` waits for in-flight requests. Default 10 000. */
|
|
75
|
+
readonly drainTimeoutMs?: number;
|
|
76
|
+
};
|
|
77
|
+
/** Wire-needs record derived from the bundle tokens. */
|
|
78
|
+
export type BundleNeeds<TBundles extends readonly BundleToken[]> = {
|
|
79
|
+
readonly [Tok in TBundles[number] as Tok extends Token<RouteBundle, infer K> ? K : never]: RouteBundle;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Stable error codes thrown by the http pip beyond the engine's own.
|
|
83
|
+
* Exported so consumers and coding agents can match against them.
|
|
84
|
+
*/
|
|
85
|
+
export declare const HTTP_PIP_ERROR_CODES: {
|
|
86
|
+
/** A bundle token's service was missing from the boot context (erased composition only). */
|
|
87
|
+
readonly bundleMissing: "ARKI_HTTP_E008";
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Build the DOT pip that serves the given bundles. See the module docs
|
|
91
|
+
* for the composition pattern and lifecycle mapping.
|
|
92
|
+
*/
|
|
93
|
+
export declare function http<const TBundles extends readonly BundleToken[]>(options: HttpOptions<TBundles>): Pip<BundleNeeds<TBundles>, HttpServices>;
|
|
94
|
+
/**
|
|
95
|
+
* Register contracts in the DOT manifest from a feature pip's `configure`
|
|
96
|
+
* hook. Converts zod schemas to JSON Schema once, at configure time, so
|
|
97
|
+
* `dot explain --openapi` renders without booting:
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* configure(ctx) {
|
|
101
|
+
* registerRoutes(ctx, [listOrders, createOrder, progress]);
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export declare function registerRoutes(ctx: DotConfigureContext, contracts: readonly ContractLike[]): void;
|
|
106
|
+
//# sourceMappingURL=dot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dot.d.ts","sourceRoot":"","sources":["../src/dot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAc,GAAG,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAGjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAOlD,+EAA+E;AAC/E,MAAM,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;AAErD;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpD,IAAI,IAAI,MAAM,GAAG,SAAS,CAAC;IAC3B,GAAG,IAAI,MAAM,GAAG,SAAS,CAAC;IAC1B,oFAAoF;IACpF,QAAQ,IAAI,MAAM,CAAC;CACpB,CAAC;AAEF,8CAA8C;AAC9C,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,QAAQ,SAAS,SAAS,WAAW,EAAE,IAAI;IACjE,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAChD,wEAAwE;IACxE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC,CAAC;AAEF,wDAAwD;AACxD,MAAM,MAAM,WAAW,CAAC,QAAQ,SAAS,SAAS,WAAW,EAAE,IAAI;IACjE,QAAQ,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,GAAG,SAAS,KAAK,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,WAAW;CACvG,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB;IAC/B,4FAA4F;;CAEpF,CAAC;AAmCX;;;GAGG;AACH,wBAAgB,IAAI,CAAC,KAAK,CAAC,QAAQ,SAAS,SAAS,WAAW,EAAE,EAChE,OAAO,EAAE,WAAW,CAAC,QAAQ,CAAC,GAC7B,GAAG,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC,CA4H1C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,mBAAmB,EAAE,SAAS,EAAE,SAAS,YAAY,EAAE,GAAG,IAAI,CAEjG"}
|
package/dist/dot.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOT adapter for `@arki/http` — the `http()` pip.
|
|
3
|
+
*
|
|
4
|
+
* Mount it LAST. Feature pips publish {@link RouteBundle}s as token
|
|
5
|
+
* services in their `boot`; `http()` derives its `needs` from the tokens
|
|
6
|
+
* you pass, so the app builder's wiring guard enforces — at compile time —
|
|
7
|
+
* that every listed bundle is provided by an earlier pip:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { defineApp } from '@arki/dot';
|
|
11
|
+
* import { http } from '@arki/http/dot';
|
|
12
|
+
*
|
|
13
|
+
* const app = await defineApp('shop-api')
|
|
14
|
+
* .use(db({ url }))
|
|
15
|
+
* .use(ordersPip) // publishes OrdersRoutes
|
|
16
|
+
* .use(billingPip) // publishes BillingRoutes
|
|
17
|
+
* .use(http({ port: 3000, bundles: [OrdersRoutes, BillingRoutes] }))
|
|
18
|
+
* .start();
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Lifecycle mapping — mounting last is what makes the drain order right:
|
|
22
|
+
*
|
|
23
|
+
* - `boot` — collect bundles, build the router, publish `httpServer`.
|
|
24
|
+
* NOT listening yet; all feature boots finish first.
|
|
25
|
+
* - `start` — listen (first among starts — the route table is final).
|
|
26
|
+
* - `stop` — stop accepting, drain in-flight handlers
|
|
27
|
+
* (`drainTimeoutMs`). Reverse order puts this FIRST:
|
|
28
|
+
* ingress stops before feature pips tear down.
|
|
29
|
+
* - `dispose` — sever remaining connections (open SSE streams end here)
|
|
30
|
+
* and release the port.
|
|
31
|
+
*
|
|
32
|
+
* `@arki/dot` is an OPTIONAL peer of `@arki/http`. Importing this adapter
|
|
33
|
+
* without `@arki/dot` installed fails at module load — intentional: the
|
|
34
|
+
* adapter only makes sense inside a DOT app.
|
|
35
|
+
*/
|
|
36
|
+
import { DotPipError, pip } from '@arki/dot/pip';
|
|
37
|
+
import { buildEngine, composeMiddleware } from './engine.js';
|
|
38
|
+
import { HTTP_ERROR_CODES, HttpConfigError } from './error.js';
|
|
39
|
+
import { listen } from './listen.js';
|
|
40
|
+
import { contractMeta } from './openapi.js';
|
|
41
|
+
/**
|
|
42
|
+
* Stable error codes thrown by the http pip beyond the engine's own.
|
|
43
|
+
* Exported so consumers and coding agents can match against them.
|
|
44
|
+
*/
|
|
45
|
+
export const HTTP_PIP_ERROR_CODES = {
|
|
46
|
+
/** A bundle token's service was missing from the boot context (erased composition only). */
|
|
47
|
+
bundleMissing: 'ARKI_HTTP_E008',
|
|
48
|
+
};
|
|
49
|
+
const DEFAULT_DRAIN_TIMEOUT_MS = 10_000;
|
|
50
|
+
function drained(gauge, timeoutMs) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
if (gauge.count === 0) {
|
|
53
|
+
resolve();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const timer = setTimeout(() => {
|
|
57
|
+
reject(new Error(`drain timeout after ${String(timeoutMs)}ms`));
|
|
58
|
+
}, timeoutMs);
|
|
59
|
+
timer.unref();
|
|
60
|
+
gauge.waiters.push(() => {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
resolve();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function displayHost(hostname) {
|
|
67
|
+
return hostname === '0.0.0.0' || hostname === '::' ? 'localhost' : hostname;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build the DOT pip that serves the given bundles. See the module docs
|
|
71
|
+
* for the composition pattern and lifecycle mapping.
|
|
72
|
+
*/
|
|
73
|
+
export function http(options) {
|
|
74
|
+
const needs = {};
|
|
75
|
+
for (const tok of options.bundles) {
|
|
76
|
+
if (needs[tok.key] !== undefined) {
|
|
77
|
+
throw new HttpConfigError({
|
|
78
|
+
code: HTTP_ERROR_CODES.duplicateRoute,
|
|
79
|
+
message: `[http] bundle token "${tok.key}" is listed twice in options.bundles.`,
|
|
80
|
+
remediation: 'List each bundle token once. To mount one bundle at two places, rename the publishing pip.',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
needs[tok.key] = tok;
|
|
84
|
+
}
|
|
85
|
+
const state = { tracked: undefined, handle: undefined };
|
|
86
|
+
const gauge = { count: 0, waiters: [] };
|
|
87
|
+
const inner = pip({
|
|
88
|
+
name: 'http',
|
|
89
|
+
version: '0.0.1',
|
|
90
|
+
// Compile-time/runtime seam: the precise needs shape is derived from
|
|
91
|
+
// `options.bundles` in the return-type cast below; here the runtime
|
|
92
|
+
// record rides in under the EmptyShape default. The kernel re-checks
|
|
93
|
+
// satisfaction at boot (DOT_LIFECYCLE_E012) for erased composition.
|
|
94
|
+
needs,
|
|
95
|
+
configure(ctx) {
|
|
96
|
+
ctx.registerService('httpServer', 'custom');
|
|
97
|
+
},
|
|
98
|
+
boot(ctx) {
|
|
99
|
+
// Erasure boundary: needs were declared dynamically above, so the
|
|
100
|
+
// typed hook context can't know the bundle keys — the tokens do.
|
|
101
|
+
const record = ctx;
|
|
102
|
+
const bundles = [];
|
|
103
|
+
for (const tok of options.bundles) {
|
|
104
|
+
const bundle = record[tok.key];
|
|
105
|
+
if (bundle === undefined) {
|
|
106
|
+
throw new DotPipError({
|
|
107
|
+
code: HTTP_PIP_ERROR_CODES.bundleMissing,
|
|
108
|
+
message: `[http] bundle "${tok.key}" was not provided by any earlier pip.`,
|
|
109
|
+
remediation: 'Mount the pip that publishes this bundle before http(...). The typed builder enforces this; erased composition reaches this check.',
|
|
110
|
+
docsUrl: 'https://arki.dev/http/errors/arki-http-e008',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
bundles.push(bundle);
|
|
114
|
+
}
|
|
115
|
+
const engine = buildEngine(bundles);
|
|
116
|
+
const composed = composeMiddleware(engine.fetch, options.middleware ?? []);
|
|
117
|
+
const tracked = async (req) => {
|
|
118
|
+
gauge.count += 1;
|
|
119
|
+
try {
|
|
120
|
+
return await composed(req);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
gauge.count -= 1;
|
|
124
|
+
if (gauge.count === 0) {
|
|
125
|
+
const waiters = gauge.waiters.splice(0);
|
|
126
|
+
for (const waiter of waiters)
|
|
127
|
+
waiter();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
state.tracked = tracked;
|
|
132
|
+
const httpServer = {
|
|
133
|
+
fetch: tracked,
|
|
134
|
+
port: () => state.handle?.port,
|
|
135
|
+
url: () => {
|
|
136
|
+
const handle = state.handle;
|
|
137
|
+
return handle === undefined
|
|
138
|
+
? undefined
|
|
139
|
+
: `http://${displayHost(handle.hostname)}:${String(handle.port)}`;
|
|
140
|
+
},
|
|
141
|
+
inflight: () => gauge.count,
|
|
142
|
+
};
|
|
143
|
+
return { httpServer };
|
|
144
|
+
},
|
|
145
|
+
async start() {
|
|
146
|
+
const tracked = state.tracked;
|
|
147
|
+
if (tracked === undefined) {
|
|
148
|
+
throw new DotPipError({
|
|
149
|
+
code: HTTP_PIP_ERROR_CODES.bundleMissing,
|
|
150
|
+
message: '[http] start ran without a booted engine — kernel contract violation.',
|
|
151
|
+
remediation: 'This should be unreachable; boot always precedes start. Report it.',
|
|
152
|
+
docsUrl: 'https://arki.dev/http/errors/arki-http-e008',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
state.handle = await listen(tracked, {
|
|
156
|
+
port: options.port,
|
|
157
|
+
...(options.hostname === undefined ? {} : { hostname: options.hostname }),
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
async stop() {
|
|
161
|
+
const handle = state.handle;
|
|
162
|
+
if (handle === undefined)
|
|
163
|
+
return;
|
|
164
|
+
handle.stopAccepting();
|
|
165
|
+
const timeoutMs = options.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
166
|
+
try {
|
|
167
|
+
await drained(gauge, timeoutMs);
|
|
168
|
+
state.handle = undefined;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
const remaining = gauge.count;
|
|
172
|
+
handle.forceClose();
|
|
173
|
+
state.handle = undefined;
|
|
174
|
+
throw new DotPipError({
|
|
175
|
+
code: HTTP_ERROR_CODES.drainTimeout,
|
|
176
|
+
message: `[http] ${String(remaining)} request(s) still in flight after the ${String(timeoutMs)}ms drain budget — connections were severed.`,
|
|
177
|
+
remediation: 'Long-running handlers or streams outlived the drain budget. Raise drainTimeoutMs on http(...), or make handlers respect ctx.signal.',
|
|
178
|
+
docsUrl: 'https://arki.dev/http/errors/arki-http-e005',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
dispose() {
|
|
183
|
+
if (state.handle !== undefined) {
|
|
184
|
+
state.handle.forceClose();
|
|
185
|
+
state.handle = undefined;
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
// Erasure seam: `inner` is Pip<{}, HttpServices> because the needs shape
|
|
190
|
+
// was runtime-built; the cast re-attaches the token-derived needs so the
|
|
191
|
+
// app builder's guard checks bundle provision. Same seam as rename()/
|
|
192
|
+
// testPip() in the kernel.
|
|
193
|
+
return inner;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Register contracts in the DOT manifest from a feature pip's `configure`
|
|
197
|
+
* hook. Converts zod schemas to JSON Schema once, at configure time, so
|
|
198
|
+
* `dot explain --openapi` renders without booting:
|
|
199
|
+
*
|
|
200
|
+
* ```ts
|
|
201
|
+
* configure(ctx) {
|
|
202
|
+
* registerRoutes(ctx, [listOrders, createOrder, progress]);
|
|
203
|
+
* }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export function registerRoutes(ctx, contracts) {
|
|
207
|
+
for (const contract of contracts)
|
|
208
|
+
ctx.registerRoute(contractMeta(contract));
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=dot.js.map
|
package/dist/dot.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dot.js","sourceRoot":"","sources":["../src/dot.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
import type { RouteBundle } from './bundle.js';
|
|
10
|
+
/** A built engine: the composed fetch handler plus its route inventory. */
|
|
11
|
+
export type Engine = {
|
|
12
|
+
readonly fetch: (req: Request) => Promise<Response>;
|
|
13
|
+
/** Contract ids + mount ids, in registration order. */
|
|
14
|
+
readonly routeIds: readonly string[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Compose bundles into a single fetch handler. Throws
|
|
18
|
+
* `ARKI_HTTP_E002`/`E006` on route collisions and malformed mounts —
|
|
19
|
+
* callers (the `http()` pip's `boot`) let these bubble as boot failures.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildEngine(bundles: readonly RouteBundle[]): Engine;
|
|
22
|
+
/**
|
|
23
|
+
* App-wide middleware: a fetch-shaped wrapper. The first middleware in a
|
|
24
|
+
* list runs outermost. Use for cross-cutting concerns that must see or
|
|
25
|
+
* touch the raw request/response — CORS, request logging, compression.
|
|
26
|
+
* For per-request *values* (auth principals, tenant handles), use
|
|
27
|
+
* derivers — they are typed into handler contexts; middleware is not.
|
|
28
|
+
*/
|
|
29
|
+
export type HttpMiddleware = (req: Request, next: (req: Request) => Promise<Response>) => Response | Promise<Response>;
|
|
30
|
+
/**
|
|
31
|
+
* Wrap a fetch handler in middleware (first = outermost). A middleware
|
|
32
|
+
* that throws resolves to the coded error envelope — `HttpError` keeps
|
|
33
|
+
* its status, anything else is a 500.
|
|
34
|
+
*/
|
|
35
|
+
export declare function composeMiddleware(fetch: (req: Request) => Promise<Response>, middleware: readonly HttpMiddleware[]): (req: Request) => Promise<Response>;
|
|
36
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAA8B,WAAW,EAAE,MAAM,aAAa,CAAC;AAO3E,2EAA2E;AAC3E,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpD,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,CAyBnE;AAuJD;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvH;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAC1C,UAAU,EAAE,SAAS,cAAc,EAAE,GACpC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAcrC"}
|