@loutrejs/loutre 0.6.0 → 0.8.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.
Files changed (71) hide show
  1. package/dist/adapters/bun.d.ts +2 -4
  2. package/dist/adapters/bun.js +36 -28
  3. package/dist/adapters/cloudflare-workers.d.ts +2 -4
  4. package/dist/adapters/cloudflare-workers.js +6 -11
  5. package/dist/adapters/server-transports.d.ts +4 -0
  6. package/dist/adapters/server-transports.js +18 -0
  7. package/dist/adapters/websocket-native.d.ts +24 -0
  8. package/dist/adapters/websocket-native.js +150 -0
  9. package/dist/application/kernel.d.ts +2 -1
  10. package/dist/core/extension.d.ts +26 -3
  11. package/dist/core/extension.js +19 -0
  12. package/dist/core/index.d.ts +2 -1
  13. package/dist/core/instrumentation.d.ts +36 -0
  14. package/dist/core/instrumentation.js +1 -0
  15. package/dist/core/model/modules.js +13 -17
  16. package/dist/core/model.js +14 -4
  17. package/dist/core/module.d.ts +3 -3
  18. package/dist/devtools/capsule.d.ts +51 -0
  19. package/dist/devtools/capsule.js +247 -0
  20. package/dist/devtools/index.d.ts +11 -0
  21. package/dist/devtools/index.js +6 -0
  22. package/dist/devtools/inspector.d.ts +57 -0
  23. package/dist/devtools/inspector.js +1 -0
  24. package/dist/devtools/module.d.ts +18 -0
  25. package/dist/devtools/module.js +26 -0
  26. package/dist/devtools/observer.d.ts +77 -0
  27. package/dist/devtools/observer.js +10 -0
  28. package/dist/devtools/protocol.d.ts +1 -0
  29. package/dist/devtools/protocol.js +1 -0
  30. package/dist/devtools/provider-instrumentation.d.ts +14 -0
  31. package/dist/devtools/provider-instrumentation.js +117 -0
  32. package/dist/devtools/runtime.d.ts +20 -0
  33. package/dist/devtools/runtime.js +519 -0
  34. package/dist/devtools/values.d.ts +35 -0
  35. package/dist/devtools/values.js +172 -0
  36. package/dist/generated/version.d.ts +1 -1
  37. package/dist/generated/version.js +1 -1
  38. package/dist/graph/model.js +37 -40
  39. package/dist/http/auth.d.ts +2 -2
  40. package/dist/http/auth.js +9 -0
  41. package/dist/http/client.js +5 -5
  42. package/dist/http/extension.d.ts +37 -10
  43. package/dist/http/extension.js +249 -57
  44. package/dist/http/index.d.ts +3 -2
  45. package/dist/http/index.js +1 -1
  46. package/dist/http/openapi.d.ts +3 -4
  47. package/dist/http/openapi.js +45 -26
  48. package/dist/http/request-head.d.ts +6 -0
  49. package/dist/http/request-head.js +1 -0
  50. package/dist/http/route-validation.d.ts +9 -0
  51. package/dist/http/route-validation.js +18 -0
  52. package/dist/http/runtime/response.js +10 -10
  53. package/dist/message-port/extension.js +20 -4
  54. package/dist/runtime/di.d.ts +4 -0
  55. package/dist/runtime/di.js +42 -31
  56. package/dist/runtime/engine.d.ts +1 -1
  57. package/dist/runtime/engine.js +1 -2
  58. package/dist/runtime/kernel.d.ts +4 -2
  59. package/dist/runtime/kernel.js +96 -3
  60. package/dist/runtime/node-capabilities.d.ts +1 -1
  61. package/dist/runtime/node-capabilities.js +2 -1
  62. package/dist/tasks/extension.js +133 -118
  63. package/dist/websocket/driver.d.ts +30 -0
  64. package/dist/websocket/driver.js +96 -0
  65. package/dist/websocket/extension.d.ts +33 -12
  66. package/dist/websocket/extension.js +72 -47
  67. package/dist/websocket/index.d.ts +3 -1
  68. package/dist/websocket/index.js +1 -0
  69. package/package.json +7 -6
  70. package/dist/adapters/deno.d.ts +0 -35
  71. package/dist/adapters/deno.js +0 -231
@@ -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: '1',
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 = '', parentResponses = {}, prefix = '', routeNames = new Set()) {
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
- assertNoInheritedResponseCollision(name, parentResponses, node.responses ?? {});
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.request?.params);
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.request === undefined
128
+ ...(node.handshake === undefined
130
129
  ? {}
131
- : { request: snapshotWebSocketRequest(node.request) }),
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 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) {
150
+ function snapshotHandshake(handshake) {
163
151
  return Object.freeze({
164
- ...request,
165
- ...(request.params === undefined
152
+ ...handshake,
153
+ ...(handshake.params === undefined
154
+ ? {}
155
+ : { params: Object.freeze({ ...handshake.params }) }),
156
+ ...(handshake.protocols === undefined
166
157
  ? {}
167
- : { params: Object.freeze({ ...request.params }) }),
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,13 +219,28 @@ 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 });
240
235
  }
241
236
  let lease;
242
237
  try {
243
- lease = applicationRuntime.beginExecution();
238
+ lease = applicationRuntime.beginExecution({
239
+ executionId: match.executionId,
240
+ executionKind: 'websocket.session',
241
+ graphNodeId: match.executionId,
242
+ name: `WS ${match.route.path}`,
243
+ });
244
244
  }
245
245
  catch (error) {
246
246
  completePendingIngress();
@@ -248,14 +248,15 @@ function createWebSocketRuntime(executions, driver, applicationRuntime) {
248
248
  }
249
249
  let upgraded;
250
250
  try {
251
- upgraded = await driver.upgrade(request);
251
+ upgraded = await driver.upgrade(request, protocol === undefined ? {} : { protocol });
252
252
  }
253
253
  catch (error) {
254
+ lease.fail?.(error);
254
255
  lease.complete();
255
256
  completePendingIngress();
256
257
  throw error;
257
258
  }
258
- const session = createSession(upgraded.connection, match.route, input, lease, handlers.get(match.executionId)?.[match.route.name]);
259
+ const session = createSession(upgraded.connection, request, applicationRuntime, match.route, input, lease, handlers.get(match.executionId)?.[match.route.name], lease.run);
259
260
  sessions.add(session.active);
260
261
  void session.completion.then(() => sessions.delete(session.active), () => sessions.delete(session.active));
261
262
  completePendingIngress();
@@ -275,14 +276,19 @@ function createWebSocketRuntime(executions, driver, applicationRuntime) {
275
276
  const results = await Promise.allSettled([...sessions].map(async (session) => {
276
277
  const graceful = session
277
278
  .close(1001, 'Going Away')
279
+ .then(() => session.completion)
278
280
  .then(() => true)
279
281
  .catch(() => false);
282
+ let timer;
280
283
  const completed = gracefulTimeoutMs === 0
281
284
  ? false
282
285
  : await Promise.race([
283
286
  graceful,
284
- delay(gracefulTimeoutMs).then(() => false),
287
+ new Promise((resolve) => {
288
+ timer = setTimeout(() => resolve(false), gracefulTimeoutMs);
289
+ }),
285
290
  ]);
291
+ clearTimeout(timer);
286
292
  if (!completed)
287
293
  await session.terminate();
288
294
  await session.completion.catch(() => undefined);
@@ -312,19 +318,19 @@ function findRoute(routes, request) {
312
318
  return undefined;
313
319
  }
314
320
  async function validateOpeningRequest(request, rawParams, route) {
315
- const params = route.request?.params
316
- ? Object.fromEntries(await Promise.all(Object.entries(route.request.params).map(async ([name, schema]) => [
321
+ const params = route.handshake?.params
322
+ ? Object.fromEntries(await Promise.all(Object.entries(route.handshake.params).map(async ([name, schema]) => [
317
323
  name,
318
324
  await validateSchema(schema, rawParams[name]),
319
325
  ])))
320
326
  : rawParams;
321
327
  const url = new URL(request.url);
322
328
  const rawQuery = decodeQuery(url.searchParams);
323
- const query = route.request?.query
324
- ? await validateSchema(route.request.query, rawQuery)
329
+ const query = route.handshake?.query
330
+ ? await validateSchema(route.handshake.query, rawQuery)
325
331
  : rawQuery;
326
- const headers = route.request?.headers
327
- ? await validateSchema(route.request.headers, Object.fromEntries(request.headers))
332
+ const headers = route.handshake?.headers
333
+ ? await validateSchema(route.handshake.headers, Object.fromEntries(request.headers))
328
334
  : request.headers;
329
335
  return { params, query, headers };
330
336
  }
@@ -341,7 +347,15 @@ function decodeQuery(searchParams) {
341
347
  }
342
348
  return query;
343
349
  }
344
- function createSession(connection, route, input, lease, handler) {
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
+ });
345
359
  let state = 'open';
346
360
  let transportFailed = false;
347
361
  let sendTail = Promise.resolve();
@@ -352,6 +366,7 @@ function createSession(connection, route, input, lease, handler) {
352
366
  return normalizeCloseInfo(info);
353
367
  }, (error) => {
354
368
  state = 'closed';
369
+ lease.fail?.(error);
355
370
  lease.abort(error);
356
371
  throw error;
357
372
  });
@@ -392,6 +407,17 @@ function createSession(connection, route, input, lease, handler) {
392
407
  };
393
408
  const messages = decodeMessages(connection.messages, route.messages);
394
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
+ },
395
421
  input: {
396
422
  ...input,
397
423
  ...(route.messages?.input === undefined ? {} : { messages }),
@@ -406,11 +432,13 @@ function createSession(connection, route, input, lease, handler) {
406
432
  if (!handler) {
407
433
  throw new Error(`LUTRE_WEBSOCKET_HANDLER_MISSING: ${route.name}`);
408
434
  }
409
- await handler(context);
435
+ const invoke = () => handler(context);
436
+ await (runInExecution ? runInExecution(invoke) : invoke());
410
437
  if (state === 'open')
411
438
  await close(1000, '');
412
439
  }
413
- catch {
440
+ catch (error) {
441
+ lease.fail?.(error);
414
442
  if (state === 'open')
415
443
  await close(1011, '');
416
444
  }
@@ -519,6 +547,3 @@ function webSocketGracefulShutdownBudget(timeoutMs) {
519
547
  return 0;
520
548
  return Math.min(5_000, Math.max(0, Math.floor(timeoutMs * 0.8)));
521
549
  }
522
- function delay(milliseconds) {
523
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
524
- }
@@ -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';
@@ -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.6.0",
3
+ "version": "0.8.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "files": [
@@ -20,6 +20,10 @@
20
20
  "types": "./dist/graph/index.d.ts",
21
21
  "import": "./dist/graph/index.js"
22
22
  },
23
+ "./devtools": {
24
+ "types": "./dist/devtools/index.d.ts",
25
+ "import": "./dist/devtools/index.js"
26
+ },
23
27
  "./runtime": {
24
28
  "types": "./dist/runtime/index.d.ts",
25
29
  "import": "./dist/runtime/index.js"
@@ -44,10 +48,6 @@
44
48
  "types": "./dist/adapters/bun.d.ts",
45
49
  "import": "./dist/adapters/bun.js"
46
50
  },
47
- "./runtime/deno": {
48
- "types": "./dist/adapters/deno.d.ts",
49
- "import": "./dist/adapters/deno.js"
50
- },
51
51
  "./runtime/cloudflare-workers": {
52
52
  "types": "./dist/adapters/cloudflare-workers.d.ts",
53
53
  "import": "./dist/adapters/cloudflare-workers.js"
@@ -71,7 +71,8 @@
71
71
  "provenance": true
72
72
  },
73
73
  "dependencies": {
74
- "@standard-schema/spec": "^1.1.0"
74
+ "@standard-schema/spec": "^1.1.0",
75
+ "ts-pattern": "^5.9.0"
75
76
  },
76
77
  "repository": {
77
78
  "type": "git",
@@ -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 {};
@@ -1,231 +0,0 @@
1
- import { createKernelApplication, } from '../application/index.js';
2
- import { bindApplicationCapability } from '../application/kernel-internal.js';
3
- import { httpExecutionExtension } from '../http/index.js';
4
- import { LOUTRE_VERSION, startStartupPresentation } from '../presentation.js';
5
- import { assertRuntimeEngine } from '../runtime/engine.js';
6
- import { serverUrl } from '../runtime/server-url.js';
7
- import { canRetryOnNextPort, initialServerPort, } from '../runtime/server-port.js';
8
- export const denoRuntime = {
9
- runtime: 'deno',
10
- capabilities: new Set([
11
- 'http.server',
12
- 'http.request.streaming',
13
- 'http.response.streaming',
14
- 'stream.readable',
15
- 'stream.writable',
16
- 'runtime.longLived',
17
- 'runtime.shutdownHook',
18
- 'env.runtime',
19
- 'crypto.random',
20
- ]),
21
- bind,
22
- create,
23
- };
24
- function bind(options) {
25
- assertRuntimeEngine('deno');
26
- if (options.application.model.extensions.get(httpExecutionExtension) ===
27
- undefined) {
28
- throw new Error('LUTRE_RUNTIME_HTTP_REQUIRED: denoRuntime.bind() requires the HTTP Execution Extension.');
29
- }
30
- const application = createKernelApplication({
31
- ...options,
32
- application: options.application,
33
- capabilities: [
34
- bindApplicationCapability(options.application.model, 'http.server', {
35
- runtime: 'deno',
36
- }),
37
- ...(options.capabilities ?? []),
38
- ],
39
- environment: 'environment' in options ? options.environment : denoEnvironment(),
40
- });
41
- let initialization;
42
- return {
43
- async fetch(request) {
44
- initialization ??= application.init();
45
- await initialization;
46
- return application.http.fetch(request);
47
- },
48
- close: (signal) => application.close(signal),
49
- };
50
- }
51
- async function create(options) {
52
- assertRuntimeEngine('deno');
53
- const deno = denoGlobal();
54
- const startedAt = performance.now();
55
- const isTTY = deno?.stdout?.isTerminal?.() === true;
56
- const presentation = startStartupPresentation({ version: LOUTRE_VERSION }, {
57
- terminal: {
58
- isTTY,
59
- color: isTTY && deno?.env?.get?.('NO_COLOR') === undefined,
60
- },
61
- write: (value) => console.log(value),
62
- });
63
- if (options.application.model.extensions.get(httpExecutionExtension) ===
64
- undefined) {
65
- throw new Error('LUTRE_RUNTIME_HTTP_REQUIRED: denoRuntime.create() requires the HTTP Execution Extension.');
66
- }
67
- const hosted = createKernelApplication({
68
- ...options,
69
- application: options.application,
70
- capabilities: [
71
- bindApplicationCapability(options.application.model, 'http.server', {
72
- runtime: 'deno',
73
- }),
74
- ...(options.capabilities ?? []),
75
- ],
76
- environment: 'environment' in options ? options.environment : denoEnvironment(),
77
- });
78
- await hosted.init();
79
- const application = hosted;
80
- const http = hosted
81
- .http;
82
- const closeApplication = application.close.bind(application);
83
- let server;
84
- let removeShutdownHooks;
85
- let serving = false;
86
- let closed = false;
87
- let closingPromise;
88
- let serverClosingPromise;
89
- const beginServerClose = () => {
90
- if (serverClosingPromise)
91
- return serverClosingPromise;
92
- if (!server)
93
- return Promise.resolve();
94
- const closingServer = server;
95
- serverClosingPromise = Promise.resolve()
96
- .then(() => closingServer.shutdown())
97
- .then(() => {
98
- if (server === closingServer)
99
- server = undefined;
100
- }, (error) => {
101
- serverClosingPromise = undefined;
102
- throw error;
103
- });
104
- void serverClosingPromise.catch(() => undefined);
105
- return serverClosingPromise;
106
- };
107
- const close = (signal) => {
108
- if (closed)
109
- return Promise.resolve();
110
- if (closingPromise)
111
- return closingPromise;
112
- closingPromise = (async () => {
113
- removeShutdownHooks?.();
114
- removeShutdownHooks = undefined;
115
- const errors = [];
116
- const serverClosing = beginServerClose();
117
- try {
118
- await closeApplication(signal);
119
- }
120
- catch (error) {
121
- errors.push(error);
122
- }
123
- if (errors.length === 0) {
124
- try {
125
- await serverClosing;
126
- }
127
- catch (error) {
128
- errors.push(error);
129
- }
130
- }
131
- if (errors.length > 0) {
132
- throw new AggregateError(errors, 'Deno runtime shutdown failed');
133
- }
134
- closed = true;
135
- })().catch((error) => {
136
- closingPromise = undefined;
137
- throw error;
138
- });
139
- return closingPromise;
140
- };
141
- const serve = async (serveOptions = {}) => {
142
- if (closed) {
143
- throw new Error('LUTRE_APP_STOPPED: Application is stopped.');
144
- }
145
- if (serving) {
146
- throw new Error('LUTRE_RUNTIME_ALREADY_SERVING: Deno runtime Application is already serving.');
147
- }
148
- if (!deno?.serve) {
149
- await close().catch(() => undefined);
150
- throw new Error('LUTRE_DENO_UNAVAILABLE: Deno.serve() is not available.');
151
- }
152
- serving = true;
153
- try {
154
- const requestedPort = serveOptions.port;
155
- let port = initialServerPort(requestedPort);
156
- while (true) {
157
- try {
158
- server = deno.serve({
159
- port,
160
- ...(serveOptions.hostname === undefined
161
- ? {}
162
- : { hostname: serveOptions.hostname }),
163
- onListen: () => undefined,
164
- }, createDenoFetchDriver(http));
165
- break;
166
- }
167
- catch (error) {
168
- if (requestedPort !== undefined || !canRetryOnNextPort(error, port)) {
169
- throw error;
170
- }
171
- port += 1;
172
- }
173
- }
174
- presentation.ready({
175
- server: serverUrl(serveOptions.hostname, port),
176
- runtime: `Deno ${deno.version?.deno ?? 'unknown'}`,
177
- environment: deno.env?.get?.('DENO_ENV') ??
178
- deno.env?.get?.('NODE_ENV') ??
179
- 'development',
180
- startupDurationMs: performance.now() - startedAt,
181
- });
182
- removeShutdownHooks =
183
- serveOptions.shutdownHooks === false
184
- ? undefined
185
- : registerDenoShutdownHooks(deno, close);
186
- return { port };
187
- }
188
- catch (error) {
189
- await close().catch(() => undefined);
190
- throw error;
191
- }
192
- };
193
- Object.assign(application, { serve, close });
194
- return application;
195
- }
196
- function registerDenoShutdownHooks(deno, close) {
197
- if (!deno.addSignalListener || !deno.removeSignalListener) {
198
- return undefined;
199
- }
200
- const handlers = new Map();
201
- const remove = () => {
202
- for (const [signal, handler] of handlers) {
203
- deno.removeSignalListener?.(signal, handler);
204
- }
205
- handlers.clear();
206
- };
207
- for (const signal of ['SIGINT', 'SIGTERM']) {
208
- const handler = () => {
209
- remove();
210
- void close(signal);
211
- };
212
- handlers.set(signal, handler);
213
- deno.addSignalListener(signal, handler);
214
- }
215
- return remove;
216
- }
217
- function createDenoFetchDriver(application) {
218
- let initialization;
219
- return async (request) => {
220
- initialization ??= application.initialize?.() ?? Promise.resolve();
221
- await initialization;
222
- return application.fetch(request);
223
- };
224
- }
225
- function denoEnvironment() {
226
- const deno = denoGlobal();
227
- return deno?.env?.toObject ? deno.env.toObject() : undefined;
228
- }
229
- function denoGlobal() {
230
- return globalThis.Deno;
231
- }