@saga-a2ui/core 0.5.6-rc.1 → 0.7.6-rc.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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/actions/binding.ts +23 -5
- package/src/actions/category.ts +13 -4
- package/src/actions/definition.ts +250 -77
- package/src/actions/index.ts +14 -2
- package/src/actions/instance-host.ts +73 -21
- package/src/actions/invocation.ts +295 -76
- package/src/actions/limits.ts +23 -6
- package/src/actions/outcome.ts +57 -12
- package/src/actions/prerequisite.ts +33 -10
- package/src/actions/scope.ts +50 -14
- package/src/actions/server.ts +45 -12
- package/src/actions/thenable.ts +3 -1
- package/src/delivered-result.ts +31 -0
- package/src/host-links.ts +49 -0
- package/src/protocols/a2ui/v0_9/decoder.ts +48 -19
- package/src/protocols/a2ui/v0_9/deep-freeze.ts +8 -0
- package/src/protocols/a2ui/v0_9/index.ts +14 -1
- package/src/protocols/a2ui/v0_9/session.ts +26 -9
- package/src/protocols/a2ui/v0_9/surface-layout.ts +256 -0
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ActionPlan,
|
|
3
|
+
CanceledContext,
|
|
4
|
+
FailureContext,
|
|
5
|
+
} from './definition.js';
|
|
2
6
|
import type {
|
|
3
7
|
ActionObserver,
|
|
4
8
|
CancelCause,
|
|
@@ -13,7 +17,12 @@ import { categorize } from './category.js';
|
|
|
13
17
|
import { startTimer, type LimitTimer, type ResolvedLimits } from './limits.js';
|
|
14
18
|
import { readAnswer, type Prerequisite } from './prerequisite.js';
|
|
15
19
|
import { InvocationResources } from './scope.js';
|
|
16
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
requestMeta,
|
|
22
|
+
requestTimeout,
|
|
23
|
+
type ServerCalls,
|
|
24
|
+
type ServerTransport,
|
|
25
|
+
} from './server.js';
|
|
17
26
|
import { discardOutcome, isThenable } from './thenable.js';
|
|
18
27
|
|
|
19
28
|
// One activation that was admitted, from the user's event to its settlement. The runtime owns every
|
|
@@ -48,9 +57,18 @@ export interface InvocationOwner<Event> {
|
|
|
48
57
|
|
|
49
58
|
let invocationCount = 0;
|
|
50
59
|
|
|
51
|
-
type Settled =
|
|
60
|
+
type Settled =
|
|
61
|
+
| { readonly ok: true; readonly value: unknown }
|
|
62
|
+
| { readonly ok: false; readonly error: unknown };
|
|
52
63
|
type WorkStage = 'prerequisites' | 'running' | 'completing';
|
|
53
|
-
type StepKind =
|
|
64
|
+
type StepKind =
|
|
65
|
+
| 'prerequisite'
|
|
66
|
+
| 'handler'
|
|
67
|
+
| 'onSuccess'
|
|
68
|
+
| 'onFailure'
|
|
69
|
+
| 'onCanceled'
|
|
70
|
+
| 'release'
|
|
71
|
+
| 'onFinally';
|
|
54
72
|
|
|
55
73
|
/** What telemetry needs to know about a definition. */
|
|
56
74
|
export interface Observable {
|
|
@@ -68,7 +86,12 @@ export function emitObservation(
|
|
|
68
86
|
const { observer } = plan;
|
|
69
87
|
if (observer === undefined) return;
|
|
70
88
|
try {
|
|
71
|
-
const returned: unknown = observer({
|
|
89
|
+
const returned: unknown = observer({
|
|
90
|
+
label: plan.label,
|
|
91
|
+
bindingId,
|
|
92
|
+
elapsedMs: Date.now() - startedAt,
|
|
93
|
+
...event,
|
|
94
|
+
});
|
|
72
95
|
if (isThenable(returned)) discardOutcome(returned);
|
|
73
96
|
} catch {
|
|
74
97
|
// An observer cannot affect the action.
|
|
@@ -136,7 +159,10 @@ export class Invocation<Event> {
|
|
|
136
159
|
/** What `atActivation` returned; `undefined` for an action without it. */
|
|
137
160
|
private activation: unknown;
|
|
138
161
|
private readonly resources: InvocationResources;
|
|
139
|
-
private provided: Record<string, unknown> = Object.create(null) as Record<
|
|
162
|
+
private provided: Record<string, unknown> = Object.create(null) as Record<
|
|
163
|
+
string,
|
|
164
|
+
unknown
|
|
165
|
+
>;
|
|
140
166
|
private result: unknown;
|
|
141
167
|
private stage: WorkStage = 'prerequisites';
|
|
142
168
|
private outcome: Outcome<unknown> | undefined;
|
|
@@ -166,7 +192,8 @@ export class Invocation<Event> {
|
|
|
166
192
|
this.resources = new InvocationResources({
|
|
167
193
|
label: plan.label,
|
|
168
194
|
// The scope is active until the outcome is decided: that is work activity, not freshness.
|
|
169
|
-
isActive: () =>
|
|
195
|
+
isActive: () =>
|
|
196
|
+
this.outcome === undefined && this.pendingCancel === undefined,
|
|
170
197
|
hookMs: () => this.limits.hookMs,
|
|
171
198
|
report: (step) => this.emit('cleanup-error', { cleanupStep: step }),
|
|
172
199
|
});
|
|
@@ -218,7 +245,8 @@ export class Invocation<Event> {
|
|
|
218
245
|
if (this.replacedFlag) return 'replaced';
|
|
219
246
|
if (this.owner.isDisposed()) return 'disposed';
|
|
220
247
|
if (!this.live()) return 'not-live';
|
|
221
|
-
if (this.seam.subject !== undefined && this.subjectChanged())
|
|
248
|
+
if (this.seam.subject !== undefined && this.subjectChanged())
|
|
249
|
+
return 'subject-changed';
|
|
222
250
|
return undefined;
|
|
223
251
|
}
|
|
224
252
|
|
|
@@ -246,7 +274,9 @@ export class Invocation<Event> {
|
|
|
246
274
|
this.expireIfOverdue();
|
|
247
275
|
if (!step.eligible) return false;
|
|
248
276
|
this.recheck();
|
|
249
|
-
return
|
|
277
|
+
return (
|
|
278
|
+
step.eligible && !this.obsoleteFlag && this.staleCause() === undefined
|
|
279
|
+
);
|
|
250
280
|
}
|
|
251
281
|
|
|
252
282
|
// -------------------------------------------------------------------------------------------
|
|
@@ -280,8 +310,10 @@ export class Invocation<Event> {
|
|
|
280
310
|
const stage = this.stage;
|
|
281
311
|
this.controller.abort();
|
|
282
312
|
this.abandonStep();
|
|
283
|
-
if (stage === 'completing')
|
|
284
|
-
|
|
313
|
+
if (stage === 'completing')
|
|
314
|
+
this.decide({ status: 'canceled', cause, stage, result: this.result });
|
|
315
|
+
else if (stage === 'running')
|
|
316
|
+
this.decide({ status: 'canceled', cause, stage });
|
|
285
317
|
else this.decide({ status: 'canceled', cause, stage });
|
|
286
318
|
}
|
|
287
319
|
|
|
@@ -318,7 +350,11 @@ export class Invocation<Event> {
|
|
|
318
350
|
this.decide({
|
|
319
351
|
status: 'failed',
|
|
320
352
|
stage: 'input',
|
|
321
|
-
failure: {
|
|
353
|
+
failure: {
|
|
354
|
+
kind: 'invalid-input',
|
|
355
|
+
error,
|
|
356
|
+
category: categorize(error, 'validation'),
|
|
357
|
+
},
|
|
322
358
|
});
|
|
323
359
|
return false;
|
|
324
360
|
}
|
|
@@ -364,7 +400,11 @@ export class Invocation<Event> {
|
|
|
364
400
|
this.decide({
|
|
365
401
|
status: 'failed',
|
|
366
402
|
stage: 'input',
|
|
367
|
-
failure: {
|
|
403
|
+
failure: {
|
|
404
|
+
kind: 'activation-error',
|
|
405
|
+
error: threw.error,
|
|
406
|
+
category: categorize(threw.error),
|
|
407
|
+
},
|
|
368
408
|
});
|
|
369
409
|
} else {
|
|
370
410
|
this.activation = returned;
|
|
@@ -373,7 +413,8 @@ export class Invocation<Event> {
|
|
|
373
413
|
// If the callback threw, its failure is already the outcome and its cleanup may have started:
|
|
374
414
|
// the cancellation had already made the invocation obsolete, and it must not abort or abandon
|
|
375
415
|
// the step that belongs to that outcome.
|
|
376
|
-
if (pending !== undefined && this.outcome === undefined)
|
|
416
|
+
if (pending !== undefined && this.outcome === undefined)
|
|
417
|
+
this.cancel(pending);
|
|
377
418
|
this.drain();
|
|
378
419
|
return this.outcome === undefined;
|
|
379
420
|
}
|
|
@@ -429,7 +470,11 @@ export class Invocation<Event> {
|
|
|
429
470
|
);
|
|
430
471
|
}
|
|
431
472
|
|
|
432
|
-
private afterPrerequisite(
|
|
473
|
+
private afterPrerequisite(
|
|
474
|
+
current: Prerequisite<object, unknown, unknown>,
|
|
475
|
+
index: number,
|
|
476
|
+
settled: Settled,
|
|
477
|
+
): void {
|
|
433
478
|
// A deadline that passed unnoticed ends the wait before this answer counts, and so does a change
|
|
434
479
|
// nobody saw until now.
|
|
435
480
|
this.expireIfOverdue();
|
|
@@ -437,32 +482,64 @@ export class Invocation<Event> {
|
|
|
437
482
|
this.recheck();
|
|
438
483
|
if (this.outcome !== undefined) return;
|
|
439
484
|
const failure = (failed: PrerequisiteFailure): void => {
|
|
440
|
-
this.decide({
|
|
485
|
+
this.decide({
|
|
486
|
+
status: 'failed',
|
|
487
|
+
stage: 'prerequisites',
|
|
488
|
+
failure: failed,
|
|
489
|
+
});
|
|
441
490
|
};
|
|
442
491
|
const broken = (error: unknown): void =>
|
|
443
|
-
failure({
|
|
492
|
+
failure({
|
|
493
|
+
kind: 'prerequisite-error',
|
|
494
|
+
prerequisite: current.label,
|
|
495
|
+
error,
|
|
496
|
+
category: categorize(error),
|
|
497
|
+
});
|
|
444
498
|
if (!settled.ok) return broken(settled.error);
|
|
445
499
|
const answer = readAnswer(settled.value);
|
|
446
500
|
// A refusal is the application's own answer. Its reason text is not evidence of why.
|
|
447
501
|
if (answer.kind === 'refuse') {
|
|
448
|
-
return failure({
|
|
502
|
+
return failure({
|
|
503
|
+
kind: 'refused',
|
|
504
|
+
prerequisite: current.label,
|
|
505
|
+
reason: answer.reason,
|
|
506
|
+
category: 'application',
|
|
507
|
+
});
|
|
449
508
|
}
|
|
450
509
|
if (answer.kind === 'malformed') {
|
|
451
|
-
return broken(
|
|
510
|
+
return broken(
|
|
511
|
+
new TypeError(
|
|
512
|
+
`Prerequisite '${current.label}' must answer with proceed(...) or refuse(...).`,
|
|
513
|
+
),
|
|
514
|
+
);
|
|
452
515
|
}
|
|
453
516
|
for (const key of Object.keys(answer.provides)) {
|
|
454
517
|
if (key in this.provided) {
|
|
455
|
-
return broken(
|
|
518
|
+
return broken(
|
|
519
|
+
new TypeError(
|
|
520
|
+
`Prerequisite '${current.label}' provides '${key}', which an earlier prerequisite already provided.`,
|
|
521
|
+
),
|
|
522
|
+
);
|
|
456
523
|
}
|
|
457
524
|
}
|
|
458
525
|
for (const [key, value] of Object.entries(answer.provides)) {
|
|
459
|
-
Object.defineProperty(this.provided, key, {
|
|
526
|
+
Object.defineProperty(this.provided, key, {
|
|
527
|
+
value,
|
|
528
|
+
enumerable: true,
|
|
529
|
+
writable: false,
|
|
530
|
+
configurable: false,
|
|
531
|
+
});
|
|
460
532
|
}
|
|
461
533
|
this.nextPrerequisite(index + 1);
|
|
462
534
|
}
|
|
463
535
|
|
|
464
536
|
private providedSnapshot(): Record<string, unknown> {
|
|
465
|
-
return Object.freeze(
|
|
537
|
+
return Object.freeze(
|
|
538
|
+
Object.assign(
|
|
539
|
+
Object.create(null) as Record<string, unknown>,
|
|
540
|
+
this.provided,
|
|
541
|
+
),
|
|
542
|
+
);
|
|
466
543
|
}
|
|
467
544
|
|
|
468
545
|
private startHandler(): void {
|
|
@@ -482,7 +559,11 @@ export class Invocation<Event> {
|
|
|
482
559
|
this.decide({
|
|
483
560
|
status: 'failed',
|
|
484
561
|
stage: 'running',
|
|
485
|
-
failure: {
|
|
562
|
+
failure: {
|
|
563
|
+
kind: 'handler-error',
|
|
564
|
+
error: settled.error,
|
|
565
|
+
category: categorize(settled.error),
|
|
566
|
+
},
|
|
486
567
|
});
|
|
487
568
|
return;
|
|
488
569
|
}
|
|
@@ -500,7 +581,9 @@ export class Invocation<Event> {
|
|
|
500
581
|
signal: this.controller.signal,
|
|
501
582
|
scope: this.resources.invocation,
|
|
502
583
|
current: () => this.currentFor(step),
|
|
503
|
-
...(this.plan.transport === undefined
|
|
584
|
+
...(this.plan.transport === undefined
|
|
585
|
+
? {}
|
|
586
|
+
: { call: this.serverCall(step, this.plan.transport) }),
|
|
504
587
|
}),
|
|
505
588
|
);
|
|
506
589
|
}
|
|
@@ -512,21 +595,41 @@ export class Invocation<Event> {
|
|
|
512
595
|
* A rejection of the transport is the handler's to see, and arrives as `handler-error` unless
|
|
513
596
|
* the deadline or a cancellation decided first.
|
|
514
597
|
*/
|
|
515
|
-
private serverCall(
|
|
598
|
+
private serverCall(
|
|
599
|
+
step: Step,
|
|
600
|
+
transport: ServerTransport,
|
|
601
|
+
): ServerCalls['call'] {
|
|
516
602
|
const { label } = this.plan;
|
|
517
603
|
return async (name, args, options) => {
|
|
518
604
|
// Reading the options first makes a malformed request fail the same way at any time.
|
|
519
|
-
const { timeoutMs: asked, meta: suppliedMeta } = (options ?? {}) as {
|
|
520
|
-
|
|
605
|
+
const { timeoutMs: asked, meta: suppliedMeta } = (options ?? {}) as {
|
|
606
|
+
timeoutMs?: unknown;
|
|
607
|
+
meta?: unknown;
|
|
608
|
+
};
|
|
609
|
+
if (typeof name !== 'string' || name === '')
|
|
610
|
+
throw new TypeError(
|
|
611
|
+
`Action '${label}': a server call needs a tool name.`,
|
|
612
|
+
);
|
|
521
613
|
this.recheck();
|
|
522
|
-
const remainingMs =
|
|
523
|
-
|
|
524
|
-
|
|
614
|
+
const remainingMs =
|
|
615
|
+
this.limits.deadlineMs - (Date.now() - this.workStartedAt);
|
|
616
|
+
if (
|
|
617
|
+
!step.eligible ||
|
|
618
|
+
this.controller.signal.aborted ||
|
|
619
|
+
remainingMs <= 0
|
|
620
|
+
) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
`Action '${label}': the invocation stopped waiting for this handler, so the request was not sent.`,
|
|
623
|
+
);
|
|
525
624
|
}
|
|
526
625
|
const timeout = requestTimeout(label, remainingMs, asked);
|
|
527
626
|
const meta = requestMeta(label, suppliedMeta);
|
|
528
627
|
return transport.callServerTool(
|
|
529
|
-
{
|
|
628
|
+
{
|
|
629
|
+
name,
|
|
630
|
+
arguments: args,
|
|
631
|
+
...(meta === undefined ? {} : { _meta: meta }),
|
|
632
|
+
},
|
|
530
633
|
{ signal: this.controller.signal, timeout },
|
|
531
634
|
);
|
|
532
635
|
};
|
|
@@ -535,7 +638,10 @@ export class Invocation<Event> {
|
|
|
535
638
|
private armDeadline(): void {
|
|
536
639
|
if (this.deadlineTimer !== undefined || this.outcome !== undefined) return;
|
|
537
640
|
// Counted from the end of the activation prefix, so progress and a later wait never extend it.
|
|
538
|
-
const remaining = Math.max(
|
|
641
|
+
const remaining = Math.max(
|
|
642
|
+
0,
|
|
643
|
+
this.limits.deadlineMs - (Date.now() - this.workStartedAt),
|
|
644
|
+
);
|
|
539
645
|
this.deadlineTimer = startTimer(remaining, () => this.onDeadline());
|
|
540
646
|
}
|
|
541
647
|
|
|
@@ -552,7 +658,8 @@ export class Invocation<Event> {
|
|
|
552
658
|
*/
|
|
553
659
|
private expireIfOverdue(): void {
|
|
554
660
|
if (this.deadlineTimer === undefined || this.outcome !== undefined) return;
|
|
555
|
-
if (Date.now() - this.workStartedAt >= this.limits.deadlineMs)
|
|
661
|
+
if (Date.now() - this.workStartedAt >= this.limits.deadlineMs)
|
|
662
|
+
this.onDeadline();
|
|
556
663
|
}
|
|
557
664
|
|
|
558
665
|
private onDeadline(): void {
|
|
@@ -565,8 +672,16 @@ export class Invocation<Event> {
|
|
|
565
672
|
this.controller.abort();
|
|
566
673
|
this.decide(
|
|
567
674
|
stage === 'prerequisites'
|
|
568
|
-
? {
|
|
569
|
-
|
|
675
|
+
? {
|
|
676
|
+
status: 'failed',
|
|
677
|
+
stage,
|
|
678
|
+
failure: { kind: 'timeout', category: 'timeout' },
|
|
679
|
+
}
|
|
680
|
+
: {
|
|
681
|
+
status: 'failed',
|
|
682
|
+
stage,
|
|
683
|
+
failure: { kind: 'timeout', category: 'timeout' },
|
|
684
|
+
},
|
|
570
685
|
);
|
|
571
686
|
}
|
|
572
687
|
|
|
@@ -602,11 +717,24 @@ export class Invocation<Event> {
|
|
|
602
717
|
this.decide({
|
|
603
718
|
status: 'failed',
|
|
604
719
|
stage: 'running',
|
|
605
|
-
failure: {
|
|
720
|
+
failure: {
|
|
721
|
+
kind: 'success-hook-error',
|
|
722
|
+
error: settled.error,
|
|
723
|
+
result,
|
|
724
|
+
category: categorize(settled.error),
|
|
725
|
+
},
|
|
606
726
|
});
|
|
607
727
|
},
|
|
608
728
|
expire: () =>
|
|
609
|
-
this.decide({
|
|
729
|
+
this.decide({
|
|
730
|
+
status: 'failed',
|
|
731
|
+
stage: 'running',
|
|
732
|
+
failure: {
|
|
733
|
+
kind: 'success-hook-timeout',
|
|
734
|
+
result,
|
|
735
|
+
category: 'timeout',
|
|
736
|
+
},
|
|
737
|
+
}),
|
|
610
738
|
},
|
|
611
739
|
);
|
|
612
740
|
}
|
|
@@ -620,13 +748,19 @@ export class Invocation<Event> {
|
|
|
620
748
|
this.abandonStep();
|
|
621
749
|
this.clearDeadline();
|
|
622
750
|
this.outcome = outcome;
|
|
623
|
-
if (
|
|
751
|
+
if (
|
|
752
|
+
this.activating &&
|
|
753
|
+
outcome.status === 'failed' &&
|
|
754
|
+
this.plan.hooks.onFailure === undefined
|
|
755
|
+
) {
|
|
624
756
|
const { failure } = outcome;
|
|
625
757
|
if ('error' in failure) this.rethrow = { error: failure.error };
|
|
626
758
|
}
|
|
627
759
|
this.emit('outcome-decided', {
|
|
628
760
|
status: outcome.status,
|
|
629
|
-
...(outcome.status === 'failed'
|
|
761
|
+
...(outcome.status === 'failed'
|
|
762
|
+
? { failureKind: outcome.failure.kind }
|
|
763
|
+
: {}),
|
|
630
764
|
});
|
|
631
765
|
this.owner.decided(this);
|
|
632
766
|
this.later(() => this.runOutcomeHook());
|
|
@@ -634,14 +768,19 @@ export class Invocation<Event> {
|
|
|
634
768
|
|
|
635
769
|
private runOutcomeHook(): void {
|
|
636
770
|
const outcome = this.outcome;
|
|
637
|
-
if (outcome === undefined || outcome.status === 'succeeded')
|
|
771
|
+
if (outcome === undefined || outcome.status === 'succeeded')
|
|
772
|
+
return this.releaseHeld();
|
|
638
773
|
if (outcome.status === 'canceled') {
|
|
639
774
|
const hook = this.plan.hooks.onCanceled;
|
|
640
775
|
if (hook === undefined) return this.releaseHeld();
|
|
641
|
-
this.runHook(
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
776
|
+
this.runHook(
|
|
777
|
+
'onCanceled',
|
|
778
|
+
(step) => hook(this.canceledContext(step, outcome)),
|
|
779
|
+
{
|
|
780
|
+
onResult: (settled) => this.afterCleanup('onCanceled', settled),
|
|
781
|
+
expire: () => this.cleanupOverran('onCanceled'),
|
|
782
|
+
},
|
|
783
|
+
);
|
|
645
784
|
return;
|
|
646
785
|
}
|
|
647
786
|
const hook = this.plan.hooks.onFailure;
|
|
@@ -652,18 +791,23 @@ export class Invocation<Event> {
|
|
|
652
791
|
this.recordCleanup({ step: 'onFailure', reason: 'obsolete' });
|
|
653
792
|
return this.releaseHeld();
|
|
654
793
|
}
|
|
655
|
-
this.runHook(
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
this.
|
|
660
|
-
|
|
794
|
+
this.runHook(
|
|
795
|
+
'onFailure',
|
|
796
|
+
(step) => hook(this.failureContext(step, outcome)),
|
|
797
|
+
{
|
|
798
|
+
onResult: (settled) => this.afterCleanup('onFailure', settled),
|
|
799
|
+
expire: () => this.cleanupOverran('onFailure'),
|
|
800
|
+
onObsolete: () => {
|
|
801
|
+
this.recordCleanup({ step: 'onFailure', reason: 'obsolete' });
|
|
802
|
+
this.later(() => this.releaseHeld());
|
|
803
|
+
},
|
|
661
804
|
},
|
|
662
|
-
|
|
805
|
+
);
|
|
663
806
|
}
|
|
664
807
|
|
|
665
808
|
private afterCleanup(step: CleanupStep, settled: Settled): void {
|
|
666
|
-
if (!settled.ok)
|
|
809
|
+
if (!settled.ok)
|
|
810
|
+
this.recordCleanup({ step, reason: 'threw', error: settled.error });
|
|
667
811
|
this.releaseHeld();
|
|
668
812
|
}
|
|
669
813
|
|
|
@@ -692,7 +836,12 @@ export class Invocation<Event> {
|
|
|
692
836
|
},
|
|
693
837
|
},
|
|
694
838
|
onResult: (settled) => {
|
|
695
|
-
if (!settled.ok)
|
|
839
|
+
if (!settled.ok)
|
|
840
|
+
this.recordCleanup({
|
|
841
|
+
step: 'release',
|
|
842
|
+
reason: 'threw',
|
|
843
|
+
error: settled.error,
|
|
844
|
+
});
|
|
696
845
|
this.releaseHeld();
|
|
697
846
|
},
|
|
698
847
|
},
|
|
@@ -705,16 +854,27 @@ export class Invocation<Event> {
|
|
|
705
854
|
const hook = this.plan.hooks.onFinally;
|
|
706
855
|
const outcome = this.outcome;
|
|
707
856
|
if (hook === undefined || outcome === undefined) return this.settle();
|
|
708
|
-
this.runHook(
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
857
|
+
this.runHook(
|
|
858
|
+
'onFinally',
|
|
859
|
+
(step) =>
|
|
860
|
+
hook({
|
|
861
|
+
...this.hookBase(step),
|
|
862
|
+
outcome,
|
|
863
|
+
cleanupErrors: [...this.cleanupErrors],
|
|
864
|
+
obsolete: this.obsoleteFlag,
|
|
865
|
+
}),
|
|
866
|
+
{
|
|
867
|
+
onResult: (settled) => {
|
|
868
|
+
if (!settled.ok)
|
|
869
|
+
this.emit('cleanup-error', { cleanupStep: 'onFinally' });
|
|
870
|
+
this.settle();
|
|
871
|
+
},
|
|
872
|
+
expire: () => {
|
|
873
|
+
this.emit('cleanup-error', { cleanupStep: 'onFinally' });
|
|
874
|
+
this.settle();
|
|
875
|
+
},
|
|
716
876
|
},
|
|
717
|
-
|
|
877
|
+
);
|
|
718
878
|
}
|
|
719
879
|
|
|
720
880
|
private settle(): void {
|
|
@@ -722,7 +882,10 @@ export class Invocation<Event> {
|
|
|
722
882
|
this.settledFlag = true;
|
|
723
883
|
this.clearDeadline();
|
|
724
884
|
this.stopWatching();
|
|
725
|
-
this.emit(
|
|
885
|
+
this.emit(
|
|
886
|
+
'finished',
|
|
887
|
+
this.outcome === undefined ? {} : { status: this.outcome.status },
|
|
888
|
+
);
|
|
726
889
|
this.owner.settled(this);
|
|
727
890
|
}
|
|
728
891
|
|
|
@@ -735,7 +898,10 @@ export class Invocation<Event> {
|
|
|
735
898
|
// Hook contexts
|
|
736
899
|
// -------------------------------------------------------------------------------------------
|
|
737
900
|
|
|
738
|
-
private hookBase(step: Step): {
|
|
901
|
+
private hookBase(step: Step): {
|
|
902
|
+
readonly signal: AbortSignal;
|
|
903
|
+
readonly current: () => boolean;
|
|
904
|
+
} {
|
|
739
905
|
return { signal: step.signal, current: () => this.currentFor(step) };
|
|
740
906
|
}
|
|
741
907
|
|
|
@@ -745,11 +911,25 @@ export class Invocation<Event> {
|
|
|
745
911
|
): FailureContext<unknown, unknown, unknown, unknown> {
|
|
746
912
|
const base = this.hookBase(step);
|
|
747
913
|
const { params, activation } = this;
|
|
748
|
-
if (outcome.stage === 'input')
|
|
914
|
+
if (outcome.stage === 'input')
|
|
915
|
+
return { ...base, stage: outcome.stage, failure: outcome.failure };
|
|
749
916
|
if (outcome.stage === 'prerequisites') {
|
|
750
|
-
return {
|
|
917
|
+
return {
|
|
918
|
+
...base,
|
|
919
|
+
stage: outcome.stage,
|
|
920
|
+
failure: outcome.failure,
|
|
921
|
+
params,
|
|
922
|
+
activation,
|
|
923
|
+
};
|
|
751
924
|
}
|
|
752
|
-
return {
|
|
925
|
+
return {
|
|
926
|
+
...base,
|
|
927
|
+
stage: outcome.stage,
|
|
928
|
+
failure: outcome.failure,
|
|
929
|
+
params,
|
|
930
|
+
activation,
|
|
931
|
+
provided: this.providedSnapshot(),
|
|
932
|
+
};
|
|
753
933
|
}
|
|
754
934
|
|
|
755
935
|
private canceledContext(
|
|
@@ -759,11 +939,27 @@ export class Invocation<Event> {
|
|
|
759
939
|
const base = this.hookBase(step);
|
|
760
940
|
const { cause } = outcome;
|
|
761
941
|
const { params, activation } = this;
|
|
762
|
-
if (outcome.stage === 'prerequisites')
|
|
942
|
+
if (outcome.stage === 'prerequisites')
|
|
943
|
+
return { ...base, stage: outcome.stage, cause, params, activation };
|
|
763
944
|
if (outcome.stage === 'running') {
|
|
764
|
-
return {
|
|
945
|
+
return {
|
|
946
|
+
...base,
|
|
947
|
+
stage: outcome.stage,
|
|
948
|
+
cause,
|
|
949
|
+
params,
|
|
950
|
+
activation,
|
|
951
|
+
provided: this.providedSnapshot(),
|
|
952
|
+
};
|
|
765
953
|
}
|
|
766
|
-
return {
|
|
954
|
+
return {
|
|
955
|
+
...base,
|
|
956
|
+
stage: outcome.stage,
|
|
957
|
+
cause,
|
|
958
|
+
params,
|
|
959
|
+
activation,
|
|
960
|
+
provided: this.providedSnapshot(),
|
|
961
|
+
result: outcome.result,
|
|
962
|
+
};
|
|
767
963
|
}
|
|
768
964
|
|
|
769
965
|
// -------------------------------------------------------------------------------------------
|
|
@@ -771,15 +967,24 @@ export class Invocation<Event> {
|
|
|
771
967
|
// -------------------------------------------------------------------------------------------
|
|
772
968
|
|
|
773
969
|
private runHook(
|
|
774
|
-
kind: Extract<
|
|
970
|
+
kind: Extract<
|
|
971
|
+
StepKind,
|
|
972
|
+
'onSuccess' | 'onFailure' | 'onCanceled' | 'onFinally'
|
|
973
|
+
>,
|
|
775
974
|
invoke: (step: Step) => unknown,
|
|
776
|
-
handling: {
|
|
975
|
+
handling: {
|
|
976
|
+
readonly onResult: (settled: Settled) => void;
|
|
977
|
+
readonly expire: () => void;
|
|
978
|
+
readonly onObsolete?: () => void;
|
|
979
|
+
},
|
|
777
980
|
): void {
|
|
778
981
|
this.runStep(
|
|
779
982
|
{
|
|
780
983
|
kind,
|
|
781
984
|
budget: { ms: this.limits.hookMs, expire: handling.expire },
|
|
782
|
-
...(handling.onObsolete === undefined
|
|
985
|
+
...(handling.onObsolete === undefined
|
|
986
|
+
? {}
|
|
987
|
+
: { onObsolete: handling.onObsolete }),
|
|
783
988
|
onResult: handling.onResult,
|
|
784
989
|
},
|
|
785
990
|
invoke,
|
|
@@ -794,7 +999,12 @@ export class Invocation<Event> {
|
|
|
794
999
|
*/
|
|
795
1000
|
private runStep(spec: StepSpec, invoke: (step: Step) => unknown): void {
|
|
796
1001
|
const own = spec.budget === undefined ? undefined : new AbortController();
|
|
797
|
-
const step = new Step(
|
|
1002
|
+
const step = new Step(
|
|
1003
|
+
spec.kind,
|
|
1004
|
+
own === undefined ? this.controller.signal : own.signal,
|
|
1005
|
+
own,
|
|
1006
|
+
spec.onObsolete,
|
|
1007
|
+
);
|
|
798
1008
|
this.step = step;
|
|
799
1009
|
let arrived = false;
|
|
800
1010
|
const arrive = (settled: Settled): void => {
|
|
@@ -868,7 +1078,16 @@ export class Invocation<Event> {
|
|
|
868
1078
|
}
|
|
869
1079
|
}
|
|
870
1080
|
|
|
871
|
-
private emit(
|
|
872
|
-
|
|
1081
|
+
private emit(
|
|
1082
|
+
phase: ObservedPhase,
|
|
1083
|
+
extra: Partial<
|
|
1084
|
+
Pick<ObservedEvent, 'status' | 'failureKind' | 'cleanupStep'>
|
|
1085
|
+
> = {},
|
|
1086
|
+
): void {
|
|
1087
|
+
emitObservation(this.plan, this.owner.bindingId, this.startedAt, {
|
|
1088
|
+
invocationId: this.id,
|
|
1089
|
+
phase,
|
|
1090
|
+
...extra,
|
|
1091
|
+
});
|
|
873
1092
|
}
|
|
874
1093
|
}
|