@loutrejs/loutre 0.7.0 → 0.8.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/dist/adapters/bun.d.ts +2 -4
- package/dist/adapters/bun.js +36 -28
- package/dist/adapters/cloudflare-workers.d.ts +2 -4
- package/dist/adapters/cloudflare-workers.js +6 -11
- package/dist/adapters/server-transports.d.ts +4 -0
- package/dist/adapters/server-transports.js +18 -0
- package/dist/adapters/websocket-native.d.ts +24 -0
- package/dist/adapters/websocket-native.js +150 -0
- package/dist/core/extension.d.ts +13 -1
- package/dist/core/extension.js +19 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/model/modules.js +13 -17
- package/dist/core/model.js +14 -4
- package/dist/core/module.d.ts +3 -3
- package/dist/devtools/capsule.js +13 -11
- package/dist/devtools/values.js +10 -13
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/graph/model.js +37 -40
- package/dist/http/auth.d.ts +2 -2
- package/dist/http/auth.js +9 -0
- package/dist/http/client.js +5 -5
- package/dist/http/extension.d.ts +37 -10
- package/dist/http/extension.js +117 -39
- package/dist/http/index.d.ts +3 -2
- package/dist/http/index.js +1 -1
- package/dist/http/openapi.d.ts +3 -4
- package/dist/http/openapi.js +45 -26
- package/dist/http/request-head.d.ts +6 -0
- package/dist/http/request-head.js +1 -0
- package/dist/http/route-validation.d.ts +9 -0
- package/dist/http/route-validation.js +18 -0
- package/dist/http/runtime/response.js +10 -10
- package/dist/runtime/di.js +40 -39
- package/dist/runtime/engine.d.ts +1 -1
- package/dist/runtime/engine.js +1 -2
- package/dist/runtime/node-capabilities.d.ts +1 -1
- package/dist/runtime/node-capabilities.js +2 -1
- package/dist/tasks/extension.js +103 -114
- package/dist/websocket/driver.d.ts +30 -0
- package/dist/websocket/driver.js +96 -0
- package/dist/websocket/extension.d.ts +33 -12
- package/dist/websocket/extension.js +60 -44
- package/dist/websocket/index.d.ts +3 -1
- package/dist/websocket/index.js +1 -0
- package/package.json +3 -6
- package/dist/adapters/deno.d.ts +0 -35
- package/dist/adapters/deno.js +0 -231
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export function createWebSocketDriverChannel(options) {
|
|
2
|
+
const queue = [];
|
|
3
|
+
let done = false;
|
|
4
|
+
let wake;
|
|
5
|
+
let resolveClosed;
|
|
6
|
+
const closed = new Promise((resolve) => {
|
|
7
|
+
resolveClosed = resolve;
|
|
8
|
+
});
|
|
9
|
+
return {
|
|
10
|
+
connection: {
|
|
11
|
+
...options,
|
|
12
|
+
closed,
|
|
13
|
+
messages: {
|
|
14
|
+
async *[Symbol.asyncIterator]() {
|
|
15
|
+
while (true) {
|
|
16
|
+
if (queue.length > 0) {
|
|
17
|
+
yield queue.shift();
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (done)
|
|
21
|
+
return;
|
|
22
|
+
await new Promise((resolve) => {
|
|
23
|
+
wake = resolve;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
receive(message) {
|
|
30
|
+
if (done)
|
|
31
|
+
return;
|
|
32
|
+
queue.push(message);
|
|
33
|
+
wake?.();
|
|
34
|
+
wake = undefined;
|
|
35
|
+
},
|
|
36
|
+
finish(info) {
|
|
37
|
+
if (done)
|
|
38
|
+
return;
|
|
39
|
+
done = true;
|
|
40
|
+
queue.length = 0;
|
|
41
|
+
resolveClosed(info);
|
|
42
|
+
wake?.();
|
|
43
|
+
wake = undefined;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function createEventWebSocketConnection(socket, protocol, options = {}) {
|
|
48
|
+
const run = (operation) => options.run ? options.run(operation) : Promise.resolve().then(operation);
|
|
49
|
+
const opened = socket.readyState === 1
|
|
50
|
+
? Promise.resolve()
|
|
51
|
+
: new Promise((resolve) => socket.addEventListener('open', resolve, { once: true }));
|
|
52
|
+
socket.binaryType = 'arraybuffer';
|
|
53
|
+
const channel = createWebSocketDriverChannel({
|
|
54
|
+
protocol,
|
|
55
|
+
async send(message) {
|
|
56
|
+
await Promise.race([
|
|
57
|
+
opened,
|
|
58
|
+
channel.connection.closed.then(() => {
|
|
59
|
+
throw new Error('WebSocketは終了しました。');
|
|
60
|
+
}),
|
|
61
|
+
]);
|
|
62
|
+
await run(() => {
|
|
63
|
+
if (socket.readyState !== 1)
|
|
64
|
+
throw new Error('WebSocketはopenではありません。');
|
|
65
|
+
socket.send(message.data);
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
async close(code = 1000, reason = '') {
|
|
69
|
+
await run(() => socket.close(code, reason));
|
|
70
|
+
},
|
|
71
|
+
async terminate() {
|
|
72
|
+
try {
|
|
73
|
+
await run(() => options.terminate
|
|
74
|
+
? options.terminate()
|
|
75
|
+
: socket.close(1001, 'Going Away'));
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
channel.finish({ code: 1006, reason: '', wasClean: false });
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
socket.addEventListener('message', ({ data }) => {
|
|
83
|
+
if (typeof data === 'string')
|
|
84
|
+
channel.receive({ type: 'text', data });
|
|
85
|
+
else if (data instanceof ArrayBuffer)
|
|
86
|
+
channel.receive({ type: 'binary', data: new Uint8Array(data) });
|
|
87
|
+
else if (ArrayBuffer.isView(data))
|
|
88
|
+
channel.receive({
|
|
89
|
+
type: 'binary',
|
|
90
|
+
data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
socket.addEventListener('close', (info) => channel.finish(info));
|
|
94
|
+
socket.addEventListener('error', () => channel.finish({ code: 1006, reason: '', wasClean: false }));
|
|
95
|
+
return channel.connection;
|
|
96
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type ExecutionDefinition, type ExecutionExtensionDrainContext, type RuntimeCapabilityBinding, type SchemaOutput, type StandardSchemaV1 } from '../core/index.js';
|
|
2
|
-
import type {
|
|
1
|
+
import { type ExecutionDefinition, type ExecutionContextView, type ExecutionExtensionDrainContext, type RuntimeCapabilityBinding, type SchemaOutput, type StandardSchemaV1 } from '../core/index.js';
|
|
2
|
+
import type { HttpRequestHeadDefinition } from '../http/request-head.js';
|
|
3
3
|
import { type HttpPathSegment } from '../http/index.js';
|
|
4
4
|
export type WebSocketDataMessage = {
|
|
5
5
|
readonly type: 'text';
|
|
@@ -14,6 +14,7 @@ export interface WebSocketCloseInfo {
|
|
|
14
14
|
readonly wasClean: boolean;
|
|
15
15
|
}
|
|
16
16
|
export interface WebSocketConnectionDriver {
|
|
17
|
+
readonly protocol: string;
|
|
17
18
|
readonly messages: AsyncIterable<WebSocketDataMessage>;
|
|
18
19
|
readonly closed: Promise<WebSocketCloseInfo>;
|
|
19
20
|
send(message: WebSocketDataMessage): Promise<void>;
|
|
@@ -24,9 +25,16 @@ export interface WebSocketUpgradeResult {
|
|
|
24
25
|
readonly response: Response;
|
|
25
26
|
readonly connection: WebSocketConnectionDriver;
|
|
26
27
|
}
|
|
28
|
+
export interface WebSocketUpgradeOptions {
|
|
29
|
+
readonly protocol?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface WebSocketSession {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly protocol: string;
|
|
34
|
+
}
|
|
27
35
|
export interface WebSocketServerDriver {
|
|
28
36
|
readonly runtime: string;
|
|
29
|
-
upgrade(request: Request): Promise<WebSocketUpgradeResult>;
|
|
37
|
+
upgrade(request: Request, options: WebSocketUpgradeOptions): Promise<WebSocketUpgradeResult>;
|
|
30
38
|
}
|
|
31
39
|
export declare const WEBSOCKET_SERVER: import("../core/extension.js").RuntimeCapability<WebSocketServerDriver>;
|
|
32
40
|
export type WebSocketCodecKind = 'json' | 'text' | 'binary';
|
|
@@ -45,13 +53,14 @@ export type WebSocketIncomingMessage<TValue> = {
|
|
|
45
53
|
};
|
|
46
54
|
export interface WebSocketBranchDefinition {
|
|
47
55
|
readonly path?: string;
|
|
48
|
-
readonly responses?: Readonly<Record<string, HttpExecutionResponseDefinition>>;
|
|
49
56
|
readonly routes: WebSocketRouteTree;
|
|
50
57
|
}
|
|
58
|
+
export interface WebSocketHandshakeDefinition extends HttpRequestHeadDefinition {
|
|
59
|
+
readonly protocols?: readonly string[];
|
|
60
|
+
}
|
|
51
61
|
export interface WebSocketRouteDefinition {
|
|
52
62
|
readonly path: string;
|
|
53
|
-
readonly
|
|
54
|
-
readonly responses?: Readonly<Record<string, HttpExecutionResponseDefinition>>;
|
|
63
|
+
readonly handshake?: WebSocketHandshakeDefinition;
|
|
55
64
|
readonly messages?: WebSocketMessageCodec;
|
|
56
65
|
}
|
|
57
66
|
export type WebSocketRouteTree = Readonly<Record<string, WebSocketRouteDefinition | WebSocketBranchDefinition>>;
|
|
@@ -67,24 +76,28 @@ type SendApi<TRoute extends WebSocketRouteDefinition> = TRoute['messages'] exten
|
|
|
67
76
|
} : {} : {};
|
|
68
77
|
type OpeningRequestInput<TRoute extends WebSocketRouteDefinition> = {
|
|
69
78
|
readonly params: TRoute extends {
|
|
70
|
-
readonly
|
|
79
|
+
readonly handshake: {
|
|
71
80
|
readonly params: infer TParams extends Readonly<Record<string, StandardSchemaV1>>;
|
|
72
81
|
};
|
|
73
82
|
} ? {
|
|
74
83
|
readonly [TName in keyof TParams]: SchemaOutput<TParams[TName]>;
|
|
75
84
|
} : Readonly<Record<string, string>>;
|
|
76
85
|
readonly query: TRoute extends {
|
|
77
|
-
readonly
|
|
86
|
+
readonly handshake: {
|
|
78
87
|
readonly query: infer TQuery extends StandardSchemaV1;
|
|
79
88
|
};
|
|
80
89
|
} ? SchemaOutput<TQuery> : Readonly<Record<string, string | string[]>>;
|
|
81
90
|
readonly headers: TRoute extends {
|
|
82
|
-
readonly
|
|
91
|
+
readonly handshake: {
|
|
83
92
|
readonly headers: infer THeaders extends StandardSchemaV1;
|
|
84
93
|
};
|
|
85
94
|
} ? SchemaOutput<THeaders> : Headers;
|
|
86
95
|
};
|
|
87
96
|
export type WebSocketHandlerContext<TRoute extends WebSocketRouteDefinition = WebSocketRouteDefinition> = {
|
|
97
|
+
readonly request: Request;
|
|
98
|
+
readonly session: WebSocketSession;
|
|
99
|
+
readonly protocol: string;
|
|
100
|
+
readonly execution: ExecutionContextView;
|
|
88
101
|
readonly input: OpeningRequestInput<TRoute> & IncomingApi<TRoute>;
|
|
89
102
|
readonly signal: AbortSignal;
|
|
90
103
|
readonly closed: Promise<WebSocketCloseInfo>;
|
|
@@ -107,8 +120,7 @@ interface CompiledWebSocketRoute {
|
|
|
107
120
|
readonly path: string;
|
|
108
121
|
readonly normalizedPath: string;
|
|
109
122
|
readonly segments: readonly HttpPathSegment[];
|
|
110
|
-
readonly
|
|
111
|
-
readonly responses: Readonly<Record<string, HttpExecutionResponseDefinition>>;
|
|
123
|
+
readonly handshake?: WebSocketHandshakeDefinition;
|
|
112
124
|
readonly messages?: WebSocketMessageCodec;
|
|
113
125
|
}
|
|
114
126
|
interface CompiledWebSocketExecution {
|
|
@@ -125,7 +137,16 @@ export interface WebSocketHostApi {
|
|
|
125
137
|
}
|
|
126
138
|
export declare const websocketExtension: import("../core/extension.js").ExecutionExtension<WebSocketImplementationDefinition<WebSocketContract<Readonly<Record<string, WebSocketBranchDefinition | WebSocketRouteDefinition>>>> & ExecutionDefinition<import("../core/extension.js").AnyExecutionExtension>, CompiledWebSocketExecution, "websocket", WebSocketHostApi, WebSocketExtensionRuntime>;
|
|
127
139
|
export type WebSocketExecutionDefinition<TContract extends WebSocketContract = WebSocketContract> = WebSocketImplementationDefinition<TContract> & ExecutionDefinition<typeof websocketExtension>;
|
|
128
|
-
|
|
140
|
+
type WebSocketV2Tree<TTree extends WebSocketRouteTree> = {
|
|
141
|
+
readonly [K in keyof TTree]: TTree[K] extends WebSocketBranchDefinition ? TTree[K] & {
|
|
142
|
+
readonly responses?: never;
|
|
143
|
+
readonly routes: WebSocketV2Tree<TTree[K]['routes']>;
|
|
144
|
+
} : TTree[K] & {
|
|
145
|
+
readonly request?: never;
|
|
146
|
+
readonly responses?: never;
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
export declare function defineWebSocketContract<const TRoutes extends WebSocketRouteTree>(routes: TRoutes & WebSocketV2Tree<TRoutes>): WebSocketContract<TRoutes>;
|
|
129
150
|
export declare function defineWebSocketImplementation<const TContract extends WebSocketContract>(definition: {
|
|
130
151
|
readonly name?: string;
|
|
131
152
|
readonly contract: TContract;
|
|
@@ -5,7 +5,7 @@ import { compareHttpPathSpecificity, HttpPathDecodeError, matchHttpPath, normali
|
|
|
5
5
|
export const WEBSOCKET_SERVER = runtimeCapability('websocket.server');
|
|
6
6
|
export const websocketExtension = defineExecutionExtension({
|
|
7
7
|
kind: 'execution-extension',
|
|
8
|
-
abiVersion: '
|
|
8
|
+
abiVersion: '2',
|
|
9
9
|
name: 'loutre:websocket',
|
|
10
10
|
compile(definition, context) {
|
|
11
11
|
return {
|
|
@@ -101,24 +101,23 @@ export const websocket = Object.freeze({
|
|
|
101
101
|
serverCapability: WEBSOCKET_SERVER,
|
|
102
102
|
bindServer: bindWebSocketServer,
|
|
103
103
|
});
|
|
104
|
-
function compileRouteTree(tree, parentPath = '',
|
|
104
|
+
function compileRouteTree(tree, parentPath = '', prefix = '', routeNames = new Set()) {
|
|
105
105
|
return Object.entries(tree).flatMap(([name, node]) => {
|
|
106
|
+
if ('request' in node || 'responses' in node)
|
|
107
|
+
throw new TypeError('WebSocket v2ではhandshakeを使用してください。');
|
|
106
108
|
if (isBranch(node)) {
|
|
107
109
|
const branchPath = node.path ?? '';
|
|
108
110
|
if (branchPath !== '')
|
|
109
111
|
parseHttpPath(branchPath);
|
|
110
|
-
|
|
111
|
-
return compileRouteTree(node.routes, joinPath(parentPath, branchPath), { ...parentResponses, ...node.responses }, prefix ? `${prefix}.${name}` : name, routeNames);
|
|
112
|
+
return compileRouteTree(node.routes, joinPath(parentPath, branchPath), prefix ? `${prefix}.${name}` : name, routeNames);
|
|
112
113
|
}
|
|
113
114
|
parseHttpPath(node.path);
|
|
114
115
|
const path = joinPath(parentPath, node.path);
|
|
115
116
|
const segments = parseHttpPath(path);
|
|
116
|
-
assertExactPathParams(path, segments, node.
|
|
117
|
-
assertNoInheritedResponseCollision(name, parentResponses, node.responses ?? {});
|
|
117
|
+
assertExactPathParams(path, segments, node.handshake?.params);
|
|
118
118
|
const routeName = prefix ? `${prefix}.${name}` : name;
|
|
119
|
-
if (routeNames.has(routeName))
|
|
119
|
+
if (routeNames.has(routeName))
|
|
120
120
|
throw new TypeError(`Duplicate nested WebSocket route name: ${routeName}`);
|
|
121
|
-
}
|
|
122
121
|
routeNames.add(routeName);
|
|
123
122
|
return [
|
|
124
123
|
Object.freeze({
|
|
@@ -126,13 +125,9 @@ function compileRouteTree(tree, parentPath = '', parentResponses = {}, prefix =
|
|
|
126
125
|
path,
|
|
127
126
|
normalizedPath: normalizeHttpPath(segments),
|
|
128
127
|
segments,
|
|
129
|
-
...(node.
|
|
128
|
+
...(node.handshake === undefined
|
|
130
129
|
? {}
|
|
131
|
-
: {
|
|
132
|
-
responses: snapshotWebSocketResponses({
|
|
133
|
-
...parentResponses,
|
|
134
|
-
...node.responses,
|
|
135
|
-
}),
|
|
130
|
+
: { handshake: snapshotHandshake(node.handshake) }),
|
|
136
131
|
...(node.messages === undefined
|
|
137
132
|
? {}
|
|
138
133
|
: { messages: Object.freeze({ ...node.messages }) }),
|
|
@@ -152,27 +147,17 @@ function assertExactPathParams(path, segments, params) {
|
|
|
152
147
|
throw new TypeError(`WebSocket ${path} request params must exactly match path parameters.`);
|
|
153
148
|
}
|
|
154
149
|
}
|
|
155
|
-
function
|
|
156
|
-
for (const name of Object.keys(declared)) {
|
|
157
|
-
if (name in inherited) {
|
|
158
|
-
throw new TypeError(`Duplicate inherited WebSocket response ${name} at ${nodeName}.`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
function snapshotWebSocketRequest(request) {
|
|
150
|
+
function snapshotHandshake(handshake) {
|
|
163
151
|
return Object.freeze({
|
|
164
|
-
...
|
|
165
|
-
...(
|
|
152
|
+
...handshake,
|
|
153
|
+
...(handshake.params === undefined
|
|
154
|
+
? {}
|
|
155
|
+
: { params: Object.freeze({ ...handshake.params }) }),
|
|
156
|
+
...(handshake.protocols === undefined
|
|
166
157
|
? {}
|
|
167
|
-
: {
|
|
158
|
+
: { protocols: Object.freeze([...handshake.protocols]) }),
|
|
168
159
|
});
|
|
169
160
|
}
|
|
170
|
-
function snapshotWebSocketResponses(responses) {
|
|
171
|
-
return Object.freeze(Object.fromEntries(Object.entries(responses).map(([name, response]) => [
|
|
172
|
-
name,
|
|
173
|
-
Object.freeze({ ...response }),
|
|
174
|
-
])));
|
|
175
|
-
}
|
|
176
161
|
function isBranch(node) {
|
|
177
162
|
return 'routes' in node;
|
|
178
163
|
}
|
|
@@ -234,6 +219,16 @@ function createWebSocketRuntime(executions, driver, applicationRuntime) {
|
|
|
234
219
|
if (state !== 'running') {
|
|
235
220
|
return Response.json({ error: 'Service Unavailable' }, { status: 503 });
|
|
236
221
|
}
|
|
222
|
+
const offered = (request.headers.get('sec-websocket-protocol') ?? '')
|
|
223
|
+
.split(',')
|
|
224
|
+
.map((value) => value.trim())
|
|
225
|
+
.filter(Boolean);
|
|
226
|
+
const protocol = match.route.handshake?.protocols?.find((value) => offered.includes(value));
|
|
227
|
+
if (match.route.handshake?.protocols?.length && protocol === undefined) {
|
|
228
|
+
return new Response('WebSocket subprotocolが一致しません。', {
|
|
229
|
+
status: 400,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
237
232
|
const completePendingIngress = ingress.enter();
|
|
238
233
|
if (!completePendingIngress) {
|
|
239
234
|
return Response.json({ error: 'Service Unavailable' }, { status: 503 });
|
|
@@ -253,7 +248,7 @@ function createWebSocketRuntime(executions, driver, applicationRuntime) {
|
|
|
253
248
|
}
|
|
254
249
|
let upgraded;
|
|
255
250
|
try {
|
|
256
|
-
upgraded = await driver.upgrade(request);
|
|
251
|
+
upgraded = await driver.upgrade(request, protocol === undefined ? {} : { protocol });
|
|
257
252
|
}
|
|
258
253
|
catch (error) {
|
|
259
254
|
lease.fail?.(error);
|
|
@@ -261,7 +256,7 @@ function createWebSocketRuntime(executions, driver, applicationRuntime) {
|
|
|
261
256
|
completePendingIngress();
|
|
262
257
|
throw error;
|
|
263
258
|
}
|
|
264
|
-
const session = createSession(upgraded.connection, match.route, input, lease, handlers.get(match.executionId)?.[match.route.name], lease.run);
|
|
259
|
+
const session = createSession(upgraded.connection, request, applicationRuntime, match.route, input, lease, handlers.get(match.executionId)?.[match.route.name], lease.run);
|
|
265
260
|
sessions.add(session.active);
|
|
266
261
|
void session.completion.then(() => sessions.delete(session.active), () => sessions.delete(session.active));
|
|
267
262
|
completePendingIngress();
|
|
@@ -281,14 +276,19 @@ function createWebSocketRuntime(executions, driver, applicationRuntime) {
|
|
|
281
276
|
const results = await Promise.allSettled([...sessions].map(async (session) => {
|
|
282
277
|
const graceful = session
|
|
283
278
|
.close(1001, 'Going Away')
|
|
279
|
+
.then(() => session.completion)
|
|
284
280
|
.then(() => true)
|
|
285
281
|
.catch(() => false);
|
|
282
|
+
let timer;
|
|
286
283
|
const completed = gracefulTimeoutMs === 0
|
|
287
284
|
? false
|
|
288
285
|
: await Promise.race([
|
|
289
286
|
graceful,
|
|
290
|
-
|
|
287
|
+
new Promise((resolve) => {
|
|
288
|
+
timer = setTimeout(() => resolve(false), gracefulTimeoutMs);
|
|
289
|
+
}),
|
|
291
290
|
]);
|
|
291
|
+
clearTimeout(timer);
|
|
292
292
|
if (!completed)
|
|
293
293
|
await session.terminate();
|
|
294
294
|
await session.completion.catch(() => undefined);
|
|
@@ -318,19 +318,19 @@ function findRoute(routes, request) {
|
|
|
318
318
|
return undefined;
|
|
319
319
|
}
|
|
320
320
|
async function validateOpeningRequest(request, rawParams, route) {
|
|
321
|
-
const params = route.
|
|
322
|
-
? Object.fromEntries(await Promise.all(Object.entries(route.
|
|
321
|
+
const params = route.handshake?.params
|
|
322
|
+
? Object.fromEntries(await Promise.all(Object.entries(route.handshake.params).map(async ([name, schema]) => [
|
|
323
323
|
name,
|
|
324
324
|
await validateSchema(schema, rawParams[name]),
|
|
325
325
|
])))
|
|
326
326
|
: rawParams;
|
|
327
327
|
const url = new URL(request.url);
|
|
328
328
|
const rawQuery = decodeQuery(url.searchParams);
|
|
329
|
-
const query = route.
|
|
330
|
-
? await validateSchema(route.
|
|
329
|
+
const query = route.handshake?.query
|
|
330
|
+
? await validateSchema(route.handshake.query, rawQuery)
|
|
331
331
|
: rawQuery;
|
|
332
|
-
const headers = route.
|
|
333
|
-
? await validateSchema(route.
|
|
332
|
+
const headers = route.handshake?.headers
|
|
333
|
+
? await validateSchema(route.handshake.headers, Object.fromEntries(request.headers))
|
|
334
334
|
: request.headers;
|
|
335
335
|
return { params, query, headers };
|
|
336
336
|
}
|
|
@@ -347,7 +347,15 @@ function decodeQuery(searchParams) {
|
|
|
347
347
|
}
|
|
348
348
|
return query;
|
|
349
349
|
}
|
|
350
|
-
function createSession(connection, route, input, lease, handler, runInExecution) {
|
|
350
|
+
function createSession(connection, request, applicationRuntime, route, input, lease, handler, runInExecution) {
|
|
351
|
+
const session = Object.freeze({
|
|
352
|
+
id: `ws_${crypto.randomUUID()}`,
|
|
353
|
+
protocol: connection.protocol,
|
|
354
|
+
});
|
|
355
|
+
lease.annotate?.({
|
|
356
|
+
'websocket.session.id': session.id,
|
|
357
|
+
'websocket.protocol': session.protocol,
|
|
358
|
+
});
|
|
351
359
|
let state = 'open';
|
|
352
360
|
let transportFailed = false;
|
|
353
361
|
let sendTail = Promise.resolve();
|
|
@@ -399,6 +407,17 @@ function createSession(connection, route, input, lease, handler, runInExecution)
|
|
|
399
407
|
};
|
|
400
408
|
const messages = decodeMessages(connection.messages, route.messages);
|
|
401
409
|
const context = {
|
|
410
|
+
request,
|
|
411
|
+
session,
|
|
412
|
+
protocol: session.protocol,
|
|
413
|
+
execution: {
|
|
414
|
+
signal: lease.signal,
|
|
415
|
+
annotate: (attributes) => lease.annotate?.(attributes),
|
|
416
|
+
beginOperation: (metadata) => {
|
|
417
|
+
const begin = () => applicationRuntime.beginOperation?.(metadata) ?? { complete() { } };
|
|
418
|
+
return lease.run ? lease.run(begin) : begin();
|
|
419
|
+
},
|
|
420
|
+
},
|
|
402
421
|
input: {
|
|
403
422
|
...input,
|
|
404
423
|
...(route.messages?.input === undefined ? {} : { messages }),
|
|
@@ -528,6 +547,3 @@ function webSocketGracefulShutdownBudget(timeoutMs) {
|
|
|
528
547
|
return 0;
|
|
529
548
|
return Math.min(5_000, Math.max(0, Math.floor(timeoutMs * 0.8)));
|
|
530
549
|
}
|
|
531
|
-
function delay(milliseconds) {
|
|
532
|
-
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
533
|
-
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { bindWebSocketServer, defineWebSocketContract, defineWebSocketImplementation, WEBSOCKET_SERVER, websocket, websocketExtension, } from './extension.js';
|
|
2
2
|
export { WebSocketConnectionNotOpenError, WebSocketMessageDecodeError, WebSocketMessageEncodeError, } from './errors.js';
|
|
3
|
-
export type { WebSocketBranchDefinition, WebSocketCloseInfo, WebSocketCodecKind, WebSocketConnectionDriver, WebSocketContract, WebSocketDataMessage, WebSocketExecutionDefinition, WebSocketExtensionRuntime, WebSocketHandlerContext, WebSocketHandlers, WebSocketHostApi, WebSocketImplementationDefinition, WebSocketIncomingMessage, WebSocketMessageCodec, WebSocketRouteDefinition, WebSocketRouteTree, WebSocketServerDriver, WebSocketUpgradeResult, } from './extension.js';
|
|
3
|
+
export type { WebSocketBranchDefinition, WebSocketHandshakeDefinition, WebSocketSession, WebSocketUpgradeOptions, WebSocketCloseInfo, WebSocketCodecKind, WebSocketConnectionDriver, WebSocketContract, WebSocketDataMessage, WebSocketExecutionDefinition, WebSocketExtensionRuntime, WebSocketHandlerContext, WebSocketHandlers, WebSocketHostApi, WebSocketImplementationDefinition, WebSocketIncomingMessage, WebSocketMessageCodec, WebSocketRouteDefinition, WebSocketRouteTree, WebSocketServerDriver, WebSocketUpgradeResult, } from './extension.js';
|
|
4
|
+
export { createEventWebSocketConnection, createWebSocketDriverChannel, } from './driver.js';
|
|
5
|
+
export type { EventWebSocket, WebSocketDriverChannel } from './driver.js';
|
package/dist/websocket/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { bindWebSocketServer, defineWebSocketContract, defineWebSocketImplementation, WEBSOCKET_SERVER, websocket, websocketExtension, } from './extension.js';
|
|
2
2
|
export { WebSocketConnectionNotOpenError, WebSocketMessageDecodeError, WebSocketMessageEncodeError, } from './errors.js';
|
|
3
|
+
export { createEventWebSocketConnection, createWebSocketDriverChannel, } from './driver.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loutrejs/loutre",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -48,10 +48,6 @@
|
|
|
48
48
|
"types": "./dist/adapters/bun.d.ts",
|
|
49
49
|
"import": "./dist/adapters/bun.js"
|
|
50
50
|
},
|
|
51
|
-
"./runtime/deno": {
|
|
52
|
-
"types": "./dist/adapters/deno.d.ts",
|
|
53
|
-
"import": "./dist/adapters/deno.js"
|
|
54
|
-
},
|
|
55
51
|
"./runtime/cloudflare-workers": {
|
|
56
52
|
"types": "./dist/adapters/cloudflare-workers.d.ts",
|
|
57
53
|
"import": "./dist/adapters/cloudflare-workers.js"
|
|
@@ -75,7 +71,8 @@
|
|
|
75
71
|
"provenance": true
|
|
76
72
|
},
|
|
77
73
|
"dependencies": {
|
|
78
|
-
"@standard-schema/spec": "^1.1.0"
|
|
74
|
+
"@standard-schema/spec": "^1.1.0",
|
|
75
|
+
"ts-pattern": "^5.9.0"
|
|
79
76
|
},
|
|
80
77
|
"repository": {
|
|
81
78
|
"type": "git",
|
package/dist/adapters/deno.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { type ApplicationDefinition, type BootstrapArguments, type KernelHostedApplication, type RequireApplicationExtension } from '../application/index.js';
|
|
2
|
-
import type { RuntimeCapabilityBinding } from '../core/index.js';
|
|
3
|
-
import { httpExecutionExtension } from '../http/index.js';
|
|
4
|
-
type HttpApplication<TDefinition extends ApplicationDefinition> = RequireApplicationExtension<TDefinition, typeof httpExecutionExtension>;
|
|
5
|
-
export type DenoRuntimeOptions<TDefinition extends ApplicationDefinition> = {
|
|
6
|
-
readonly application: HttpApplication<TDefinition>;
|
|
7
|
-
readonly environment?: unknown;
|
|
8
|
-
readonly capabilities?: readonly RuntimeCapabilityBinding[];
|
|
9
|
-
readonly forceShutdownTimeoutMs?: number;
|
|
10
|
-
} & BootstrapArguments<TDefinition>;
|
|
11
|
-
export type DenoCreateOptions<TDefinition extends ApplicationDefinition> = DenoRuntimeOptions<TDefinition>;
|
|
12
|
-
export interface DenoServeOptions {
|
|
13
|
-
readonly port?: number;
|
|
14
|
-
readonly hostname?: string;
|
|
15
|
-
readonly shutdownHooks?: boolean;
|
|
16
|
-
}
|
|
17
|
-
export interface DenoBinding {
|
|
18
|
-
fetch(request: Request): Promise<Response>;
|
|
19
|
-
close(signal?: string): Promise<void>;
|
|
20
|
-
}
|
|
21
|
-
export interface DenoListenerHandle {
|
|
22
|
-
readonly port: number;
|
|
23
|
-
}
|
|
24
|
-
export type DenoRuntimeApplication<TDefinition extends ApplicationDefinition = ApplicationDefinition> = KernelHostedApplication<TDefinition> & {
|
|
25
|
-
serve(options?: DenoServeOptions): Promise<DenoListenerHandle>;
|
|
26
|
-
};
|
|
27
|
-
export declare const denoRuntime: {
|
|
28
|
-
readonly runtime: 'deno';
|
|
29
|
-
readonly capabilities: Set<string>;
|
|
30
|
-
readonly bind: typeof bind;
|
|
31
|
-
readonly create: typeof create;
|
|
32
|
-
};
|
|
33
|
-
declare function bind<const TDefinition extends ApplicationDefinition>(options: DenoRuntimeOptions<TDefinition>): DenoBinding;
|
|
34
|
-
declare function create<const TDefinition extends ApplicationDefinition>(options: DenoCreateOptions<TDefinition>): Promise<DenoRuntimeApplication<TDefinition>>;
|
|
35
|
-
export {};
|