@nifrajs/core 1.4.0 → 1.5.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.
- package/dist/diff.d.ts +66 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +264 -0
- package/dist/diff.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/reflection.d.ts +2 -0
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +1 -0
- package/dist/reflection.js.map +1 -1
- package/dist/server/context.d.ts +8 -0
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/contract.d.ts +2 -0
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js.map +1 -1
- package/dist/server/registry.d.ts +8 -0
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/server.d.ts +40 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +88 -4
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts +16 -0
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +13 -0
- package/dist/server/sse.js.map +1 -1
- package/package.json +6 -1
- package/src/diff.ts +383 -0
- package/src/index.ts +5 -1
- package/src/reflection.ts +3 -0
- package/src/server/context.ts +8 -0
- package/src/server/contract.ts +5 -0
- package/src/server/registry.ts +9 -0
- package/src/server/server.ts +125 -4
- package/src/server/sse.ts +29 -0
package/dist/server/sse.d.ts
CHANGED
|
@@ -50,5 +50,21 @@ export interface SSEInit {
|
|
|
50
50
|
/** Send a `: ` comment ping every N ms so idle proxies don't drop the connection. */
|
|
51
51
|
readonly keepAlive?: number;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The stream handed to an `app.sse()` handler: `send` takes the route's TYPED event payload and
|
|
55
|
+
* serializes it (JSON) into the SSE `data:` field — the compile-time half of the `sse` contract.
|
|
56
|
+
*/
|
|
57
|
+
export interface TypedSSEStream<Event> {
|
|
58
|
+
/** Send one typed event. `init` sets the optional SSE frame fields (`event`, `id`, `retry`). */
|
|
59
|
+
send(event: Event, init?: Pick<SSEMessage, "event" | "id" | "retry">): void;
|
|
60
|
+
/** Send a `: comment` line (e.g. an explicit ping). */
|
|
61
|
+
comment(text: string): void;
|
|
62
|
+
/** End the stream now. */
|
|
63
|
+
close(): void;
|
|
64
|
+
/** Aborts when the client disconnects — use it to tear down subscriptions/loops. */
|
|
65
|
+
readonly signal: AbortSignal;
|
|
66
|
+
}
|
|
67
|
+
/** Wrap a raw {@link SSEStream} in the typed, JSON-serializing surface `app.sse()` hands out. */
|
|
68
|
+
export declare function typedSSEStream<Event>(stream: SSEStream): TypedSSEStream<Event>;
|
|
53
69
|
export declare function sse(c: SSEContext, run: (stream: SSEStream) => void | Promise<void>, init?: SSEInit): Response;
|
|
54
70
|
//# sourceMappingURL=sse.d.ts.map
|
package/dist/server/sse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/server/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,4GAA4G;AAC5G,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;IACpB,kEAAkE;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,mDAAmD;IACnD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,+CAA+C;AAC/C,MAAM,WAAW,SAAS;IACxB,+DAA+D;IAC/D,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAA;IAC/B,0BAA0B;IAC1B,KAAK,IAAI,IAAI,CAAA;IACb,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAC7B;AAED,8FAA8F;AAC9F,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;CACtB;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IAGxB,QAAQ,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;IAC3D,qFAAqF;IACrF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAwBD,wBAAgB,GAAG,CACjB,CAAC,EAAE,UAAU,EACb,GAAG,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAChD,IAAI,GAAE,OAAY,GACjB,QAAQ,CA2EV"}
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/server/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,4GAA4G;AAC5G,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;IACpB,kEAAkE;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,mDAAmD;IACnD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,+CAA+C;AAC/C,MAAM,WAAW,SAAS;IACxB,+DAA+D;IAC/D,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAA;IAC/B,0BAA0B;IAC1B,KAAK,IAAI,IAAI,CAAA;IACb,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAC7B;AAED,8FAA8F;AAC9F,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;CACtB;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IAGxB,QAAQ,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;IAC3D,qFAAqF;IACrF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc,CAAC,KAAK;IACnC,gGAAgG;IAChG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,GAAG,IAAI,CAAA;IAC3E,uDAAuD;IACvD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,0BAA0B;IAC1B,KAAK,IAAI,IAAI,CAAA;IACb,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAC7B;AAED,iGAAiG;AACjG,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,GAAG,cAAc,CAAC,KAAK,CAAC,CAW9E;AAwBD,wBAAgB,GAAG,CACjB,CAAC,EAAE,UAAU,EACb,GAAG,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAChD,IAAI,GAAE,OAAY,GACjB,QAAQ,CA2EV"}
|
package/dist/server/sse.js
CHANGED
|
@@ -21,6 +21,19 @@
|
|
|
21
21
|
* stream.signal.addEventListener("abort", () => { off(); resolve() }, { once: true }))
|
|
22
22
|
* }, { keepAlive: 15_000 }))
|
|
23
23
|
*/
|
|
24
|
+
/** Wrap a raw {@link SSEStream} in the typed, JSON-serializing surface `app.sse()` hands out. */
|
|
25
|
+
export function typedSSEStream(stream) {
|
|
26
|
+
return {
|
|
27
|
+
send(event, init = {}) {
|
|
28
|
+
stream.send({ ...init, data: JSON.stringify(event) });
|
|
29
|
+
},
|
|
30
|
+
comment(text) {
|
|
31
|
+
stream.send({ comment: text });
|
|
32
|
+
},
|
|
33
|
+
close: stream.close,
|
|
34
|
+
signal: stream.signal,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
24
37
|
const CRLF = /[\r\n]/g;
|
|
25
38
|
const LINE_SPLIT = /\r\n|\r|\n/;
|
|
26
39
|
/** `event:`/`id:`/`comment` must be single-line — stripping CR/LF prevents SSE frame injection. */
|
package/dist/server/sse.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/server/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;
|
|
1
|
+
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/server/sse.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Bun-native, contract-first HTTP framework — the router, server, and route descriptor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
"bun": "./src/reflection.ts",
|
|
37
37
|
"types": "./dist/reflection.d.ts",
|
|
38
38
|
"default": "./dist/reflection.js"
|
|
39
|
+
},
|
|
40
|
+
"./diff": {
|
|
41
|
+
"bun": "./src/diff.ts",
|
|
42
|
+
"types": "./dist/diff.d.ts",
|
|
43
|
+
"default": "./dist/diff.js"
|
|
39
44
|
}
|
|
40
45
|
},
|
|
41
46
|
"files": [
|
package/src/diff.ts
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API breaking-change detection over route reflection.
|
|
3
|
+
*
|
|
4
|
+
* `snapshotRoutes` turns an app (or reflected descriptors) into a plain-JSON snapshot — the
|
|
5
|
+
* `standard` validators are dropped, so a snapshot survives `JSON.stringify` and can be committed
|
|
6
|
+
* as a CI baseline. `diffRouteSnapshots` compares two snapshots and classifies every change by
|
|
7
|
+
* wire compatibility, direction-aware:
|
|
8
|
+
*
|
|
9
|
+
* request direction (body/query — what clients SEND): a new required field, a removed field, or
|
|
10
|
+
* a narrowed type breaks existing callers; widening (new optional field, enum superset,
|
|
11
|
+
* required→optional) is compatible.
|
|
12
|
+
* response direction (response/sse/errors — what clients RECEIVE): a removed field, a field made
|
|
13
|
+
* optional, or a widened type breaks existing readers; narrowing (new field, optional→required,
|
|
14
|
+
* enum subset) is compatible.
|
|
15
|
+
*
|
|
16
|
+
* The classifier FAILS CLOSED: a schema change it cannot prove compatible is reported as breaking.
|
|
17
|
+
* Schemas without JSON Schema metadata (validation-only Standard Schemas) cannot be compared and
|
|
18
|
+
* yield `info` — never a silent pass presented as proof.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
type JsonSchema,
|
|
23
|
+
type ReflectedRoute,
|
|
24
|
+
type ReflectedSchemaField,
|
|
25
|
+
reflectRoutes,
|
|
26
|
+
} from "./reflection.ts"
|
|
27
|
+
|
|
28
|
+
/** One schema position in a snapshot: JSON Schema metadata only, no validator. */
|
|
29
|
+
export interface SchemaSnapshot {
|
|
30
|
+
readonly jsonSchema?: JsonSchema
|
|
31
|
+
readonly fields?: readonly ReflectedSchemaField[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RouteSnapshotSchema {
|
|
35
|
+
readonly body?: SchemaSnapshot
|
|
36
|
+
readonly query?: SchemaSnapshot
|
|
37
|
+
readonly response?: SchemaSnapshot
|
|
38
|
+
readonly sse?: SchemaSnapshot
|
|
39
|
+
readonly errors?: Readonly<Record<string, SchemaSnapshot>>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One route in a snapshot — plain JSON, safe to persist as a CI baseline. */
|
|
43
|
+
export interface RouteSnapshot {
|
|
44
|
+
readonly method: string
|
|
45
|
+
readonly path: string
|
|
46
|
+
readonly schema?: RouteSnapshotSchema
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type DiffSeverity = "breaking" | "compatible" | "info"
|
|
50
|
+
|
|
51
|
+
export interface RouteChange {
|
|
52
|
+
readonly severity: DiffSeverity
|
|
53
|
+
readonly method: string
|
|
54
|
+
readonly path: string
|
|
55
|
+
/** Which part of the contract changed; "route" for add/remove of the whole route. */
|
|
56
|
+
readonly section: "route" | "body" | "query" | "response" | "sse" | "errors"
|
|
57
|
+
/** The top-level field (or error status) the change is about, when field-granular. */
|
|
58
|
+
readonly field?: string
|
|
59
|
+
readonly message: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RoutesDiff {
|
|
63
|
+
readonly changes: readonly RouteChange[]
|
|
64
|
+
/** True when any change is `breaking` — the CI-gate signal. */
|
|
65
|
+
readonly hasBreaking: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const schemaSnapshot = (
|
|
69
|
+
reflection:
|
|
70
|
+
| {
|
|
71
|
+
readonly jsonSchema?: JsonSchema | undefined
|
|
72
|
+
readonly fields?: readonly ReflectedSchemaField[] | undefined
|
|
73
|
+
}
|
|
74
|
+
| undefined,
|
|
75
|
+
): SchemaSnapshot | undefined => {
|
|
76
|
+
if (reflection === undefined) return undefined
|
|
77
|
+
return {
|
|
78
|
+
...(reflection.jsonSchema !== undefined ? { jsonSchema: reflection.jsonSchema } : {}),
|
|
79
|
+
...(reflection.fields !== undefined ? { fields: reflection.fields } : {}),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Snapshot an app's routes (anything `reflectRoutes` accepts) as plain JSON. Validators are
|
|
85
|
+
* dropped; only introspectable JSON Schema metadata is kept, so the result round-trips through
|
|
86
|
+
* `JSON.stringify` unchanged.
|
|
87
|
+
*/
|
|
88
|
+
export function snapshotRoutes(source: unknown): readonly RouteSnapshot[] {
|
|
89
|
+
return reflectRoutes(source).map((route) => snapshotRoute(route))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const snapshotRoute = (route: ReflectedRoute): RouteSnapshot => {
|
|
93
|
+
const schema = route.schema
|
|
94
|
+
if (schema === undefined) return { method: route.method, path: route.path }
|
|
95
|
+
const errors: Record<string, SchemaSnapshot> = {}
|
|
96
|
+
for (const [status, reflection] of Object.entries(schema.errors ?? {})) {
|
|
97
|
+
const snapped = schemaSnapshot(reflection)
|
|
98
|
+
if (snapped !== undefined) errors[status] = snapped
|
|
99
|
+
}
|
|
100
|
+
const body = schemaSnapshot(schema.body)
|
|
101
|
+
const query = schemaSnapshot(schema.query)
|
|
102
|
+
const response = schemaSnapshot(schema.response)
|
|
103
|
+
const sse = schemaSnapshot(schema.sse)
|
|
104
|
+
const snapped: RouteSnapshotSchema = {
|
|
105
|
+
...(body !== undefined ? { body } : {}),
|
|
106
|
+
...(query !== undefined ? { query } : {}),
|
|
107
|
+
...(response !== undefined ? { response } : {}),
|
|
108
|
+
...(sse !== undefined ? { sse } : {}),
|
|
109
|
+
...(Object.keys(errors).length > 0 ? { errors } : {}),
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
method: route.method,
|
|
113
|
+
path: route.path,
|
|
114
|
+
...(Object.keys(snapped).length > 0 ? { schema: snapped } : {}),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type Direction = "request" | "response"
|
|
119
|
+
|
|
120
|
+
const jsonEqual = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b)
|
|
121
|
+
|
|
122
|
+
const enumOf = (schema: JsonSchema | undefined): readonly unknown[] | undefined => {
|
|
123
|
+
if (typeof schema !== "object" || schema === null) return undefined
|
|
124
|
+
const candidate = (schema as { enum?: unknown }).enum
|
|
125
|
+
return Array.isArray(candidate) ? candidate : undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const withoutEnum = (schema: JsonSchema): unknown => {
|
|
129
|
+
if (typeof schema !== "object" || schema === null) return schema
|
|
130
|
+
const { enum: _omitted, ...rest } = schema as Record<string, unknown>
|
|
131
|
+
return rest
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const isSuperset = (outer: readonly unknown[], inner: readonly unknown[]): boolean => {
|
|
135
|
+
const keys = new Set(outer.map((value) => JSON.stringify(value)))
|
|
136
|
+
return inner.every((value) => keys.has(JSON.stringify(value)))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Compare two JSON Schemas for one position. Returns the change severity, or `undefined` when
|
|
141
|
+
* they are equivalent. The only refinement proven compatible is an enum-only change in the
|
|
142
|
+
* safe direction (request may widen, response may narrow); everything else fails closed.
|
|
143
|
+
*/
|
|
144
|
+
const compareJsonSchema = (
|
|
145
|
+
direction: Direction,
|
|
146
|
+
before: JsonSchema,
|
|
147
|
+
after: JsonSchema,
|
|
148
|
+
): DiffSeverity | undefined => {
|
|
149
|
+
if (jsonEqual(before, after)) return undefined
|
|
150
|
+
const beforeEnum = enumOf(before)
|
|
151
|
+
const afterEnum = enumOf(after)
|
|
152
|
+
if (
|
|
153
|
+
beforeEnum !== undefined &&
|
|
154
|
+
afterEnum !== undefined &&
|
|
155
|
+
jsonEqual(withoutEnum(before), withoutEnum(after))
|
|
156
|
+
) {
|
|
157
|
+
const compatible =
|
|
158
|
+
direction === "request"
|
|
159
|
+
? isSuperset(afterEnum, beforeEnum)
|
|
160
|
+
: isSuperset(beforeEnum, afterEnum)
|
|
161
|
+
return compatible ? "compatible" : "breaking"
|
|
162
|
+
}
|
|
163
|
+
return "breaking"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface SectionContext {
|
|
167
|
+
readonly method: string
|
|
168
|
+
readonly path: string
|
|
169
|
+
readonly section: RouteChange["section"]
|
|
170
|
+
readonly direction: Direction
|
|
171
|
+
readonly changes: RouteChange[]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const push = (
|
|
175
|
+
ctx: SectionContext,
|
|
176
|
+
severity: DiffSeverity,
|
|
177
|
+
message: string,
|
|
178
|
+
field?: string,
|
|
179
|
+
): void => {
|
|
180
|
+
ctx.changes.push({
|
|
181
|
+
severity,
|
|
182
|
+
method: ctx.method,
|
|
183
|
+
path: ctx.path,
|
|
184
|
+
section: ctx.section,
|
|
185
|
+
...(field !== undefined ? { field } : {}),
|
|
186
|
+
message,
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const fieldMap = (
|
|
191
|
+
snapshot: SchemaSnapshot,
|
|
192
|
+
): ReadonlyMap<string, ReflectedSchemaField> | undefined => {
|
|
193
|
+
if (snapshot.fields === undefined) return undefined
|
|
194
|
+
return new Map(snapshot.fields.map((field) => [field.name, field]))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const diffFields = (ctx: SectionContext, before: SchemaSnapshot, after: SchemaSnapshot): void => {
|
|
198
|
+
const beforeFields = fieldMap(before)
|
|
199
|
+
const afterFields = fieldMap(after)
|
|
200
|
+
if (beforeFields === undefined || afterFields === undefined) {
|
|
201
|
+
// Non-object schemas: compare the whole JSON Schema in one step.
|
|
202
|
+
const severity = compareJsonSchema(
|
|
203
|
+
ctx.direction,
|
|
204
|
+
before.jsonSchema as JsonSchema,
|
|
205
|
+
after.jsonSchema as JsonSchema,
|
|
206
|
+
)
|
|
207
|
+
if (severity !== undefined) push(ctx, severity, `${ctx.section} schema changed`)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
for (const [name, beforeField] of beforeFields) {
|
|
211
|
+
const afterField = afterFields.get(name)
|
|
212
|
+
if (afterField === undefined) {
|
|
213
|
+
// Request: strict validators reject payloads still sending the field. Response: readers lose it.
|
|
214
|
+
push(ctx, "breaking", `field "${name}" removed`, name)
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
if (beforeField.required !== afterField.required) {
|
|
218
|
+
const nowRequired = afterField.required
|
|
219
|
+
const breaking = ctx.direction === "request" ? nowRequired : !nowRequired
|
|
220
|
+
push(
|
|
221
|
+
ctx,
|
|
222
|
+
breaking ? "breaking" : "compatible",
|
|
223
|
+
`field "${name}" is now ${nowRequired ? "required" : "optional"}`,
|
|
224
|
+
name,
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
const severity = compareJsonSchema(ctx.direction, beforeField.schema, afterField.schema)
|
|
228
|
+
if (severity !== undefined) push(ctx, severity, `field "${name}" type changed`, name)
|
|
229
|
+
}
|
|
230
|
+
for (const [name, afterField] of afterFields) {
|
|
231
|
+
if (beforeFields.has(name)) continue
|
|
232
|
+
if (ctx.direction === "request") {
|
|
233
|
+
push(
|
|
234
|
+
ctx,
|
|
235
|
+
afterField.required ? "breaking" : "compatible",
|
|
236
|
+
`${afterField.required ? "required" : "optional"} field "${name}" added`,
|
|
237
|
+
name,
|
|
238
|
+
)
|
|
239
|
+
} else {
|
|
240
|
+
push(ctx, "compatible", `field "${name}" added`, name)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const diffSchemaSection = (
|
|
246
|
+
ctx: SectionContext,
|
|
247
|
+
before: SchemaSnapshot | undefined,
|
|
248
|
+
after: SchemaSnapshot | undefined,
|
|
249
|
+
): void => {
|
|
250
|
+
if (before === undefined && after === undefined) return
|
|
251
|
+
if (before === undefined) {
|
|
252
|
+
// A new request contract rejects payloads old clients send freely; a new response contract
|
|
253
|
+
// only documents what was already returned.
|
|
254
|
+
push(
|
|
255
|
+
ctx,
|
|
256
|
+
ctx.direction === "request" ? "breaking" : "compatible",
|
|
257
|
+
`${ctx.section} schema added`,
|
|
258
|
+
)
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
if (after === undefined) {
|
|
262
|
+
// Dropping a request contract loosens validation; dropping a response contract removes a
|
|
263
|
+
// documented shape clients (and typed consumers) rely on.
|
|
264
|
+
push(
|
|
265
|
+
ctx,
|
|
266
|
+
ctx.direction === "request" ? "compatible" : "breaking",
|
|
267
|
+
`${ctx.section} schema removed`,
|
|
268
|
+
)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
if (before.jsonSchema === undefined || after.jsonSchema === undefined) {
|
|
272
|
+
// Validation-only schemas expose no JSON Schema metadata — nothing provable either way.
|
|
273
|
+
if (before.jsonSchema !== after.jsonSchema || before.fields !== after.fields) {
|
|
274
|
+
push(ctx, "info", `${ctx.section} schema is not introspectable — cannot verify compatibility`)
|
|
275
|
+
}
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
diffFields(ctx, before, after)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const diffErrors = (
|
|
282
|
+
method: string,
|
|
283
|
+
path: string,
|
|
284
|
+
changes: RouteChange[],
|
|
285
|
+
before: Readonly<Record<string, SchemaSnapshot>> | undefined,
|
|
286
|
+
after: Readonly<Record<string, SchemaSnapshot>> | undefined,
|
|
287
|
+
): void => {
|
|
288
|
+
const beforeEntries = before ?? {}
|
|
289
|
+
const afterEntries = after ?? {}
|
|
290
|
+
for (const [status, beforeSchema] of Object.entries(beforeEntries)) {
|
|
291
|
+
const afterSchema = afterEntries[status]
|
|
292
|
+
if (afterSchema === undefined) {
|
|
293
|
+
// Clients parsing this structured error body lose a documented shape.
|
|
294
|
+
changes.push({
|
|
295
|
+
severity: "breaking",
|
|
296
|
+
method,
|
|
297
|
+
path,
|
|
298
|
+
section: "errors",
|
|
299
|
+
field: status,
|
|
300
|
+
message: `error ${status} removed from the contract`,
|
|
301
|
+
})
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
const ctx: SectionContext = { method, path, section: "errors", direction: "response", changes }
|
|
305
|
+
if (beforeSchema.jsonSchema === undefined || afterSchema.jsonSchema === undefined) {
|
|
306
|
+
if (!jsonEqual(beforeSchema, afterSchema)) {
|
|
307
|
+
push(ctx, "info", `error ${status} schema is not introspectable`, status)
|
|
308
|
+
}
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
const severity = compareJsonSchema("response", beforeSchema.jsonSchema, afterSchema.jsonSchema)
|
|
312
|
+
if (severity !== undefined) push(ctx, severity, `error ${status} schema changed`, status)
|
|
313
|
+
}
|
|
314
|
+
for (const status of Object.keys(afterEntries)) {
|
|
315
|
+
if (status in beforeEntries) continue
|
|
316
|
+
changes.push({
|
|
317
|
+
severity: "compatible",
|
|
318
|
+
method,
|
|
319
|
+
path,
|
|
320
|
+
section: "errors",
|
|
321
|
+
field: status,
|
|
322
|
+
message: `error ${status} added to the contract`,
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const routeKey = (route: RouteSnapshot): string => `${route.method.toUpperCase()} ${route.path}`
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Diff two route snapshots (`snapshotRoutes` output, possibly restored from JSON). Every change is
|
|
331
|
+
* classified breaking/compatible/info; `hasBreaking` is the CI-gate bit.
|
|
332
|
+
*/
|
|
333
|
+
export function diffRouteSnapshots(
|
|
334
|
+
before: readonly RouteSnapshot[],
|
|
335
|
+
after: readonly RouteSnapshot[],
|
|
336
|
+
): RoutesDiff {
|
|
337
|
+
const changes: RouteChange[] = []
|
|
338
|
+
const afterByKey = new Map(after.map((route) => [routeKey(route), route]))
|
|
339
|
+
const beforeKeys = new Set(before.map((route) => routeKey(route)))
|
|
340
|
+
for (const beforeRoute of before) {
|
|
341
|
+
const afterRoute = afterByKey.get(routeKey(beforeRoute))
|
|
342
|
+
if (afterRoute === undefined) {
|
|
343
|
+
changes.push({
|
|
344
|
+
severity: "breaking",
|
|
345
|
+
method: beforeRoute.method,
|
|
346
|
+
path: beforeRoute.path,
|
|
347
|
+
section: "route",
|
|
348
|
+
message: "route removed",
|
|
349
|
+
})
|
|
350
|
+
continue
|
|
351
|
+
}
|
|
352
|
+
const method = beforeRoute.method
|
|
353
|
+
const path = beforeRoute.path
|
|
354
|
+
const beforeSchema = beforeRoute.schema ?? {}
|
|
355
|
+
const afterSchema = afterRoute.schema ?? {}
|
|
356
|
+
for (const section of ["body", "query"] as const) {
|
|
357
|
+
diffSchemaSection(
|
|
358
|
+
{ method, path, section, direction: "request", changes },
|
|
359
|
+
beforeSchema[section],
|
|
360
|
+
afterSchema[section],
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
for (const section of ["response", "sse"] as const) {
|
|
364
|
+
diffSchemaSection(
|
|
365
|
+
{ method, path, section, direction: "response", changes },
|
|
366
|
+
beforeSchema[section],
|
|
367
|
+
afterSchema[section],
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
diffErrors(method, path, changes, beforeSchema.errors, afterSchema.errors)
|
|
371
|
+
}
|
|
372
|
+
for (const afterRoute of after) {
|
|
373
|
+
if (beforeKeys.has(routeKey(afterRoute))) continue
|
|
374
|
+
changes.push({
|
|
375
|
+
severity: "compatible",
|
|
376
|
+
method: afterRoute.method,
|
|
377
|
+
path: afterRoute.path,
|
|
378
|
+
section: "route",
|
|
379
|
+
message: "route added",
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
return { changes, hasBreaking: changes.some((change) => change.severity === "breaking") }
|
|
383
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* (it shipped at "0.0.0" through 1.0.0 before those guards existed). Kept narrow (`as const`) so
|
|
13
13
|
* consumers can pin it at the type level — see `test/version.test-d.ts`.
|
|
14
14
|
*/
|
|
15
|
-
export const VERSION = "1.
|
|
15
|
+
export const VERSION = "1.5.0" as const
|
|
16
16
|
|
|
17
17
|
export type Version = typeof VERSION
|
|
18
18
|
|
|
19
|
+
export * from "./diff.ts"
|
|
19
20
|
export { FrameworkError, RouteConfigError, type RouteConfigErrorCode } from "./errors.ts"
|
|
20
21
|
export { FRAMEWORK_NAME, type FrameworkName } from "./internal/brand.ts"
|
|
21
22
|
export * from "./reflection.ts"
|
|
@@ -92,6 +93,7 @@ export {
|
|
|
92
93
|
type OnRequestResult,
|
|
93
94
|
type PromptArgument,
|
|
94
95
|
type PromptMessage,
|
|
96
|
+
type ResponseFinalization,
|
|
95
97
|
type RouteDescriptor,
|
|
96
98
|
type RunningServer,
|
|
97
99
|
type ScheduledController,
|
|
@@ -108,6 +110,8 @@ export {
|
|
|
108
110
|
type SSEMessage,
|
|
109
111
|
type SSEStream,
|
|
110
112
|
sse,
|
|
113
|
+
type TypedSSEStream,
|
|
114
|
+
typedSSEStream,
|
|
111
115
|
} from "./server/sse.ts"
|
|
112
116
|
export {
|
|
113
117
|
type SignatureEncoding,
|
package/src/reflection.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface ReflectedRouteSchema {
|
|
|
35
35
|
readonly query?: SchemaReflection
|
|
36
36
|
readonly response?: SchemaReflection
|
|
37
37
|
readonly errors?: Readonly<Record<string, SchemaReflection>>
|
|
38
|
+
/** The SSE event-payload schema of a typed streaming route (`app.sse()`). */
|
|
39
|
+
readonly sse?: SchemaReflection
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export interface ReflectedRoute {
|
|
@@ -156,6 +158,7 @@ const reflectedRouteSchema = (value: unknown): ReflectedRouteSchema | undefined
|
|
|
156
158
|
...(schema.query !== undefined ? { query: reflectSchema(schema.query) } : {}),
|
|
157
159
|
...(schema.response !== undefined ? { response: reflectSchema(schema.response) } : {}),
|
|
158
160
|
...(Object.keys(reflectedErrors).length > 0 ? { errors: reflectedErrors } : {}),
|
|
161
|
+
...(schema.sse !== undefined ? { sse: reflectSchema(schema.sse) } : {}),
|
|
159
162
|
}
|
|
160
163
|
}
|
|
161
164
|
|
package/src/server/context.ts
CHANGED
|
@@ -39,6 +39,14 @@ export interface RouteSchema {
|
|
|
39
39
|
* to tooling and coding agents. Like `response`, it is **not** validated at runtime (zero hot-path cost);
|
|
40
40
|
* it's a compile-time + introspection contract. Example: `{ 404: NotFound, 409: Conflict }`. */
|
|
41
41
|
readonly errors?: Readonly<Record<number, StandardSchemaV1>>
|
|
42
|
+
/**
|
|
43
|
+
* Optional **SSE event contract**: the Standard Schema of each event's `data` payload on a
|
|
44
|
+
* streaming route (declared via `app.sse()`). Marks the route as a typed event stream — the
|
|
45
|
+
* typed client grows a `.subscribe()` for it, and the schema flows into reflection. Like
|
|
46
|
+
* `response`, it is **not** validated at runtime (zero hot-path cost); the server-side
|
|
47
|
+
* `stream.send()` is compile-time-checked against it instead.
|
|
48
|
+
*/
|
|
49
|
+
readonly sse?: StandardSchemaV1
|
|
42
50
|
/**
|
|
43
51
|
* Hook fired when the request fails `body`/`query` validation, before the handler runs. `kind` says
|
|
44
52
|
* which input failed (`"body"` | `"query"`). Its return value selects one of three outcomes (may be async):
|
package/src/server/contract.ts
CHANGED
|
@@ -92,6 +92,9 @@ type RouteInfoForOp<O extends OperationDef> = {
|
|
|
92
92
|
// The error union from the op's non-2xx `responses` — so a decoupled contract client sees typed error
|
|
93
93
|
// bodies, just like an inline route's `errors`. `unknown` when the op declares no error responses.
|
|
94
94
|
readonly errors: OpErrors<O>
|
|
95
|
+
// Contract operations cannot declare an SSE event contract (yet) — `never` keeps the contract
|
|
96
|
+
// registry mutually assignable with the inline registry's RouteInfoFor (mode conformance).
|
|
97
|
+
readonly sse: never
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
/** Re-key the name-keyed ops into the `path → method → RouteInfo` registry. */
|
|
@@ -191,6 +194,8 @@ export type RegistryFromImpl<C extends ContractShape, H extends HandlersFor<C>>
|
|
|
191
194
|
// The error union from the op's non-2xx `responses` (see OpErrors) — a graduated contract client sees
|
|
192
195
|
// the same typed error bodies as the inline `errors` path.
|
|
193
196
|
readonly errors: OpErrors<C[K]>
|
|
197
|
+
// Mirrors RouteInfoForOp: no SSE contract on contract ops (yet) — mode conformance holds.
|
|
198
|
+
readonly sse: never
|
|
194
199
|
}
|
|
195
200
|
}
|
|
196
201
|
}
|
package/src/server/registry.ts
CHANGED
|
@@ -19,6 +19,9 @@ export interface RouteInfo {
|
|
|
19
19
|
/** Union of the route's declared error-response body types (from `schema.errors`); `unknown` when the
|
|
20
20
|
* route declares none. Surfaced by the typed client as the failure `errorData`. */
|
|
21
21
|
readonly errors?: unknown
|
|
22
|
+
/** The SSE event payload type (from `schema.sse`, declared via `app.sse()`); `never` for ordinary
|
|
23
|
+
* routes. The typed client keys `.subscribe()` availability and its event type off this. */
|
|
24
|
+
readonly sse?: unknown
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
/** The accumulated, type-level map of every route on a Server: path → method → RouteInfo. */
|
|
@@ -51,6 +54,11 @@ type RegistryErrors<S extends RouteSchema> = S extends {
|
|
|
51
54
|
? { [K in keyof E]: E[K] extends StandardSchemaV1 ? InferOutput<E[K]> : never }[keyof E]
|
|
52
55
|
: unknown
|
|
53
56
|
|
|
57
|
+
/** The SSE event payload type from a route's `sse` schema; `never` for ordinary routes. */
|
|
58
|
+
type RegistrySse<S extends RouteSchema> = S extends { sse: infer E extends StandardSchemaV1 }
|
|
59
|
+
? InferOutput<E>
|
|
60
|
+
: never
|
|
61
|
+
|
|
54
62
|
/** Build a {@link RouteInfo} from a route's path, schema, and handler output type. */
|
|
55
63
|
export type RouteInfoFor<Path extends string, S extends RouteSchema, Output> = {
|
|
56
64
|
readonly params: Params<Path>
|
|
@@ -58,6 +66,7 @@ export type RouteInfoFor<Path extends string, S extends RouteSchema, Output> = {
|
|
|
58
66
|
readonly body: RegistryBody<S>
|
|
59
67
|
readonly output: RegistryOutput<S, Output>
|
|
60
68
|
readonly errors: RegistryErrors<S>
|
|
69
|
+
readonly sse: RegistrySse<S>
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
/** The client-visible output of a handler: its awaited return, minus raw `Response`. */
|