@ontrails/core 1.0.0-beta.13 → 1.0.0-beta.15
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +17 -0
- package/README.md +19 -14
- package/dist/branded.d.ts +1 -1
- package/dist/branded.d.ts.map +1 -1
- package/dist/branded.js +1 -1
- package/dist/branded.js.map +1 -1
- package/dist/context.d.ts +13 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +19 -3
- package/dist/context.js.map +1 -1
- package/dist/contour.d.ts +81 -0
- package/dist/contour.d.ts.map +1 -0
- package/dist/contour.js +176 -0
- package/dist/contour.js.map +1 -0
- package/dist/cross-schema.d.ts +18 -0
- package/dist/cross-schema.d.ts.map +1 -0
- package/dist/cross-schema.js +29 -0
- package/dist/cross-schema.js.map +1 -0
- package/dist/derive.d.ts +6 -0
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +29 -5
- package/dist/derive.js.map +1 -1
- package/dist/draft.d.ts +28 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +157 -0
- package/dist/draft.js.map +1 -0
- package/dist/errors.d.ts +62 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +44 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +26 -9
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +441 -20
- package/dist/execute.js.map +1 -1
- package/dist/fire.d.ts +43 -0
- package/dist/fire.d.ts.map +1 -0
- package/dist/fire.js +185 -0
- package/dist/fire.js.map +1 -0
- package/dist/index.d.ts +33 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/cross-batch.d.ts +44 -0
- package/dist/internal/cross-batch.d.ts.map +1 -0
- package/dist/internal/cross-batch.js +58 -0
- package/dist/internal/cross-batch.js.map +1 -0
- package/dist/internal/fork-ctx.d.ts +41 -0
- package/dist/internal/fork-ctx.d.ts.map +1 -0
- package/dist/internal/fork-ctx.js +47 -0
- package/dist/internal/fork-ctx.js.map +1 -0
- package/dist/internal/signal-ref.d.ts +15 -0
- package/dist/internal/signal-ref.d.ts.map +1 -0
- package/dist/internal/signal-ref.js +41 -0
- package/dist/internal/signal-ref.js.map +1 -0
- package/dist/internal/topo-saves.d.ts +53 -0
- package/dist/internal/topo-saves.d.ts.map +1 -0
- package/dist/internal/topo-saves.js +449 -0
- package/dist/internal/topo-saves.js.map +1 -0
- package/dist/internal/topo-snapshots.d.ts +57 -0
- package/dist/internal/topo-snapshots.d.ts.map +1 -0
- package/dist/internal/topo-snapshots.js +372 -0
- package/dist/internal/topo-snapshots.js.map +1 -0
- package/dist/internal/topo-store-read.d.ts +70 -0
- package/dist/internal/topo-store-read.d.ts.map +1 -0
- package/dist/internal/topo-store-read.js +221 -0
- package/dist/internal/topo-store-read.js.map +1 -0
- package/dist/internal/topo-store.d.ts +26 -0
- package/dist/internal/topo-store.d.ts.map +1 -0
- package/dist/internal/topo-store.js +621 -0
- package/dist/internal/topo-store.js.map +1 -0
- package/dist/internal/tracing.d.ts +98 -0
- package/dist/internal/tracing.d.ts.map +1 -0
- package/dist/internal/tracing.js +102 -0
- package/dist/internal/tracing.js.map +1 -0
- package/dist/internal/trails-db.d.ts +16 -0
- package/dist/internal/trails-db.d.ts.map +1 -0
- package/dist/internal/trails-db.js +118 -0
- package/dist/internal/trails-db.js.map +1 -0
- package/dist/internal/zod-wrappers.d.ts +40 -0
- package/dist/internal/zod-wrappers.d.ts.map +1 -0
- package/dist/internal/zod-wrappers.js +59 -0
- package/dist/internal/zod-wrappers.js.map +1 -0
- package/dist/layer.d.ts +22 -5
- package/dist/layer.d.ts.map +1 -1
- package/dist/layer.js +20 -3
- package/dist/layer.js.map +1 -1
- package/dist/path-security.d.ts +1 -1
- package/dist/path-security.d.ts.map +1 -1
- package/dist/path-security.js +1 -1
- package/dist/path-security.js.map +1 -1
- package/dist/resilience.d.ts +1 -1
- package/dist/resilience.d.ts.map +1 -1
- package/dist/resilience.js +3 -3
- package/dist/resilience.js.map +1 -1
- package/dist/resource-config.d.ts +22 -0
- package/dist/resource-config.d.ts.map +1 -0
- package/dist/resource-config.js +210 -0
- package/dist/resource-config.js.map +1 -0
- package/dist/resource.d.ts +74 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +61 -0
- package/dist/resource.js.map +1 -0
- package/dist/run.d.ts +4 -3
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +5 -4
- package/dist/run.js.map +1 -1
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +2 -1
- package/dist/serialization.js.map +1 -1
- package/dist/store/accessor-protocol.d.ts +51 -0
- package/dist/store/accessor-protocol.d.ts.map +1 -0
- package/dist/store/accessor-protocol.js +2 -0
- package/dist/store/accessor-protocol.js.map +1 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +2 -0
- package/dist/store/index.js.map +1 -0
- package/dist/surface-filter.d.ts +13 -0
- package/dist/surface-filter.d.ts.map +1 -0
- package/dist/surface-filter.js +76 -0
- package/dist/surface-filter.js.map +1 -0
- package/dist/topo-store.d.ts +62 -0
- package/dist/topo-store.d.ts.map +1 -0
- package/dist/topo-store.js +319 -0
- package/dist/topo-store.js.map +1 -0
- package/dist/topo.d.ts +23 -9
- package/dist/topo.d.ts.map +1 -1
- package/dist/topo.js +175 -46
- package/dist/topo.js.map +1 -1
- package/dist/trail.d.ts +86 -19
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +59 -4
- package/dist/trail.js.map +1 -1
- package/dist/trails/derive-trail.d.ts +66 -0
- package/dist/trails/derive-trail.d.ts.map +1 -0
- package/dist/trails/derive-trail.js +378 -0
- package/dist/trails/derive-trail.js.map +1 -0
- package/dist/trails/index.d.ts +5 -0
- package/dist/trails/index.d.ts.map +1 -0
- package/dist/trails/index.js +3 -0
- package/dist/trails/index.js.map +1 -0
- package/dist/trails/ingest.d.ts +24 -0
- package/dist/trails/ingest.d.ts.map +1 -0
- package/dist/trails/ingest.js +63 -0
- package/dist/trails/ingest.js.map +1 -0
- package/dist/transport-error-map.d.ts +106 -0
- package/dist/transport-error-map.d.ts.map +1 -0
- package/dist/transport-error-map.js +24 -0
- package/dist/transport-error-map.js.map +1 -0
- package/dist/type-checks.test-d.d.ts +71 -0
- package/dist/type-checks.test-d.d.ts.map +1 -0
- package/dist/type-checks.test-d.js +12 -0
- package/dist/type-checks.test-d.js.map +1 -0
- package/dist/type-utils.d.ts +11 -2
- package/dist/type-utils.d.ts.map +1 -1
- package/dist/type-utils.js.map +1 -1
- package/dist/types.d.ts +113 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validate-established-topo.d.ts +76 -0
- package/dist/validate-established-topo.d.ts.map +1 -0
- package/dist/validate-established-topo.js +43 -0
- package/dist/validate-established-topo.js.map +1 -0
- package/dist/validate-topo.d.ts.map +1 -1
- package/dist/validate-topo.js +27 -8
- package/dist/validate-topo.js.map +1 -1
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +1 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +10 -1
- package/src/__tests__/branded.test.ts +5 -5
- package/src/__tests__/context.test.ts +3 -3
- package/src/__tests__/contour.test.ts +263 -0
- package/src/__tests__/derive-trail.test.ts +668 -0
- package/src/__tests__/derive.test.ts +58 -1
- package/src/__tests__/errors.test.ts +118 -0
- package/src/__tests__/execute.test.ts +1179 -141
- package/src/__tests__/fire.test.ts +1056 -0
- package/src/__tests__/fork-ctx.test.ts +83 -0
- package/src/__tests__/ingest.test.ts +191 -0
- package/src/__tests__/{gate.test.ts → layer.test.ts} +21 -21
- package/src/__tests__/path-security.test.ts +7 -7
- package/src/__tests__/resilience.test.ts +8 -8
- package/src/__tests__/resource.test.ts +204 -0
- package/src/__tests__/run.test.ts +13 -13
- package/src/__tests__/service-config.test.ts +28 -32
- package/src/__tests__/surface-filter.test.ts +196 -0
- package/src/__tests__/topo-store-read.test.ts +256 -0
- package/src/__tests__/topo-store.test.ts +869 -0
- package/src/__tests__/topo.test.ts +350 -54
- package/src/__tests__/trail.test.ts +189 -24
- package/src/__tests__/trails-db.test.ts +198 -0
- package/src/__tests__/transport-error-map.test.ts +82 -0
- package/src/__tests__/type-utils.test.ts +29 -1
- package/src/__tests__/validate-topo.test.ts +305 -16
- package/src/__tests__/workspace.test.ts +6 -6
- package/src/__tests__/zod-wrappers.test.ts +62 -0
- package/src/branded.ts +1 -1
- package/src/context.ts +24 -4
- package/src/contour.ts +344 -0
- package/src/cross-schema.ts +36 -0
- package/src/derive.ts +39 -8
- package/src/draft.ts +338 -0
- package/src/errors.ts +62 -18
- package/src/execute.ts +840 -29
- package/src/fire.ts +295 -0
- package/src/index.ts +142 -26
- package/src/internal/cross-batch.ts +77 -0
- package/src/internal/fork-ctx.ts +69 -0
- package/src/internal/signal-ref.ts +87 -0
- package/src/internal/topo-snapshots.ts +517 -0
- package/src/internal/topo-store-read.ts +481 -0
- package/src/internal/topo-store.ts +1192 -0
- package/src/internal/tracing.ts +187 -0
- package/src/internal/trails-db.ts +187 -0
- package/src/internal/zod-wrappers.ts +78 -0
- package/src/layer.ts +50 -0
- package/src/path-security.ts +1 -1
- package/src/resilience.ts +3 -3
- package/src/{provision-config.ts → resource-config.ts} +109 -123
- package/src/resource.ts +155 -0
- package/src/run.ts +5 -4
- package/src/serialization.ts +2 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/surface-filter.ts +182 -0
- package/src/topo-store.ts +510 -0
- package/src/topo.ts +328 -57
- package/src/trail.ts +199 -31
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +146 -0
- package/src/transport-error-map.ts +56 -0
- package/src/type-checks.test-d.ts +106 -0
- package/src/type-utils.ts +17 -2
- package/src/types.ts +148 -11
- package/src/validate-established-topo.ts +63 -0
- package/src/validate-topo.ts +37 -9
- package/src/workspace.ts +1 -1
- package/tsconfig.tests.json +10 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/adapters.d.ts +0 -39
- package/dist/adapters.d.ts.map +0 -1
- package/dist/adapters.js +0 -2
- package/dist/adapters.js.map +0 -1
- package/dist/dispatch.d.ts +0 -27
- package/dist/dispatch.d.ts.map +0 -1
- package/dist/dispatch.js +0 -34
- package/dist/dispatch.js.map +0 -1
- package/dist/event.d.ts +0 -8
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -7
- package/dist/event.js.map +0 -1
- package/dist/gate.d.ts +0 -17
- package/dist/gate.d.ts.map +0 -1
- package/dist/gate.js +0 -21
- package/dist/gate.js.map +0 -1
- package/dist/health.d.ts +0 -18
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -5
- package/dist/health.js.map +0 -1
- package/dist/job.d.ts +0 -24
- package/dist/job.d.ts.map +0 -1
- package/dist/job.js +0 -17
- package/dist/job.js.map +0 -1
- package/dist/provision-config.d.ts +0 -22
- package/dist/provision-config.d.ts.map +0 -1
- package/dist/provision-config.js +0 -210
- package/dist/provision-config.js.map +0 -1
- package/dist/provision.d.ts +0 -71
- package/dist/provision.d.ts.map +0 -1
- package/dist/provision.js +0 -56
- package/dist/provision.js.map +0 -1
- package/dist/service-config.d.ts +0 -22
- package/dist/service-config.d.ts.map +0 -1
- package/dist/service-config.js +0 -210
- package/dist/service-config.js.map +0 -1
- package/dist/service.d.ts +0 -71
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -56
- package/dist/service.js.map +0 -1
- package/src/__tests__/service.test.ts +0 -197
- package/src/event.ts +0 -15
- package/src/gate.ts +0 -44
- package/src/provision.ts +0 -148
package/src/fire.ts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal emission and auto-activation.
|
|
3
|
+
*
|
|
4
|
+
* `createFireFn(topo, producerCtx?, executor)` returns a `FireFn` bound to
|
|
5
|
+
* a topo. Calling `fire(signalId, payload)` looks up the signal, validates
|
|
6
|
+
* the payload against its schema, finds every trail with the signal in its
|
|
7
|
+
* `on:` array, and invokes each consumer via the supplied executor.
|
|
8
|
+
*
|
|
9
|
+
* The `executor` parameter is an indirection that lets `execute.ts` pass in
|
|
10
|
+
* `executeTrail` without `fire.ts` importing it directly — keeping the two
|
|
11
|
+
* modules dependency-cycle-free.
|
|
12
|
+
*
|
|
13
|
+
* Consumer contexts inherit the producer's full ctx (logger, extensions,
|
|
14
|
+
* resources, abortSignal, requestId, env, workspaceRoot, permit) with
|
|
15
|
+
* `fire` rebound to the same closure so consumers can fan out further.
|
|
16
|
+
* Each consumer gets its own derived context so sibling fan-out branches do
|
|
17
|
+
* not share mutable top-level state. The consumer logger is derived from the
|
|
18
|
+
* producer logger as a child tagged with `signalId` and `consumerId` when
|
|
19
|
+
* `logger.child` exists.
|
|
20
|
+
*
|
|
21
|
+
* Error semantics match the fire-and-forget framing: producers get
|
|
22
|
+
* `Result.ok(undefined)` unless the signal id is unknown or the payload
|
|
23
|
+
* fails schema validation. Consumer errors are logged via the producer's
|
|
24
|
+
* logger but do NOT propagate back to the producer. Consumers that need
|
|
25
|
+
* transactional coupling should use `crosses:`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { NotFoundError, ValidationError } from './errors.js';
|
|
29
|
+
import { forkCtx } from './internal/fork-ctx.js';
|
|
30
|
+
import { Result } from './result.js';
|
|
31
|
+
import type { Topo } from './topo.js';
|
|
32
|
+
import type { AnyTrail } from './trail.js';
|
|
33
|
+
import type { FireFn, Logger, TrailContextInit } from './types.js';
|
|
34
|
+
|
|
35
|
+
/** Signature execute.ts passes in to avoid a fire ↔ execute import cycle. */
|
|
36
|
+
export type ConsumerExecutor = (
|
|
37
|
+
consumer: AnyTrail,
|
|
38
|
+
input: unknown,
|
|
39
|
+
ctx: Partial<TrailContextInit>
|
|
40
|
+
) => Promise<Result<unknown, Error>>;
|
|
41
|
+
|
|
42
|
+
type MutableConsumerContext = {
|
|
43
|
+
-readonly [K in keyof Partial<TrailContextInit>]: Partial<TrailContextInit>[K];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const FIRE_STACK_KEY = '__trails_fire_stack';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Maximum depth for signal fan-out chains.
|
|
50
|
+
*
|
|
51
|
+
* Cycle detection catches re-entrant fires of the same signal ID (A→B→A),
|
|
52
|
+
* but a chain of distinct signals (A→B→C→D→...) bypasses it. This limit
|
|
53
|
+
* prevents runaway fan-out in pathological topologies.
|
|
54
|
+
*/
|
|
55
|
+
const MAX_FIRE_DEPTH = 16;
|
|
56
|
+
|
|
57
|
+
const getFireStack = (
|
|
58
|
+
ctx: Pick<TrailContextInit, 'extensions'> | undefined
|
|
59
|
+
): readonly string[] => {
|
|
60
|
+
const value = ctx?.extensions?.[FIRE_STACK_KEY];
|
|
61
|
+
return Array.isArray(value) ? (value as readonly string[]) : [];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Binds a per-consumer `fire` onto a mutable consumer context. */
|
|
65
|
+
type ConsumerFireBinder = (
|
|
66
|
+
consumerCtx: MutableConsumerContext
|
|
67
|
+
) => MutableConsumerContext;
|
|
68
|
+
|
|
69
|
+
const deriveConsumerLogger = (
|
|
70
|
+
producerCtx: TrailContextInit | undefined,
|
|
71
|
+
signalId: string,
|
|
72
|
+
consumerId: string
|
|
73
|
+
): Logger | undefined =>
|
|
74
|
+
producerCtx?.logger?.child?.({ consumerId, signalId }) ?? producerCtx?.logger;
|
|
75
|
+
|
|
76
|
+
const deriveConsumerEnv = (
|
|
77
|
+
producerCtx: TrailContextInit | undefined
|
|
78
|
+
): TrailContextInit['env'] =>
|
|
79
|
+
producerCtx?.env ? { ...producerCtx.env } : undefined;
|
|
80
|
+
|
|
81
|
+
const deriveConsumerExtensions = (
|
|
82
|
+
producerCtx: TrailContextInit | undefined,
|
|
83
|
+
signalId: string
|
|
84
|
+
): TrailContextInit['extensions'] => ({
|
|
85
|
+
...producerCtx?.extensions,
|
|
86
|
+
[FIRE_STACK_KEY]: [...getFireStack(producerCtx), signalId],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const deriveConsumerCtx = (
|
|
90
|
+
producerCtx: TrailContextInit | undefined,
|
|
91
|
+
signalId: string,
|
|
92
|
+
consumerId: string
|
|
93
|
+
): MutableConsumerContext =>
|
|
94
|
+
producerCtx
|
|
95
|
+
? forkCtx(producerCtx as MutableConsumerContext, {
|
|
96
|
+
env: deriveConsumerEnv(producerCtx),
|
|
97
|
+
extensions: deriveConsumerExtensions(producerCtx, signalId),
|
|
98
|
+
logger: deriveConsumerLogger(producerCtx, signalId, consumerId),
|
|
99
|
+
})
|
|
100
|
+
: {};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Fan out a validated signal payload to its consumer trails.
|
|
104
|
+
*
|
|
105
|
+
* @remarks
|
|
106
|
+
* Consumers fan out in parallel by design. Signal delivery is fire-and-forget
|
|
107
|
+
* notification, not ordered orchestration; if one consumer depends on another,
|
|
108
|
+
* the dependency belongs in `crosses:` instead of sibling signal sequencing.
|
|
109
|
+
*
|
|
110
|
+
* `Promise.allSettled` preserves failure isolation and waits for every branch
|
|
111
|
+
* to settle. Each consumer gets its own derived context so sibling branches do
|
|
112
|
+
* not share mutable top-level state while they overlap. Re-entrant suppression
|
|
113
|
+
* elsewhere in this module is still based on signal-id membership in the
|
|
114
|
+
* current fire stack: it prevents infinite loops, but it can over-suppress
|
|
115
|
+
* legitimate diamond re-fires. Per-path provenance is a documented future
|
|
116
|
+
* direction rather than part of the pre-v1 runtime contract.
|
|
117
|
+
*/
|
|
118
|
+
const fanOutToConsumers = async (
|
|
119
|
+
consumers: readonly AnyTrail[],
|
|
120
|
+
payload: unknown,
|
|
121
|
+
signalId: string,
|
|
122
|
+
producerCtx: TrailContextInit | undefined,
|
|
123
|
+
bindFire: ConsumerFireBinder,
|
|
124
|
+
executor: ConsumerExecutor,
|
|
125
|
+
logger: Logger | undefined
|
|
126
|
+
): Promise<void> => {
|
|
127
|
+
const settled = await Promise.allSettled(
|
|
128
|
+
consumers.map(async (consumer) => {
|
|
129
|
+
const consumerCtx = bindFire(
|
|
130
|
+
deriveConsumerCtx(producerCtx, signalId, consumer.id)
|
|
131
|
+
);
|
|
132
|
+
const consumerResult = await executor(consumer, payload, consumerCtx);
|
|
133
|
+
if (consumerResult.isErr()) {
|
|
134
|
+
(consumerCtx.logger ?? logger)?.warn('Signal consumer failed', {
|
|
135
|
+
consumerId: consumer.id,
|
|
136
|
+
error: consumerResult.error.message,
|
|
137
|
+
signalId,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return consumer.id;
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
for (const [index, entry] of settled.entries()) {
|
|
144
|
+
if (entry.status !== 'rejected') {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// `executeTrail` normalizes throws into `Result.err`, so reaching this
|
|
148
|
+
// branch means the executor (or the warn call above) rejected
|
|
149
|
+
// unexpectedly. Log at debug to preserve provenance without propagating
|
|
150
|
+
// the failure to the producer (fire-and-forget semantics).
|
|
151
|
+
logger?.debug('Signal consumer rejected unexpectedly', {
|
|
152
|
+
consumerId: consumers[index]?.id,
|
|
153
|
+
error:
|
|
154
|
+
entry.reason instanceof Error
|
|
155
|
+
? entry.reason.message
|
|
156
|
+
: String(entry.reason),
|
|
157
|
+
signalId,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const resolveFireDispatch = (
|
|
163
|
+
topo: Topo,
|
|
164
|
+
signalId: string,
|
|
165
|
+
payload: unknown
|
|
166
|
+
): Result<
|
|
167
|
+
{ readonly consumers: readonly AnyTrail[]; readonly payload: unknown },
|
|
168
|
+
Error
|
|
169
|
+
> => {
|
|
170
|
+
const signal = topo.signals.get(signalId);
|
|
171
|
+
if (signal === undefined) {
|
|
172
|
+
return Result.err(
|
|
173
|
+
new NotFoundError(`Signal "${signalId}" not found in topo "${topo.name}"`)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const parsed = signal.payload.safeParse(payload);
|
|
177
|
+
if (!parsed.success) {
|
|
178
|
+
return Result.err(
|
|
179
|
+
new ValidationError(
|
|
180
|
+
`Invalid payload for signal "${signalId}": ${parsed.error.message}`
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return Result.ok({
|
|
185
|
+
consumers: topo.list().filter((trail) => trail.on.includes(signalId)),
|
|
186
|
+
payload: parsed.data,
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const resolveSignalId = (signalOrId: unknown): Result<string, Error> => {
|
|
191
|
+
if (typeof signalOrId === 'string') {
|
|
192
|
+
return Result.ok(signalOrId);
|
|
193
|
+
}
|
|
194
|
+
if (
|
|
195
|
+
typeof signalOrId === 'object' &&
|
|
196
|
+
signalOrId !== null &&
|
|
197
|
+
'id' in signalOrId &&
|
|
198
|
+
typeof (signalOrId as { id: unknown }).id === 'string'
|
|
199
|
+
) {
|
|
200
|
+
return Result.ok((signalOrId as { id: string }).id);
|
|
201
|
+
}
|
|
202
|
+
return Result.err(
|
|
203
|
+
new ValidationError(
|
|
204
|
+
'ctx.fire() requires a signal id string or a Signal value'
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Build a `FireFn` closure bound to a topo.
|
|
211
|
+
*
|
|
212
|
+
* When `producerCtx` is provided, consumer trails activated via `on:`
|
|
213
|
+
* inherit the producer's logger, extensions, resources, abortSignal,
|
|
214
|
+
* requestId, env, workspaceRoot, and permit. `ctx.fire` on the consumer
|
|
215
|
+
* is rebound to the same closure so consumers can emit downstream
|
|
216
|
+
* signals naturally.
|
|
217
|
+
*/
|
|
218
|
+
export const createFireFn = (
|
|
219
|
+
topo: Topo,
|
|
220
|
+
producerCtx: TrailContextInit | undefined,
|
|
221
|
+
executor: ConsumerExecutor
|
|
222
|
+
): FireFn => {
|
|
223
|
+
const bindConsumerFire: ConsumerFireBinder = (consumerCtx) => ({
|
|
224
|
+
...consumerCtx,
|
|
225
|
+
// Pre-bind fire on the consumer ctx as a safety net for direct
|
|
226
|
+
// executeTrail calls that skip the topo-aware path. In the normal
|
|
227
|
+
// fan-out flow below, bindFireToCtx in execute.ts rebinds fire to
|
|
228
|
+
// the fully-traced ctx before the blaze runs, so this assignment
|
|
229
|
+
// is superseded — but keeping it makes consumerCtx self-sufficient
|
|
230
|
+
// for any caller that inspects it pre-execution.
|
|
231
|
+
fire: createFireFn(topo, consumerCtx as TrailContextInit, executor),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const dispatchFire = async (
|
|
235
|
+
signalId: string,
|
|
236
|
+
payload: unknown
|
|
237
|
+
): Promise<Result<void, Error>> => {
|
|
238
|
+
const dispatch = resolveFireDispatch(topo, signalId, payload);
|
|
239
|
+
if (dispatch.isErr()) {
|
|
240
|
+
return Result.err(dispatch.error);
|
|
241
|
+
}
|
|
242
|
+
await fanOutToConsumers(
|
|
243
|
+
dispatch.value.consumers,
|
|
244
|
+
dispatch.value.payload,
|
|
245
|
+
signalId,
|
|
246
|
+
producerCtx,
|
|
247
|
+
bindConsumerFire,
|
|
248
|
+
executor,
|
|
249
|
+
producerCtx?.logger
|
|
250
|
+
);
|
|
251
|
+
return Result.ok();
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/** Return an early Result if the fire should be suppressed, or null to proceed. */
|
|
255
|
+
const guardFire = (
|
|
256
|
+
signalId: string,
|
|
257
|
+
stack: readonly string[]
|
|
258
|
+
): Result<void, Error> | null => {
|
|
259
|
+
if (stack.length >= MAX_FIRE_DEPTH) {
|
|
260
|
+
producerCtx?.logger?.warn(
|
|
261
|
+
'Signal fan-out depth limit reached — skipping fire',
|
|
262
|
+
{ depth: stack.length, signalId }
|
|
263
|
+
);
|
|
264
|
+
return Result.ok();
|
|
265
|
+
}
|
|
266
|
+
if (stack.includes(signalId)) {
|
|
267
|
+
producerCtx?.logger?.debug('Signal fan-out suppressed due to cycle', {
|
|
268
|
+
fireStack: [...stack],
|
|
269
|
+
signalId,
|
|
270
|
+
});
|
|
271
|
+
producerCtx?.logger?.warn(
|
|
272
|
+
'Signal cycle detected — skipping re-entrant fire',
|
|
273
|
+
{ fireStack: [...stack], signalId }
|
|
274
|
+
);
|
|
275
|
+
return Result.ok();
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const fireImpl: FireFn = async (
|
|
281
|
+
signalOrId: unknown,
|
|
282
|
+
payload: unknown
|
|
283
|
+
): Promise<Result<void, Error>> => {
|
|
284
|
+
const resolved = resolveSignalId(signalOrId);
|
|
285
|
+
if (resolved.isErr()) {
|
|
286
|
+
return Result.err(resolved.error);
|
|
287
|
+
}
|
|
288
|
+
const suppressed = guardFire(resolved.value, getFireStack(producerCtx));
|
|
289
|
+
if (suppressed) {
|
|
290
|
+
return suppressed;
|
|
291
|
+
}
|
|
292
|
+
return await dispatchFire(resolved.value, payload);
|
|
293
|
+
};
|
|
294
|
+
return fireImpl;
|
|
295
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -17,92 +17,208 @@ export {
|
|
|
17
17
|
InternalError,
|
|
18
18
|
AuthError,
|
|
19
19
|
CancelledError,
|
|
20
|
+
DerivationError,
|
|
21
|
+
errorCategories,
|
|
20
22
|
exitCodeMap,
|
|
21
23
|
statusCodeMap,
|
|
22
24
|
jsonRpcCodeMap,
|
|
23
25
|
retryableMap,
|
|
26
|
+
RetryExhaustedError,
|
|
24
27
|
isRetryable,
|
|
25
28
|
isTrailsError,
|
|
26
29
|
} from './errors.js';
|
|
27
30
|
export type { ErrorCategory } from './errors.js';
|
|
31
|
+
export {
|
|
32
|
+
createTransportErrorMapper,
|
|
33
|
+
mapTransportError,
|
|
34
|
+
transportErrorMap,
|
|
35
|
+
transportErrorRegistry,
|
|
36
|
+
transportNames,
|
|
37
|
+
} from './transport-error-map.js';
|
|
38
|
+
export type {
|
|
39
|
+
TransportErrorCode,
|
|
40
|
+
TransportErrorMapper,
|
|
41
|
+
TransportErrorMappings,
|
|
42
|
+
TransportName,
|
|
43
|
+
} from './transport-error-map.js';
|
|
28
44
|
|
|
29
45
|
// Types
|
|
30
46
|
export type {
|
|
47
|
+
Detour,
|
|
48
|
+
DetourAttempt,
|
|
31
49
|
Implementation,
|
|
32
50
|
TrailContext,
|
|
33
51
|
TrailContextInit,
|
|
52
|
+
CrossBatchOptions,
|
|
34
53
|
CrossFn,
|
|
54
|
+
FireFn,
|
|
35
55
|
BasePermit,
|
|
36
56
|
PermitRequirement,
|
|
37
57
|
ProgressCallback,
|
|
38
58
|
ProgressEvent,
|
|
39
59
|
Logger,
|
|
40
|
-
|
|
60
|
+
ResourceLookup,
|
|
41
61
|
} from './types.js';
|
|
42
62
|
export { TRAILHEAD_KEY } from './types.js';
|
|
43
63
|
|
|
44
64
|
// Context factory
|
|
45
|
-
export { createTrailContext } from './context.js';
|
|
65
|
+
export { createTrailContext, passthroughTrace } from './context.js';
|
|
46
66
|
|
|
47
|
-
//
|
|
67
|
+
// Resource
|
|
48
68
|
export {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
} from './
|
|
69
|
+
createResourceLookup,
|
|
70
|
+
findDuplicateResourceId,
|
|
71
|
+
isResource,
|
|
72
|
+
resource,
|
|
73
|
+
} from './resource.js';
|
|
54
74
|
export type {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
} from './
|
|
75
|
+
AnyResource,
|
|
76
|
+
Resource,
|
|
77
|
+
ResourceContext,
|
|
78
|
+
ResourceOverrideMap,
|
|
79
|
+
ResourceSpec,
|
|
80
|
+
} from './resource.js';
|
|
61
81
|
|
|
62
82
|
// Trail
|
|
63
83
|
export { trail } from './trail.js';
|
|
64
84
|
export type {
|
|
65
85
|
AnyTrail,
|
|
86
|
+
BlazeInput,
|
|
66
87
|
Intent,
|
|
67
88
|
Trail,
|
|
68
89
|
TrailSpec,
|
|
69
90
|
TrailExample,
|
|
91
|
+
TrailVisibility,
|
|
70
92
|
} from './trail.js';
|
|
93
|
+
export {
|
|
94
|
+
filterSurfaceTrails,
|
|
95
|
+
matchesTrailPattern,
|
|
96
|
+
shouldIncludeTrailForSurface,
|
|
97
|
+
} from './surface-filter.js';
|
|
98
|
+
export type { SurfaceFilterOptions } from './surface-filter.js';
|
|
71
99
|
|
|
72
100
|
// Type utilities
|
|
73
|
-
export type {
|
|
101
|
+
export type {
|
|
102
|
+
CrossInput,
|
|
103
|
+
TrailInput,
|
|
104
|
+
TrailOutput,
|
|
105
|
+
TrailResult,
|
|
106
|
+
} from './type-utils.js';
|
|
74
107
|
export { inputOf, outputOf } from './type-utils.js';
|
|
75
108
|
|
|
76
109
|
// Signal
|
|
77
110
|
export { signal } from './signal.js';
|
|
78
111
|
export type { AnySignal, Signal, SignalSpec } from './signal.js';
|
|
79
|
-
|
|
80
|
-
|
|
112
|
+
|
|
113
|
+
// Contour
|
|
114
|
+
export { contour } from './contour.js';
|
|
115
|
+
export {
|
|
116
|
+
CONTOUR_ID_METADATA,
|
|
117
|
+
getContourIdMetadata,
|
|
118
|
+
getContourReferences,
|
|
119
|
+
} from './contour.js';
|
|
120
|
+
export type {
|
|
121
|
+
AnyContour,
|
|
122
|
+
Contour,
|
|
123
|
+
ContourIdBrand,
|
|
124
|
+
ContourIdMetadata,
|
|
125
|
+
ContourIdSchema,
|
|
126
|
+
ContourIdValue,
|
|
127
|
+
ContourOptions,
|
|
128
|
+
ContourReference,
|
|
129
|
+
} from './contour.js';
|
|
81
130
|
|
|
82
131
|
// Topo
|
|
83
132
|
export { topo } from './topo.js';
|
|
84
|
-
export type { Topo } from './topo.js';
|
|
133
|
+
export type { Topo, TopoIdentity } from './topo.js';
|
|
134
|
+
export {
|
|
135
|
+
createTopoSnapshot,
|
|
136
|
+
createMockTopoStore,
|
|
137
|
+
createTopoStore,
|
|
138
|
+
listTopoSnapshots,
|
|
139
|
+
pinTopoSnapshot,
|
|
140
|
+
topoStore,
|
|
141
|
+
unpinTopoSnapshot,
|
|
142
|
+
} from './topo-store.js';
|
|
143
|
+
export type {
|
|
144
|
+
CreateTopoSnapshotInput,
|
|
145
|
+
ListTopoSnapshotsOptions,
|
|
146
|
+
MockTopoStoreSeed,
|
|
147
|
+
ReadOnlyTopoStore,
|
|
148
|
+
TopoSnapshot,
|
|
149
|
+
TopoStoreExportRecord,
|
|
150
|
+
TopoStoreResourceRecord,
|
|
151
|
+
TopoStoreRef,
|
|
152
|
+
TopoStoreTrailDetailRecord,
|
|
153
|
+
TopoStoreTrailRecord,
|
|
154
|
+
} from './topo-store.js';
|
|
155
|
+
|
|
156
|
+
// Draft state
|
|
157
|
+
export {
|
|
158
|
+
DRAFT_ID_PREFIX,
|
|
159
|
+
deriveDraftReport,
|
|
160
|
+
isDraftId,
|
|
161
|
+
validateDraftFreeTopo,
|
|
162
|
+
} from './draft.js';
|
|
163
|
+
export type {
|
|
164
|
+
DraftDependency,
|
|
165
|
+
DraftDependencyKind,
|
|
166
|
+
DraftFinding,
|
|
167
|
+
DraftReport,
|
|
168
|
+
} from './draft.js';
|
|
85
169
|
|
|
86
170
|
// Topo validation
|
|
87
171
|
export { validateTopo } from './validate-topo.js';
|
|
88
172
|
export type { TopoIssue } from './validate-topo.js';
|
|
173
|
+
export { validateEstablishedTopo } from './validate-established-topo.js';
|
|
89
174
|
|
|
90
|
-
//
|
|
91
|
-
export {
|
|
92
|
-
export type {
|
|
175
|
+
// Layer
|
|
176
|
+
export { composeLayers } from './layer.js';
|
|
177
|
+
export type { Layer } from './layer.js';
|
|
93
178
|
|
|
94
179
|
// Derive
|
|
95
|
-
export { deriveFields } from './derive.js';
|
|
180
|
+
export { deriveCliPath, deriveFields } from './derive.js';
|
|
96
181
|
export type { Field, FieldOverride } from './derive.js';
|
|
97
182
|
|
|
183
|
+
// Cross schema
|
|
184
|
+
export { buildCrossValidationSchema } from './cross-schema.js';
|
|
185
|
+
|
|
98
186
|
// Execute
|
|
99
187
|
export { executeTrail } from './execute.js';
|
|
100
188
|
export type { ExecuteTrailOptions } from './execute.js';
|
|
101
189
|
|
|
190
|
+
// Intrinsic tracing
|
|
191
|
+
export {
|
|
192
|
+
clearTraceSink,
|
|
193
|
+
createTraceRecord,
|
|
194
|
+
getTraceContext,
|
|
195
|
+
getTraceSink,
|
|
196
|
+
NOOP_SINK,
|
|
197
|
+
registerTraceSink,
|
|
198
|
+
TRACE_CONTEXT_KEY,
|
|
199
|
+
} from './internal/tracing.js';
|
|
200
|
+
export type {
|
|
201
|
+
TraceContext,
|
|
202
|
+
TraceRecord,
|
|
203
|
+
TraceSink,
|
|
204
|
+
} from './internal/tracing.js';
|
|
205
|
+
export type { TraceFn } from './types.js';
|
|
206
|
+
|
|
102
207
|
// Run
|
|
103
208
|
export { run } from './run.js';
|
|
104
209
|
export type { RunOptions } from './run.js';
|
|
105
210
|
|
|
211
|
+
// Trail factories
|
|
212
|
+
export { deriveTrail, ingest } from './trails/index.js';
|
|
213
|
+
export type {
|
|
214
|
+
DeriveTrailInput,
|
|
215
|
+
DeriveTrailOperation,
|
|
216
|
+
DeriveTrailOutput,
|
|
217
|
+
DeriveTrailSpec,
|
|
218
|
+
IngestOptions,
|
|
219
|
+
IngestTransform,
|
|
220
|
+
} from './trails/index.js';
|
|
221
|
+
|
|
106
222
|
// Validation
|
|
107
223
|
export {
|
|
108
224
|
validateInput,
|
|
@@ -120,7 +236,7 @@ export {
|
|
|
120
236
|
retry,
|
|
121
237
|
withTimeout,
|
|
122
238
|
shouldRetry,
|
|
123
|
-
|
|
239
|
+
deriveBackoffDelay,
|
|
124
240
|
} from './resilience.js';
|
|
125
241
|
export type { RetryOptions } from './resilience.js';
|
|
126
242
|
|
|
@@ -135,7 +251,7 @@ export {
|
|
|
135
251
|
nonEmptyString,
|
|
136
252
|
positiveInt,
|
|
137
253
|
shortId,
|
|
138
|
-
|
|
254
|
+
deriveIdHash,
|
|
139
255
|
} from './branded.js';
|
|
140
256
|
export type {
|
|
141
257
|
Branded,
|
|
@@ -146,13 +262,13 @@ export type {
|
|
|
146
262
|
} from './branded.js';
|
|
147
263
|
|
|
148
264
|
// Path Security
|
|
149
|
-
export { securePath, isPathSafe,
|
|
265
|
+
export { securePath, isPathSafe, deriveSafePath } from './path-security.js';
|
|
150
266
|
|
|
151
267
|
// Workspace
|
|
152
268
|
export {
|
|
153
269
|
findWorkspaceRoot,
|
|
154
270
|
isInsideWorkspace,
|
|
155
|
-
|
|
271
|
+
deriveRelativePath,
|
|
156
272
|
} from './workspace.js';
|
|
157
273
|
|
|
158
274
|
// Blob
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for `ctx.cross([...])` batch execution.
|
|
3
|
+
*
|
|
4
|
+
* These helpers normalize batch options, produce validation results, and
|
|
5
|
+
* implement the unlimited/limited worker-pool execution strategies used by
|
|
6
|
+
* both the real executor (`packages/core/src/execute.ts`) and the scenario
|
|
7
|
+
* runner in `@ontrails/testing`. Extracting them here keeps the validation
|
|
8
|
+
* rule, error message, and worker-pool semantics authored in one place so
|
|
9
|
+
* the two call sites cannot drift.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { ValidationError } from '../errors.js';
|
|
15
|
+
import { Result } from '../result.js';
|
|
16
|
+
import type { CrossBatchOptions } from '../types.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate the `concurrency` option on a batch `ctx.cross()` call.
|
|
20
|
+
*
|
|
21
|
+
* Returns `Ok(undefined)` when no limit is requested, `Ok(n)` when a
|
|
22
|
+
* positive integer is supplied, and `Err(ValidationError)` for any other
|
|
23
|
+
* value. The error message is load-bearing: callers and tests depend on
|
|
24
|
+
* the exact string.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export const normalizeCrossBatchConcurrency = (
|
|
29
|
+
options: CrossBatchOptions | undefined
|
|
30
|
+
): Result<number | undefined, Error> => {
|
|
31
|
+
const concurrency = options?.concurrency;
|
|
32
|
+
if (concurrency === undefined) {
|
|
33
|
+
return Result.ok();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
37
|
+
return Result.err(
|
|
38
|
+
new ValidationError(
|
|
39
|
+
'ctx.cross() batch concurrency must be a positive integer'
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Result.ok(concurrency);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Produce one validation-error result per call, preserving the original
|
|
49
|
+
* call order. Used when `normalizeCrossBatchConcurrency` fails so the caller
|
|
50
|
+
* can surface a uniform batch shape to the trail implementation.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export const createCrossBatchValidationResults = <TCall>(
|
|
55
|
+
calls: readonly TCall[],
|
|
56
|
+
error: Error
|
|
57
|
+
): Result<unknown, Error>[] => calls.map(() => Result.err(error));
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Claim the next branch index from a shared counter. Safe to call from
|
|
61
|
+
* multiple worker coroutines because JavaScript is single-threaded between
|
|
62
|
+
* awaits — the read/increment pair runs without interleaving.
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export const claimNextCrossBatchIndex = <TCall>(
|
|
67
|
+
nextIndex: { value: number },
|
|
68
|
+
calls: readonly TCall[]
|
|
69
|
+
): number | undefined => {
|
|
70
|
+
if (nextIndex.value >= calls.length) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const branchIndex = nextIndex.value;
|
|
75
|
+
nextIndex.value += 1;
|
|
76
|
+
return branchIndex;
|
|
77
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helper for "forking" a trail context.
|
|
3
|
+
*
|
|
4
|
+
* Several execution sites need to derive a child context from a parent
|
|
5
|
+
* context while **resetting** a well-known set of bound closures (`cross`,
|
|
6
|
+
* `fire`, `resource`). Those closures capture the parent scope, so reusing
|
|
7
|
+
* them on the child would re-enter execution with the wrong attribution,
|
|
8
|
+
* the wrong resource scope, or the wrong fan-out identity. The reset list
|
|
9
|
+
* is the same at every fork site.
|
|
10
|
+
*
|
|
11
|
+
* Codifying the reset list in one helper keeps that contract in a single
|
|
12
|
+
* place. Callers supply the overrides they need (typically `env`,
|
|
13
|
+
* `extensions`, and `logger`) and let the helper handle the reset.
|
|
14
|
+
*
|
|
15
|
+
* This module is internal — do not export it from the package entry point.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* The helper is intentionally generic over the concrete context shape. Some
|
|
19
|
+
* callers work with the fully-resolved `TrailContext` produced inside the
|
|
20
|
+
* executor; others work with `Partial<TrailContextInit>` during consumer
|
|
21
|
+
* context derivation in `fire.ts`. Both shapes share the same reset keys.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { TrailContext, TrailContextInit } from '../types.js';
|
|
25
|
+
|
|
26
|
+
/** Keys cleared by default when forking a context. */
|
|
27
|
+
export type ForkCtxResetKey = 'cross' | 'fire' | 'resource';
|
|
28
|
+
|
|
29
|
+
/** Override fields callers are allowed to apply when forking a context. */
|
|
30
|
+
export type ForkCtxOverrides = Readonly<
|
|
31
|
+
Partial<Pick<TrailContext, 'env' | 'extensions' | 'logger'>>
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
const DEFAULT_RESET_KEYS: readonly ForkCtxResetKey[] = [
|
|
35
|
+
'cross',
|
|
36
|
+
'fire',
|
|
37
|
+
'resource',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fork a parent context into a child context.
|
|
42
|
+
*
|
|
43
|
+
* Spreads the parent, clears each reset key to `undefined`, and applies the
|
|
44
|
+
* supplied overrides last so callers may replace logger, env, and extensions
|
|
45
|
+
* with branch-local values.
|
|
46
|
+
*
|
|
47
|
+
* The generic parameter is constrained to the minimal shape the helper
|
|
48
|
+
* reads and writes. This lets the helper serve both `TrailContext`
|
|
49
|
+
* (executor scope) and `Partial<TrailContextInit>` (fire-consumer scope)
|
|
50
|
+
* without widening the public surface of either type.
|
|
51
|
+
*/
|
|
52
|
+
export const forkCtx = <
|
|
53
|
+
TCtx extends Partial<
|
|
54
|
+
Pick<
|
|
55
|
+
TrailContextInit,
|
|
56
|
+
'cross' | 'env' | 'extensions' | 'fire' | 'logger' | 'resource'
|
|
57
|
+
>
|
|
58
|
+
>,
|
|
59
|
+
>(
|
|
60
|
+
parentCtx: TCtx,
|
|
61
|
+
overrides: ForkCtxOverrides = {},
|
|
62
|
+
reset: readonly ForkCtxResetKey[] = DEFAULT_RESET_KEYS
|
|
63
|
+
): TCtx => {
|
|
64
|
+
const forked: TCtx = { ...parentCtx };
|
|
65
|
+
for (const key of reset) {
|
|
66
|
+
forked[key as keyof TCtx] = undefined as TCtx[keyof TCtx];
|
|
67
|
+
}
|
|
68
|
+
return { ...forked, ...overrides };
|
|
69
|
+
};
|