@unlaxer/tramli 3.2.0 → 3.3.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.
@@ -10,6 +10,7 @@ export interface TransitionLogEntry {
10
10
  from: string | null;
11
11
  to: string;
12
12
  trigger: string;
13
+ durationMicros: number;
13
14
  }
14
15
  export interface StateLogEntry {
15
16
  flowId: string;
@@ -25,6 +26,7 @@ export interface ErrorLogEntry {
25
26
  to: string | null;
26
27
  trigger: string;
27
28
  cause: Error | null;
29
+ durationMicros: number;
28
30
  }
29
31
  export interface GuardLogEntry {
30
32
  flowId: string;
@@ -33,6 +35,7 @@ export interface GuardLogEntry {
33
35
  guardName: string;
34
36
  result: 'accepted' | 'rejected' | 'expired';
35
37
  reason?: string;
38
+ durationMicros: number;
36
39
  }
37
40
  export declare class FlowEngine {
38
41
  private readonly store;
@@ -97,10 +97,13 @@ class FlowEngine {
97
97
  }
98
98
  const guard = transition.guard;
99
99
  if (guard) {
100
+ const guardStart = this.guardLogger ? performance.now() : 0;
100
101
  const output = await guard.validate(flow.context);
102
+ const guardDurationMicros = this.guardLogger ? Math.round((performance.now() - guardStart) * 1000) : 0;
101
103
  switch (output.type) {
102
104
  case 'accepted': {
103
- this.logGuard(flow, currentState, guard.name, 'accepted');
105
+ this.logGuard(flow, currentState, guard.name, 'accepted', guardDurationMicros);
106
+ const transStart = this.transitionLogger ? performance.now() : 0;
104
107
  const backup = flow.context.snapshot();
105
108
  if (output.data) {
106
109
  for (const [key, value] of output.data)
@@ -114,7 +117,7 @@ class FlowEngine {
114
117
  flow.transitionTo(transition.to);
115
118
  this.fireEnter(flow, transition.to);
116
119
  this.store.recordTransition(flow.id, from, transition.to, guard.name, flow.context);
117
- this.logTransition(flow, from, transition.to, guard.name);
120
+ this.logTransition(flow, from, transition.to, guard.name, transStart);
118
121
  }
119
122
  catch (e) {
120
123
  flow.context.restoreFrom(backup);
@@ -125,7 +128,7 @@ class FlowEngine {
125
128
  break;
126
129
  }
127
130
  case 'rejected': {
128
- this.logGuard(flow, currentState, guard.name, 'rejected', output.reason);
131
+ this.logGuard(flow, currentState, guard.name, 'rejected', guardDurationMicros, output.reason);
129
132
  flow.incrementGuardFailure(guard.name);
130
133
  if (flow.guardFailureCount >= definition.maxGuardRetries) {
131
134
  this.handleError(flow, currentState);
@@ -134,7 +137,7 @@ class FlowEngine {
134
137
  return flow;
135
138
  }
136
139
  case 'expired': {
137
- this.logGuard(flow, currentState, guard.name, 'expired');
140
+ this.logGuard(flow, currentState, guard.name, 'expired', guardDurationMicros);
138
141
  flow.complete('EXPIRED');
139
142
  this.store.save(flow);
140
143
  return flow;
@@ -142,12 +145,13 @@ class FlowEngine {
142
145
  }
143
146
  }
144
147
  else {
148
+ const transStart = this.transitionLogger ? performance.now() : 0;
145
149
  const from = flow.currentState;
146
150
  this.fireExit(flow, from);
147
151
  flow.transitionTo(transition.to);
148
152
  this.fireEnter(flow, transition.to);
149
153
  this.store.recordTransition(flow.id, from, transition.to, 'external', flow.context);
150
- this.logTransition(flow, from, transition.to, 'external');
154
+ this.logTransition(flow, from, transition.to, 'external', transStart);
151
155
  }
152
156
  await this.executeAutoChain(flow);
153
157
  this.store.save(flow);
@@ -175,6 +179,7 @@ class FlowEngine {
175
179
  if (!autoOrBranch)
176
180
  break;
177
181
  const backup = flow.context.snapshot();
182
+ const stepStart = this.transitionLogger ? performance.now() : 0;
178
183
  try {
179
184
  if (autoOrBranch.type === 'auto') {
180
185
  if (autoOrBranch.processor) {
@@ -187,7 +192,7 @@ class FlowEngine {
187
192
  this.fireEnter(flow, autoOrBranch.to);
188
193
  const trigger = autoOrBranch.processor?.name ?? 'auto';
189
194
  this.store.recordTransition(flow.id, from, autoOrBranch.to, trigger, flow.context);
190
- this.logTransition(flow, from, autoOrBranch.to, trigger);
195
+ this.logTransition(flow, from, autoOrBranch.to, trigger, stepStart);
191
196
  }
192
197
  else {
193
198
  const branch = autoOrBranch.branch;
@@ -205,7 +210,7 @@ class FlowEngine {
205
210
  this.fireEnter(flow, target);
206
211
  const trigger = `${branch.name}:${label}`;
207
212
  this.store.recordTransition(flow.id, from, target, trigger, flow.context);
208
- this.logTransition(flow, from, target, trigger);
213
+ this.logTransition(flow, from, target, trigger, stepStart);
209
214
  }
210
215
  }
211
216
  catch (e) {
@@ -229,13 +234,14 @@ class FlowEngine {
229
234
  parentFlow.setActiveSubFlow(null);
230
235
  const target = exitMappings.get(subFlow.exitState);
231
236
  if (target) {
237
+ const sfStart = this.transitionLogger ? performance.now() : 0;
232
238
  const from = parentFlow.currentState;
233
239
  this.fireExit(parentFlow, from);
234
240
  parentFlow.transitionTo(target);
235
241
  this.fireEnter(parentFlow, target);
236
242
  const trigger = `subFlow:${subDef.name}/${subFlow.exitState}`;
237
243
  this.store.recordTransition(parentFlow.id, from, target, trigger, parentFlow.context);
238
- this.logTransition(parentFlow, from, target, trigger);
244
+ this.logTransition(parentFlow, from, target, trigger, sfStart);
239
245
  return 1;
240
246
  }
241
247
  // Error bubbling: no exit mapping → fall back to parent's error transitions
@@ -253,17 +259,20 @@ class FlowEngine {
253
259
  }
254
260
  const guard = transition.guard;
255
261
  if (guard) {
262
+ const guardStart = this.guardLogger ? performance.now() : 0;
256
263
  const output = await guard.validate(parentFlow.context);
264
+ const guardDur = this.guardLogger ? Math.round((performance.now() - guardStart) * 1000) : 0;
257
265
  if (output.type === 'accepted') {
258
266
  if (output.data) {
259
267
  for (const [key, value] of output.data)
260
268
  parentFlow.context.put(key, value);
261
269
  }
270
+ const sfStart = this.transitionLogger ? performance.now() : 0;
262
271
  const sfFrom = subFlow.currentState;
263
272
  subFlow.transitionTo(transition.to);
264
273
  this.store.recordTransition(parentFlow.id, sfFrom, transition.to, guard.name, parentFlow.context);
265
- this.logTransition(parentFlow, sfFrom, transition.to, guard.name);
266
- this.logGuard(parentFlow, sfFrom, guard.name, 'accepted');
274
+ this.logTransition(parentFlow, sfFrom, transition.to, guard.name, sfStart);
275
+ this.logGuard(parentFlow, sfFrom, guard.name, 'accepted', guardDur);
267
276
  }
268
277
  else if (output.type === 'rejected') {
269
278
  subFlow.incrementGuardFailure();
@@ -290,13 +299,14 @@ class FlowEngine {
290
299
  if (subFlowT?.exitMappings) {
291
300
  const target = subFlowT.exitMappings.get(subFlow.exitState);
292
301
  if (target) {
302
+ const exitStart = this.transitionLogger ? performance.now() : 0;
293
303
  const from = parentFlow.currentState;
294
304
  this.fireExit(parentFlow, from);
295
305
  parentFlow.transitionTo(target);
296
306
  this.fireEnter(parentFlow, target);
297
307
  const trigger = `subFlow:${subDef.name}/${subFlow.exitState}`;
298
308
  this.store.recordTransition(parentFlow.id, from, target, trigger, parentFlow.context);
299
- this.logTransition(parentFlow, from, target, trigger);
309
+ this.logTransition(parentFlow, from, target, trigger, exitStart);
300
310
  await this.executeAutoChain(parentFlow);
301
311
  }
302
312
  }
@@ -323,16 +333,23 @@ class FlowEngine {
323
333
  if (action)
324
334
  action(flow.context);
325
335
  }
326
- logTransition(flow, from, to, trigger) {
327
- this.transitionLogger?.({ flowId: flow.id, flowName: flow.definition.name, from, to, trigger });
336
+ logTransition(flow, from, to, trigger, startMs) {
337
+ if (this.transitionLogger) {
338
+ const durationMicros = Math.round((performance.now() - startMs) * 1000);
339
+ this.transitionLogger({ flowId: flow.id, flowName: flow.definition.name, from, to, trigger, durationMicros });
340
+ }
328
341
  }
329
- logError(flow, from, to, trigger, cause) {
330
- this.errorLogger?.({ flowId: flow.id, flowName: flow.definition.name, from, to, trigger, cause });
342
+ logError(flow, from, to, trigger, cause, startMs) {
343
+ if (this.errorLogger) {
344
+ const durationMicros = Math.round((performance.now() - startMs) * 1000);
345
+ this.errorLogger({ flowId: flow.id, flowName: flow.definition.name, from, to, trigger, cause, durationMicros });
346
+ }
331
347
  }
332
- logGuard(flow, state, guardName, result, reason) {
333
- this.guardLogger?.({ flowId: flow.id, flowName: flow.definition.name, state, guardName, result, reason });
348
+ logGuard(flow, state, guardName, result, durationMicros, reason) {
349
+ this.guardLogger?.({ flowId: flow.id, flowName: flow.definition.name, state, guardName, result, reason, durationMicros });
334
350
  }
335
351
  handleError(flow, fromState, cause) {
352
+ const errorStart = (this.transitionLogger || this.errorLogger) ? performance.now() : 0;
336
353
  if (cause) {
337
354
  flow.setLastError(`${cause.constructor.name}: ${cause.message}`);
338
355
  if (cause instanceof flow_error_js_1.FlowError) {
@@ -342,7 +359,7 @@ class FlowEngine {
342
359
  cause.withContextSnapshot(available, new Set());
343
360
  }
344
361
  }
345
- this.logError(flow, fromState, null, 'error', cause ?? null);
362
+ this.logError(flow, fromState, null, 'error', cause ?? null, errorStart);
346
363
  // 1. Try exception-typed routes first (onStepError)
347
364
  if (cause && flow.definition.exceptionRoutes) {
348
365
  const routes = flow.definition.exceptionRoutes.get(fromState);
@@ -353,7 +370,7 @@ class FlowEngine {
353
370
  flow.transitionTo(route.target);
354
371
  const trigger = `error:${cause.constructor.name}`;
355
372
  this.store.recordTransition(flow.id, from, route.target, trigger, flow.context);
356
- this.logTransition(flow, from, route.target, trigger);
373
+ this.logTransition(flow, from, route.target, trigger, errorStart);
357
374
  if (flow.definition.stateConfig[route.target]?.terminal)
358
375
  flow.complete(route.target);
359
376
  return;
@@ -367,7 +384,7 @@ class FlowEngine {
367
384
  const from = flow.currentState;
368
385
  flow.transitionTo(errorTarget);
369
386
  this.store.recordTransition(flow.id, from, errorTarget, 'error', flow.context);
370
- this.logTransition(flow, from, errorTarget, 'error');
387
+ this.logTransition(flow, from, errorTarget, 'error', errorStart);
371
388
  if (flow.definition.stateConfig[errorTarget]?.terminal)
372
389
  flow.complete(errorTarget);
373
390
  }
@@ -92,14 +92,16 @@ class Pipeline {
92
92
  const completed = [];
93
93
  let prev = 'initial';
94
94
  for (const step of this.steps) {
95
- this.transitionLogger?.({ flowId, flowName: this.name, from: prev, to: step.name, trigger: step.name });
95
+ const stepStart = (this.transitionLogger || this.errorLogger) ? performance.now() : 0;
96
+ this.transitionLogger?.({ flowId, flowName: this.name, from: prev, to: step.name, trigger: step.name, durationMicros: 0 });
96
97
  const keysBefore = this.stateLogger ? new Set(ctx.snapshot().keys()) : null;
97
98
  try {
98
99
  await step.process(ctx);
99
100
  }
100
101
  catch (e) {
101
102
  const err = e instanceof Error ? e : new Error(String(e));
102
- this.errorLogger?.({ flowId, flowName: this.name, from: prev, to: step.name, trigger: step.name, cause: err });
103
+ const durationMicros = Math.round((performance.now() - stepStart) * 1000);
104
+ this.errorLogger?.({ flowId, flowName: this.name, from: prev, to: step.name, trigger: step.name, cause: err, durationMicros });
103
105
  throw new PipelineException(step.name, [...completed], ctx, err);
104
106
  }
105
107
  if (this.strictMode) {
@@ -10,6 +10,7 @@ export interface TransitionLogEntry {
10
10
  from: string | null;
11
11
  to: string;
12
12
  trigger: string;
13
+ durationMicros: number;
13
14
  }
14
15
  export interface StateLogEntry {
15
16
  flowId: string;
@@ -25,6 +26,7 @@ export interface ErrorLogEntry {
25
26
  to: string | null;
26
27
  trigger: string;
27
28
  cause: Error | null;
29
+ durationMicros: number;
28
30
  }
29
31
  export interface GuardLogEntry {
30
32
  flowId: string;
@@ -33,6 +35,7 @@ export interface GuardLogEntry {
33
35
  guardName: string;
34
36
  result: 'accepted' | 'rejected' | 'expired';
35
37
  reason?: string;
38
+ durationMicros: number;
36
39
  }
37
40
  export declare class FlowEngine {
38
41
  private readonly store;
@@ -94,10 +94,13 @@ export class FlowEngine {
94
94
  }
95
95
  const guard = transition.guard;
96
96
  if (guard) {
97
+ const guardStart = this.guardLogger ? performance.now() : 0;
97
98
  const output = await guard.validate(flow.context);
99
+ const guardDurationMicros = this.guardLogger ? Math.round((performance.now() - guardStart) * 1000) : 0;
98
100
  switch (output.type) {
99
101
  case 'accepted': {
100
- this.logGuard(flow, currentState, guard.name, 'accepted');
102
+ this.logGuard(flow, currentState, guard.name, 'accepted', guardDurationMicros);
103
+ const transStart = this.transitionLogger ? performance.now() : 0;
101
104
  const backup = flow.context.snapshot();
102
105
  if (output.data) {
103
106
  for (const [key, value] of output.data)
@@ -111,7 +114,7 @@ export class FlowEngine {
111
114
  flow.transitionTo(transition.to);
112
115
  this.fireEnter(flow, transition.to);
113
116
  this.store.recordTransition(flow.id, from, transition.to, guard.name, flow.context);
114
- this.logTransition(flow, from, transition.to, guard.name);
117
+ this.logTransition(flow, from, transition.to, guard.name, transStart);
115
118
  }
116
119
  catch (e) {
117
120
  flow.context.restoreFrom(backup);
@@ -122,7 +125,7 @@ export class FlowEngine {
122
125
  break;
123
126
  }
124
127
  case 'rejected': {
125
- this.logGuard(flow, currentState, guard.name, 'rejected', output.reason);
128
+ this.logGuard(flow, currentState, guard.name, 'rejected', guardDurationMicros, output.reason);
126
129
  flow.incrementGuardFailure(guard.name);
127
130
  if (flow.guardFailureCount >= definition.maxGuardRetries) {
128
131
  this.handleError(flow, currentState);
@@ -131,7 +134,7 @@ export class FlowEngine {
131
134
  return flow;
132
135
  }
133
136
  case 'expired': {
134
- this.logGuard(flow, currentState, guard.name, 'expired');
137
+ this.logGuard(flow, currentState, guard.name, 'expired', guardDurationMicros);
135
138
  flow.complete('EXPIRED');
136
139
  this.store.save(flow);
137
140
  return flow;
@@ -139,12 +142,13 @@ export class FlowEngine {
139
142
  }
140
143
  }
141
144
  else {
145
+ const transStart = this.transitionLogger ? performance.now() : 0;
142
146
  const from = flow.currentState;
143
147
  this.fireExit(flow, from);
144
148
  flow.transitionTo(transition.to);
145
149
  this.fireEnter(flow, transition.to);
146
150
  this.store.recordTransition(flow.id, from, transition.to, 'external', flow.context);
147
- this.logTransition(flow, from, transition.to, 'external');
151
+ this.logTransition(flow, from, transition.to, 'external', transStart);
148
152
  }
149
153
  await this.executeAutoChain(flow);
150
154
  this.store.save(flow);
@@ -172,6 +176,7 @@ export class FlowEngine {
172
176
  if (!autoOrBranch)
173
177
  break;
174
178
  const backup = flow.context.snapshot();
179
+ const stepStart = this.transitionLogger ? performance.now() : 0;
175
180
  try {
176
181
  if (autoOrBranch.type === 'auto') {
177
182
  if (autoOrBranch.processor) {
@@ -184,7 +189,7 @@ export class FlowEngine {
184
189
  this.fireEnter(flow, autoOrBranch.to);
185
190
  const trigger = autoOrBranch.processor?.name ?? 'auto';
186
191
  this.store.recordTransition(flow.id, from, autoOrBranch.to, trigger, flow.context);
187
- this.logTransition(flow, from, autoOrBranch.to, trigger);
192
+ this.logTransition(flow, from, autoOrBranch.to, trigger, stepStart);
188
193
  }
189
194
  else {
190
195
  const branch = autoOrBranch.branch;
@@ -202,7 +207,7 @@ export class FlowEngine {
202
207
  this.fireEnter(flow, target);
203
208
  const trigger = `${branch.name}:${label}`;
204
209
  this.store.recordTransition(flow.id, from, target, trigger, flow.context);
205
- this.logTransition(flow, from, target, trigger);
210
+ this.logTransition(flow, from, target, trigger, stepStart);
206
211
  }
207
212
  }
208
213
  catch (e) {
@@ -226,13 +231,14 @@ export class FlowEngine {
226
231
  parentFlow.setActiveSubFlow(null);
227
232
  const target = exitMappings.get(subFlow.exitState);
228
233
  if (target) {
234
+ const sfStart = this.transitionLogger ? performance.now() : 0;
229
235
  const from = parentFlow.currentState;
230
236
  this.fireExit(parentFlow, from);
231
237
  parentFlow.transitionTo(target);
232
238
  this.fireEnter(parentFlow, target);
233
239
  const trigger = `subFlow:${subDef.name}/${subFlow.exitState}`;
234
240
  this.store.recordTransition(parentFlow.id, from, target, trigger, parentFlow.context);
235
- this.logTransition(parentFlow, from, target, trigger);
241
+ this.logTransition(parentFlow, from, target, trigger, sfStart);
236
242
  return 1;
237
243
  }
238
244
  // Error bubbling: no exit mapping → fall back to parent's error transitions
@@ -250,17 +256,20 @@ export class FlowEngine {
250
256
  }
251
257
  const guard = transition.guard;
252
258
  if (guard) {
259
+ const guardStart = this.guardLogger ? performance.now() : 0;
253
260
  const output = await guard.validate(parentFlow.context);
261
+ const guardDur = this.guardLogger ? Math.round((performance.now() - guardStart) * 1000) : 0;
254
262
  if (output.type === 'accepted') {
255
263
  if (output.data) {
256
264
  for (const [key, value] of output.data)
257
265
  parentFlow.context.put(key, value);
258
266
  }
267
+ const sfStart = this.transitionLogger ? performance.now() : 0;
259
268
  const sfFrom = subFlow.currentState;
260
269
  subFlow.transitionTo(transition.to);
261
270
  this.store.recordTransition(parentFlow.id, sfFrom, transition.to, guard.name, parentFlow.context);
262
- this.logTransition(parentFlow, sfFrom, transition.to, guard.name);
263
- this.logGuard(parentFlow, sfFrom, guard.name, 'accepted');
271
+ this.logTransition(parentFlow, sfFrom, transition.to, guard.name, sfStart);
272
+ this.logGuard(parentFlow, sfFrom, guard.name, 'accepted', guardDur);
264
273
  }
265
274
  else if (output.type === 'rejected') {
266
275
  subFlow.incrementGuardFailure();
@@ -287,13 +296,14 @@ export class FlowEngine {
287
296
  if (subFlowT?.exitMappings) {
288
297
  const target = subFlowT.exitMappings.get(subFlow.exitState);
289
298
  if (target) {
299
+ const exitStart = this.transitionLogger ? performance.now() : 0;
290
300
  const from = parentFlow.currentState;
291
301
  this.fireExit(parentFlow, from);
292
302
  parentFlow.transitionTo(target);
293
303
  this.fireEnter(parentFlow, target);
294
304
  const trigger = `subFlow:${subDef.name}/${subFlow.exitState}`;
295
305
  this.store.recordTransition(parentFlow.id, from, target, trigger, parentFlow.context);
296
- this.logTransition(parentFlow, from, target, trigger);
306
+ this.logTransition(parentFlow, from, target, trigger, exitStart);
297
307
  await this.executeAutoChain(parentFlow);
298
308
  }
299
309
  }
@@ -320,16 +330,23 @@ export class FlowEngine {
320
330
  if (action)
321
331
  action(flow.context);
322
332
  }
323
- logTransition(flow, from, to, trigger) {
324
- this.transitionLogger?.({ flowId: flow.id, flowName: flow.definition.name, from, to, trigger });
333
+ logTransition(flow, from, to, trigger, startMs) {
334
+ if (this.transitionLogger) {
335
+ const durationMicros = Math.round((performance.now() - startMs) * 1000);
336
+ this.transitionLogger({ flowId: flow.id, flowName: flow.definition.name, from, to, trigger, durationMicros });
337
+ }
325
338
  }
326
- logError(flow, from, to, trigger, cause) {
327
- this.errorLogger?.({ flowId: flow.id, flowName: flow.definition.name, from, to, trigger, cause });
339
+ logError(flow, from, to, trigger, cause, startMs) {
340
+ if (this.errorLogger) {
341
+ const durationMicros = Math.round((performance.now() - startMs) * 1000);
342
+ this.errorLogger({ flowId: flow.id, flowName: flow.definition.name, from, to, trigger, cause, durationMicros });
343
+ }
328
344
  }
329
- logGuard(flow, state, guardName, result, reason) {
330
- this.guardLogger?.({ flowId: flow.id, flowName: flow.definition.name, state, guardName, result, reason });
345
+ logGuard(flow, state, guardName, result, durationMicros, reason) {
346
+ this.guardLogger?.({ flowId: flow.id, flowName: flow.definition.name, state, guardName, result, reason, durationMicros });
331
347
  }
332
348
  handleError(flow, fromState, cause) {
349
+ const errorStart = (this.transitionLogger || this.errorLogger) ? performance.now() : 0;
333
350
  if (cause) {
334
351
  flow.setLastError(`${cause.constructor.name}: ${cause.message}`);
335
352
  if (cause instanceof FlowError) {
@@ -339,7 +356,7 @@ export class FlowEngine {
339
356
  cause.withContextSnapshot(available, new Set());
340
357
  }
341
358
  }
342
- this.logError(flow, fromState, null, 'error', cause ?? null);
359
+ this.logError(flow, fromState, null, 'error', cause ?? null, errorStart);
343
360
  // 1. Try exception-typed routes first (onStepError)
344
361
  if (cause && flow.definition.exceptionRoutes) {
345
362
  const routes = flow.definition.exceptionRoutes.get(fromState);
@@ -350,7 +367,7 @@ export class FlowEngine {
350
367
  flow.transitionTo(route.target);
351
368
  const trigger = `error:${cause.constructor.name}`;
352
369
  this.store.recordTransition(flow.id, from, route.target, trigger, flow.context);
353
- this.logTransition(flow, from, route.target, trigger);
370
+ this.logTransition(flow, from, route.target, trigger, errorStart);
354
371
  if (flow.definition.stateConfig[route.target]?.terminal)
355
372
  flow.complete(route.target);
356
373
  return;
@@ -364,7 +381,7 @@ export class FlowEngine {
364
381
  const from = flow.currentState;
365
382
  flow.transitionTo(errorTarget);
366
383
  this.store.recordTransition(flow.id, from, errorTarget, 'error', flow.context);
367
- this.logTransition(flow, from, errorTarget, 'error');
384
+ this.logTransition(flow, from, errorTarget, 'error', errorStart);
368
385
  if (flow.definition.stateConfig[errorTarget]?.terminal)
369
386
  flow.complete(errorTarget);
370
387
  }
@@ -87,14 +87,16 @@ export class Pipeline {
87
87
  const completed = [];
88
88
  let prev = 'initial';
89
89
  for (const step of this.steps) {
90
- this.transitionLogger?.({ flowId, flowName: this.name, from: prev, to: step.name, trigger: step.name });
90
+ const stepStart = (this.transitionLogger || this.errorLogger) ? performance.now() : 0;
91
+ this.transitionLogger?.({ flowId, flowName: this.name, from: prev, to: step.name, trigger: step.name, durationMicros: 0 });
91
92
  const keysBefore = this.stateLogger ? new Set(ctx.snapshot().keys()) : null;
92
93
  try {
93
94
  await step.process(ctx);
94
95
  }
95
96
  catch (e) {
96
97
  const err = e instanceof Error ? e : new Error(String(e));
97
- this.errorLogger?.({ flowId, flowName: this.name, from: prev, to: step.name, trigger: step.name, cause: err });
98
+ const durationMicros = Math.round((performance.now() - stepStart) * 1000);
99
+ this.errorLogger?.({ flowId, flowName: this.name, from: prev, to: step.name, trigger: step.name, cause: err, durationMicros });
98
100
  throw new PipelineException(step.name, [...completed], ctx, err);
99
101
  }
100
102
  if (this.strictMode) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unlaxer/tramli",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Constrained flow engine — state machines that prevent invalid transitions at build time",
5
5
  "type": "module",
6
6
  "exports": {