@ontrails/core 1.0.0-beta.17 → 1.0.0-beta.19
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/CHANGELOG.md +28 -0
- package/README.md +14 -27
- package/package.json +1 -1
- package/src/{cross-batch.ts → compose-batch.ts} +10 -10
- package/src/compose-schema.ts +36 -0
- package/src/contour.ts +2 -0
- package/src/draft.ts +2 -2
- package/src/errors.ts +65 -1
- package/src/execute.ts +304 -103
- package/src/fire.ts +2 -2
- package/src/index.ts +66 -10
- package/src/internal/fork-ctx.ts +4 -4
- package/src/layer-projection.ts +1 -0
- package/src/layer.ts +1 -1
- package/src/observe.ts +5 -5
- package/src/resource.ts +51 -32
- package/src/run.ts +24 -3
- package/src/schedule.ts +2 -0
- package/src/signal.ts +2 -0
- package/src/structured-examples.ts +8 -5
- package/src/surface-versioning.ts +42 -0
- package/src/topo.ts +3 -3
- package/src/trail.ts +673 -38
- package/src/type-utils.ts +21 -13
- package/src/types.ts +43 -21
- package/src/validate-established-topo.ts +3 -3
- package/src/validate-topo.ts +120 -30
- package/src/validation.ts +69 -10
- package/src/version-marker.ts +716 -0
- package/src/version-resolution.ts +308 -0
- package/src/version-runtime.ts +120 -0
- package/src/webhook.ts +2 -0
- package/src/cross-schema.ts +0 -36
package/src/execute.ts
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
import type { z } from 'zod';
|
|
10
10
|
|
|
11
|
-
import type { AnyTrail } from './trail.js';
|
|
11
|
+
import type { AnyTrail, TrailVersionForkEntry } from './trail.js';
|
|
12
12
|
import type { Layer } from './layer.js';
|
|
13
13
|
import type { ResourceOverrideMap } from './resource.js';
|
|
14
14
|
import type { TraceContext, TraceRecord } from './tracing.js';
|
|
15
15
|
import type { Topo } from './topo.js';
|
|
16
|
+
import type { TrailVersionReference } from './version-resolution.js';
|
|
16
17
|
|
|
17
18
|
import {
|
|
18
19
|
buildActivationProvenanceTraceAttrs,
|
|
@@ -26,8 +27,9 @@ import {
|
|
|
26
27
|
} from './fire.js';
|
|
27
28
|
import type { BasePermit } from './permits.js';
|
|
28
29
|
import type {
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
ComposeBatchOptions,
|
|
31
|
+
ComposeOptions,
|
|
32
|
+
ComposeFn,
|
|
31
33
|
Detour,
|
|
32
34
|
Implementation,
|
|
33
35
|
TraceFn,
|
|
@@ -36,7 +38,7 @@ import type {
|
|
|
36
38
|
} from './types.js';
|
|
37
39
|
|
|
38
40
|
import { createTrailContext, passthroughTrace } from './context.js';
|
|
39
|
-
import {
|
|
41
|
+
import { buildComposeValidationSchema } from './compose-schema.js';
|
|
40
42
|
import {
|
|
41
43
|
CancelledError,
|
|
42
44
|
InternalError,
|
|
@@ -47,10 +49,10 @@ import {
|
|
|
47
49
|
ValidationError,
|
|
48
50
|
} from './errors.js';
|
|
49
51
|
import {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
} from './
|
|
52
|
+
claimNextComposeBatchIndex,
|
|
53
|
+
createComposeBatchValidationResults,
|
|
54
|
+
normalizeComposeBatchConcurrency,
|
|
55
|
+
} from './compose-batch.js';
|
|
54
56
|
import { forkCtx } from './internal/fork-ctx.js';
|
|
55
57
|
import {
|
|
56
58
|
TRACE_CONTEXT_KEY,
|
|
@@ -72,11 +74,22 @@ import { createResourceLookup } from './resource.js';
|
|
|
72
74
|
import { createResources } from './resource-config.js';
|
|
73
75
|
import { LAYER_INPUTS_KEY, SURFACE_KEY } from './types.js';
|
|
74
76
|
import { validateInput, validateOutput } from './validation.js';
|
|
77
|
+
import { executeTrailRevision } from './version-runtime.js';
|
|
78
|
+
import type { TrailVersionCurrentExecutor } from './version-runtime.js';
|
|
79
|
+
import {
|
|
80
|
+
parseTrailIdVersionReference,
|
|
81
|
+
resolveTrailVersion,
|
|
82
|
+
} from './version-resolution.js';
|
|
75
83
|
|
|
76
84
|
type MutableTrailContext = {
|
|
77
85
|
-readonly [K in keyof TrailContext]: TrailContext[K];
|
|
78
86
|
};
|
|
79
87
|
|
|
88
|
+
type ComposeForwardOptions = Omit<
|
|
89
|
+
ExecuteTrailInternalOptions,
|
|
90
|
+
'createContext' | 'composeValidation' | 'validationSchema' | 'version'
|
|
91
|
+
>;
|
|
92
|
+
|
|
80
93
|
// ---------------------------------------------------------------------------
|
|
81
94
|
// Options
|
|
82
95
|
// ---------------------------------------------------------------------------
|
|
@@ -90,7 +103,7 @@ export interface ExecuteTrailOptions {
|
|
|
90
103
|
/**
|
|
91
104
|
* Typed layers supplied for this execution.
|
|
92
105
|
*
|
|
93
|
-
* Layers compose around the
|
|
106
|
+
* Layers compose around the blaze. Layers without `input`
|
|
94
107
|
* schemas are surface-invisible wrappers for concerns such as tenant guards,
|
|
95
108
|
* rate limiting, circuit breaking, or custom audit logging.
|
|
96
109
|
*/
|
|
@@ -153,15 +166,41 @@ export interface ExecuteTrailOptions {
|
|
|
153
166
|
* @see TRL-473 for the CLI projection contract.
|
|
154
167
|
*/
|
|
155
168
|
readonly layerInputs?: Readonly<Record<string, unknown>> | undefined;
|
|
169
|
+
/**
|
|
170
|
+
* Execute a specific live trail version.
|
|
171
|
+
*
|
|
172
|
+
* Omit for the current top-level contract. Number and numeric-string
|
|
173
|
+
* references select authored versions; marker references select projected
|
|
174
|
+
* content-addressed markers by unambiguous prefix.
|
|
175
|
+
*/
|
|
176
|
+
readonly version?: TrailVersionReference | undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Internal executor options used by framework-managed compose and fork dispatch.
|
|
181
|
+
*
|
|
182
|
+
* These fields intentionally stay out of the exported public
|
|
183
|
+
* {@link ExecuteTrailOptions} surface.
|
|
184
|
+
*/
|
|
185
|
+
interface ExecuteTrailInternalOptions extends ExecuteTrailOptions {
|
|
186
|
+
/**
|
|
187
|
+
* Marks this invocation as a `ctx.compose()` dispatch so versioned fork
|
|
188
|
+
* entries validate against their own `composeInput`.
|
|
189
|
+
*
|
|
190
|
+
* Used by the compose execution path; not part of the public API.
|
|
191
|
+
*
|
|
192
|
+
* @internal
|
|
193
|
+
*/
|
|
194
|
+
readonly composeValidation?: boolean | undefined;
|
|
156
195
|
/**
|
|
157
196
|
* Override the validation schema used for input validation.
|
|
158
197
|
*
|
|
159
|
-
* When a trail is invoked via `ctx.
|
|
160
|
-
* `
|
|
161
|
-
* `trail.
|
|
198
|
+
* When a trail is invoked via `ctx.compose()` and the target declares
|
|
199
|
+
* `composeInput`, the compose function merges `trail.input` with
|
|
200
|
+
* `trail.composeInput` and passes the merged schema here so validation
|
|
162
201
|
* accepts both public and composition-only fields.
|
|
163
202
|
*
|
|
164
|
-
* Used by the
|
|
203
|
+
* Used by the compose execution path; not part of the public API.
|
|
165
204
|
*
|
|
166
205
|
* @internal
|
|
167
206
|
*/
|
|
@@ -174,7 +213,7 @@ export interface ExecuteTrailOptions {
|
|
|
174
213
|
|
|
175
214
|
const applyContextOverrides = (
|
|
176
215
|
base: TrailContextInit,
|
|
177
|
-
options?:
|
|
216
|
+
options?: ExecuteTrailInternalOptions
|
|
178
217
|
): TrailContextInit => {
|
|
179
218
|
const withOverrides = options?.ctx
|
|
180
219
|
? {
|
|
@@ -202,7 +241,7 @@ const applyContextOverrides = (
|
|
|
202
241
|
return withPermit;
|
|
203
242
|
}
|
|
204
243
|
// Merge per-layer inputs onto any inherited LAYER_INPUTS_KEY slot so
|
|
205
|
-
//
|
|
244
|
+
// composed/forked contexts can preserve outer-surface metadata.
|
|
206
245
|
const inheritedExtensions = withPermit.extensions ?? {};
|
|
207
246
|
const inheritedLayerInputs = (inheritedExtensions[LAYER_INPUTS_KEY] ?? {}) as
|
|
208
247
|
| Readonly<Record<string, unknown>>
|
|
@@ -221,7 +260,7 @@ const applyContextOverrides = (
|
|
|
221
260
|
|
|
222
261
|
const bindResourceLookup = (
|
|
223
262
|
resolved: TrailContextInit,
|
|
224
|
-
options?:
|
|
263
|
+
options?: ExecuteTrailInternalOptions
|
|
225
264
|
): TrailContext => {
|
|
226
265
|
if (
|
|
227
266
|
options?.ctx?.extensions === undefined &&
|
|
@@ -250,7 +289,7 @@ const bindResourceLookup = (
|
|
|
250
289
|
* already carries.
|
|
251
290
|
*/
|
|
252
291
|
const resolveContext = async (
|
|
253
|
-
options?:
|
|
292
|
+
options?: ExecuteTrailInternalOptions
|
|
254
293
|
): Promise<TrailContext> => {
|
|
255
294
|
const seed = options?.createContext
|
|
256
295
|
? await options.createContext()
|
|
@@ -335,7 +374,7 @@ const enforcePermitRequirement = (
|
|
|
335
374
|
|
|
336
375
|
const prepareContext = async (
|
|
337
376
|
trail: AnyTrail,
|
|
338
|
-
options?:
|
|
377
|
+
options?: ExecuteTrailInternalOptions
|
|
339
378
|
): Promise<
|
|
340
379
|
Result<
|
|
341
380
|
{ readonly ctx: TrailContext; readonly releaseResources: () => void },
|
|
@@ -435,8 +474,8 @@ const extractPermit = (
|
|
|
435
474
|
* The returned function reads the *current* trace context from its captured
|
|
436
475
|
* parent. That means direct nesting (`ctx.trace('a', () => ctx.trace('b',
|
|
437
476
|
* ...))`) produces siblings under `a`'s parent, not children of `a`. For
|
|
438
|
-
* true child nesting, callers should
|
|
439
|
-
* its own root record parented by this one) — full
|
|
477
|
+
* true child nesting, callers should compose into another trail (which gets
|
|
478
|
+
* its own root record parented by this one) — full compose-trail parenting
|
|
440
479
|
* is implemented in a later phase. For Phase 1, sibling spans under the
|
|
441
480
|
* trail's root are the supported shape.
|
|
442
481
|
*/
|
|
@@ -464,7 +503,7 @@ const buildTracedContext = (
|
|
|
464
503
|
sink: ReturnType<typeof getTraceSink>
|
|
465
504
|
): { readonly record: TraceRecord; readonly tracedCtx: TrailContext } => {
|
|
466
505
|
// If a parent trace context is present (set by an outer executeTrail when
|
|
467
|
-
// the current trail was invoked via ctx.
|
|
506
|
+
// the current trail was invoked via ctx.compose or ctx.fire), inherit its
|
|
468
507
|
// traceId/rootId so the trace tree spans trail boundaries. Otherwise this
|
|
469
508
|
// execution becomes a fresh root.
|
|
470
509
|
const parent = ctx.extensions?.[TRACE_CONTEXT_KEY] as
|
|
@@ -553,25 +592,35 @@ const runImplWithRootRecord = async (
|
|
|
553
592
|
}
|
|
554
593
|
};
|
|
555
594
|
|
|
556
|
-
|
|
595
|
+
interface ResolvedComposeTarget {
|
|
596
|
+
readonly trail: AnyTrail;
|
|
597
|
+
readonly version?: TrailVersionReference | undefined;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const resolveComposeTarget = (
|
|
557
601
|
trailOrId: AnyTrail | string,
|
|
558
602
|
topo: Topo | undefined
|
|
559
|
-
): Result<
|
|
603
|
+
): Result<ResolvedComposeTarget, Error> => {
|
|
560
604
|
if (typeof trailOrId !== 'string') {
|
|
561
|
-
return Result.ok(trailOrId);
|
|
605
|
+
return Result.ok({ trail: trailOrId });
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const parsed = parseTrailIdVersionReference(trailOrId);
|
|
609
|
+
if (parsed.isErr()) {
|
|
610
|
+
return parsed;
|
|
562
611
|
}
|
|
563
612
|
|
|
564
613
|
if (topo === undefined) {
|
|
565
614
|
return Result.err(
|
|
566
615
|
new NotFoundError(
|
|
567
|
-
`Trail "${trailOrId}" cannot be
|
|
616
|
+
`Trail "${trailOrId}" cannot be composed without topo access`
|
|
568
617
|
)
|
|
569
618
|
);
|
|
570
619
|
}
|
|
571
620
|
|
|
572
|
-
const target = topo.get(
|
|
621
|
+
const target = topo.get(parsed.value.id);
|
|
573
622
|
return target
|
|
574
|
-
? Result.ok(target)
|
|
623
|
+
? Result.ok({ trail: target, version: parsed.value.version })
|
|
575
624
|
: Result.err(
|
|
576
625
|
new NotFoundError(
|
|
577
626
|
`Trail "${trailOrId}" not found in topo "${topo.name}"`
|
|
@@ -613,7 +662,7 @@ const deriveConcurrentBranchObserveMetadata = (
|
|
|
613
662
|
return {
|
|
614
663
|
...readObserveLoggerMetadata(ctx),
|
|
615
664
|
branchIndex,
|
|
616
|
-
|
|
665
|
+
composedTrailId: target.id,
|
|
617
666
|
};
|
|
618
667
|
};
|
|
619
668
|
|
|
@@ -645,19 +694,19 @@ const deriveConcurrentBranchLogger = (
|
|
|
645
694
|
) =>
|
|
646
695
|
ctx.logger?.child?.({
|
|
647
696
|
branchIndex,
|
|
648
|
-
|
|
697
|
+
composedTrailId: target.id,
|
|
649
698
|
}) ?? ctx.logger;
|
|
650
699
|
|
|
651
700
|
/**
|
|
652
|
-
* Build a child context for one concurrent
|
|
701
|
+
* Build a child context for one concurrent composing branch.
|
|
653
702
|
*
|
|
654
|
-
* Concurrent
|
|
703
|
+
* Concurrent compositions should not inherit already-resolved resource instances
|
|
655
704
|
* from the parent execution scope. Stripping resource IDs from extensions
|
|
656
705
|
* forces each branch to resolve its own scope while still carrying forward
|
|
657
706
|
* request-scoped values like tracing, surface identity, permits, and the
|
|
658
707
|
* shared AbortSignal.
|
|
659
708
|
*
|
|
660
|
-
* `
|
|
709
|
+
* `compose`, `fire`, and `resource` are cleared so the child execution can
|
|
661
710
|
* rebind them to the branch-local context instead of reusing closures that
|
|
662
711
|
* capture the parent scope.
|
|
663
712
|
*/
|
|
@@ -672,115 +721,131 @@ const buildConcurrentBranchContext = (
|
|
|
672
721
|
logger: deriveConcurrentBranchLogger(ctx, target, branchIndex),
|
|
673
722
|
});
|
|
674
723
|
|
|
675
|
-
const
|
|
724
|
+
const executeResolvedComposeTarget = async (
|
|
676
725
|
target: AnyTrail,
|
|
677
726
|
input: unknown,
|
|
678
727
|
ctx: TrailContext,
|
|
679
728
|
topo: Topo | undefined,
|
|
680
|
-
forwarded:
|
|
729
|
+
forwarded: ComposeForwardOptions,
|
|
730
|
+
version?: TrailVersionReference | undefined
|
|
681
731
|
): Promise<Result<unknown, Error>> =>
|
|
682
732
|
await // eslint-disable-next-line no-use-before-define -- executor closure runs only after executeTrail is defined
|
|
683
|
-
|
|
733
|
+
executeTrailInternal(target, input, {
|
|
684
734
|
...forwarded,
|
|
735
|
+
composeValidation: true,
|
|
685
736
|
ctx,
|
|
686
737
|
topo,
|
|
687
|
-
|
|
738
|
+
...(version === undefined ? {} : { version }),
|
|
739
|
+
validationSchema: buildComposeValidationSchema(target),
|
|
688
740
|
});
|
|
689
741
|
|
|
690
|
-
const
|
|
742
|
+
const executeComposeTarget = async (
|
|
691
743
|
trailOrId: AnyTrail | string,
|
|
692
744
|
input: unknown,
|
|
693
745
|
ctx: TrailContext,
|
|
694
746
|
topo: Topo | undefined,
|
|
695
|
-
forwarded:
|
|
747
|
+
forwarded: ComposeForwardOptions,
|
|
748
|
+
composeOptions?: ComposeOptions | undefined
|
|
696
749
|
): Promise<Result<unknown, Error>> => {
|
|
697
|
-
const target =
|
|
750
|
+
const target = resolveComposeTarget(trailOrId, topo);
|
|
698
751
|
if (target.isErr()) {
|
|
699
752
|
return target;
|
|
700
753
|
}
|
|
754
|
+
if (
|
|
755
|
+
target.value.version !== undefined &&
|
|
756
|
+
composeOptions?.version !== undefined
|
|
757
|
+
) {
|
|
758
|
+
return Result.err(
|
|
759
|
+
new ValidationError(
|
|
760
|
+
`Trail "${target.value.trail.id}" version was provided both in the id reference and ctx.compose() options`
|
|
761
|
+
)
|
|
762
|
+
);
|
|
763
|
+
}
|
|
701
764
|
|
|
702
|
-
return await
|
|
703
|
-
target.value,
|
|
765
|
+
return await executeResolvedComposeTarget(
|
|
766
|
+
target.value.trail,
|
|
704
767
|
input,
|
|
705
768
|
ctx,
|
|
706
769
|
topo,
|
|
707
|
-
forwarded
|
|
770
|
+
forwarded,
|
|
771
|
+
composeOptions?.version ?? target.value.version
|
|
708
772
|
);
|
|
709
773
|
};
|
|
710
774
|
|
|
711
|
-
type
|
|
775
|
+
type ComposeBatchCall = readonly [AnyTrail | string, unknown];
|
|
712
776
|
|
|
713
|
-
const
|
|
714
|
-
call:
|
|
777
|
+
const executeConcurrentComposeBatchCall = async (
|
|
778
|
+
call: ComposeBatchCall,
|
|
715
779
|
branchIndex: number,
|
|
716
780
|
ctx: TrailContext,
|
|
717
781
|
topo: Topo | undefined,
|
|
718
|
-
forwarded:
|
|
782
|
+
forwarded: ComposeForwardOptions
|
|
719
783
|
): Promise<Result<unknown, Error>> => {
|
|
720
784
|
const [trailOrId, batchInput] = call;
|
|
721
|
-
const target =
|
|
785
|
+
const target = resolveComposeTarget(trailOrId, topo);
|
|
722
786
|
if (target.isErr()) {
|
|
723
787
|
return target;
|
|
724
788
|
}
|
|
725
789
|
|
|
726
|
-
return await
|
|
727
|
-
target.value,
|
|
790
|
+
return await executeResolvedComposeTarget(
|
|
791
|
+
target.value.trail,
|
|
728
792
|
batchInput,
|
|
729
|
-
buildConcurrentBranchContext(ctx, target.value, topo, branchIndex),
|
|
793
|
+
buildConcurrentBranchContext(ctx, target.value.trail, topo, branchIndex),
|
|
730
794
|
topo,
|
|
731
|
-
forwarded
|
|
795
|
+
forwarded,
|
|
796
|
+
target.value.version
|
|
732
797
|
);
|
|
733
798
|
};
|
|
734
799
|
|
|
735
|
-
const
|
|
736
|
-
calls: readonly
|
|
800
|
+
const executeUnlimitedComposeBatch = async (
|
|
801
|
+
calls: readonly ComposeBatchCall[],
|
|
737
802
|
ctx: TrailContext,
|
|
738
803
|
topo: Topo | undefined,
|
|
739
|
-
forwarded:
|
|
804
|
+
forwarded: ComposeForwardOptions
|
|
740
805
|
): Promise<Result<unknown, Error>[]> =>
|
|
741
806
|
await Promise.all(
|
|
742
807
|
calls.map((call, branchIndex) =>
|
|
743
|
-
|
|
808
|
+
executeConcurrentComposeBatchCall(call, branchIndex, ctx, topo, forwarded)
|
|
744
809
|
)
|
|
745
810
|
);
|
|
746
811
|
|
|
747
|
-
const
|
|
748
|
-
calls: readonly
|
|
812
|
+
const createComposeBatchResults = (
|
|
813
|
+
calls: readonly ComposeBatchCall[]
|
|
749
814
|
): Result<unknown, Error>[] =>
|
|
750
815
|
Array.from<Result<unknown, Error>>({ length: calls.length });
|
|
751
816
|
|
|
752
|
-
const
|
|
753
|
-
calls: readonly
|
|
817
|
+
const executeLimitedComposeBatch = async (
|
|
818
|
+
calls: readonly ComposeBatchCall[],
|
|
754
819
|
ctx: TrailContext,
|
|
755
820
|
topo: Topo | undefined,
|
|
756
|
-
forwarded:
|
|
821
|
+
forwarded: ComposeForwardOptions,
|
|
757
822
|
limit: number
|
|
758
823
|
): Promise<Result<unknown, Error>[]> => {
|
|
759
|
-
const results =
|
|
824
|
+
const results = createComposeBatchResults(calls);
|
|
760
825
|
const nextIndex = { value: 0 };
|
|
761
826
|
|
|
762
827
|
const runWorker = async () => {
|
|
763
828
|
while (true) {
|
|
764
|
-
const branchIndex =
|
|
829
|
+
const branchIndex = claimNextComposeBatchIndex(nextIndex, calls);
|
|
765
830
|
if (branchIndex === undefined) {
|
|
766
831
|
return;
|
|
767
832
|
}
|
|
768
833
|
|
|
769
834
|
const call = calls[branchIndex];
|
|
770
835
|
if (call === undefined) {
|
|
771
|
-
// Defensive: `
|
|
836
|
+
// Defensive: `claimNextComposeBatchIndex` only returns indices within
|
|
772
837
|
// bounds, so this slot should always be populated. If it ever isn't,
|
|
773
838
|
// surface a clear InternalError in place of the missing slot and keep
|
|
774
839
|
// the worker loop running so sibling branches still get processed.
|
|
775
840
|
results[branchIndex] = Result.err(
|
|
776
841
|
new InternalError(
|
|
777
|
-
`unreachable: concurrent
|
|
842
|
+
`unreachable: concurrent compose batch call missing at index ${branchIndex}`
|
|
778
843
|
)
|
|
779
844
|
);
|
|
780
845
|
continue;
|
|
781
846
|
}
|
|
782
847
|
|
|
783
|
-
results[branchIndex] = await
|
|
848
|
+
results[branchIndex] = await executeConcurrentComposeBatchCall(
|
|
784
849
|
call,
|
|
785
850
|
branchIndex,
|
|
786
851
|
ctx,
|
|
@@ -794,81 +859,85 @@ const executeLimitedCrossBatch = async (
|
|
|
794
859
|
return results;
|
|
795
860
|
};
|
|
796
861
|
|
|
797
|
-
const
|
|
798
|
-
calls: readonly
|
|
862
|
+
const executeComposeBatch = async (
|
|
863
|
+
calls: readonly ComposeBatchCall[],
|
|
799
864
|
ctx: TrailContext,
|
|
800
865
|
topo: Topo | undefined,
|
|
801
|
-
forwarded:
|
|
802
|
-
batchOptions?:
|
|
866
|
+
forwarded: ComposeForwardOptions,
|
|
867
|
+
batchOptions?: ComposeBatchOptions
|
|
803
868
|
): Promise<Result<unknown, Error>[]> => {
|
|
804
869
|
if (calls.length === 0) {
|
|
805
870
|
return [];
|
|
806
871
|
}
|
|
807
872
|
|
|
808
|
-
const concurrency =
|
|
873
|
+
const concurrency = normalizeComposeBatchConcurrency(batchOptions);
|
|
809
874
|
if (concurrency.isErr()) {
|
|
810
|
-
return
|
|
875
|
+
return createComposeBatchValidationResults(calls, concurrency.error);
|
|
811
876
|
}
|
|
812
877
|
|
|
813
878
|
const limit = concurrency.value ?? calls.length;
|
|
814
879
|
return limit >= calls.length
|
|
815
|
-
? await
|
|
816
|
-
: await
|
|
880
|
+
? await executeUnlimitedComposeBatch(calls, ctx, topo, forwarded)
|
|
881
|
+
: await executeLimitedComposeBatch(calls, ctx, topo, forwarded, limit);
|
|
817
882
|
};
|
|
818
883
|
|
|
819
|
-
const
|
|
884
|
+
const bindComposeToCtx = (
|
|
820
885
|
ctx: TrailContext,
|
|
821
886
|
topo: Topo | undefined,
|
|
822
|
-
options:
|
|
887
|
+
options: ExecuteTrailInternalOptions | undefined
|
|
823
888
|
): TrailContext => {
|
|
824
|
-
if (ctx.
|
|
889
|
+
if (ctx.compose !== undefined) {
|
|
825
890
|
return ctx;
|
|
826
891
|
}
|
|
827
892
|
|
|
828
893
|
const {
|
|
829
894
|
createContext: _omit,
|
|
895
|
+
composeValidation: _omitComposeValidation,
|
|
830
896
|
validationSchema: _omitSchema,
|
|
897
|
+
version: _omitVersion,
|
|
831
898
|
...forwarded
|
|
832
899
|
} = options ?? {};
|
|
833
|
-
const
|
|
900
|
+
const compose = (async (
|
|
834
901
|
trailOrCalls:
|
|
835
902
|
| AnyTrail
|
|
836
903
|
| string
|
|
837
904
|
| readonly (readonly [AnyTrail | string, unknown])[],
|
|
838
|
-
inputOrOptions?:
|
|
905
|
+
inputOrOptions?: unknown,
|
|
906
|
+
singleOptions?: ComposeOptions
|
|
839
907
|
) => {
|
|
840
908
|
if (Array.isArray(trailOrCalls)) {
|
|
841
|
-
return await
|
|
909
|
+
return await executeComposeBatch(
|
|
842
910
|
trailOrCalls,
|
|
843
911
|
ctx,
|
|
844
912
|
topo,
|
|
845
913
|
forwarded,
|
|
846
|
-
inputOrOptions as
|
|
914
|
+
inputOrOptions as ComposeBatchOptions | undefined
|
|
847
915
|
);
|
|
848
916
|
}
|
|
849
917
|
|
|
850
|
-
return await
|
|
918
|
+
return await executeComposeTarget(
|
|
851
919
|
trailOrCalls as AnyTrail | string,
|
|
852
920
|
inputOrOptions,
|
|
853
921
|
ctx,
|
|
854
922
|
topo,
|
|
855
|
-
forwarded
|
|
923
|
+
forwarded,
|
|
924
|
+
singleOptions
|
|
856
925
|
);
|
|
857
|
-
}) as
|
|
926
|
+
}) as ComposeFn;
|
|
858
927
|
|
|
859
928
|
return {
|
|
860
929
|
...ctx,
|
|
861
|
-
|
|
930
|
+
compose,
|
|
862
931
|
};
|
|
863
932
|
};
|
|
864
933
|
|
|
865
934
|
const bindFireToCtx = (
|
|
866
935
|
ctx: TrailContext,
|
|
867
936
|
topo: Topo | undefined,
|
|
868
|
-
options:
|
|
937
|
+
options: ExecuteTrailInternalOptions | undefined,
|
|
869
938
|
producerTrailId?: string | undefined
|
|
870
939
|
): TrailContext => {
|
|
871
|
-
// Symmetric with
|
|
940
|
+
// Symmetric with bindComposeToCtx: a caller-supplied ctx.fire (e.g. test
|
|
872
941
|
// helper, scenario harness, or runtime intercepting signal fan-out) is
|
|
873
942
|
// preserved as-is. Without this guard, passing both `topo: app` and a
|
|
874
943
|
// custom `ctx.fire` would silently clobber the injected mock with the
|
|
@@ -886,10 +955,12 @@ const bindFireToCtx = (
|
|
|
886
955
|
// already-resolved ctx via `consumerCtx`, and re-running the factory would
|
|
887
956
|
// clobber that.
|
|
888
957
|
// Strip createContext (consumers inherit resolved ctx) and validationSchema
|
|
889
|
-
// (consumers validate against their own schema, not the producer's
|
|
958
|
+
// (consumers validate against their own schema, not the producer's compose schema).
|
|
890
959
|
const {
|
|
891
960
|
createContext: _omit,
|
|
961
|
+
composeValidation: _omitComposeValidation,
|
|
892
962
|
validationSchema: _omitSchema,
|
|
963
|
+
version: _omitVersion,
|
|
893
964
|
...forwarded
|
|
894
965
|
} = options ?? {};
|
|
895
966
|
const trackedCtx = withFireDispatchTracking(ctx);
|
|
@@ -898,7 +969,7 @@ const bindFireToCtx = (
|
|
|
898
969
|
trackedCtx,
|
|
899
970
|
(consumer, input, consumerCtx) =>
|
|
900
971
|
// eslint-disable-next-line no-use-before-define -- executor closure runs only after executeTrail is defined
|
|
901
|
-
|
|
972
|
+
executeTrailInternal(consumer, input, {
|
|
902
973
|
...forwarded,
|
|
903
974
|
ctx: consumerCtx,
|
|
904
975
|
topo,
|
|
@@ -908,20 +979,20 @@ const bindFireToCtx = (
|
|
|
908
979
|
return { ...trackedCtx, fire };
|
|
909
980
|
};
|
|
910
981
|
|
|
911
|
-
const
|
|
982
|
+
const bindComposeAtLayerBoundary =
|
|
912
983
|
<I, O>(
|
|
913
984
|
implementation: Implementation<I, O>,
|
|
914
985
|
topo: Topo | undefined,
|
|
915
|
-
options:
|
|
986
|
+
options: ExecuteTrailInternalOptions | undefined
|
|
916
987
|
): Implementation<I, O> =>
|
|
917
988
|
(input, ctx) =>
|
|
918
|
-
implementation(input,
|
|
989
|
+
implementation(input, bindComposeToCtx(ctx, topo, options));
|
|
919
990
|
|
|
920
991
|
const bindFireAtLayerBoundary = <I, O>(
|
|
921
992
|
implementation: Implementation<I, O>,
|
|
922
993
|
trail: AnyTrail,
|
|
923
994
|
topo: Topo | undefined,
|
|
924
|
-
options:
|
|
995
|
+
options: ExecuteTrailInternalOptions | undefined
|
|
925
996
|
): Implementation<I, O> => {
|
|
926
997
|
if (topo === undefined) {
|
|
927
998
|
return implementation;
|
|
@@ -948,7 +1019,7 @@ const findMatchingDetour = (
|
|
|
948
1019
|
): Detour<any, any, TrailsError> | undefined =>
|
|
949
1020
|
detours.find((d) => error instanceof d.on);
|
|
950
1021
|
|
|
951
|
-
/** Execute a single detour recovery attempt,
|
|
1022
|
+
/** Execute a single detour recovery attempt, tracing through ctx.trace when available. */
|
|
952
1023
|
const executeDetourAttempt = async (
|
|
953
1024
|
/* oxlint-disable-next-line no-explicit-any -- existential detour from AnyTrail */
|
|
954
1025
|
detour: Detour<any, any, TrailsError>,
|
|
@@ -1107,13 +1178,13 @@ const prepareRunImpl = (
|
|
|
1107
1178
|
ctx: TrailContext,
|
|
1108
1179
|
layers: readonly Layer[],
|
|
1109
1180
|
topo: Topo | undefined,
|
|
1110
|
-
options:
|
|
1181
|
+
options: ExecuteTrailInternalOptions | undefined
|
|
1111
1182
|
): {
|
|
1112
1183
|
readonly ctxWithIntrinsics: TrailContext;
|
|
1113
1184
|
readonly impl: Implementation<unknown, unknown>;
|
|
1114
1185
|
} => {
|
|
1115
1186
|
const ctxWithIntrinsics = bindFireToCtx(
|
|
1116
|
-
|
|
1187
|
+
bindComposeToCtx(ctx, topo, options),
|
|
1117
1188
|
topo,
|
|
1118
1189
|
options,
|
|
1119
1190
|
trail.id
|
|
@@ -1121,7 +1192,7 @@ const prepareRunImpl = (
|
|
|
1121
1192
|
// Detour loop wraps the blaze (inside layer stack, closest to blaze)
|
|
1122
1193
|
let impl = wrapWithDetours(
|
|
1123
1194
|
bindFireAtLayerBoundary(
|
|
1124
|
-
|
|
1195
|
+
bindComposeAtLayerBoundary(
|
|
1125
1196
|
trail.blaze as Implementation<unknown, unknown>,
|
|
1126
1197
|
topo,
|
|
1127
1198
|
options
|
|
@@ -1137,7 +1208,7 @@ const prepareRunImpl = (
|
|
|
1137
1208
|
const layer = layers[i];
|
|
1138
1209
|
if (layer) {
|
|
1139
1210
|
impl = bindFireAtLayerBoundary(
|
|
1140
|
-
|
|
1211
|
+
bindComposeAtLayerBoundary(
|
|
1141
1212
|
layer.wrap(trail, impl as never) as Implementation<unknown, unknown>,
|
|
1142
1213
|
topo,
|
|
1143
1214
|
options
|
|
@@ -1161,7 +1232,7 @@ const runImplWithoutTracing = async (
|
|
|
1161
1232
|
ctx: TrailContext,
|
|
1162
1233
|
layers: readonly Layer[],
|
|
1163
1234
|
topo: Topo | undefined,
|
|
1164
|
-
options:
|
|
1235
|
+
options: ExecuteTrailInternalOptions | undefined
|
|
1165
1236
|
): Promise<Result<unknown, Error>> => {
|
|
1166
1237
|
const prepared = prepareRunImpl(
|
|
1167
1238
|
trail,
|
|
@@ -1183,7 +1254,7 @@ const runTrailWithTracing = async (
|
|
|
1183
1254
|
ctx: TrailContext,
|
|
1184
1255
|
layers: readonly Layer[],
|
|
1185
1256
|
topo: Topo | undefined,
|
|
1186
|
-
options:
|
|
1257
|
+
options: ExecuteTrailInternalOptions | undefined,
|
|
1187
1258
|
sink: ReturnType<typeof getTraceSink>
|
|
1188
1259
|
): Promise<Result<unknown, Error>> => {
|
|
1189
1260
|
const { record, tracedCtx } = buildTracedContext(trail, ctx, sink);
|
|
@@ -1216,7 +1287,7 @@ const runTrail = async (
|
|
|
1216
1287
|
ctx: TrailContext,
|
|
1217
1288
|
layers: readonly Layer[],
|
|
1218
1289
|
topo: Topo | undefined,
|
|
1219
|
-
options:
|
|
1290
|
+
options: ExecuteTrailInternalOptions | undefined
|
|
1220
1291
|
): Promise<Result<unknown, Error>> => {
|
|
1221
1292
|
const sink = topo?.observe?.trace ?? getTraceSink();
|
|
1222
1293
|
return isTracingDisabled(sink)
|
|
@@ -1238,7 +1309,7 @@ const runTrail = async (
|
|
|
1238
1309
|
*/
|
|
1239
1310
|
const composeAttachedLayers = (
|
|
1240
1311
|
trail: AnyTrail,
|
|
1241
|
-
options:
|
|
1312
|
+
options: ExecuteTrailInternalOptions | undefined
|
|
1242
1313
|
): readonly Layer[] => [
|
|
1243
1314
|
...(options?.topoLayers ?? []),
|
|
1244
1315
|
...(options?.surfaceLayers ?? []),
|
|
@@ -1246,6 +1317,125 @@ const composeAttachedLayers = (
|
|
|
1246
1317
|
...(options?.layers ?? []),
|
|
1247
1318
|
];
|
|
1248
1319
|
|
|
1320
|
+
const stripVersionOption = (
|
|
1321
|
+
options?: ExecuteTrailInternalOptions
|
|
1322
|
+
): Omit<ExecuteTrailInternalOptions, 'version'> | undefined => {
|
|
1323
|
+
if (options === undefined) {
|
|
1324
|
+
return undefined;
|
|
1325
|
+
}
|
|
1326
|
+
const { version: _version, ...forwarded } = options;
|
|
1327
|
+
return forwarded;
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
const executeRequestedCurrentTrailVersion = async (
|
|
1331
|
+
trail: AnyTrail,
|
|
1332
|
+
rawInput: unknown,
|
|
1333
|
+
options: ExecuteTrailInternalOptions
|
|
1334
|
+
): Promise<Result<unknown, Error>> =>
|
|
1335
|
+
// eslint-disable-next-line no-use-before-define -- recursive dispatch strips version before re-entering the current pipeline
|
|
1336
|
+
await executeTrailInternal(trail, rawInput, stripVersionOption(options));
|
|
1337
|
+
|
|
1338
|
+
const executeCurrentTrailForRevision: TrailVersionCurrentExecutor<
|
|
1339
|
+
ExecuteTrailInternalOptions
|
|
1340
|
+
> = async (trail, input, internalOptions) => {
|
|
1341
|
+
const {
|
|
1342
|
+
validationSchema: _validationSchema,
|
|
1343
|
+
version: _version,
|
|
1344
|
+
...forwarded
|
|
1345
|
+
} = internalOptions ?? {};
|
|
1346
|
+
// eslint-disable-next-line no-use-before-define -- revision runtime calls back into current execution after options are normalized
|
|
1347
|
+
return await executeTrailInternal(trail, input, forwarded);
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
const createForkTrailVersion = (
|
|
1351
|
+
trail: AnyTrail,
|
|
1352
|
+
entry: TrailVersionForkEntry
|
|
1353
|
+
): AnyTrail => {
|
|
1354
|
+
const {
|
|
1355
|
+
blaze: _blaze,
|
|
1356
|
+
composeInput: _composeInput,
|
|
1357
|
+
composes: _composes,
|
|
1358
|
+
detours: _detours,
|
|
1359
|
+
input: _input,
|
|
1360
|
+
output: _output,
|
|
1361
|
+
resources: _resources,
|
|
1362
|
+
version: _version,
|
|
1363
|
+
versions: _versions,
|
|
1364
|
+
...base
|
|
1365
|
+
} = trail;
|
|
1366
|
+
|
|
1367
|
+
return Object.freeze({
|
|
1368
|
+
...base,
|
|
1369
|
+
blaze: entry.blaze,
|
|
1370
|
+
composes: Object.freeze([...(entry.composes ?? [])]),
|
|
1371
|
+
detours: Object.freeze([...(entry.detours ?? [])]),
|
|
1372
|
+
...(entry.composeInput === undefined
|
|
1373
|
+
? {}
|
|
1374
|
+
: { composeInput: entry.composeInput }),
|
|
1375
|
+
input: entry.input,
|
|
1376
|
+
output: entry.output,
|
|
1377
|
+
resources: Object.freeze([...(entry.resources ?? [])]),
|
|
1378
|
+
}) as AnyTrail;
|
|
1379
|
+
};
|
|
1380
|
+
|
|
1381
|
+
const executeRequestedForkTrailVersion = async (
|
|
1382
|
+
trail: AnyTrail,
|
|
1383
|
+
entry: TrailVersionForkEntry,
|
|
1384
|
+
rawInput: unknown,
|
|
1385
|
+
options: ExecuteTrailInternalOptions
|
|
1386
|
+
): Promise<Result<unknown, Error>> => {
|
|
1387
|
+
const forkTrail = createForkTrailVersion(trail, entry);
|
|
1388
|
+
const validationSchema = options.composeValidation
|
|
1389
|
+
? buildComposeValidationSchema(forkTrail)
|
|
1390
|
+
: options.validationSchema;
|
|
1391
|
+
|
|
1392
|
+
// eslint-disable-next-line no-use-before-define -- recursive dispatch strips version before re-entering the fork pipeline
|
|
1393
|
+
return await executeTrailInternal(forkTrail, rawInput, {
|
|
1394
|
+
...stripVersionOption(options),
|
|
1395
|
+
validationSchema,
|
|
1396
|
+
});
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
const executeRequestedTrailVersion = async (
|
|
1400
|
+
trail: AnyTrail,
|
|
1401
|
+
rawInput: unknown,
|
|
1402
|
+
options: ExecuteTrailInternalOptions
|
|
1403
|
+
): Promise<Result<unknown, Error>> => {
|
|
1404
|
+
const reference = options.version;
|
|
1405
|
+
if (reference === undefined) {
|
|
1406
|
+
return Result.err(
|
|
1407
|
+
new InternalError(
|
|
1408
|
+
'unreachable: executeRequestedTrailVersion without reference'
|
|
1409
|
+
)
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const resolved = resolveTrailVersion(trail, reference);
|
|
1414
|
+
if (resolved.isErr()) {
|
|
1415
|
+
return Result.err(resolved.error);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
if (resolved.value.current) {
|
|
1419
|
+
return await executeRequestedCurrentTrailVersion(trail, rawInput, options);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
return resolved.value.kind === 'revision'
|
|
1423
|
+
? await executeTrailRevision(
|
|
1424
|
+
trail,
|
|
1425
|
+
resolved.value.version,
|
|
1426
|
+
resolved.value.entry,
|
|
1427
|
+
rawInput,
|
|
1428
|
+
options,
|
|
1429
|
+
executeCurrentTrailForRevision
|
|
1430
|
+
)
|
|
1431
|
+
: await executeRequestedForkTrailVersion(
|
|
1432
|
+
trail,
|
|
1433
|
+
resolved.value.entry,
|
|
1434
|
+
rawInput,
|
|
1435
|
+
options
|
|
1436
|
+
);
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1249
1439
|
const isLayerInputMap = (
|
|
1250
1440
|
value: unknown
|
|
1251
1441
|
): value is Readonly<Record<string, unknown>> =>
|
|
@@ -1330,12 +1520,16 @@ const validateContextLayerInputs = (
|
|
|
1330
1520
|
* The function never throws -- unexpected exceptions are caught and
|
|
1331
1521
|
* returned as `Result.err(InternalError)`.
|
|
1332
1522
|
*/
|
|
1333
|
-
|
|
1523
|
+
const executeTrailInternal = async (
|
|
1334
1524
|
trail: AnyTrail,
|
|
1335
1525
|
rawInput: unknown,
|
|
1336
|
-
options?:
|
|
1526
|
+
options?: ExecuteTrailInternalOptions
|
|
1337
1527
|
): Promise<Result<unknown, Error>> => {
|
|
1338
1528
|
try {
|
|
1529
|
+
if (options?.version !== undefined) {
|
|
1530
|
+
return await executeRequestedTrailVersion(trail, rawInput, options);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1339
1533
|
const validated = validateInput(
|
|
1340
1534
|
options?.validationSchema ?? trail.input,
|
|
1341
1535
|
rawInput
|
|
@@ -1374,3 +1568,10 @@ export const executeTrail = async (
|
|
|
1374
1568
|
return Result.err(new InternalError(message));
|
|
1375
1569
|
}
|
|
1376
1570
|
};
|
|
1571
|
+
|
|
1572
|
+
export const executeTrail = async (
|
|
1573
|
+
trail: AnyTrail,
|
|
1574
|
+
rawInput: unknown,
|
|
1575
|
+
options?: ExecuteTrailOptions
|
|
1576
|
+
): Promise<Result<unknown, Error>> =>
|
|
1577
|
+
await executeTrailInternal(trail, rawInput, options);
|