@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.
Files changed (48) 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/core/extension.d.ts +13 -1
  10. package/dist/core/extension.js +19 -0
  11. package/dist/core/index.d.ts +1 -1
  12. package/dist/core/model/modules.js +13 -17
  13. package/dist/core/model.js +14 -4
  14. package/dist/core/module.d.ts +3 -3
  15. package/dist/devtools/capsule.js +13 -11
  16. package/dist/devtools/values.js +10 -13
  17. package/dist/generated/version.d.ts +1 -1
  18. package/dist/generated/version.js +1 -1
  19. package/dist/graph/model.js +37 -40
  20. package/dist/http/auth.d.ts +2 -2
  21. package/dist/http/auth.js +9 -0
  22. package/dist/http/client.js +5 -5
  23. package/dist/http/extension.d.ts +37 -10
  24. package/dist/http/extension.js +117 -39
  25. package/dist/http/index.d.ts +3 -2
  26. package/dist/http/index.js +1 -1
  27. package/dist/http/openapi.d.ts +3 -4
  28. package/dist/http/openapi.js +45 -26
  29. package/dist/http/request-head.d.ts +6 -0
  30. package/dist/http/request-head.js +1 -0
  31. package/dist/http/route-validation.d.ts +9 -0
  32. package/dist/http/route-validation.js +18 -0
  33. package/dist/http/runtime/response.js +10 -10
  34. package/dist/runtime/di.js +40 -39
  35. package/dist/runtime/engine.d.ts +1 -1
  36. package/dist/runtime/engine.js +1 -2
  37. package/dist/runtime/node-capabilities.d.ts +1 -1
  38. package/dist/runtime/node-capabilities.js +2 -1
  39. package/dist/tasks/extension.js +103 -114
  40. package/dist/websocket/driver.d.ts +30 -0
  41. package/dist/websocket/driver.js +96 -0
  42. package/dist/websocket/extension.d.ts +33 -12
  43. package/dist/websocket/extension.js +60 -44
  44. package/dist/websocket/index.d.ts +3 -1
  45. package/dist/websocket/index.js +1 -0
  46. package/package.json +3 -6
  47. package/dist/adapters/deno.d.ts +0 -35
  48. package/dist/adapters/deno.js +0 -231
@@ -1,9 +1,7 @@
1
- import { type ApplicationDefinition, type BootstrapArguments, type KernelHostedApplication, type RequireApplicationExtension } from '../application/index.js';
1
+ import { type ApplicationDefinition, type BootstrapArguments, type KernelHostedApplication } from '../application/index.js';
2
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
3
  export type BunCreateOptions<TDefinition extends ApplicationDefinition> = {
6
- readonly application: HttpApplication<TDefinition>;
4
+ readonly application: TDefinition;
7
5
  readonly environment?: unknown;
8
6
  readonly capabilities?: readonly RuntimeCapabilityBinding[];
9
7
  readonly forceShutdownTimeoutMs?: number;
@@ -1,6 +1,6 @@
1
+ import { createBunWebSocketDriver, } from './websocket-native.js';
2
+ import { serverTransportBindings, dispatchServerRequest, } from './server-transports.js';
1
3
  import { createKernelApplication, } from '../application/index.js';
2
- import { bindApplicationCapability } from '../application/kernel-internal.js';
3
- import { httpExecutionExtension } from '../http/index.js';
4
4
  import { LOUTRE_VERSION, detectPresentationTerminal, startStartupPresentation, } from '../presentation.js';
5
5
  import { assertRuntimeEngine } from '../runtime/engine.js';
6
6
  import { serverUrl } from '../runtime/server-url.js';
@@ -9,6 +9,7 @@ export const bunRuntime = {
9
9
  runtime: 'bun',
10
10
  capabilities: new Set([
11
11
  'http.server',
12
+ 'websocket.server',
12
13
  'http.request.streaming',
13
14
  'http.response.streaming',
14
15
  'stream.readable',
@@ -29,25 +30,18 @@ async function create(options) {
29
30
  terminal: detectPresentationTerminal(process.stdout, environment),
30
31
  write: (value) => console.log(value),
31
32
  });
32
- if (options.application.model.extensions.get(httpExecutionExtension) ===
33
- undefined) {
34
- throw new Error('LUTRE_RUNTIME_HTTP_REQUIRED: bunRuntime.create() requires the HTTP Execution Extension.');
35
- }
33
+ const websocketDriver = createBunWebSocketDriver();
36
34
  const hosted = createKernelApplication({
37
35
  ...options,
38
36
  application: options.application,
39
37
  capabilities: [
40
- bindApplicationCapability(options.application.model, 'http.server', {
41
- runtime: 'bun',
42
- }),
38
+ ...serverTransportBindings(options.application.model, 'bun', websocketDriver.driver),
43
39
  ...(options.capabilities ?? []),
44
40
  ],
45
41
  environment: 'environment' in options ? options.environment : environment,
46
42
  });
47
43
  await hosted.init();
48
44
  const application = hosted;
49
- const http = hosted
50
- .http;
51
45
  const closeApplication = application.close.bind(application);
52
46
  let server;
53
47
  let removeShutdownHooks;
@@ -63,20 +57,41 @@ async function create(options) {
63
57
  removeShutdownHooks?.();
64
58
  removeShutdownHooks = undefined;
65
59
  const errors = [];
66
- if (server) {
60
+ try {
61
+ await closeApplication(signal);
62
+ }
63
+ catch (error) {
64
+ errors.push(error);
65
+ }
66
+ if (server && errors.length === 0) {
67
+ let timer;
67
68
  try {
68
- await server.stop(true);
69
+ // Bun 1.3のserver側WebSocket close後はstopが未完了になるため、Kernel drain後も無期限には待たない。
70
+ // https://github.com/oven-sh/bun/issues/36223
71
+ await Promise.race([
72
+ Promise.resolve(server.stop(false)),
73
+ new Promise((resolve, reject) => {
74
+ timer = setTimeout(() => {
75
+ try {
76
+ const forced = server.stop(true);
77
+ server.unref?.();
78
+ void Promise.resolve(forced).then(resolve, reject);
79
+ resolve();
80
+ }
81
+ catch (error) {
82
+ reject(error);
83
+ }
84
+ }, options.forceShutdownTimeoutMs ?? 5_000);
85
+ }),
86
+ ]);
69
87
  server = undefined;
70
88
  }
71
89
  catch (error) {
72
90
  errors.push(error);
73
91
  }
74
- }
75
- try {
76
- await closeApplication(signal);
77
- }
78
- catch (error) {
79
- errors.push(error);
92
+ finally {
93
+ clearTimeout(timer);
94
+ }
80
95
  }
81
96
  if (errors.length > 0) {
82
97
  throw new AggregateError(errors, 'Bun runtime shutdown failed');
@@ -110,7 +125,8 @@ async function create(options) {
110
125
  ...(serveOptions.hostname === undefined
111
126
  ? {}
112
127
  : { hostname: serveOptions.hostname }),
113
- fetch: createBunFetchDriver(http),
128
+ fetch: (request, upgradeServer) => websocketDriver.fetch(hosted, request, upgradeServer, dispatchServerRequest),
129
+ websocket: websocketDriver.websocket,
114
130
  });
115
131
  break;
116
132
  }
@@ -158,14 +174,6 @@ function registerBunShutdownHooks(close) {
158
174
  }
159
175
  return remove;
160
176
  }
161
- function createBunFetchDriver(application) {
162
- let initialization;
163
- return async (request) => {
164
- initialization ??= application.initialize?.() ?? Promise.resolve();
165
- await initialization;
166
- return application.fetch(request);
167
- };
168
- }
169
177
  function bunGlobal() {
170
178
  return globalThis.Bun;
171
179
  }
@@ -1,9 +1,7 @@
1
- import { type ApplicationDefinition, type BootstrapArguments, type RequireApplicationExtension } from '../application/index.js';
1
+ import { type ApplicationDefinition, type BootstrapArguments } from '../application/index.js';
2
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
3
  export type CloudflareWorkersBindOptions<TDefinition extends ApplicationDefinition> = {
6
- readonly application: HttpApplication<TDefinition>;
4
+ readonly application: TDefinition;
7
5
  readonly capabilities?: readonly RuntimeCapabilityBinding[];
8
6
  } & BootstrapArguments<TDefinition>;
9
7
  export interface CloudflareWorkersBinding {
@@ -1,12 +1,13 @@
1
+ import { createCloudflareWebSocketDriver } from './websocket-native.js';
2
+ import { serverTransportBindings, dispatchServerRequest, } from './server-transports.js';
1
3
  import { createKernelApplication, } from '../application/index.js';
2
- import { bindApplicationCapability } from '../application/kernel-internal.js';
3
- import { httpExecutionExtension } from '../http/index.js';
4
4
  import { assertRuntimeEngine } from '../runtime/engine.js';
5
5
  export const cloudflareWorkersRuntime = {
6
6
  runtime: 'cloudflare-workers',
7
7
  compatibilityDateMinimum: '2026-08-04',
8
8
  capabilities: new Set([
9
9
  'http.server',
10
+ 'websocket.server',
10
11
  'http.request.streaming',
11
12
  'http.response.streaming',
12
13
  'stream.readable',
@@ -19,10 +20,6 @@ export const cloudflareWorkersRuntime = {
19
20
  };
20
21
  function bind(options) {
21
22
  assertRuntimeEngine('cloudflare-workers');
22
- if (options.application.model.extensions.get(httpExecutionExtension) ===
23
- undefined) {
24
- throw new Error('LUTRE_RUNTIME_HTTP_REQUIRED: cloudflareWorkersRuntime.bind() requires the HTTP Execution Extension.');
25
- }
26
23
  let application;
27
24
  let initialization;
28
25
  const resolve = async (environment) => {
@@ -30,20 +27,18 @@ function bind(options) {
30
27
  ...options,
31
28
  application: options.application,
32
29
  capabilities: [
33
- bindApplicationCapability(options.application.model, 'http.server', {
34
- runtime: 'cloudflare-workers',
35
- }),
30
+ ...serverTransportBindings(options.application.model, 'cloudflare-workers', createCloudflareWebSocketDriver()),
36
31
  ...(options.capabilities ?? []),
37
32
  ],
38
33
  environment,
39
34
  });
40
35
  initialization ??= application.init();
41
36
  await initialization;
42
- return application.http;
37
+ return application;
43
38
  };
44
39
  return {
45
40
  async fetch(request, environment) {
46
- return (await resolve(environment)).fetch(request);
41
+ return dispatchServerRequest(await resolve(environment), request);
47
42
  },
48
43
  async close(signal) {
49
44
  await application?.close(signal);
@@ -0,0 +1,4 @@
1
+ import type { ApplicationModel, RuntimeCapabilityBinding } from '../core/index.js';
2
+ import { type WebSocketServerDriver } from '../websocket/index.js';
3
+ export declare function serverTransportBindings(model: ApplicationModel, runtime: string, websocket: WebSocketServerDriver): RuntimeCapabilityBinding[];
4
+ export declare function dispatchServerRequest(application: object, request: Request): Promise<Response>;
@@ -0,0 +1,18 @@
1
+ import { bindHttpServer } from '../http/index.js';
2
+ import { bindWebSocketServer, } from '../websocket/index.js';
3
+ export function serverTransportBindings(model, runtime, websocket) {
4
+ const requires = (id) => model.executions.some((execution) => execution.capabilities.some((capability) => capability.id === id));
5
+ return [
6
+ ...(requires('http.server') ? [bindHttpServer({ runtime })] : []),
7
+ ...(requires('websocket.server') ? [bindWebSocketServer(websocket)] : []),
8
+ ];
9
+ }
10
+ export function dispatchServerRequest(application, request) {
11
+ const transports = application;
12
+ if (request.headers.get('upgrade')?.toLowerCase() === 'websocket') {
13
+ return (transports.websocket?.upgrade(request) ??
14
+ Promise.resolve(new Response('Not Found', { status: 404 })));
15
+ }
16
+ return (transports.http?.fetch(request) ??
17
+ Promise.resolve(new Response('Not Found', { status: 404 })));
18
+ }
@@ -0,0 +1,24 @@
1
+ import { type WebSocketServerDriver, type WebSocketDriverChannel } from '../websocket/index.js';
2
+ export declare function createCloudflareWebSocketDriver(): WebSocketServerDriver;
3
+ interface BunSocket {
4
+ readonly data: WebSocketDriverChannel;
5
+ send(message: string | Uint8Array): number;
6
+ close(code?: number, reason?: string): void;
7
+ terminate(): void;
8
+ }
9
+ export interface BunUpgradeServer {
10
+ upgrade(request: Request, options: {
11
+ headers: Record<string, string>;
12
+ data: WebSocketDriverChannel;
13
+ }): boolean;
14
+ }
15
+ export declare function createBunWebSocketDriver(): {
16
+ driver: WebSocketServerDriver;
17
+ fetch(application: object, request: Request, server: BunUpgradeServer, dispatch: (application: object, request: Request) => Promise<Response>): Promise<Response | undefined>;
18
+ websocket: {
19
+ open(socket: BunSocket): void;
20
+ message(socket: BunSocket, message: string | Uint8Array): void;
21
+ close(socket: BunSocket, code: number, reason: string): void;
22
+ };
23
+ };
24
+ export {};
@@ -0,0 +1,150 @@
1
+ import { createEventWebSocketConnection, createWebSocketDriverChannel, } from '../websocket/index.js';
2
+ export function createCloudflareWebSocketDriver() {
3
+ return {
4
+ runtime: 'cloudflare-workers',
5
+ async upgrade(_request, { protocol }) {
6
+ const Pair = globalThis.WebSocketPair;
7
+ const pair = new Pair();
8
+ pair[1].accept();
9
+ // WorkersのI/Oはopening requestへ所属するため、別requestからのdrainをそのcontextへ戻す。
10
+ const io = createOwnedWebSocketIo();
11
+ const connection = createEventWebSocketConnection(pair[1], protocol ?? '', { run: io.run });
12
+ void connection.closed.then(io.stop);
13
+ const response = new Response(null, {
14
+ status: 101,
15
+ webSocket: pair[0],
16
+ headers: protocol ? { 'sec-websocket-protocol': protocol } : {},
17
+ });
18
+ return { response, connection };
19
+ },
20
+ };
21
+ }
22
+ export function createBunWebSocketDriver() {
23
+ const requests = new WeakMap();
24
+ const upgraded = new WeakSet();
25
+ const driver = {
26
+ runtime: 'bun',
27
+ async upgrade(request, { protocol }) {
28
+ const native = requests.get(request);
29
+ const server = native?.server;
30
+ if (!server)
31
+ throw new Error('Bunのupgrade serverが見つかりません。');
32
+ let socket;
33
+ let open;
34
+ const opened = new Promise((resolve) => {
35
+ open = resolve;
36
+ });
37
+ const channel = createWebSocketDriverChannel({
38
+ protocol: protocol ?? '',
39
+ async send(message) {
40
+ await opened;
41
+ socket.send(message.data);
42
+ },
43
+ async close(code = 1000, reason = '') {
44
+ await opened;
45
+ socket.close(code, reason);
46
+ },
47
+ terminate() {
48
+ socket?.terminate();
49
+ channel.finish({ code: 1006, reason: '', wasClean: false });
50
+ },
51
+ });
52
+ pending.set(channel, (value) => {
53
+ socket = value;
54
+ open();
55
+ });
56
+ if (!server.upgrade(request, {
57
+ headers: protocol ? { 'sec-websocket-protocol': protocol } : {},
58
+ data: channel,
59
+ })) {
60
+ pending.delete(channel);
61
+ throw new Error('Bun WebSocket upgradeに失敗しました。');
62
+ }
63
+ upgraded.add(request);
64
+ native.accepted();
65
+ return { response: new Response(), connection: channel.connection };
66
+ },
67
+ };
68
+ const pending = new WeakMap();
69
+ return {
70
+ driver,
71
+ async fetch(application, request, server, dispatch) {
72
+ let accept;
73
+ const accepted = new Promise((resolve) => {
74
+ accept = () => resolve(undefined);
75
+ });
76
+ requests.set(request, { server, accepted: accept });
77
+ try {
78
+ const dispatched = dispatch(application, request).then((response) => upgraded.has(request) ? undefined : response);
79
+ return await Promise.race([dispatched, accepted]);
80
+ }
81
+ finally {
82
+ requests.delete(request);
83
+ }
84
+ },
85
+ websocket: {
86
+ open(socket) {
87
+ pending.get(socket.data)?.(socket);
88
+ pending.delete(socket.data);
89
+ },
90
+ message(socket, message) {
91
+ socket.data.receive(typeof message === 'string'
92
+ ? { type: 'text', data: message }
93
+ : { type: 'binary', data: new Uint8Array(message) });
94
+ },
95
+ close(socket, code, reason) {
96
+ socket.data.finish({ code, reason, wasClean: code !== 1006 });
97
+ },
98
+ },
99
+ };
100
+ }
101
+ function createOwnedWebSocketIo() {
102
+ const queue = [];
103
+ let active = true;
104
+ let wake;
105
+ const stopped = new Error('WebSocketは終了しました。');
106
+ const pump = async () => {
107
+ while (true) {
108
+ if (!active)
109
+ return;
110
+ const task = queue.shift();
111
+ if (task)
112
+ task.run();
113
+ else
114
+ await new Promise((resolve) => {
115
+ wake = resolve;
116
+ });
117
+ }
118
+ };
119
+ void pump();
120
+ return {
121
+ run(operation) {
122
+ if (!active)
123
+ return Promise.reject(stopped);
124
+ return new Promise((resolve, reject) => {
125
+ queue.push({
126
+ run() {
127
+ try {
128
+ resolve(operation());
129
+ }
130
+ catch (error) {
131
+ reject(error);
132
+ }
133
+ },
134
+ cancel() {
135
+ reject(stopped);
136
+ },
137
+ });
138
+ wake?.();
139
+ wake = undefined;
140
+ });
141
+ },
142
+ stop() {
143
+ active = false;
144
+ for (const task of queue.splice(0))
145
+ task.cancel();
146
+ wake?.();
147
+ wake = undefined;
148
+ },
149
+ };
150
+ }
@@ -104,11 +104,23 @@ export interface ExecutionDefinition<TExtension extends AnyExecutionExtension =
104
104
  readonly extension: TExtension;
105
105
  readonly [executionDefinitionBrand]: true;
106
106
  }
107
+ export interface ExecutionGroup<TExecutions extends readonly ExecutionDeclaration[] = readonly ExecutionDeclaration[]> {
108
+ readonly kind: 'execution-group';
109
+ readonly name?: string;
110
+ readonly executions: TExecutions;
111
+ }
112
+ export type ExecutionDeclaration = ExecutionDefinition | ExecutionGroup;
113
+ export declare function flattenExecutions(declarations: readonly unknown[]): unknown[];
114
+ export interface ExecutionContextView {
115
+ readonly signal: AbortSignal;
116
+ annotate(attributes: Readonly<Record<string, unknown>>): void;
117
+ beginOperation(metadata: RuntimeOperationMetadata): ExecutionOperationLease;
118
+ }
107
119
  export declare function defineExecution<const TExtension extends ExecutionExtension, const TDefinition extends object>(extension: TExtension, definition: TDefinition): Readonly<TDefinition & ExecutionDefinition<TExtension>>;
108
120
  export declare function isExecutionDefinition(value: unknown): value is ExecutionDefinition;
109
121
  export type ExtensionOfDefinition<TDefinition> = TDefinition extends {
110
122
  readonly extension: infer TExtension extends AnyExecutionExtension;
111
- } ? TExtension : never;
123
+ } ? TExtension : TDefinition extends ExecutionGroup<infer TExecutions> ? ExecutionGroup extends TDefinition ? AnyExecutionExtension : ExtensionOfDefinition<TExecutions[number]> : never;
112
124
  export type AnyExecutionExtension = ExecutionExtension<any, any, any, any, any>;
113
125
  export type HostApiOfExtension<TExtension> = TExtension extends {
114
126
  readonly host?: infer THost;
@@ -56,6 +56,25 @@ export function defineExecutionExtension(extension) {
56
56
  });
57
57
  }
58
58
  export const executionDefinitionBrand = Symbol.for('loutre.execution-definition');
59
+ export function flattenExecutions(declarations) {
60
+ const active = new Set();
61
+ const visit = (values) => values.flatMap((value) => {
62
+ if (typeof value !== 'object' ||
63
+ value === null ||
64
+ !('kind' in value) ||
65
+ value.kind !== 'execution-group')
66
+ return [value];
67
+ if (active.has(value))
68
+ throw new TypeError('ExecutionGroupに循環参照があります。');
69
+ if (!('executions' in value) || !Array.isArray(value.executions))
70
+ throw new TypeError('ExecutionGroup.executionsは配列である必要があります。');
71
+ active.add(value);
72
+ const executions = visit(value.executions);
73
+ active.delete(value);
74
+ return executions;
75
+ });
76
+ return visit(declarations);
77
+ }
59
78
  export function defineExecution(extension, definition) {
60
79
  return Object.freeze({
61
80
  ...definition,
@@ -7,7 +7,7 @@ export type { EnvClass, EnvKey, EnvSchema } from './env.js';
7
7
  export { defineError, DomainError } from './error.js';
8
8
  export type { ErrorDefinition } from './error.js';
9
9
  export { bindRuntimeCapability, defineExecution, defineExecutionExtension, executionDefinitionBrand, isExecutionDefinition, runtimeCapability, RuntimeCapabilityRegistry, } from './extension.js';
10
- export type { AnyExecutionExtension, ExecutionCompileContext, ExecutionContribution, ExecutionDefinition, ExecutionExtension, ExecutionExtensionDrainContext, ExecutionExtensionRuntime, ExecutionExtensionRuntimeContext, ExecutionExtensionValidationContext, ExecutionKernelRuntime, ExecutionLease, ExecutionProjectionContext, ExecutionOperationLease, ExtensionOfDefinition, HostApiOfExtension, HostExtension, HostExtensionContext, RuntimeCapability, RuntimeCapabilityBinding, RuntimeCapabilityBindings, RuntimeCapabilityValue, } from './extension.js';
10
+ export type { AnyExecutionExtension, ExecutionCompileContext, ExecutionContribution, ExecutionDefinition, ExecutionDeclaration, ExecutionGroup, ExecutionContextView, ExecutionExtension, ExecutionExtensionDrainContext, ExecutionExtensionRuntime, ExecutionExtensionRuntimeContext, ExecutionExtensionValidationContext, ExecutionKernelRuntime, ExecutionLease, ExecutionProjectionContext, ExecutionOperationLease, ExtensionOfDefinition, HostApiOfExtension, HostExtension, HostExtensionContext, RuntimeCapability, RuntimeCapabilityBinding, RuntimeCapabilityBindings, RuntimeCapabilityValue, } from './extension.js';
11
11
  export type { RuntimeExecutionMetadata, RuntimeInstrumentation, RuntimeInstrumentationScope, RuntimeInvocationRegistration, RuntimeProviderMetadata, RuntimeOperationMetadata, } from './instrumentation.js';
12
12
  export { composeLayers, defineLayer } from './generic-layer.js';
13
13
  export type { GenericLayer, GenericLayerContext, GenericLayerNext, } from './generic-layer.js';
@@ -6,6 +6,7 @@ import { tokenName } from '../token.js';
6
6
  import { appendLifecycleNodes } from './lifecycle.js';
7
7
  import { snapshotProvider } from './snapshot.js';
8
8
  import { getSourceLocation } from '../source-location.js';
9
+ import { match } from 'ts-pattern';
9
10
  export function collectModuleDeclarations(context) {
10
11
  const { modules, moduleIds, nodes, edges, diagnostics, providers, providerNodes, } = context;
11
12
  const moduleNames = new Set();
@@ -79,23 +80,18 @@ export function collectModuleDeclarations(context) {
79
80
  }
80
81
  }
81
82
  function collectProviderDependencies(provider) {
82
- switch (provider.kind) {
83
- case 'class':
84
- return Object.freeze(collectInjectedDependencies(provider.provide, () => Reflect.construct(provider.useClass, [])));
85
- case 'factory':
86
- return Object.freeze([...provider.inject]);
87
- case 'conditional': {
88
- const dependencies = new Set();
89
- for (const implementation of Object.values(provider.mapping)) {
90
- for (const dependency of collectInjectedDependencies(provider.provide, () => Reflect.construct(implementation, []))) {
91
- dependencies.add(dependency);
92
- }
83
+ return match(provider)
84
+ .with({ kind: 'class' }, (candidate) => Object.freeze(collectInjectedDependencies(candidate.provide, () => Reflect.construct(candidate.useClass, []))))
85
+ .with({ kind: 'factory' }, (candidate) => Object.freeze([...candidate.inject]))
86
+ .with({ kind: 'conditional' }, (candidate) => {
87
+ const dependencies = new Set();
88
+ for (const implementation of Object.values(candidate.mapping)) {
89
+ for (const dependency of collectInjectedDependencies(candidate.provide, () => Reflect.construct(implementation, []))) {
90
+ dependencies.add(dependency);
93
91
  }
94
- return Object.freeze([...dependencies]);
95
92
  }
96
- case 'value':
97
- case 'environment':
98
- case 'arguments':
99
- return Object.freeze([]);
100
- }
93
+ return Object.freeze([...dependencies]);
94
+ })
95
+ .with({ kind: 'value' }, { kind: 'environment' }, { kind: 'arguments' }, () => Object.freeze([]))
96
+ .exhaustive();
101
97
  }
@@ -1,5 +1,5 @@
1
1
  import { diagnostic, isErrorDiagnostic } from './diagnostic.js';
2
- import { isExecutionDefinition, } from './extension.js';
2
+ import { isExecutionDefinition, flattenExecutions, } from './extension.js';
3
3
  import { isFrameworkProvidedToken } from './token-internal.js';
4
4
  import { tokenName } from './token.js';
5
5
  import { createModelBuildContext } from './model/context.js';
@@ -84,9 +84,19 @@ export function buildApplicationModel(options) {
84
84
  return result;
85
85
  }
86
86
  };
87
+ const moduleExecutions = new Map();
88
+ for (const module of modules) {
89
+ try {
90
+ moduleExecutions.set(module, flattenExecutions(module.definition.executions ?? []));
91
+ }
92
+ catch (error) {
93
+ diagnostics.push(diagnostic('LUTRE_EXECUTION_GROUP_INVALID', String(error), moduleIds.get(module)));
94
+ moduleExecutions.set(module, []);
95
+ }
96
+ }
87
97
  const rootDefinitionOwners = new Map();
88
98
  for (const module of modules) {
89
- for (const value of module.definition.executions ?? []) {
99
+ for (const value of moduleExecutions.get(module) ?? []) {
90
100
  if (!isExecutionDefinition(value))
91
101
  continue;
92
102
  const owners = rootDefinitionOwners.get(value) ?? new Set();
@@ -96,7 +106,7 @@ export function buildApplicationModel(options) {
96
106
  }
97
107
  const reachableDefinitionModules = new Map();
98
108
  for (const module of modules) {
99
- const queue = (module.definition.executions ?? []).filter(isExecutionDefinition);
109
+ const queue = (moduleExecutions.get(module) ?? []).filter(isExecutionDefinition);
100
110
  const seen = new Set();
101
111
  let queueIndex = 0;
102
112
  while (queueIndex < queue.length) {
@@ -147,7 +157,7 @@ export function buildApplicationModel(options) {
147
157
  }
148
158
  for (const module of modules) {
149
159
  const moduleId = moduleIds.get(module);
150
- const rootDefinitions = module.definition.executions ?? [];
160
+ const rootDefinitions = moduleExecutions.get(module) ?? [];
151
161
  const rootDefinitionIndexes = new Map();
152
162
  for (const [definitionIndex, value] of rootDefinitions.entries()) {
153
163
  if (isExecutionDefinition(value) && !rootDefinitionIndexes.has(value)) {
@@ -1,5 +1,5 @@
1
1
  import type { EnvClass } from './env.js';
2
- import type { ExecutionDefinition, ExtensionOfDefinition, HostApiOfExtension } from './extension.js';
2
+ import type { ExecutionDeclaration, ExtensionOfDefinition, HostApiOfExtension } from './extension.js';
3
3
  import type { ModuleLifecycle } from './lifecycle.js';
4
4
  import { type ProviderDeclaration } from './provider.js';
5
5
  export interface ModuleDefinition {
@@ -8,7 +8,7 @@ export interface ModuleDefinition {
8
8
  readonly imports?: readonly ModuleInstance[];
9
9
  readonly environment?: readonly EnvClass[];
10
10
  readonly providers?: readonly ProviderDeclaration[];
11
- readonly executions?: readonly ExecutionDefinition[];
11
+ readonly executions?: readonly ExecutionDeclaration[];
12
12
  readonly exports?: readonly unknown[];
13
13
  readonly lifecycle?: ModuleLifecycle;
14
14
  }
@@ -16,7 +16,7 @@ export declare const moduleTypeInfo: unique symbol;
16
16
  export interface ModuleTypeInfo<TDefinition extends ModuleDefinition = ModuleDefinition> {
17
17
  readonly extensions: ExtensionsOfModuleDefinition<TDefinition>;
18
18
  }
19
- type DirectExtensions<TDefinition extends ModuleDefinition> = TDefinition['executions'] extends readonly ExecutionDefinition[] ? ExtensionOfDefinition<TDefinition['executions'][number]> : never;
19
+ type DirectExtensions<TDefinition extends ModuleDefinition> = TDefinition['executions'] extends readonly ExecutionDeclaration[] ? ExtensionOfDefinition<TDefinition['executions'][number]> : never;
20
20
  type ImportedExtensions<TDefinition extends ModuleDefinition> = TDefinition['imports'] extends readonly ModuleInstance[] ? ModuleExtensions<TDefinition['imports'][number]> : never;
21
21
  export type ExtensionsOfModuleDefinition<TDefinition extends ModuleDefinition> = DirectExtensions<TDefinition> | ImportedExtensions<TDefinition>;
22
22
  export type ModuleExtensions<TModule> = TModule extends ModuleInstance<infer TDefinition> ? ExtensionsOfModuleDefinition<TDefinition> : never;
@@ -1,5 +1,6 @@
1
1
  import { serializeDevtoolsError } from './observer.js';
2
2
  import { previewDevtoolsValue } from './values.js';
3
+ import { match } from 'ts-pattern';
3
4
  const defaultCapsuleTtlMs = 5 * 60_000;
4
5
  const defaultMaxCapsules = 1_000;
5
6
  export class ReplayCapsuleStore {
@@ -151,17 +152,18 @@ export class ReplayCapsuleStore {
151
152
  return { mode: 'snapshot', value: undefined };
152
153
  }
153
154
  #restore(captured) {
154
- switch (captured.mode) {
155
- case 'snapshot':
156
- return structuredClone(captured.value);
157
- case 'reference':
158
- if (this.#references.has(captured.handleId)) {
159
- return this.#references.get(captured.handleId);
160
- }
161
- throw new Error(`LUTRE_DEVTOOLS_REFERENCE_UNAVAILABLE: ${captured.handleId}`);
162
- case 'unavailable':
163
- throw new Error(`LUTRE_DEVTOOLS_CAPTURE_UNAVAILABLE: ${captured.reason}`);
164
- }
155
+ return match(captured)
156
+ .with({ mode: 'snapshot' }, (value) => structuredClone(value.value))
157
+ .with({ mode: 'reference' }, (value) => {
158
+ if (this.#references.has(value.handleId)) {
159
+ return this.#references.get(value.handleId);
160
+ }
161
+ throw new Error(`LUTRE_DEVTOOLS_REFERENCE_UNAVAILABLE: ${value.handleId}`);
162
+ })
163
+ .with({ mode: 'unavailable' }, (value) => {
164
+ throw new Error(`LUTRE_DEVTOOLS_CAPTURE_UNAVAILABLE: ${value.reason}`);
165
+ })
166
+ .exhaustive();
165
167
  }
166
168
  #purgeExpired() {
167
169
  const now = Date.now();
@@ -1,3 +1,4 @@
1
+ import { match } from 'ts-pattern';
1
2
  const defaultMaxValueBytes = 64 * 1024;
2
3
  const defaultMaxDepth = 8;
3
4
  export function previewDevtoolsValue(value, options = {}) {
@@ -139,19 +140,15 @@ function shouldRedact(key, path, redactions) {
139
140
  return redactions.has(normalizedKey) || redactions.has(normalizedPath);
140
141
  }
141
142
  function valueType(value) {
142
- if (value === null)
143
- return 'null';
144
- if (Array.isArray(value))
145
- return 'array';
146
- if (value instanceof Date)
147
- return 'date';
148
- if (value instanceof Uint8Array)
149
- return 'bytes';
150
- if (value instanceof Map)
151
- return 'map';
152
- if (value instanceof Set)
153
- return 'set';
154
- return typeof value === 'object' ? 'object' : typeof value;
143
+ return match(value)
144
+ .with(null, () => 'null')
145
+ .when(Array.isArray, () => 'array')
146
+ .when((candidate) => candidate instanceof Date, () => 'date')
147
+ .when((candidate) => candidate instanceof Uint8Array, () => 'bytes')
148
+ .when((candidate) => candidate instanceof Map, () => 'map')
149
+ .when((candidate) => candidate instanceof Set, () => 'set')
150
+ .when((candidate) => typeof candidate === 'object', () => 'object')
151
+ .otherwise((candidate) => typeof candidate);
155
152
  }
156
153
  function safeValueType(value) {
157
154
  try {
@@ -1 +1 @@
1
- export declare const LOUTRE_VERSION = "0.7.0";
1
+ export declare const LOUTRE_VERSION = "0.8.1";