@loutrejs/loutre 0.6.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.
@@ -1,4 +1,4 @@
1
- import { assertValidApplicationModel, loadArgs, loadEnv, RuntimeCapabilityRegistry, } from '../core/index.js';
1
+ import { assertValidApplicationModel, loadArgs, loadEnv, RuntimeCapabilityRegistry, tokenName, } from '../core/index.js';
2
2
  import { Container } from './di.js';
3
3
  import { ExecutionTracker } from './execution-tracker.js';
4
4
  import { Logger } from './logger.js';
@@ -9,29 +9,39 @@ export class ApplicationKernelRuntime {
9
9
  capabilities;
10
10
  #extensionRuntimes = new Map();
11
11
  #providerInstances = new Map();
12
+ #providerNodes = new Map();
12
13
  #initializedModuleIds = new Set();
13
14
  #executions = new ExecutionTracker();
14
15
  #extensionDrainOperations = new Map();
15
16
  #environmentSource;
16
17
  #argumentsSource;
17
18
  #forceShutdownTimeoutMs;
19
+ #instrumentation;
18
20
  #state = 'created';
19
21
  #initialization;
20
22
  #shutdown;
21
23
  constructor(model, options = {}) {
22
24
  this.model = assertValidApplicationModel(model);
25
+ for (const node of this.model.nodes) {
26
+ if (node.kind === 'provider')
27
+ this.#providerNodes.set(node.provider, node);
28
+ }
23
29
  this.capabilities = new RuntimeCapabilityRegistry(options.capabilities ?? []);
24
30
  this.#environmentSource = options.environmentSource;
25
31
  this.#argumentsSource =
26
32
  'argumentsSource' in options ? options.argumentsSource : Object.freeze({});
27
33
  this.#forceShutdownTimeoutMs =
28
34
  options.forceShutdownTimeoutMs ?? defaultForceShutdownTimeoutMs;
35
+ this.#instrumentation = options.instrumentation;
29
36
  if (!Number.isFinite(this.#forceShutdownTimeoutMs) ||
30
37
  this.#forceShutdownTimeoutMs < 0) {
31
38
  throw new TypeError('LUTRE_FORCE_SHUTDOWN_TIMEOUT: forceShutdownTimeoutMs must be a non-negative finite number.');
32
39
  }
33
40
  this.container = new Container(model.providers, {
34
41
  logger: options.logger ?? new Logger(),
42
+ hooks: {
43
+ created: (provider, value) => this.#notifyProviderCreated(provider, value),
44
+ },
35
45
  });
36
46
  }
37
47
  initialize() {
@@ -149,10 +159,48 @@ export class ApplicationKernelRuntime {
149
159
  }
150
160
  return runtime;
151
161
  }
152
- beginExecution() {
162
+ beginExecution(metadata, invocation) {
153
163
  if (this.#state !== 'running')
154
164
  throw applicationStateError(this.#state);
155
- return this.#executions.begin();
165
+ const tracked = this.#executions.begin();
166
+ if (metadata === undefined)
167
+ return tracked;
168
+ const scope = safely(() => this.#instrumentation?.beginExecution?.(metadata, invocation));
169
+ if (!scope)
170
+ return tracked;
171
+ const instrumented = instrumentationLease(scope);
172
+ return {
173
+ signal: tracked.signal,
174
+ run: instrumented.run,
175
+ abort(reason) {
176
+ callInstrumentation(() => scope.abort?.(reason));
177
+ tracked.abort(reason);
178
+ },
179
+ fail: instrumented.fail,
180
+ annotate: instrumented.annotate,
181
+ complete(...result) {
182
+ instrumented.complete(...result);
183
+ tracked.complete();
184
+ },
185
+ };
186
+ }
187
+ beginOperation(metadata, invocation) {
188
+ return instrumentationLease(safely(() => this.#instrumentation?.beginOperation?.(metadata, invocation)));
189
+ }
190
+ #notifyProviderCreated(provider, value) {
191
+ const instrumentation = this.#instrumentation;
192
+ if (!instrumentation?.providerCreated)
193
+ return;
194
+ const providerNode = this.#providerNodes.get(provider);
195
+ if (!providerNode)
196
+ return;
197
+ callInstrumentation(() => instrumentation.providerCreated({
198
+ providerId: providerNode.id,
199
+ graphNodeId: providerNode.id,
200
+ name: tokenName(providerNode.token),
201
+ scope: provider.scope,
202
+ providerKind: provider.kind,
203
+ }, value));
156
204
  }
157
205
  shutdown(signal) {
158
206
  if (this.#state === 'stopped')
@@ -171,6 +219,7 @@ export class ApplicationKernelRuntime {
171
219
  await this.#initialization.catch(() => undefined);
172
220
  if (this.#state === 'created') {
173
221
  this.#state = 'stopped';
222
+ await closeInstrumentation(this.#instrumentation);
174
223
  return;
175
224
  }
176
225
  if (this.#state === 'stopped')
@@ -225,6 +274,7 @@ export class ApplicationKernelRuntime {
225
274
  this.#extensionDrainOperations.clear();
226
275
  await this.#cleanupProviders(signal, errors);
227
276
  this.#state = 'stopped';
277
+ await closeInstrumentation(this.#instrumentation);
228
278
  if (errors.length > 0) {
229
279
  throw new AggregateError(errors, 'Application shutdown failed.');
230
280
  }
@@ -281,6 +331,7 @@ export class ApplicationKernelRuntime {
281
331
  await collectError(() => callLifecycle(instance, 'onModuleDestroy'), errors);
282
332
  }
283
333
  }
334
+ await closeInstrumentation(this.#instrumentation);
284
335
  return errors;
285
336
  }
286
337
  async #runHook(hook) {
@@ -306,6 +357,48 @@ function lifecycleHookOf(model, moduleId, phase) {
306
357
  node.moduleId === moduleId &&
307
358
  node.phase === phase)?.hook;
308
359
  }
360
+ function instrumentationLease(scope) {
361
+ return {
362
+ run(operation) {
363
+ if (!scope?.run)
364
+ return operation();
365
+ return scope.run(operation);
366
+ },
367
+ fail(reason) {
368
+ callInstrumentation(() => scope?.fail?.(reason));
369
+ },
370
+ annotate(attributes) {
371
+ callInstrumentation(() => scope?.annotate?.(attributes));
372
+ },
373
+ complete(...result) {
374
+ callInstrumentation(() => scope?.complete(...result));
375
+ },
376
+ };
377
+ }
378
+ function safely(operation) {
379
+ try {
380
+ return operation();
381
+ }
382
+ catch {
383
+ return undefined;
384
+ }
385
+ }
386
+ function callInstrumentation(operation) {
387
+ try {
388
+ operation();
389
+ }
390
+ catch {
391
+ // instrumentation障害をApplication semanticsへ伝播させてはならない。
392
+ }
393
+ }
394
+ async function closeInstrumentation(instrumentation) {
395
+ try {
396
+ await instrumentation?.close?.();
397
+ }
398
+ catch {
399
+ // instrumentationのcleanup失敗をshutdown失敗として扱わない。
400
+ }
401
+ }
309
402
  async function withTimeout(operation, timeoutMs, createTimeoutError) {
310
403
  let timer;
311
404
  try {
@@ -238,6 +238,7 @@ export function consume(declaration) {
238
238
  }
239
239
  function createTasksRuntime(executions, applicationRuntime) {
240
240
  const runtimes = new Map();
241
+ const runtimeExecutionIds = new Map();
241
242
  const triggers = [];
242
243
  for (const execution of executions) {
243
244
  const compiled = execution.compiled;
@@ -254,6 +255,7 @@ function createTasksRuntime(executions, applicationRuntime) {
254
255
  resolve: (dependency) => applicationRuntime.resolve(dependency, execution.id),
255
256
  }, () => compiled.factory());
256
257
  runtimes.set(compiled.taskExecutionId, runtime);
258
+ runtimeExecutionIds.set(compiled.taskExecutionId, execution.id);
257
259
  }
258
260
  let triggerHandles = [];
259
261
  let triggerStartup;
@@ -261,7 +263,7 @@ function createTasksRuntime(executions, applicationRuntime) {
261
263
  let triggersStarted = false;
262
264
  let state = 'running';
263
265
  const activeInvocations = new Set();
264
- const run = (taskExecutionId, taskName, ...arguments_) => {
266
+ const invokeTask = (taskExecutionId, taskName, arguments_) => {
265
267
  if (state !== 'running') {
266
268
  return Promise.reject(new Error(`LUTRE_TASKS_${state.toUpperCase()}`));
267
269
  }
@@ -270,18 +272,42 @@ function createTasksRuntime(executions, applicationRuntime) {
270
272
  return Promise.reject(new Error(`LUTRE_TASK_NOT_REGISTERED: ${taskName}`));
271
273
  }
272
274
  const invocation = (async () => {
273
- const lease = applicationRuntime.beginExecution();
275
+ const executionId = runtimeExecutionIds.get(taskExecutionId) ?? taskName;
276
+ const hasInput = arguments_.length > 0;
277
+ const input = hasInput ? arguments_[0] : undefined;
278
+ const lease = applicationRuntime.beginExecution({
279
+ executionId,
280
+ executionKind: 'task.invocation',
281
+ graphNodeId: executionId,
282
+ name: taskName,
283
+ }, {
284
+ input,
285
+ invoke: (nextInput) => invokeTask(taskExecutionId, taskName, hasInput ? [nextInput] : []),
286
+ });
287
+ let result;
288
+ let hasResult = false;
274
289
  try {
275
- return await Reflect.apply(runtime, undefined, arguments_);
290
+ const invoke = () => Reflect.apply(runtime, undefined, arguments_);
291
+ result = await (lease.run ? lease.run(invoke) : invoke());
292
+ hasResult = true;
293
+ return result;
294
+ }
295
+ catch (error) {
296
+ lease.fail?.(error);
297
+ throw error;
276
298
  }
277
299
  finally {
278
- lease.complete();
300
+ if (hasResult)
301
+ lease.complete(result);
302
+ else
303
+ lease.complete();
279
304
  }
280
305
  })();
281
306
  activeInvocations.add(invocation);
282
307
  void invocation.then(() => activeInvocations.delete(invocation), () => activeInvocations.delete(invocation));
283
308
  return invocation;
284
309
  };
310
+ const run = (taskExecutionId, taskName, ...arguments_) => invokeTask(taskExecutionId, taskName, arguments_);
285
311
  const stopTriggers = () => {
286
312
  if (triggerStop)
287
313
  return triggerStop;
@@ -240,7 +240,12 @@ function createWebSocketRuntime(executions, driver, applicationRuntime) {
240
240
  }
241
241
  let lease;
242
242
  try {
243
- lease = applicationRuntime.beginExecution();
243
+ lease = applicationRuntime.beginExecution({
244
+ executionId: match.executionId,
245
+ executionKind: 'websocket.session',
246
+ graphNodeId: match.executionId,
247
+ name: `WS ${match.route.path}`,
248
+ });
244
249
  }
245
250
  catch (error) {
246
251
  completePendingIngress();
@@ -251,11 +256,12 @@ function createWebSocketRuntime(executions, driver, applicationRuntime) {
251
256
  upgraded = await driver.upgrade(request);
252
257
  }
253
258
  catch (error) {
259
+ lease.fail?.(error);
254
260
  lease.complete();
255
261
  completePendingIngress();
256
262
  throw error;
257
263
  }
258
- const session = createSession(upgraded.connection, match.route, input, lease, handlers.get(match.executionId)?.[match.route.name]);
264
+ const session = createSession(upgraded.connection, match.route, input, lease, handlers.get(match.executionId)?.[match.route.name], lease.run);
259
265
  sessions.add(session.active);
260
266
  void session.completion.then(() => sessions.delete(session.active), () => sessions.delete(session.active));
261
267
  completePendingIngress();
@@ -341,7 +347,7 @@ function decodeQuery(searchParams) {
341
347
  }
342
348
  return query;
343
349
  }
344
- function createSession(connection, route, input, lease, handler) {
350
+ function createSession(connection, route, input, lease, handler, runInExecution) {
345
351
  let state = 'open';
346
352
  let transportFailed = false;
347
353
  let sendTail = Promise.resolve();
@@ -352,6 +358,7 @@ function createSession(connection, route, input, lease, handler) {
352
358
  return normalizeCloseInfo(info);
353
359
  }, (error) => {
354
360
  state = 'closed';
361
+ lease.fail?.(error);
355
362
  lease.abort(error);
356
363
  throw error;
357
364
  });
@@ -406,11 +413,13 @@ function createSession(connection, route, input, lease, handler) {
406
413
  if (!handler) {
407
414
  throw new Error(`LUTRE_WEBSOCKET_HANDLER_MISSING: ${route.name}`);
408
415
  }
409
- await handler(context);
416
+ const invoke = () => handler(context);
417
+ await (runInExecution ? runInExecution(invoke) : invoke());
410
418
  if (state === 'open')
411
419
  await close(1000, '');
412
420
  }
413
- catch {
421
+ catch (error) {
422
+ lease.fail?.(error);
414
423
  if (state === 'open')
415
424
  await close(1011, '');
416
425
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loutrejs/loutre",
3
- "version": "0.6.0",
3
+ "version": "0.7.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"