@loutrejs/loutre 0.5.0 → 0.7.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/README.md +63 -32
- package/dist/adapters/aws-lambda.d.ts +5 -3
- package/dist/adapters/aws-lambda.js +53 -23
- package/dist/adapters/bun.d.ts +7 -4
- package/dist/adapters/bun.js +52 -42
- package/dist/adapters/cloudflare-workers.d.ts +5 -3
- package/dist/adapters/cloudflare-workers.js +23 -28
- package/dist/adapters/deno.d.ts +7 -4
- package/dist/adapters/deno.js +94 -57
- package/dist/adapters/electron.d.ts +13 -5
- package/dist/adapters/electron.js +76 -28
- package/dist/application/definition.d.ts +31 -0
- package/dist/application/definition.js +16 -0
- package/dist/application/index.d.ts +4 -63
- package/dist/application/index.js +2 -11
- package/dist/application/kernel-internal.d.ts +2 -0
- package/dist/application/kernel-internal.js +10 -0
- package/dist/application/kernel.d.ts +22 -0
- package/dist/application/kernel.js +65 -0
- package/dist/core/diagnostic.d.ts +9 -0
- package/dist/core/diagnostic.js +9 -0
- package/dist/core/extension.d.ts +118 -0
- package/dist/core/extension.js +73 -0
- package/dist/core/generic-layer.d.ts +26 -0
- package/dist/core/generic-layer.js +116 -0
- package/dist/core/index.d.ts +32 -15
- package/dist/core/index.js +15 -15
- package/dist/core/injection.d.ts +4 -13
- package/dist/core/injection.js +63 -0
- package/dist/core/instrumentation.d.ts +36 -0
- package/dist/core/model/context.d.ts +18 -0
- package/dist/core/model/context.js +34 -0
- package/dist/core/model/lifecycle.d.ts +5 -0
- package/dist/core/model/lifecycle.js +44 -0
- package/dist/core/model/modules.d.ts +2 -0
- package/dist/core/model/modules.js +101 -0
- package/dist/core/model/providers.d.ts +4 -0
- package/dist/core/model/providers.js +74 -0
- package/dist/core/model/snapshot.d.ts +6 -0
- package/dist/core/model/snapshot.js +31 -0
- package/dist/core/model/types.d.ts +78 -0
- package/dist/core/model/types.js +1 -0
- package/dist/core/model/validation.d.ts +4 -0
- package/dist/core/model/validation.js +40 -0
- package/dist/core/model/visibility.d.ts +4 -0
- package/dist/core/model/visibility.js +20 -0
- package/dist/core/model.d.ts +10 -0
- package/dist/core/model.js +346 -0
- package/dist/core/module.d.ts +8 -13
- package/dist/core/source-location.d.ts +8 -0
- package/dist/core/source-location.js +31 -0
- package/dist/core/token-internal.d.ts +3 -0
- package/dist/core/token-internal.js +4 -0
- package/dist/core/type.d.ts +7 -0
- package/dist/core/type.js +4 -0
- package/dist/devtools/capsule.d.ts +51 -0
- package/dist/devtools/capsule.js +245 -0
- package/dist/devtools/index.d.ts +11 -0
- package/dist/devtools/index.js +6 -0
- package/dist/devtools/inspector.d.ts +57 -0
- package/dist/devtools/inspector.js +1 -0
- package/dist/devtools/module.d.ts +18 -0
- package/dist/devtools/module.js +26 -0
- package/dist/devtools/observer.d.ts +77 -0
- package/dist/devtools/observer.js +10 -0
- package/dist/devtools/protocol.d.ts +1 -0
- package/dist/devtools/protocol.js +1 -0
- package/dist/devtools/provider-instrumentation.d.ts +14 -0
- package/dist/devtools/provider-instrumentation.js +117 -0
- package/dist/devtools/runtime.d.ts +20 -0
- package/dist/devtools/runtime.js +519 -0
- package/dist/devtools/values.d.ts +35 -0
- package/dist/devtools/values.js +175 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/graph/index.d.ts +2 -3
- package/dist/graph/index.js +1 -3
- package/dist/graph/model.d.ts +34 -0
- package/dist/graph/model.js +127 -0
- package/dist/http/auth.d.ts +46 -0
- package/dist/http/auth.js +96 -0
- package/dist/http/client.d.ts +28 -31
- package/dist/http/client.js +58 -44
- package/dist/http/cors-internal.d.ts +29 -0
- package/dist/http/cors-internal.js +160 -0
- package/dist/http/cors.d.ts +5 -8
- package/dist/http/cors.js +7 -165
- package/dist/http/extension.d.ts +348 -0
- package/dist/http/extension.js +1061 -0
- package/dist/http/index.d.ts +9 -7
- package/dist/http/index.js +5 -7
- package/dist/{application → http}/openapi.d.ts +12 -3
- package/dist/{application → http}/openapi.js +164 -102
- package/dist/http/path.d.ts +1 -42
- package/dist/http/path.js +1 -2
- package/dist/http/runtime/request.d.ts +17 -0
- package/dist/http/runtime/request.js +109 -0
- package/dist/http/runtime/response.d.ts +18 -0
- package/dist/http/runtime/response.js +108 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/message-port/extension.d.ts +66 -0
- package/dist/message-port/extension.js +411 -0
- package/dist/message-port/index.d.ts +4 -73
- package/dist/message-port/index.js +2 -189
- package/dist/message-port/transport.d.ts +11 -0
- package/dist/message-port/transport.js +45 -0
- package/dist/runtime/async-iterator-lifecycle.d.ts +12 -0
- package/dist/runtime/async-iterator-lifecycle.js +95 -0
- package/dist/runtime/capability.d.ts +11 -11
- package/dist/runtime/capability.js +3 -3
- package/dist/runtime/di.d.ts +5 -21
- package/dist/runtime/di.js +22 -289
- package/dist/runtime/execution-tracker.d.ts +8 -0
- package/dist/runtime/execution-tracker.js +54 -0
- package/dist/runtime/index.d.ts +15 -9
- package/dist/runtime/index.js +9 -9
- package/dist/runtime/ingress-gate.d.ts +7 -0
- package/dist/runtime/ingress-gate.js +33 -0
- package/dist/runtime/kernel.d.ts +25 -0
- package/dist/runtime/kernel.js +447 -0
- package/dist/runtime/logger.d.ts +2 -0
- package/dist/runtime/logger.js +2 -0
- package/dist/runtime/node-capabilities.d.ts +1 -1
- package/dist/runtime/node-capabilities.js +1 -1
- package/dist/runtime/server-port.d.ts +2 -0
- package/dist/runtime/server-port.js +11 -0
- package/dist/tasks/extension.d.ts +144 -0
- package/dist/tasks/extension.js +525 -0
- package/dist/tasks/index.d.ts +2 -0
- package/dist/tasks/index.js +1 -0
- package/dist/tasks/runtime/cron.d.ts +6 -0
- package/dist/tasks/runtime/cron.js +89 -0
- package/dist/websocket/errors.d.ts +9 -0
- package/dist/websocket/errors.js +18 -0
- package/dist/websocket/extension.d.ts +154 -0
- package/dist/websocket/extension.js +533 -0
- package/dist/websocket/index.d.ts +3 -0
- package/dist/websocket/index.js +2 -0
- package/package.json +16 -16
- package/dist/application/binding.d.ts +0 -33
- package/dist/application/binding.js +0 -295
- package/dist/application/host.d.ts +0 -8
- package/dist/application/host.js +0 -14
- package/dist/application/queue.d.ts +0 -3
- package/dist/application/queue.js +0 -4
- package/dist/application/scheduler.d.ts +0 -2
- package/dist/application/scheduler.js +0 -45
- package/dist/core/context-key.d.ts +0 -16
- package/dist/core/context-key.js +0 -17
- package/dist/core/contract-internal.d.ts +0 -22
- package/dist/core/contract-internal.js +0 -61
- package/dist/core/contract.d.ts +0 -96
- package/dist/core/contract.js +0 -88
- package/dist/core/execution.d.ts +0 -78
- package/dist/core/execution.js +0 -61
- package/dist/core/implementation.d.ts +0 -44
- package/dist/core/implementation.js +0 -35
- package/dist/core/layer.d.ts +0 -137
- package/dist/core/layer.js +0 -81
- package/dist/graph/compiler.d.ts +0 -4
- package/dist/graph/compiler.js +0 -228
- package/dist/graph/graph.d.ts +0 -18
- package/dist/graph/graph.js +0 -1118
- package/dist/graph/ir.d.ts +0 -175
- package/dist/http/application.d.ts +0 -18
- package/dist/http/application.js +0 -515
- package/dist/http/basic-auth.d.ts +0 -37
- package/dist/http/basic-auth.js +0 -67
- package/dist/http/definitions.d.ts +0 -363
- package/dist/http/definitions.js +0 -310
- package/dist/http/hosted-protocol.d.ts +0 -4
- package/dist/http/hosted-protocol.js +0 -4
- package/dist/http/internal.d.ts +0 -8
- package/dist/http/internal.js +0 -1
- package/dist/http/params.d.ts +0 -2
- package/dist/http/params.js +0 -19
- package/dist/http/request-body.d.ts +0 -4
- package/dist/http/request-body.js +0 -6
- package/dist/http/validate.d.ts +0 -20
- package/dist/http/validate.js +0 -6
- package/dist/message-port/environment.d.ts +0 -2
- package/dist/message-port/environment.js +0 -4
- package/dist/runtime/application.d.ts +0 -38
- package/dist/runtime/application.js +0 -339
- package/dist/runtime/pipeline.d.ts +0 -11
- package/dist/runtime/pipeline.js +0 -161
- /package/dist/{graph/ir.js → core/instrumentation.js} +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { type ExecutionDefinition, type ExecutionExtensionDrainContext, type RuntimeCapabilityBinding, type SchemaOutput, type StandardSchemaV1 } from '../core/index.js';
|
|
2
|
+
import type { HttpExecutionRequestDefinition, HttpExecutionResponseDefinition } from '../http/index.js';
|
|
3
|
+
import { type HttpPathSegment } from '../http/index.js';
|
|
4
|
+
export type WebSocketDataMessage = {
|
|
5
|
+
readonly type: 'text';
|
|
6
|
+
readonly data: string;
|
|
7
|
+
} | {
|
|
8
|
+
readonly type: 'binary';
|
|
9
|
+
readonly data: Uint8Array;
|
|
10
|
+
};
|
|
11
|
+
export interface WebSocketCloseInfo {
|
|
12
|
+
readonly code: number;
|
|
13
|
+
readonly reason: string;
|
|
14
|
+
readonly wasClean: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface WebSocketConnectionDriver {
|
|
17
|
+
readonly messages: AsyncIterable<WebSocketDataMessage>;
|
|
18
|
+
readonly closed: Promise<WebSocketCloseInfo>;
|
|
19
|
+
send(message: WebSocketDataMessage): Promise<void>;
|
|
20
|
+
close(code?: number, reason?: string): Promise<void>;
|
|
21
|
+
terminate(): void | Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface WebSocketUpgradeResult {
|
|
24
|
+
readonly response: Response;
|
|
25
|
+
readonly connection: WebSocketConnectionDriver;
|
|
26
|
+
}
|
|
27
|
+
export interface WebSocketServerDriver {
|
|
28
|
+
readonly runtime: string;
|
|
29
|
+
upgrade(request: Request): Promise<WebSocketUpgradeResult>;
|
|
30
|
+
}
|
|
31
|
+
export declare const WEBSOCKET_SERVER: import("../core/extension.js").RuntimeCapability<WebSocketServerDriver>;
|
|
32
|
+
export type WebSocketCodecKind = 'json' | 'text' | 'binary';
|
|
33
|
+
export interface WebSocketMessageCodec<TInput extends StandardSchemaV1 | undefined = StandardSchemaV1 | undefined, TOutput extends StandardSchemaV1 | undefined = StandardSchemaV1 | undefined> {
|
|
34
|
+
readonly kind: WebSocketCodecKind;
|
|
35
|
+
readonly input: TInput;
|
|
36
|
+
readonly output: TOutput;
|
|
37
|
+
}
|
|
38
|
+
export type WebSocketIncomingMessage<TValue> = {
|
|
39
|
+
readonly isValid: true;
|
|
40
|
+
readonly value: TValue;
|
|
41
|
+
} | {
|
|
42
|
+
readonly isValid: false;
|
|
43
|
+
readonly raw: unknown;
|
|
44
|
+
readonly error: Error;
|
|
45
|
+
};
|
|
46
|
+
export interface WebSocketBranchDefinition {
|
|
47
|
+
readonly path?: string;
|
|
48
|
+
readonly responses?: Readonly<Record<string, HttpExecutionResponseDefinition>>;
|
|
49
|
+
readonly routes: WebSocketRouteTree;
|
|
50
|
+
}
|
|
51
|
+
export interface WebSocketRouteDefinition {
|
|
52
|
+
readonly path: string;
|
|
53
|
+
readonly request?: Omit<HttpExecutionRequestDefinition, 'body'>;
|
|
54
|
+
readonly responses?: Readonly<Record<string, HttpExecutionResponseDefinition>>;
|
|
55
|
+
readonly messages?: WebSocketMessageCodec;
|
|
56
|
+
}
|
|
57
|
+
export type WebSocketRouteTree = Readonly<Record<string, WebSocketRouteDefinition | WebSocketBranchDefinition>>;
|
|
58
|
+
export interface WebSocketContract<TRoutes extends WebSocketRouteTree = WebSocketRouteTree> {
|
|
59
|
+
readonly kind: 'websocket-contract';
|
|
60
|
+
readonly routes: TRoutes;
|
|
61
|
+
}
|
|
62
|
+
type IncomingApi<TRoute extends WebSocketRouteDefinition> = TRoute['messages'] extends WebSocketMessageCodec<infer TInput, any> ? TInput extends StandardSchemaV1 ? {
|
|
63
|
+
readonly messages: AsyncIterable<WebSocketIncomingMessage<SchemaOutput<TInput>>>;
|
|
64
|
+
} : {} : {};
|
|
65
|
+
type SendApi<TRoute extends WebSocketRouteDefinition> = TRoute['messages'] extends WebSocketMessageCodec<any, infer TOutput> ? TOutput extends StandardSchemaV1 ? {
|
|
66
|
+
send(value: SchemaOutput<TOutput>): Promise<void>;
|
|
67
|
+
} : {} : {};
|
|
68
|
+
type OpeningRequestInput<TRoute extends WebSocketRouteDefinition> = {
|
|
69
|
+
readonly params: TRoute extends {
|
|
70
|
+
readonly request: {
|
|
71
|
+
readonly params: infer TParams extends Readonly<Record<string, StandardSchemaV1>>;
|
|
72
|
+
};
|
|
73
|
+
} ? {
|
|
74
|
+
readonly [TName in keyof TParams]: SchemaOutput<TParams[TName]>;
|
|
75
|
+
} : Readonly<Record<string, string>>;
|
|
76
|
+
readonly query: TRoute extends {
|
|
77
|
+
readonly request: {
|
|
78
|
+
readonly query: infer TQuery extends StandardSchemaV1;
|
|
79
|
+
};
|
|
80
|
+
} ? SchemaOutput<TQuery> : Readonly<Record<string, string | string[]>>;
|
|
81
|
+
readonly headers: TRoute extends {
|
|
82
|
+
readonly request: {
|
|
83
|
+
readonly headers: infer THeaders extends StandardSchemaV1;
|
|
84
|
+
};
|
|
85
|
+
} ? SchemaOutput<THeaders> : Headers;
|
|
86
|
+
};
|
|
87
|
+
export type WebSocketHandlerContext<TRoute extends WebSocketRouteDefinition = WebSocketRouteDefinition> = {
|
|
88
|
+
readonly input: OpeningRequestInput<TRoute> & IncomingApi<TRoute>;
|
|
89
|
+
readonly signal: AbortSignal;
|
|
90
|
+
readonly closed: Promise<WebSocketCloseInfo>;
|
|
91
|
+
close(code?: number, reason?: string): Promise<void>;
|
|
92
|
+
} & SendApi<TRoute>;
|
|
93
|
+
type UnionToIntersection<TValue> = (TValue extends unknown ? (value: TValue) => void : never) extends (value: infer TIntersection) => void ? TIntersection : never;
|
|
94
|
+
type WebSocketHandlersForTree<TTree extends WebSocketRouteTree, TPrefix extends string = ''> = UnionToIntersection<{
|
|
95
|
+
readonly [TName in keyof TTree & string]: TTree[TName] extends WebSocketBranchDefinition ? WebSocketHandlersForTree<TTree[TName]['routes'], `${TPrefix}${TName}.`> : TTree[TName] extends WebSocketRouteDefinition ? {
|
|
96
|
+
readonly [THandlerName in `${TPrefix}${TName}`]: (context: WebSocketHandlerContext<TTree[TName]>) => void | Promise<void>;
|
|
97
|
+
} : never;
|
|
98
|
+
}[keyof TTree & string]>;
|
|
99
|
+
export type WebSocketHandlers<TContract extends WebSocketContract> = WebSocketHandlersForTree<TContract['routes']>;
|
|
100
|
+
export interface WebSocketImplementationDefinition<TContract extends WebSocketContract = WebSocketContract> {
|
|
101
|
+
readonly name: string;
|
|
102
|
+
readonly contract: TContract;
|
|
103
|
+
readonly factory: () => WebSocketHandlers<TContract>;
|
|
104
|
+
}
|
|
105
|
+
interface CompiledWebSocketRoute {
|
|
106
|
+
readonly name: string;
|
|
107
|
+
readonly path: string;
|
|
108
|
+
readonly normalizedPath: string;
|
|
109
|
+
readonly segments: readonly HttpPathSegment[];
|
|
110
|
+
readonly request?: Omit<HttpExecutionRequestDefinition, 'body'>;
|
|
111
|
+
readonly responses: Readonly<Record<string, HttpExecutionResponseDefinition>>;
|
|
112
|
+
readonly messages?: WebSocketMessageCodec;
|
|
113
|
+
}
|
|
114
|
+
interface CompiledWebSocketExecution {
|
|
115
|
+
readonly routes: readonly CompiledWebSocketRoute[];
|
|
116
|
+
readonly factory: WebSocketImplementationDefinition['factory'];
|
|
117
|
+
}
|
|
118
|
+
export interface WebSocketExtensionRuntime {
|
|
119
|
+
upgrade(request: Request): Promise<Response>;
|
|
120
|
+
drain(context: ExecutionExtensionDrainContext): Promise<void>;
|
|
121
|
+
close(): void;
|
|
122
|
+
}
|
|
123
|
+
export interface WebSocketHostApi {
|
|
124
|
+
upgrade(request: Request): Promise<Response>;
|
|
125
|
+
}
|
|
126
|
+
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
|
+
export type WebSocketExecutionDefinition<TContract extends WebSocketContract = WebSocketContract> = WebSocketImplementationDefinition<TContract> & ExecutionDefinition<typeof websocketExtension>;
|
|
128
|
+
export declare function defineWebSocketContract<const TRoutes extends WebSocketRouteTree>(routes: TRoutes): WebSocketContract<TRoutes>;
|
|
129
|
+
export declare function defineWebSocketImplementation<const TContract extends WebSocketContract>(definition: {
|
|
130
|
+
readonly name?: string;
|
|
131
|
+
readonly contract: TContract;
|
|
132
|
+
readonly factory: WebSocketImplementationDefinition<TContract>['factory'];
|
|
133
|
+
}): WebSocketExecutionDefinition<TContract>;
|
|
134
|
+
export declare function bindWebSocketServer(driver: WebSocketServerDriver): RuntimeCapabilityBinding<WebSocketServerDriver>;
|
|
135
|
+
export declare const websocket: Readonly<{
|
|
136
|
+
contract: typeof defineWebSocketContract;
|
|
137
|
+
implementation: typeof defineWebSocketImplementation;
|
|
138
|
+
json: <const TInput extends StandardSchemaV1 | undefined = undefined, const TOutput extends StandardSchemaV1 | undefined = undefined>(definition: {
|
|
139
|
+
readonly input?: TInput;
|
|
140
|
+
readonly output?: TOutput;
|
|
141
|
+
}) => WebSocketMessageCodec<TInput, TOutput>;
|
|
142
|
+
text: <const TInput extends StandardSchemaV1 | undefined = undefined, const TOutput extends StandardSchemaV1 | undefined = undefined>(definition: {
|
|
143
|
+
readonly input?: TInput;
|
|
144
|
+
readonly output?: TOutput;
|
|
145
|
+
}) => WebSocketMessageCodec<TInput, TOutput>;
|
|
146
|
+
binary: <const TInput extends StandardSchemaV1 | undefined = undefined, const TOutput extends StandardSchemaV1 | undefined = undefined>(definition: {
|
|
147
|
+
readonly input?: TInput;
|
|
148
|
+
readonly output?: TOutput;
|
|
149
|
+
}) => WebSocketMessageCodec<TInput, TOutput>;
|
|
150
|
+
extension: import("../core/extension.js").ExecutionExtension<WebSocketImplementationDefinition<WebSocketContract<Readonly<Record<string, WebSocketBranchDefinition | WebSocketRouteDefinition>>>> & ExecutionDefinition<import("../core/extension.js").AnyExecutionExtension>, CompiledWebSocketExecution, "websocket", WebSocketHostApi, WebSocketExtensionRuntime>;
|
|
151
|
+
serverCapability: import("../core/extension.js").RuntimeCapability<WebSocketServerDriver>;
|
|
152
|
+
bindServer: typeof bindWebSocketServer;
|
|
153
|
+
}>;
|
|
154
|
+
export {};
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { collectInjectedDependencies, defineExecution, defineExecutionExtension, runtimeCapability, runInInjectionContext, validateSchema, } from '../core/index.js';
|
|
2
|
+
import { IngressGate } from '../runtime/ingress-gate.js';
|
|
3
|
+
import { WebSocketConnectionNotOpenError, WebSocketMessageDecodeError, WebSocketMessageEncodeError, } from './errors.js';
|
|
4
|
+
import { compareHttpPathSpecificity, HttpPathDecodeError, matchHttpPath, normalizeHttpPath, parseHttpPath, } from '../http/index.js';
|
|
5
|
+
export const WEBSOCKET_SERVER = runtimeCapability('websocket.server');
|
|
6
|
+
export const websocketExtension = defineExecutionExtension({
|
|
7
|
+
kind: 'execution-extension',
|
|
8
|
+
abiVersion: '1',
|
|
9
|
+
name: 'loutre:websocket',
|
|
10
|
+
compile(definition, context) {
|
|
11
|
+
return {
|
|
12
|
+
kind: 'execution',
|
|
13
|
+
id: definition.name ||
|
|
14
|
+
`${context.moduleId}.websocket.${context.definitionIndex}`,
|
|
15
|
+
executionKind: 'websocket.session',
|
|
16
|
+
dependencies: collectInjectedDependencies({
|
|
17
|
+
kind: 'execution',
|
|
18
|
+
id: `websocket:${definition.name || `${context.moduleId}.websocket.${context.definitionIndex}`}`,
|
|
19
|
+
name: definition.name ||
|
|
20
|
+
`${context.moduleId}.websocket.${context.definitionIndex}`,
|
|
21
|
+
}, () => definition.factory()),
|
|
22
|
+
capabilities: [WEBSOCKET_SERVER],
|
|
23
|
+
compiled: Object.freeze({
|
|
24
|
+
routes: Object.freeze(compileRouteTree(definition.contract.routes)),
|
|
25
|
+
factory: definition.factory,
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
validate({ executions }) {
|
|
30
|
+
const paths = new Map();
|
|
31
|
+
return executions.flatMap((execution) => execution.compiled.routes.flatMap((route) => {
|
|
32
|
+
const owner = paths.get(route.normalizedPath);
|
|
33
|
+
if (owner) {
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
code: 'LUTRE_WEBSOCKET_DUPLICATE_ROUTE',
|
|
37
|
+
message: `${route.path} conflicts with ${owner}.`,
|
|
38
|
+
path: execution.id,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
paths.set(route.normalizedPath, execution.id);
|
|
43
|
+
return [];
|
|
44
|
+
}));
|
|
45
|
+
},
|
|
46
|
+
createRuntime(context) {
|
|
47
|
+
return createWebSocketRuntime(context.executions, context.capabilities.get(WEBSOCKET_SERVER), context.applicationRuntime);
|
|
48
|
+
},
|
|
49
|
+
projectGraph: ({ execution }) => ({
|
|
50
|
+
routes: execution.compiled.routes.map((route) => ({
|
|
51
|
+
name: route.name,
|
|
52
|
+
path: route.path,
|
|
53
|
+
...(route.messages === undefined
|
|
54
|
+
? {}
|
|
55
|
+
: {
|
|
56
|
+
messages: {
|
|
57
|
+
codec: route.messages.kind,
|
|
58
|
+
input: route.messages.input !== undefined,
|
|
59
|
+
output: route.messages.output !== undefined,
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
})),
|
|
63
|
+
}),
|
|
64
|
+
host: {
|
|
65
|
+
namespace: 'websocket',
|
|
66
|
+
create: ({ runtime }) => ({
|
|
67
|
+
upgrade: (request) => runtime.upgrade(request),
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
export function defineWebSocketContract(routes) {
|
|
72
|
+
return Object.freeze({ kind: 'websocket-contract', routes });
|
|
73
|
+
}
|
|
74
|
+
export function defineWebSocketImplementation(definition) {
|
|
75
|
+
return defineExecution(websocketExtension, {
|
|
76
|
+
name: definition.name ?? '',
|
|
77
|
+
contract: definition.contract,
|
|
78
|
+
factory: definition.factory,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function bindWebSocketServer(driver) {
|
|
82
|
+
return { capability: WEBSOCKET_SERVER, value: driver };
|
|
83
|
+
}
|
|
84
|
+
function defineCodec(kind, definition) {
|
|
85
|
+
if (definition.input === undefined && definition.output === undefined) {
|
|
86
|
+
throw new Error('LUTRE_WEBSOCKET_CODEC_EMPTY: input or output must be declared.');
|
|
87
|
+
}
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
kind,
|
|
90
|
+
input: definition.input,
|
|
91
|
+
output: definition.output,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export const websocket = Object.freeze({
|
|
95
|
+
contract: defineWebSocketContract,
|
|
96
|
+
implementation: defineWebSocketImplementation,
|
|
97
|
+
json: (definition) => defineCodec('json', definition),
|
|
98
|
+
text: (definition) => defineCodec('text', definition),
|
|
99
|
+
binary: (definition) => defineCodec('binary', definition),
|
|
100
|
+
extension: websocketExtension,
|
|
101
|
+
serverCapability: WEBSOCKET_SERVER,
|
|
102
|
+
bindServer: bindWebSocketServer,
|
|
103
|
+
});
|
|
104
|
+
function compileRouteTree(tree, parentPath = '', parentResponses = {}, prefix = '', routeNames = new Set()) {
|
|
105
|
+
return Object.entries(tree).flatMap(([name, node]) => {
|
|
106
|
+
if (isBranch(node)) {
|
|
107
|
+
const branchPath = node.path ?? '';
|
|
108
|
+
if (branchPath !== '')
|
|
109
|
+
parseHttpPath(branchPath);
|
|
110
|
+
assertNoInheritedResponseCollision(name, parentResponses, node.responses ?? {});
|
|
111
|
+
return compileRouteTree(node.routes, joinPath(parentPath, branchPath), { ...parentResponses, ...node.responses }, prefix ? `${prefix}.${name}` : name, routeNames);
|
|
112
|
+
}
|
|
113
|
+
parseHttpPath(node.path);
|
|
114
|
+
const path = joinPath(parentPath, node.path);
|
|
115
|
+
const segments = parseHttpPath(path);
|
|
116
|
+
assertExactPathParams(path, segments, node.request?.params);
|
|
117
|
+
assertNoInheritedResponseCollision(name, parentResponses, node.responses ?? {});
|
|
118
|
+
const routeName = prefix ? `${prefix}.${name}` : name;
|
|
119
|
+
if (routeNames.has(routeName)) {
|
|
120
|
+
throw new TypeError(`Duplicate nested WebSocket route name: ${routeName}`);
|
|
121
|
+
}
|
|
122
|
+
routeNames.add(routeName);
|
|
123
|
+
return [
|
|
124
|
+
Object.freeze({
|
|
125
|
+
name: routeName,
|
|
126
|
+
path,
|
|
127
|
+
normalizedPath: normalizeHttpPath(segments),
|
|
128
|
+
segments,
|
|
129
|
+
...(node.request === undefined
|
|
130
|
+
? {}
|
|
131
|
+
: { request: snapshotWebSocketRequest(node.request) }),
|
|
132
|
+
responses: snapshotWebSocketResponses({
|
|
133
|
+
...parentResponses,
|
|
134
|
+
...node.responses,
|
|
135
|
+
}),
|
|
136
|
+
...(node.messages === undefined
|
|
137
|
+
? {}
|
|
138
|
+
: { messages: Object.freeze({ ...node.messages }) }),
|
|
139
|
+
}),
|
|
140
|
+
];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function assertExactPathParams(path, segments, params) {
|
|
144
|
+
if (!params)
|
|
145
|
+
return;
|
|
146
|
+
const pathParams = segments
|
|
147
|
+
.flatMap((segment) => (segment.kind === 'param' ? [segment.name] : []))
|
|
148
|
+
.toSorted();
|
|
149
|
+
const schemaParams = Object.keys(params).toSorted();
|
|
150
|
+
if (pathParams.length !== schemaParams.length ||
|
|
151
|
+
pathParams.some((name, index) => name !== schemaParams[index])) {
|
|
152
|
+
throw new TypeError(`WebSocket ${path} request params must exactly match path parameters.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function assertNoInheritedResponseCollision(nodeName, inherited, declared) {
|
|
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) {
|
|
163
|
+
return Object.freeze({
|
|
164
|
+
...request,
|
|
165
|
+
...(request.params === undefined
|
|
166
|
+
? {}
|
|
167
|
+
: { params: Object.freeze({ ...request.params }) }),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
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
|
+
function isBranch(node) {
|
|
177
|
+
return 'routes' in node;
|
|
178
|
+
}
|
|
179
|
+
function joinPath(parent, child) {
|
|
180
|
+
if (parent === '')
|
|
181
|
+
return child || '/';
|
|
182
|
+
if (child === '' || child === '/')
|
|
183
|
+
return parent;
|
|
184
|
+
if (parent === '/')
|
|
185
|
+
return child;
|
|
186
|
+
return `${parent}${child}`;
|
|
187
|
+
}
|
|
188
|
+
function createWebSocketRuntime(executions, driver, applicationRuntime) {
|
|
189
|
+
const handlers = new Map();
|
|
190
|
+
const sessions = new Set();
|
|
191
|
+
let state = 'running';
|
|
192
|
+
const ingress = new IngressGate();
|
|
193
|
+
for (const execution of executions) {
|
|
194
|
+
handlers.set(execution.id, runInInjectionContext({
|
|
195
|
+
consumer: {
|
|
196
|
+
kind: 'execution',
|
|
197
|
+
id: `websocket:${execution.id}`,
|
|
198
|
+
name: execution.id,
|
|
199
|
+
},
|
|
200
|
+
resolve: (token) => applicationRuntime.resolve(token, execution.id),
|
|
201
|
+
}, () => execution.compiled.factory()));
|
|
202
|
+
}
|
|
203
|
+
const routes = executions
|
|
204
|
+
.flatMap((execution) => execution.compiled.routes.map((route) => ({
|
|
205
|
+
executionId: execution.id,
|
|
206
|
+
route,
|
|
207
|
+
})))
|
|
208
|
+
.toSorted((left, right) => compareHttpPathSpecificity(left.route.segments, right.route.segments));
|
|
209
|
+
return {
|
|
210
|
+
async upgrade(request) {
|
|
211
|
+
if (state !== 'running') {
|
|
212
|
+
return Response.json({ error: 'Service Unavailable' }, { status: 503 });
|
|
213
|
+
}
|
|
214
|
+
let match;
|
|
215
|
+
try {
|
|
216
|
+
match = findRoute(routes, request);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (error instanceof HttpPathDecodeError) {
|
|
220
|
+
return Response.json({ error: 'Invalid request' }, { status: 400 });
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
if (!match) {
|
|
225
|
+
return Response.json({ error: 'Not Found' }, { status: 404 });
|
|
226
|
+
}
|
|
227
|
+
let input;
|
|
228
|
+
try {
|
|
229
|
+
input = await validateOpeningRequest(request, match.params, match.route);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return Response.json({ error: 'Invalid request' }, { status: 400 });
|
|
233
|
+
}
|
|
234
|
+
if (state !== 'running') {
|
|
235
|
+
return Response.json({ error: 'Service Unavailable' }, { status: 503 });
|
|
236
|
+
}
|
|
237
|
+
const completePendingIngress = ingress.enter();
|
|
238
|
+
if (!completePendingIngress) {
|
|
239
|
+
return Response.json({ error: 'Service Unavailable' }, { status: 503 });
|
|
240
|
+
}
|
|
241
|
+
let lease;
|
|
242
|
+
try {
|
|
243
|
+
lease = applicationRuntime.beginExecution({
|
|
244
|
+
executionId: match.executionId,
|
|
245
|
+
executionKind: 'websocket.session',
|
|
246
|
+
graphNodeId: match.executionId,
|
|
247
|
+
name: `WS ${match.route.path}`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
completePendingIngress();
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
let upgraded;
|
|
255
|
+
try {
|
|
256
|
+
upgraded = await driver.upgrade(request);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
lease.fail?.(error);
|
|
260
|
+
lease.complete();
|
|
261
|
+
completePendingIngress();
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
const session = createSession(upgraded.connection, match.route, input, lease, handlers.get(match.executionId)?.[match.route.name], lease.run);
|
|
265
|
+
sessions.add(session.active);
|
|
266
|
+
void session.completion.then(() => sessions.delete(session.active), () => sessions.delete(session.active));
|
|
267
|
+
completePendingIngress();
|
|
268
|
+
if (state !== 'running') {
|
|
269
|
+
await session.completion.catch(() => undefined);
|
|
270
|
+
return Response.json({ error: 'Service Unavailable' }, { status: 503 });
|
|
271
|
+
}
|
|
272
|
+
return upgraded.response;
|
|
273
|
+
},
|
|
274
|
+
async drain({ timeoutMs }) {
|
|
275
|
+
if (state === 'stopped')
|
|
276
|
+
return;
|
|
277
|
+
state = 'draining';
|
|
278
|
+
ingress.stopAccepting();
|
|
279
|
+
await ingress.waitForIdle();
|
|
280
|
+
const gracefulTimeoutMs = webSocketGracefulShutdownBudget(timeoutMs);
|
|
281
|
+
const results = await Promise.allSettled([...sessions].map(async (session) => {
|
|
282
|
+
const graceful = session
|
|
283
|
+
.close(1001, 'Going Away')
|
|
284
|
+
.then(() => true)
|
|
285
|
+
.catch(() => false);
|
|
286
|
+
const completed = gracefulTimeoutMs === 0
|
|
287
|
+
? false
|
|
288
|
+
: await Promise.race([
|
|
289
|
+
graceful,
|
|
290
|
+
delay(gracefulTimeoutMs).then(() => false),
|
|
291
|
+
]);
|
|
292
|
+
if (!completed)
|
|
293
|
+
await session.terminate();
|
|
294
|
+
await session.completion.catch(() => undefined);
|
|
295
|
+
}));
|
|
296
|
+
const errors = results.flatMap((result) => result.status === 'rejected' ? [result.reason] : []);
|
|
297
|
+
if (errors.length > 0) {
|
|
298
|
+
throw new AggregateError(errors, 'WebSocket session drain failed.');
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
close() {
|
|
302
|
+
state = 'stopped';
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function findRoute(routes, request) {
|
|
307
|
+
const url = new URL(request.url);
|
|
308
|
+
for (const candidate of routes) {
|
|
309
|
+
const params = matchHttpPath(candidate.route.segments, url.pathname);
|
|
310
|
+
if (params) {
|
|
311
|
+
return {
|
|
312
|
+
executionId: candidate.executionId,
|
|
313
|
+
route: candidate.route,
|
|
314
|
+
params,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
async function validateOpeningRequest(request, rawParams, route) {
|
|
321
|
+
const params = route.request?.params
|
|
322
|
+
? Object.fromEntries(await Promise.all(Object.entries(route.request.params).map(async ([name, schema]) => [
|
|
323
|
+
name,
|
|
324
|
+
await validateSchema(schema, rawParams[name]),
|
|
325
|
+
])))
|
|
326
|
+
: rawParams;
|
|
327
|
+
const url = new URL(request.url);
|
|
328
|
+
const rawQuery = decodeQuery(url.searchParams);
|
|
329
|
+
const query = route.request?.query
|
|
330
|
+
? await validateSchema(route.request.query, rawQuery)
|
|
331
|
+
: rawQuery;
|
|
332
|
+
const headers = route.request?.headers
|
|
333
|
+
? await validateSchema(route.request.headers, Object.fromEntries(request.headers))
|
|
334
|
+
: request.headers;
|
|
335
|
+
return { params, query, headers };
|
|
336
|
+
}
|
|
337
|
+
function decodeQuery(searchParams) {
|
|
338
|
+
const query = {};
|
|
339
|
+
for (const [key, value] of searchParams) {
|
|
340
|
+
const current = query[key];
|
|
341
|
+
query[key] =
|
|
342
|
+
current === undefined
|
|
343
|
+
? value
|
|
344
|
+
: Array.isArray(current)
|
|
345
|
+
? [...current, value]
|
|
346
|
+
: [current, value];
|
|
347
|
+
}
|
|
348
|
+
return query;
|
|
349
|
+
}
|
|
350
|
+
function createSession(connection, route, input, lease, handler, runInExecution) {
|
|
351
|
+
let state = 'open';
|
|
352
|
+
let transportFailed = false;
|
|
353
|
+
let sendTail = Promise.resolve();
|
|
354
|
+
let closeOperation;
|
|
355
|
+
const closed = connection.closed.then((info) => {
|
|
356
|
+
state = 'closed';
|
|
357
|
+
lease.abort(info);
|
|
358
|
+
return normalizeCloseInfo(info);
|
|
359
|
+
}, (error) => {
|
|
360
|
+
state = 'closed';
|
|
361
|
+
lease.fail?.(error);
|
|
362
|
+
lease.abort(error);
|
|
363
|
+
throw error;
|
|
364
|
+
});
|
|
365
|
+
const close = (code = 1000, reason = '') => {
|
|
366
|
+
if (closeOperation)
|
|
367
|
+
return closeOperation;
|
|
368
|
+
if (state === 'closed')
|
|
369
|
+
return closed.then(() => undefined);
|
|
370
|
+
state = 'closing';
|
|
371
|
+
closeOperation = sendTail
|
|
372
|
+
.catch(() => undefined)
|
|
373
|
+
.then(() => connection.close(code, reason))
|
|
374
|
+
.then(() => closed)
|
|
375
|
+
.then(() => undefined);
|
|
376
|
+
return closeOperation;
|
|
377
|
+
};
|
|
378
|
+
const send = (value) => {
|
|
379
|
+
if (state !== 'open' || transportFailed) {
|
|
380
|
+
return Promise.reject(new WebSocketConnectionNotOpenError());
|
|
381
|
+
}
|
|
382
|
+
const operation = sendTail
|
|
383
|
+
.catch(() => undefined)
|
|
384
|
+
.then(async () => {
|
|
385
|
+
if (state !== 'open' || transportFailed) {
|
|
386
|
+
throw new WebSocketConnectionNotOpenError();
|
|
387
|
+
}
|
|
388
|
+
const message = await encodeMessage(route.messages, value);
|
|
389
|
+
try {
|
|
390
|
+
await connection.send(message);
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
transportFailed = true;
|
|
394
|
+
throw new WebSocketConnectionNotOpenError(error);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
sendTail = operation;
|
|
398
|
+
return operation;
|
|
399
|
+
};
|
|
400
|
+
const messages = decodeMessages(connection.messages, route.messages);
|
|
401
|
+
const context = {
|
|
402
|
+
input: {
|
|
403
|
+
...input,
|
|
404
|
+
...(route.messages?.input === undefined ? {} : { messages }),
|
|
405
|
+
},
|
|
406
|
+
signal: lease.signal,
|
|
407
|
+
closed,
|
|
408
|
+
close,
|
|
409
|
+
...(route.messages?.output === undefined ? {} : { send }),
|
|
410
|
+
};
|
|
411
|
+
const completion = (async () => {
|
|
412
|
+
try {
|
|
413
|
+
if (!handler) {
|
|
414
|
+
throw new Error(`LUTRE_WEBSOCKET_HANDLER_MISSING: ${route.name}`);
|
|
415
|
+
}
|
|
416
|
+
const invoke = () => handler(context);
|
|
417
|
+
await (runInExecution ? runInExecution(invoke) : invoke());
|
|
418
|
+
if (state === 'open')
|
|
419
|
+
await close(1000, '');
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
lease.fail?.(error);
|
|
423
|
+
if (state === 'open')
|
|
424
|
+
await close(1011, '');
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
try {
|
|
428
|
+
await closed;
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
lease.complete();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
})();
|
|
435
|
+
return {
|
|
436
|
+
active: {
|
|
437
|
+
close,
|
|
438
|
+
terminate: () => connection.terminate(),
|
|
439
|
+
completion,
|
|
440
|
+
},
|
|
441
|
+
completion,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
async function* decodeMessages(source, codec) {
|
|
445
|
+
if (!codec?.input)
|
|
446
|
+
return;
|
|
447
|
+
for await (const message of source) {
|
|
448
|
+
let decoded;
|
|
449
|
+
try {
|
|
450
|
+
decoded = decodeMessage(codec.kind, message);
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
yield {
|
|
454
|
+
isValid: false,
|
|
455
|
+
raw: message.data,
|
|
456
|
+
error: error instanceof WebSocketMessageDecodeError
|
|
457
|
+
? error
|
|
458
|
+
: new WebSocketMessageDecodeError(error),
|
|
459
|
+
};
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
yield {
|
|
464
|
+
isValid: true,
|
|
465
|
+
value: await validateSchema(codec.input, decoded),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
yield {
|
|
470
|
+
isValid: false,
|
|
471
|
+
raw: decoded,
|
|
472
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function decodeMessage(kind, message) {
|
|
478
|
+
if (kind === 'json') {
|
|
479
|
+
if (message.type !== 'text')
|
|
480
|
+
throw new WebSocketMessageDecodeError();
|
|
481
|
+
try {
|
|
482
|
+
return JSON.parse(message.data);
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
throw new WebSocketMessageDecodeError(error);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (kind === 'text') {
|
|
489
|
+
if (message.type !== 'text')
|
|
490
|
+
throw new WebSocketMessageDecodeError();
|
|
491
|
+
return message.data;
|
|
492
|
+
}
|
|
493
|
+
if (message.type !== 'binary')
|
|
494
|
+
throw new WebSocketMessageDecodeError();
|
|
495
|
+
return message.data;
|
|
496
|
+
}
|
|
497
|
+
async function encodeMessage(codec, value) {
|
|
498
|
+
if (!codec?.output)
|
|
499
|
+
throw new WebSocketMessageEncodeError();
|
|
500
|
+
const output = await validateSchema(codec.output, value);
|
|
501
|
+
try {
|
|
502
|
+
if (codec.kind === 'json') {
|
|
503
|
+
return { type: 'text', data: JSON.stringify(output) };
|
|
504
|
+
}
|
|
505
|
+
if (codec.kind === 'text') {
|
|
506
|
+
if (typeof output !== 'string')
|
|
507
|
+
throw new TypeError('Expected string');
|
|
508
|
+
return { type: 'text', data: output };
|
|
509
|
+
}
|
|
510
|
+
if (!(output instanceof Uint8Array)) {
|
|
511
|
+
throw new TypeError('Expected Uint8Array');
|
|
512
|
+
}
|
|
513
|
+
return { type: 'binary', data: output };
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
throw new WebSocketMessageEncodeError(error);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function normalizeCloseInfo(info) {
|
|
520
|
+
return {
|
|
521
|
+
code: Number.isInteger(info.code) ? info.code : 1006,
|
|
522
|
+
reason: info.reason ?? '',
|
|
523
|
+
wasClean: info.wasClean === true,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function webSocketGracefulShutdownBudget(timeoutMs) {
|
|
527
|
+
if (timeoutMs <= 0)
|
|
528
|
+
return 0;
|
|
529
|
+
return Math.min(5_000, Math.max(0, Math.floor(timeoutMs * 0.8)));
|
|
530
|
+
}
|
|
531
|
+
function delay(milliseconds) {
|
|
532
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
533
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { bindWebSocketServer, defineWebSocketContract, defineWebSocketImplementation, WEBSOCKET_SERVER, websocket, websocketExtension, } from './extension.js';
|
|
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';
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { bindWebSocketServer, defineWebSocketContract, defineWebSocketImplementation, WEBSOCKET_SERVER, websocket, websocketExtension, } from './extension.js';
|
|
2
|
+
export { WebSocketConnectionNotOpenError, WebSocketMessageDecodeError, WebSocketMessageEncodeError, } from './errors.js';
|