@kaito-http/core 3.0.0-beta.16 → 3.0.0-beta.21
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/index.cjs +27 -26
- package/dist/index.d.cts +80 -25
- package/dist/index.d.ts +80 -25
- package/dist/index.js +27 -26
- package/dist/stream/stream.cjs +142 -0
- package/dist/stream/stream.d.cts +39 -0
- package/dist/stream/stream.d.ts +39 -0
- package/dist/stream/stream.js +111 -0
- package/package.json +18 -4
- package/src/error.ts +26 -0
- package/src/index.ts +7 -0
- package/src/request.ts +47 -0
- package/src/response.ts +72 -0
- package/src/route.ts +53 -0
- package/src/router/router.test.ts +269 -0
- package/src/router/router.ts +254 -0
- package/src/router/types.ts +1 -0
- package/src/server.ts +87 -0
- package/src/stream/stream.ts +159 -0
- package/src/util.ts +66 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export class KaitoStreamResponse<R> extends Response {
|
|
2
|
+
constructor(body: ReadableStream<R>) {
|
|
3
|
+
super(body, {
|
|
4
|
+
headers: {
|
|
5
|
+
'Content-Type': 'text/event-stream',
|
|
6
|
+
'Cache-Control': 'no-cache',
|
|
7
|
+
Connection: 'keep-alive',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async *[Symbol.asyncIterator]() {
|
|
13
|
+
for await (const chunk of this.body!) {
|
|
14
|
+
yield chunk;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class KaitoSSEResponse<_ClientType> extends KaitoStreamResponse<string> {}
|
|
20
|
+
|
|
21
|
+
export function stream<R>(body: UnderlyingDefaultSource<R>): KaitoStreamResponse<R> {
|
|
22
|
+
return new KaitoStreamResponse<R>(new ReadableStream(body));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SSEEvent<T, E extends string> = (
|
|
26
|
+
| {
|
|
27
|
+
data: T;
|
|
28
|
+
event?: E | undefined;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
data?: T | undefined;
|
|
32
|
+
event: E;
|
|
33
|
+
}
|
|
34
|
+
) & {
|
|
35
|
+
retry?: number;
|
|
36
|
+
id?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Converts an SSE Event into a string, ready for sending to the client
|
|
41
|
+
* @param event The SSE Event
|
|
42
|
+
* @returns A stringified version
|
|
43
|
+
*/
|
|
44
|
+
export function sseEventToString(event: SSEEvent<unknown, string>): string {
|
|
45
|
+
let result = '';
|
|
46
|
+
|
|
47
|
+
if (event.event) {
|
|
48
|
+
result += `event:${event.event}\n`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (event.id) {
|
|
52
|
+
result += `id:${event.id}\n`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (event.retry) {
|
|
56
|
+
result += `retry:${event.retry}\n`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (event.data !== undefined) {
|
|
60
|
+
result += `data:${JSON.stringify(event.data)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class SSEController<U, E extends string> implements Disposable {
|
|
67
|
+
private readonly controller: ReadableStreamDefaultController<string>;
|
|
68
|
+
|
|
69
|
+
public constructor(controller: ReadableStreamDefaultController<string>) {
|
|
70
|
+
this.controller = controller;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public enqueue(event: SSEEvent<U, E>): void {
|
|
74
|
+
this.controller.enqueue(sseEventToString(event) + '\n\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public close(): void {
|
|
78
|
+
this.controller.close();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[Symbol.dispose](): void {
|
|
82
|
+
this.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface SSESource<U, E extends string> {
|
|
87
|
+
cancel?: UnderlyingSourceCancelCallback;
|
|
88
|
+
start?(controller: SSEController<U, E>): Promise<void>;
|
|
89
|
+
pull?(controller: SSEController<U, E>): Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sseFromSource<U, E extends string>(source: SSESource<U, E>) {
|
|
93
|
+
const start = source.start;
|
|
94
|
+
const pull = source.pull;
|
|
95
|
+
const cancel = source.cancel;
|
|
96
|
+
|
|
97
|
+
const readable = new ReadableStream<string>({
|
|
98
|
+
...(cancel ? {cancel} : {}),
|
|
99
|
+
|
|
100
|
+
...(start
|
|
101
|
+
? {
|
|
102
|
+
start: async controller => {
|
|
103
|
+
await start(new SSEController<U, E>(controller));
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
: {}),
|
|
107
|
+
|
|
108
|
+
...(pull
|
|
109
|
+
? {
|
|
110
|
+
pull: async controller => {
|
|
111
|
+
await pull(new SSEController<U, E>(controller));
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
: {}),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return new KaitoSSEResponse<SSEEvent<U, E>>(readable);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function sse<U, E extends string, T extends SSEEvent<U, E>>(
|
|
121
|
+
source: SSESource<U, E> | AsyncGenerator<T, unknown, unknown> | (() => AsyncGenerator<T, unknown, unknown>),
|
|
122
|
+
): KaitoSSEResponse<T> {
|
|
123
|
+
const evaluated = typeof source === 'function' ? source() : source;
|
|
124
|
+
|
|
125
|
+
if ('next' in evaluated) {
|
|
126
|
+
const generator = evaluated;
|
|
127
|
+
return sseFromSource<U, E>({
|
|
128
|
+
async start(controller) {
|
|
129
|
+
// TODO: use `using` once Node.js supports it
|
|
130
|
+
// // ensures close is called on controller when we're done
|
|
131
|
+
// using c = controller;
|
|
132
|
+
try {
|
|
133
|
+
for await (const event of generator) {
|
|
134
|
+
controller.enqueue(event);
|
|
135
|
+
}
|
|
136
|
+
} finally {
|
|
137
|
+
controller.close();
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
// if the SSESource interface is used only strings are permitted.
|
|
143
|
+
// serialization / deserialization for objects is left to the user
|
|
144
|
+
return sseFromSource<U, E>(evaluated);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function sseFromAnyReadable<R, U, E extends string>(
|
|
149
|
+
stream: ReadableStream<R>,
|
|
150
|
+
transform: (chunk: R) => SSEEvent<U, E>,
|
|
151
|
+
): KaitoSSEResponse<SSEEvent<U, E>> {
|
|
152
|
+
const transformer = new TransformStream({
|
|
153
|
+
transform: (chunk, controller) => {
|
|
154
|
+
controller.enqueue(transform(chunk));
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return sse(stream.pipeThrough(transformer));
|
|
159
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type {KaitoRequest} from './request.ts';
|
|
2
|
+
import type {KaitoResponse} from './response.ts';
|
|
3
|
+
import {Router} from './router/router.ts';
|
|
4
|
+
|
|
5
|
+
export type ErroredAPIResponse = {success: false; data: null; message: string};
|
|
6
|
+
export type SuccessfulAPIResponse<T> = {success: true; data: T; message: 'OK'};
|
|
7
|
+
export type APIResponse<T> = ErroredAPIResponse | SuccessfulAPIResponse<T>;
|
|
8
|
+
export type AnyResponse = APIResponse<unknown>;
|
|
9
|
+
export type MakeOptional<T, K extends keyof T> = T extends T ? Omit<T, K> & Partial<Pick<T, K>> : never;
|
|
10
|
+
|
|
11
|
+
export type ExtractRouteParams<T extends string> = string extends T
|
|
12
|
+
? Record<string, string>
|
|
13
|
+
: T extends `${string}:${infer Param}/${infer Rest}`
|
|
14
|
+
? {[k in Param | keyof ExtractRouteParams<Rest>]: string}
|
|
15
|
+
: T extends `${string}:${infer Param}`
|
|
16
|
+
? {[k in Param]: string}
|
|
17
|
+
: {};
|
|
18
|
+
|
|
19
|
+
export type GetContext<Result> = (req: KaitoRequest, res: KaitoResponse) => Promise<Result>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A helper function to create typed necessary functions
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const {router, getContext} = createUtilities(async (req, res) => {
|
|
27
|
+
* // Return context here
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* const app = router().get('/', async () => "hello");
|
|
31
|
+
*
|
|
32
|
+
* const server = createServer({
|
|
33
|
+
* router: app,
|
|
34
|
+
* getContext,
|
|
35
|
+
* // ...
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function createUtilities<Context>(getContext: GetContext<Context>): {
|
|
40
|
+
getContext: GetContext<Context>;
|
|
41
|
+
router: () => Router<Context, Context, never>;
|
|
42
|
+
} {
|
|
43
|
+
return {
|
|
44
|
+
getContext,
|
|
45
|
+
router: () => Router.create<Context>(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface Parsable<Output = any, Input = Output> {
|
|
50
|
+
_input: Input;
|
|
51
|
+
parse: (value: unknown) => Output;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type InferParsable<T> =
|
|
55
|
+
T extends Parsable<infer Output, infer Input>
|
|
56
|
+
? {
|
|
57
|
+
input: Input;
|
|
58
|
+
output: Output;
|
|
59
|
+
}
|
|
60
|
+
: never;
|
|
61
|
+
|
|
62
|
+
export function parsable<T>(parse: (value: unknown) => T): Parsable<T, T> {
|
|
63
|
+
return {
|
|
64
|
+
parse,
|
|
65
|
+
} as Parsable<T, T>;
|
|
66
|
+
}
|