@ontrails/core 1.0.0-beta.15 → 1.0.0-beta.17
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 +69 -0
- package/README.md +64 -19
- package/package.json +9 -8
- package/src/activation-provenance.ts +111 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-projection.ts +221 -0
- package/src/activation-source.ts +91 -0
- package/src/blob-ref.ts +51 -0
- package/src/context.ts +4 -0
- package/src/{internal/cross-batch.ts → cross-batch.ts} +3 -11
- package/src/detours.ts +8 -0
- package/src/draft.ts +4 -0
- package/src/error-projection.ts +51 -0
- package/src/errors.ts +192 -39
- package/src/execute.ts +443 -44
- package/src/fire.ts +965 -91
- package/src/index.ts +285 -38
- package/src/layer-projection.ts +192 -0
- package/src/layer.ts +32 -1
- package/src/observe.ts +361 -0
- package/src/permits.ts +12 -0
- package/src/redaction/patterns.ts +6 -3
- package/src/resilience.ts +8 -8
- package/src/resource-config.ts +477 -44
- package/src/resource.ts +21 -1
- package/src/result.ts +18 -0
- package/src/run.ts +2 -2
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +324 -0
- package/src/serialization.ts +90 -57
- package/src/signal-diagnostics.ts +633 -0
- package/src/{internal/signal-ref.ts → signal-ref.ts} +1 -1
- package/src/signal.ts +37 -0
- package/src/store/accessor-protocol.ts +5 -5
- package/src/structured-examples.ts +245 -0
- package/src/surface-derivation.ts +91 -0
- package/src/surface-filter.ts +7 -13
- package/src/topo.ts +386 -6
- package/src/tracing.ts +345 -0
- package/src/trail.ts +168 -22
- package/src/trails/derive-trail.ts +1 -1
- package/src/trails/ingest.ts +7 -15
- package/src/{internal/trails-db.ts → trails-db.ts} +42 -21
- package/src/transport-error-map.ts +146 -28
- package/src/types.ts +82 -28
- package/src/validate-established-topo.ts +6 -0
- package/src/validate-topo.ts +212 -5
- package/src/validation.ts +58 -1
- package/src/webhook.ts +276 -0
- package/src/{internal/zod-wrappers.ts → zod-wrappers.ts} +0 -6
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/blob-ref.d.ts +0 -20
- package/dist/blob-ref.d.ts.map +0 -1
- package/dist/blob-ref.js +0 -22
- package/dist/blob-ref.js.map +0 -1
- package/dist/branded.d.ts +0 -36
- package/dist/branded.d.ts.map +0 -1
- package/dist/branded.js +0 -89
- package/dist/branded.js.map +0 -1
- package/dist/collections.d.ts +0 -31
- package/dist/collections.d.ts.map +0 -1
- package/dist/collections.js +0 -60
- package/dist/collections.js.map +0 -1
- package/dist/context.d.ts +0 -22
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -37
- package/dist/context.js.map +0 -1
- package/dist/contour.d.ts +0 -81
- package/dist/contour.d.ts.map +0 -1
- package/dist/contour.js +0 -176
- package/dist/contour.js.map +0 -1
- package/dist/cross-schema.d.ts +0 -18
- package/dist/cross-schema.d.ts.map +0 -1
- package/dist/cross-schema.js +0 -29
- package/dist/cross-schema.js.map +0 -1
- package/dist/derive.d.ts +0 -44
- package/dist/derive.d.ts.map +0 -1
- package/dist/derive.js +0 -149
- package/dist/derive.js.map +0 -1
- package/dist/draft.d.ts +0 -28
- package/dist/draft.d.ts.map +0 -1
- package/dist/draft.js +0 -157
- package/dist/draft.js.map +0 -1
- package/dist/errors.d.ts +0 -141
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -185
- package/dist/errors.js.map +0 -1
- package/dist/execute.d.ts +0 -52
- package/dist/execute.d.ts.map +0 -1
- package/dist/execute.js +0 -511
- package/dist/execute.js.map +0 -1
- package/dist/fetch.d.ts +0 -15
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js +0 -102
- package/dist/fetch.js.map +0 -1
- package/dist/fire.d.ts +0 -43
- package/dist/fire.d.ts.map +0 -1
- package/dist/fire.js +0 -185
- package/dist/fire.js.map +0 -1
- package/dist/guards.d.ts +0 -17
- package/dist/guards.d.ts.map +0 -1
- package/dist/guards.js +0 -25
- package/dist/guards.js.map +0 -1
- package/dist/index.d.ts +0 -59
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -61
- package/dist/index.js.map +0 -1
- package/dist/internal/cross-batch.d.ts +0 -44
- package/dist/internal/cross-batch.d.ts.map +0 -1
- package/dist/internal/cross-batch.js +0 -58
- package/dist/internal/cross-batch.js.map +0 -1
- package/dist/internal/fork-ctx.d.ts +0 -41
- package/dist/internal/fork-ctx.d.ts.map +0 -1
- package/dist/internal/fork-ctx.js +0 -47
- package/dist/internal/fork-ctx.js.map +0 -1
- package/dist/internal/signal-ref.d.ts +0 -15
- package/dist/internal/signal-ref.d.ts.map +0 -1
- package/dist/internal/signal-ref.js +0 -41
- package/dist/internal/signal-ref.js.map +0 -1
- package/dist/internal/topo-saves.d.ts +0 -53
- package/dist/internal/topo-saves.d.ts.map +0 -1
- package/dist/internal/topo-saves.js +0 -449
- package/dist/internal/topo-saves.js.map +0 -1
- package/dist/internal/topo-snapshots.d.ts +0 -57
- package/dist/internal/topo-snapshots.d.ts.map +0 -1
- package/dist/internal/topo-snapshots.js +0 -372
- package/dist/internal/topo-snapshots.js.map +0 -1
- package/dist/internal/topo-store-read.d.ts +0 -70
- package/dist/internal/topo-store-read.d.ts.map +0 -1
- package/dist/internal/topo-store-read.js +0 -221
- package/dist/internal/topo-store-read.js.map +0 -1
- package/dist/internal/topo-store.d.ts +0 -26
- package/dist/internal/topo-store.d.ts.map +0 -1
- package/dist/internal/topo-store.js +0 -621
- package/dist/internal/topo-store.js.map +0 -1
- package/dist/internal/tracing.d.ts +0 -98
- package/dist/internal/tracing.d.ts.map +0 -1
- package/dist/internal/tracing.js +0 -102
- package/dist/internal/tracing.js.map +0 -1
- package/dist/internal/trails-db.d.ts +0 -16
- package/dist/internal/trails-db.d.ts.map +0 -1
- package/dist/internal/trails-db.js +0 -118
- package/dist/internal/trails-db.js.map +0 -1
- package/dist/internal/zod-wrappers.d.ts +0 -40
- package/dist/internal/zod-wrappers.d.ts.map +0 -1
- package/dist/internal/zod-wrappers.js +0 -59
- package/dist/internal/zod-wrappers.js.map +0 -1
- package/dist/layer.d.ts +0 -23
- package/dist/layer.d.ts.map +0 -1
- package/dist/layer.js +0 -21
- package/dist/layer.js.map +0 -1
- package/dist/path-security.d.ts +0 -28
- package/dist/path-security.d.ts.map +0 -1
- package/dist/path-security.js +0 -63
- package/dist/path-security.js.map +0 -1
- package/dist/patterns/bulk.d.ts +0 -15
- package/dist/patterns/bulk.d.ts.map +0 -1
- package/dist/patterns/bulk.js +0 -14
- package/dist/patterns/bulk.js.map +0 -1
- package/dist/patterns/change.d.ts +0 -10
- package/dist/patterns/change.d.ts.map +0 -1
- package/dist/patterns/change.js +0 -10
- package/dist/patterns/change.js.map +0 -1
- package/dist/patterns/date-range.d.ts +0 -10
- package/dist/patterns/date-range.d.ts.map +0 -1
- package/dist/patterns/date-range.js +0 -10
- package/dist/patterns/date-range.js.map +0 -1
- package/dist/patterns/index.d.ts +0 -9
- package/dist/patterns/index.d.ts.map +0 -1
- package/dist/patterns/index.js +0 -9
- package/dist/patterns/index.js.map +0 -1
- package/dist/patterns/pagination.d.ts +0 -18
- package/dist/patterns/pagination.d.ts.map +0 -1
- package/dist/patterns/pagination.js +0 -18
- package/dist/patterns/pagination.js.map +0 -1
- package/dist/patterns/progress.d.ts +0 -11
- package/dist/patterns/progress.d.ts.map +0 -1
- package/dist/patterns/progress.js +0 -11
- package/dist/patterns/progress.js.map +0 -1
- package/dist/patterns/sorting.d.ts +0 -13
- package/dist/patterns/sorting.d.ts.map +0 -1
- package/dist/patterns/sorting.js +0 -10
- package/dist/patterns/sorting.js.map +0 -1
- package/dist/patterns/status.d.ts +0 -15
- package/dist/patterns/status.d.ts.map +0 -1
- package/dist/patterns/status.js +0 -9
- package/dist/patterns/status.js.map +0 -1
- package/dist/patterns/timestamps.d.ts +0 -10
- package/dist/patterns/timestamps.d.ts.map +0 -1
- package/dist/patterns/timestamps.js +0 -10
- package/dist/patterns/timestamps.js.map +0 -1
- package/dist/redaction/index.d.ts +0 -4
- package/dist/redaction/index.d.ts.map +0 -1
- package/dist/redaction/index.js +0 -3
- package/dist/redaction/index.js.map +0 -1
- package/dist/redaction/patterns.d.ts +0 -9
- package/dist/redaction/patterns.d.ts.map +0 -1
- package/dist/redaction/patterns.js +0 -39
- package/dist/redaction/patterns.js.map +0 -1
- package/dist/redaction/redactor.d.ts +0 -27
- package/dist/redaction/redactor.d.ts.map +0 -1
- package/dist/redaction/redactor.js +0 -89
- package/dist/redaction/redactor.js.map +0 -1
- package/dist/resilience.d.ts +0 -34
- package/dist/resilience.d.ts.map +0 -1
- package/dist/resilience.js +0 -164
- package/dist/resilience.js.map +0 -1
- package/dist/resource-config.d.ts +0 -22
- package/dist/resource-config.d.ts.map +0 -1
- package/dist/resource-config.js +0 -210
- package/dist/resource-config.js.map +0 -1
- package/dist/resource.d.ts +0 -74
- package/dist/resource.d.ts.map +0 -1
- package/dist/resource.js +0 -61
- package/dist/resource.js.map +0 -1
- package/dist/result.d.ts +0 -57
- package/dist/result.d.ts.map +0 -1
- package/dist/result.js +0 -156
- package/dist/result.js.map +0 -1
- package/dist/run.d.ts +0 -28
- package/dist/run.d.ts.map +0 -1
- package/dist/run.js +0 -35
- package/dist/run.js.map +0 -1
- package/dist/serialization.d.ts +0 -27
- package/dist/serialization.d.ts.map +0 -1
- package/dist/serialization.js +0 -154
- package/dist/serialization.js.map +0 -1
- package/dist/signal.d.ts +0 -33
- package/dist/signal.d.ts.map +0 -1
- package/dist/signal.js +0 -17
- package/dist/signal.js.map +0 -1
- package/dist/store/accessor-protocol.d.ts +0 -51
- package/dist/store/accessor-protocol.d.ts.map +0 -1
- package/dist/store/accessor-protocol.js +0 -2
- package/dist/store/accessor-protocol.js.map +0 -1
- package/dist/store/index.d.ts +0 -2
- package/dist/store/index.d.ts.map +0 -1
- package/dist/store/index.js +0 -2
- package/dist/store/index.js.map +0 -1
- package/dist/surface-filter.d.ts +0 -13
- package/dist/surface-filter.d.ts.map +0 -1
- package/dist/surface-filter.js +0 -76
- package/dist/surface-filter.js.map +0 -1
- package/dist/topo-store.d.ts +0 -62
- package/dist/topo-store.d.ts.map +0 -1
- package/dist/topo-store.js +0 -319
- package/dist/topo-store.js.map +0 -1
- package/dist/topo.d.ts +0 -39
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -241
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -163
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -75
- package/dist/trail.js.map +0 -1
- package/dist/trails/derive-trail.d.ts +0 -66
- package/dist/trails/derive-trail.d.ts.map +0 -1
- package/dist/trails/derive-trail.js +0 -378
- package/dist/trails/derive-trail.js.map +0 -1
- package/dist/trails/index.d.ts +0 -5
- package/dist/trails/index.d.ts.map +0 -1
- package/dist/trails/index.js +0 -3
- package/dist/trails/index.js.map +0 -1
- package/dist/trails/ingest.d.ts +0 -24
- package/dist/trails/ingest.d.ts.map +0 -1
- package/dist/trails/ingest.js +0 -63
- package/dist/trails/ingest.js.map +0 -1
- package/dist/transport-error-map.d.ts +0 -106
- package/dist/transport-error-map.d.ts.map +0 -1
- package/dist/transport-error-map.js +0 -24
- package/dist/transport-error-map.js.map +0 -1
- package/dist/type-checks.test-d.d.ts +0 -71
- package/dist/type-checks.test-d.d.ts.map +0 -1
- package/dist/type-checks.test-d.js +0 -12
- package/dist/type-checks.test-d.js.map +0 -1
- package/dist/type-utils.d.ts +0 -33
- package/dist/type-utils.d.ts.map +0 -1
- package/dist/type-utils.js +0 -12
- package/dist/type-utils.js.map +0 -1
- package/dist/types.d.ts +0 -177
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/validate-established-topo.d.ts +0 -76
- package/dist/validate-established-topo.d.ts.map +0 -1
- package/dist/validate-established-topo.js +0 -43
- package/dist/validate-established-topo.js.map +0 -1
- package/dist/validate-topo.d.ts +0 -24
- package/dist/validate-topo.d.ts.map +0 -1
- package/dist/validate-topo.js +0 -193
- package/dist/validate-topo.js.map +0 -1
- package/dist/validation.d.ts +0 -27
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -165
- package/dist/validation.js.map +0 -1
- package/dist/workspace.d.ts +0 -25
- package/dist/workspace.d.ts.map +0 -1
- package/dist/workspace.js +0 -57
- package/dist/workspace.js.map +0 -1
- package/src/__tests__/blob-ref.test.ts +0 -103
- package/src/__tests__/branded.test.ts +0 -148
- package/src/__tests__/collections.test.ts +0 -126
- package/src/__tests__/context.test.ts +0 -77
- package/src/__tests__/contour.test.ts +0 -263
- package/src/__tests__/derive-trail.test.ts +0 -668
- package/src/__tests__/derive.test.ts +0 -260
- package/src/__tests__/errors.test.ts +0 -427
- package/src/__tests__/execute.test.ts +0 -1563
- package/src/__tests__/fetch.test.ts +0 -217
- package/src/__tests__/fire.test.ts +0 -1056
- package/src/__tests__/fork-ctx.test.ts +0 -83
- package/src/__tests__/guards.test.ts +0 -102
- package/src/__tests__/ingest.test.ts +0 -191
- package/src/__tests__/layer.test.ts +0 -125
- package/src/__tests__/path-security.test.ts +0 -114
- package/src/__tests__/patterns.test.ts +0 -273
- package/src/__tests__/redaction.test.ts +0 -244
- package/src/__tests__/resilience.test.ts +0 -246
- package/src/__tests__/resource.test.ts +0 -204
- package/src/__tests__/result.test.ts +0 -155
- package/src/__tests__/run.test.ts +0 -181
- package/src/__tests__/serialization.test.ts +0 -401
- package/src/__tests__/service-config.test.ts +0 -224
- package/src/__tests__/signal.test.ts +0 -82
- package/src/__tests__/surface-filter.test.ts +0 -196
- package/src/__tests__/topo-store-read.test.ts +0 -256
- package/src/__tests__/topo-store.test.ts +0 -869
- package/src/__tests__/topo.test.ts +0 -573
- package/src/__tests__/trail-permit.test.ts +0 -60
- package/src/__tests__/trail.test.ts +0 -426
- package/src/__tests__/trails-db.test.ts +0 -198
- package/src/__tests__/transport-error-map.test.ts +0 -82
- package/src/__tests__/type-utils.test.ts +0 -118
- package/src/__tests__/validate-topo.test.ts +0 -611
- package/src/__tests__/validation.test.ts +0 -336
- package/src/__tests__/workspace.test.ts +0 -183
- package/src/__tests__/zod-wrappers.test.ts +0 -62
- package/src/internal/topo-snapshots.ts +0 -517
- package/src/internal/topo-store-read.ts +0 -481
- package/src/internal/topo-store.ts +0 -1192
- package/src/internal/tracing.ts +0 -187
- package/src/topo-store.ts +0 -510
- package/src/type-checks.test-d.ts +0 -106
- package/tsconfig.json +0 -9
- package/tsconfig.tests.json +0 -10
- package/tsconfig.tsbuildinfo +0 -1
package/src/fire.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Signal emission and auto-activation.
|
|
3
3
|
*
|
|
4
|
-
* `createFireFn(topo, producerCtx?, executor)` returns a
|
|
5
|
-
* a topo. Calling `fire(
|
|
6
|
-
* the payload against its schema, finds every trail with
|
|
7
|
-
* `on:` array, and invokes each consumer via the supplied
|
|
4
|
+
* `createFireFn(topo, producerCtx?, executor, producerTrailId?)` returns a
|
|
5
|
+
* `FireFn` bound to a topo. Calling `fire(signal, payload)` looks up the
|
|
6
|
+
* signal, validates the payload against its schema, finds every trail with
|
|
7
|
+
* the signal in its `on:` array, and invokes each consumer via the supplied
|
|
8
|
+
* executor.
|
|
8
9
|
*
|
|
9
10
|
* The `executor` parameter is an indirection that lets `execute.ts` pass in
|
|
10
11
|
* `executeTrail` without `fire.ts` importing it directly — keeping the two
|
|
@@ -18,16 +19,57 @@
|
|
|
18
19
|
* producer logger as a child tagged with `signalId` and `consumerId` when
|
|
19
20
|
* `logger.child` exists.
|
|
20
21
|
*
|
|
21
|
-
* Error semantics match the fire-and-forget framing:
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* transactional coupling should use `crosses:`.
|
|
22
|
+
* Error semantics match the fire-and-forget framing: producer-facing
|
|
23
|
+
* `ctx.fire()` resolves after dispatch is initiated and without a value.
|
|
24
|
+
* Unknown signals, invalid payloads, guard suppression, and consumer errors
|
|
25
|
+
* are logged/diagnosed but do NOT propagate back to the producer. Consumers
|
|
26
|
+
* that need transactional coupling should use `crosses:`.
|
|
26
27
|
*/
|
|
27
28
|
|
|
28
|
-
import {
|
|
29
|
+
import type { z } from 'zod';
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
buildActivationProvenanceTraceAttrs,
|
|
33
|
+
getActivationProvenance,
|
|
34
|
+
withActivationProvenance,
|
|
35
|
+
} from './activation-provenance.js';
|
|
36
|
+
import type { ActivationProvenance } from './activation-provenance.js';
|
|
37
|
+
import type { ActivationWhereSpec } from './activation-source.js';
|
|
38
|
+
import { getActivationWherePredicate } from './activation-source.js';
|
|
39
|
+
import { activationSourceKey } from './activation-source-projection.js';
|
|
40
|
+
import { NotFoundError, TrailsError, ValidationError } from './errors.js';
|
|
29
41
|
import { forkCtx } from './internal/fork-ctx.js';
|
|
42
|
+
import {
|
|
43
|
+
OBSERVE_LOGGER_CONTEXT_KEY,
|
|
44
|
+
OBSERVE_LOGGER_METADATA_KEY,
|
|
45
|
+
} from './observe.js';
|
|
46
|
+
import {
|
|
47
|
+
getTraceContext,
|
|
48
|
+
getTraceSink,
|
|
49
|
+
isTracingDisabled,
|
|
50
|
+
writeActivationTraceRecord,
|
|
51
|
+
writeSignalTraceRecord,
|
|
52
|
+
} from './tracing.js';
|
|
53
|
+
import type { SignalTraceRecordName, TraceSink } from './tracing.js';
|
|
30
54
|
import { Result } from './result.js';
|
|
55
|
+
import type { AnySignal } from './signal.js';
|
|
56
|
+
import {
|
|
57
|
+
createSignalFireSuppressedDiagnostic,
|
|
58
|
+
createSignalHandlerFailedDiagnostic,
|
|
59
|
+
createSignalHandlerRejectedDiagnostic,
|
|
60
|
+
createSignalInvalidDiagnostic,
|
|
61
|
+
createSignalPredicateFailedDiagnostic,
|
|
62
|
+
createSignalUnknownDiagnostic,
|
|
63
|
+
recordSignalDiagnostic,
|
|
64
|
+
signalDiagnosticCauseFromUnknown,
|
|
65
|
+
summarizeSignalPayload,
|
|
66
|
+
} from './signal-diagnostics.js';
|
|
67
|
+
import type {
|
|
68
|
+
SignalDiagnostic,
|
|
69
|
+
SignalDiagnosticSchemaIssue,
|
|
70
|
+
SignalInvalidDiagnostic,
|
|
71
|
+
SignalPayloadSummary,
|
|
72
|
+
} from './signal-diagnostics.js';
|
|
31
73
|
import type { Topo } from './topo.js';
|
|
32
74
|
import type { AnyTrail } from './trail.js';
|
|
33
75
|
import type { FireFn, Logger, TrailContextInit } from './types.js';
|
|
@@ -43,7 +85,75 @@ type MutableConsumerContext = {
|
|
|
43
85
|
-readonly [K in keyof Partial<TrailContextInit>]: Partial<TrailContextInit>[K];
|
|
44
86
|
};
|
|
45
87
|
|
|
88
|
+
interface ConsumerActivation {
|
|
89
|
+
readonly trail: AnyTrail;
|
|
90
|
+
readonly wheres: readonly ActivationWhereSpec[];
|
|
91
|
+
}
|
|
92
|
+
|
|
46
93
|
const FIRE_STACK_KEY = '__trails_fire_stack';
|
|
94
|
+
const FIRE_PENDING_DISPATCHES_KEY = '__trails_fire_pending_dispatches';
|
|
95
|
+
|
|
96
|
+
const frameworkFireFns = new WeakSet<FireFn>();
|
|
97
|
+
|
|
98
|
+
export const isFrameworkFireFn = (fire: FireFn | undefined): boolean =>
|
|
99
|
+
fire !== undefined && frameworkFireFns.has(fire);
|
|
100
|
+
|
|
101
|
+
type FireDispatchTracker = Set<Promise<void>>;
|
|
102
|
+
|
|
103
|
+
const getFireDispatchTracker = (
|
|
104
|
+
ctx: Pick<TrailContextInit, 'extensions'> | undefined
|
|
105
|
+
): FireDispatchTracker | undefined => {
|
|
106
|
+
const tracker = ctx?.extensions?.[FIRE_PENDING_DISPATCHES_KEY];
|
|
107
|
+
return tracker instanceof Set ? (tracker as FireDispatchTracker) : undefined;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const withFireDispatchTracking = <T extends TrailContextInit>(
|
|
111
|
+
ctx: T
|
|
112
|
+
): T => {
|
|
113
|
+
if (getFireDispatchTracker(ctx) !== undefined) {
|
|
114
|
+
return ctx;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
...ctx,
|
|
119
|
+
extensions: {
|
|
120
|
+
...ctx.extensions,
|
|
121
|
+
[FIRE_PENDING_DISPATCHES_KEY]: new Set<Promise<void>>(),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const trackFireDispatch = (
|
|
127
|
+
ctx: Pick<TrailContextInit, 'extensions'> | undefined,
|
|
128
|
+
dispatch: Promise<void>
|
|
129
|
+
): void => {
|
|
130
|
+
const tracker = getFireDispatchTracker(ctx);
|
|
131
|
+
if (tracker === undefined) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
tracker.add(dispatch);
|
|
135
|
+
const untrack = async (): Promise<void> => {
|
|
136
|
+
try {
|
|
137
|
+
await dispatch;
|
|
138
|
+
} finally {
|
|
139
|
+
tracker.delete(dispatch);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
void untrack();
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const waitForPendingFireDispatches = async (
|
|
146
|
+
ctx: Pick<TrailContextInit, 'extensions'>
|
|
147
|
+
): Promise<void> => {
|
|
148
|
+
const tracker = getFireDispatchTracker(ctx);
|
|
149
|
+
if (tracker === undefined) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
while (tracker.size > 0) {
|
|
154
|
+
await Promise.allSettled(tracker);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
47
157
|
|
|
48
158
|
/**
|
|
49
159
|
* Maximum depth for signal fan-out chains.
|
|
@@ -63,7 +173,8 @@ const getFireStack = (
|
|
|
63
173
|
|
|
64
174
|
/** Binds a per-consumer `fire` onto a mutable consumer context. */
|
|
65
175
|
type ConsumerFireBinder = (
|
|
66
|
-
consumerCtx: MutableConsumerContext
|
|
176
|
+
consumerCtx: MutableConsumerContext,
|
|
177
|
+
consumerId: string
|
|
67
178
|
) => MutableConsumerContext;
|
|
68
179
|
|
|
69
180
|
const deriveConsumerLogger = (
|
|
@@ -78,66 +189,583 @@ const deriveConsumerEnv = (
|
|
|
78
189
|
): TrailContextInit['env'] =>
|
|
79
190
|
producerCtx?.env ? { ...producerCtx.env } : undefined;
|
|
80
191
|
|
|
192
|
+
const readExistingObserveMetadata = (
|
|
193
|
+
producerCtx: TrailContextInit | undefined
|
|
194
|
+
): Record<string, unknown> => {
|
|
195
|
+
const value = producerCtx?.extensions?.[OBSERVE_LOGGER_METADATA_KEY];
|
|
196
|
+
return value !== null && typeof value === 'object'
|
|
197
|
+
? (value as Record<string, unknown>)
|
|
198
|
+
: {};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const deriveConsumerObserveMetadata = (
|
|
202
|
+
producerCtx: TrailContextInit | undefined,
|
|
203
|
+
signalId: string,
|
|
204
|
+
consumerId: string
|
|
205
|
+
): Record<string, unknown> | undefined => {
|
|
206
|
+
// Only carry observe metadata forward when the producer ctx is using a
|
|
207
|
+
// topo-managed observe logger. Otherwise we'd attach signal fan-out fields
|
|
208
|
+
// to caller-supplied loggers that didn't opt into the structured contract.
|
|
209
|
+
if (producerCtx?.extensions?.[OBSERVE_LOGGER_CONTEXT_KEY] !== true) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
...readExistingObserveMetadata(producerCtx),
|
|
214
|
+
consumerId,
|
|
215
|
+
signalId,
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
81
219
|
const deriveConsumerExtensions = (
|
|
82
220
|
producerCtx: TrailContextInit | undefined,
|
|
83
|
-
signalId: string
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
[
|
|
87
|
-
}
|
|
221
|
+
signalId: string,
|
|
222
|
+
consumerId: string
|
|
223
|
+
): TrailContextInit['extensions'] => {
|
|
224
|
+
const { [FIRE_PENDING_DISPATCHES_KEY]: _pending, ...extensions } =
|
|
225
|
+
producerCtx?.extensions ?? {};
|
|
226
|
+
const observeMetadata = deriveConsumerObserveMetadata(
|
|
227
|
+
producerCtx,
|
|
228
|
+
signalId,
|
|
229
|
+
consumerId
|
|
230
|
+
);
|
|
231
|
+
return {
|
|
232
|
+
...extensions,
|
|
233
|
+
[FIRE_STACK_KEY]: [...getFireStack(producerCtx), signalId],
|
|
234
|
+
...(observeMetadata === undefined
|
|
235
|
+
? {}
|
|
236
|
+
: { [OBSERVE_LOGGER_METADATA_KEY]: observeMetadata }),
|
|
237
|
+
};
|
|
238
|
+
};
|
|
88
239
|
|
|
89
240
|
const deriveConsumerCtx = (
|
|
90
241
|
producerCtx: TrailContextInit | undefined,
|
|
91
242
|
signalId: string,
|
|
92
|
-
consumerId: string
|
|
243
|
+
consumerId: string,
|
|
244
|
+
activation: ActivationProvenance
|
|
93
245
|
): MutableConsumerContext =>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
246
|
+
withActivationProvenance(
|
|
247
|
+
producerCtx
|
|
248
|
+
? forkCtx(producerCtx as MutableConsumerContext, {
|
|
249
|
+
env: deriveConsumerEnv(producerCtx),
|
|
250
|
+
extensions: deriveConsumerExtensions(
|
|
251
|
+
producerCtx,
|
|
252
|
+
signalId,
|
|
253
|
+
consumerId
|
|
254
|
+
),
|
|
255
|
+
logger: deriveConsumerLogger(producerCtx, signalId, consumerId),
|
|
256
|
+
})
|
|
257
|
+
: {},
|
|
258
|
+
activation
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
interface FireDiagnosticMetadata {
|
|
262
|
+
readonly activation: ActivationProvenance;
|
|
263
|
+
readonly producerTrailId?: string | undefined;
|
|
264
|
+
readonly runId?: string | undefined;
|
|
265
|
+
readonly traceId?: string | undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface SignalTraceAttrsInput {
|
|
269
|
+
readonly consumerIds?: readonly string[] | undefined;
|
|
270
|
+
readonly errorName?: string | undefined;
|
|
271
|
+
readonly handlerTrailId?: string | undefined;
|
|
272
|
+
readonly payload?: SignalPayloadSummary | undefined;
|
|
273
|
+
readonly producerTrailId?: string | undefined;
|
|
274
|
+
readonly runId?: string | undefined;
|
|
275
|
+
readonly schemaIssues?: readonly SignalDiagnosticSchemaIssue[] | undefined;
|
|
276
|
+
readonly signalId: string;
|
|
277
|
+
readonly activation?: ActivationProvenance | undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const deriveFireDiagnosticMetadata = (
|
|
281
|
+
producerCtx: TrailContextInit | undefined,
|
|
282
|
+
producerTrailId: string | undefined,
|
|
283
|
+
signalId: string
|
|
284
|
+
): FireDiagnosticMetadata => {
|
|
285
|
+
const trace = producerCtx ? getTraceContext(producerCtx) : undefined;
|
|
286
|
+
const parent = getActivationProvenance(producerCtx);
|
|
287
|
+
const fireId = Bun.randomUUIDv7();
|
|
288
|
+
return {
|
|
289
|
+
activation: {
|
|
290
|
+
fireId,
|
|
291
|
+
...(parent?.fireId === undefined ? {} : { parentFireId: parent.fireId }),
|
|
292
|
+
rootFireId: parent?.rootFireId ?? fireId,
|
|
293
|
+
source: {
|
|
294
|
+
id: signalId,
|
|
295
|
+
kind: 'signal',
|
|
296
|
+
...(producerTrailId === undefined ? {} : { producerTrailId }),
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
producerTrailId,
|
|
300
|
+
runId: trace?.spanId,
|
|
301
|
+
traceId: trace?.traceId,
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const recordRuntimeSignalDiagnostic = async (
|
|
306
|
+
producerCtx: TrailContextInit | undefined,
|
|
307
|
+
diagnostic: SignalDiagnostic
|
|
308
|
+
): Promise<boolean> => {
|
|
309
|
+
const record = await recordSignalDiagnostic(producerCtx, diagnostic);
|
|
310
|
+
if (record.promoted) {
|
|
311
|
+
producerCtx?.logger?.warn('Signal diagnostic promoted by strict mode', {
|
|
312
|
+
code: diagnostic.code,
|
|
313
|
+
producerTrailId: diagnostic.producerTrailId,
|
|
314
|
+
runId: diagnostic.runId,
|
|
315
|
+
signalId: diagnostic.signalId,
|
|
316
|
+
traceId: diagnostic.traceId,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return record.promoted;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const deriveSignalErrorCategory = (error: unknown): string =>
|
|
323
|
+
error instanceof TrailsError ? error.category : 'internal';
|
|
324
|
+
|
|
325
|
+
const signalIssuePathLabel = (issue: SignalDiagnosticSchemaIssue): string => {
|
|
326
|
+
if (issue.path.length === 0) {
|
|
327
|
+
return '$';
|
|
328
|
+
}
|
|
329
|
+
return issue.path.map(String).join('.');
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const addPayloadSummaryAttrs = (
|
|
333
|
+
attrs: Record<string, unknown>,
|
|
334
|
+
payload: SignalPayloadSummary
|
|
335
|
+
): void => {
|
|
336
|
+
attrs['trails.signal.payload.byte_length'] = payload.byteLength;
|
|
337
|
+
attrs['trails.signal.payload.digest'] = payload.digest;
|
|
338
|
+
attrs['trails.signal.payload.redacted'] = payload.redacted;
|
|
339
|
+
attrs['trails.signal.payload.shape'] = payload.shape;
|
|
340
|
+
if (payload.topLevelEntryCount !== undefined) {
|
|
341
|
+
attrs['trails.signal.payload.top_level_entry_count'] =
|
|
342
|
+
payload.topLevelEntryCount;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const buildSignalTraceAttrs = (
|
|
347
|
+
input: SignalTraceAttrsInput
|
|
348
|
+
): Readonly<Record<string, unknown>> => {
|
|
349
|
+
const attrs: Record<string, unknown> = {
|
|
350
|
+
...buildActivationProvenanceTraceAttrs(input.activation),
|
|
351
|
+
'trails.signal.id': input.signalId,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
if (input.producerTrailId !== undefined) {
|
|
355
|
+
attrs['trails.signal.producer_trail.id'] = input.producerTrailId;
|
|
356
|
+
}
|
|
357
|
+
if (input.runId !== undefined) {
|
|
358
|
+
attrs['trails.signal.run.id'] = input.runId;
|
|
359
|
+
}
|
|
360
|
+
if (input.handlerTrailId !== undefined) {
|
|
361
|
+
attrs['trails.signal.handler_trail.id'] = input.handlerTrailId;
|
|
362
|
+
}
|
|
363
|
+
if (input.consumerIds !== undefined) {
|
|
364
|
+
attrs['trails.signal.consumer_count'] = input.consumerIds.length;
|
|
365
|
+
attrs['trails.signal.consumer_ids'] = input.consumerIds
|
|
366
|
+
.toSorted()
|
|
367
|
+
.join(',');
|
|
368
|
+
}
|
|
369
|
+
if (input.errorName !== undefined) {
|
|
370
|
+
attrs['trails.signal.error.name'] = input.errorName;
|
|
371
|
+
}
|
|
372
|
+
if (input.payload !== undefined) {
|
|
373
|
+
addPayloadSummaryAttrs(attrs, input.payload);
|
|
374
|
+
}
|
|
375
|
+
if (input.schemaIssues !== undefined) {
|
|
376
|
+
const issuePaths = input.schemaIssues.map(signalIssuePathLabel);
|
|
377
|
+
attrs['trails.signal.schema_issue_count'] = input.schemaIssues.length;
|
|
378
|
+
attrs['trails.signal.schema_issue_paths'] = issuePaths.join(',');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return attrs;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const recordSignalLifecycleTrace = async (
|
|
385
|
+
producerCtx: TrailContextInit | undefined,
|
|
386
|
+
name: SignalTraceRecordName,
|
|
387
|
+
attrs: Readonly<Record<string, unknown>>,
|
|
388
|
+
status?: Parameters<typeof writeSignalTraceRecord>[3],
|
|
389
|
+
errorCategory?: string | undefined,
|
|
390
|
+
sink?: TraceSink | undefined
|
|
391
|
+
): Promise<void> => {
|
|
392
|
+
if (producerCtx === undefined) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
await writeSignalTraceRecord(
|
|
396
|
+
producerCtx,
|
|
397
|
+
name,
|
|
398
|
+
attrs,
|
|
399
|
+
status,
|
|
400
|
+
errorCategory,
|
|
401
|
+
sink
|
|
402
|
+
);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const recordActivationGuardTrace = async (
|
|
406
|
+
producerCtx: TrailContextInit | undefined,
|
|
407
|
+
diagnosticMetadata: FireDiagnosticMetadata,
|
|
408
|
+
reason: 'cycle' | 'depth',
|
|
409
|
+
signalId: string,
|
|
410
|
+
fireStack: readonly string[],
|
|
411
|
+
limit?: number | undefined,
|
|
412
|
+
sink?: TraceSink | undefined
|
|
413
|
+
): Promise<void> => {
|
|
414
|
+
const attrs: Record<string, unknown> = {
|
|
415
|
+
...buildActivationProvenanceTraceAttrs(diagnosticMetadata.activation),
|
|
416
|
+
'trails.activation.guard.fire_stack': fireStack.join(','),
|
|
417
|
+
'trails.activation.guard.reason': reason,
|
|
418
|
+
'trails.signal.id': signalId,
|
|
419
|
+
};
|
|
420
|
+
if (limit !== undefined) {
|
|
421
|
+
attrs['trails.activation.guard.limit'] = limit;
|
|
422
|
+
}
|
|
423
|
+
await writeActivationTraceRecord(
|
|
424
|
+
'activation.cycle_detected',
|
|
425
|
+
attrs,
|
|
426
|
+
'ok',
|
|
427
|
+
undefined,
|
|
428
|
+
producerCtx === undefined ? undefined : getTraceContext(producerCtx),
|
|
429
|
+
sink
|
|
430
|
+
);
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const activationEntriesForSignal = (
|
|
434
|
+
trail: AnyTrail,
|
|
435
|
+
signalId: string
|
|
436
|
+
): readonly ConsumerActivation[] => {
|
|
437
|
+
const activations = new Map<string, readonly ActivationWhereSpec[]>();
|
|
438
|
+
for (const activation of trail.activationSources ?? []) {
|
|
439
|
+
if (
|
|
440
|
+
activation.source.kind !== 'signal' ||
|
|
441
|
+
activation.source.id !== signalId
|
|
442
|
+
) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const key = activationSourceKey(activation.source);
|
|
446
|
+
if (activation.where === undefined) {
|
|
447
|
+
activations.set(key, []);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const wheres = activations.get(key);
|
|
452
|
+
if (wheres === undefined) {
|
|
453
|
+
activations.set(key, [activation.where]);
|
|
454
|
+
} else if (wheres.length > 0) {
|
|
455
|
+
activations.set(key, [...wheres, activation.where]);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (activations.size > 0) {
|
|
459
|
+
return [...activations.values()].map((wheres) => ({
|
|
460
|
+
trail,
|
|
461
|
+
wheres,
|
|
462
|
+
}));
|
|
463
|
+
}
|
|
464
|
+
return trail.on.includes(signalId) ? [{ trail, wheres: [] }] : [];
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const listConsumerActivations = (
|
|
468
|
+
topo: Topo,
|
|
469
|
+
signalId: string
|
|
470
|
+
): readonly ConsumerActivation[] =>
|
|
471
|
+
topo.list().flatMap((trail) => activationEntriesForSignal(trail, signalId));
|
|
472
|
+
|
|
473
|
+
const consumerTrailsFromActivations = (
|
|
474
|
+
activations: readonly ConsumerActivation[]
|
|
475
|
+
): readonly AnyTrail[] => [
|
|
476
|
+
...new Map(
|
|
477
|
+
activations.map((activation) => [activation.trail.id, activation.trail])
|
|
478
|
+
).values(),
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
const hasActiveSignalTraceContext = (
|
|
482
|
+
producerCtx: TrailContextInit | undefined,
|
|
483
|
+
sink: TraceSink
|
|
484
|
+
): boolean =>
|
|
485
|
+
producerCtx !== undefined &&
|
|
486
|
+
getTraceContext(producerCtx) !== undefined &&
|
|
487
|
+
!isTracingDisabled(sink);
|
|
488
|
+
|
|
489
|
+
const summarizeSignalPayloadForTrace = (
|
|
490
|
+
producerCtx: TrailContextInit | undefined,
|
|
491
|
+
payload: unknown,
|
|
492
|
+
sink: TraceSink
|
|
493
|
+
): SignalPayloadSummary | undefined => {
|
|
494
|
+
if (!hasActiveSignalTraceContext(producerCtx, sink)) {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
return summarizeSignalPayload(payload);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
producerCtx?.logger?.debug('Signal payload summary skipped', {
|
|
501
|
+
error: error instanceof Error ? error.message : String(error),
|
|
502
|
+
});
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const recordPredicateTrace = async (
|
|
508
|
+
producerCtx: TrailContextInit | undefined,
|
|
509
|
+
name:
|
|
510
|
+
| 'signal.handler.predicate_failed'
|
|
511
|
+
| 'signal.handler.predicate_matched'
|
|
512
|
+
| 'signal.handler.predicate_skipped',
|
|
513
|
+
input: {
|
|
514
|
+
readonly diagnosticMetadata: FireDiagnosticMetadata;
|
|
515
|
+
readonly errorCategory?: string | undefined;
|
|
516
|
+
readonly errorName?: string | undefined;
|
|
517
|
+
readonly handlerTrailId: string;
|
|
518
|
+
readonly payloadSummary?: SignalPayloadSummary | undefined;
|
|
519
|
+
readonly signalId: string;
|
|
520
|
+
readonly status?: Parameters<typeof recordSignalLifecycleTrace>[3];
|
|
521
|
+
readonly traceSink: TraceSink;
|
|
522
|
+
}
|
|
523
|
+
): Promise<void> => {
|
|
524
|
+
await recordSignalLifecycleTrace(
|
|
525
|
+
producerCtx,
|
|
526
|
+
name,
|
|
527
|
+
buildSignalTraceAttrs({
|
|
528
|
+
activation: input.diagnosticMetadata.activation,
|
|
529
|
+
errorName: input.errorName,
|
|
530
|
+
handlerTrailId: input.handlerTrailId,
|
|
531
|
+
payload: input.payloadSummary,
|
|
532
|
+
producerTrailId: input.diagnosticMetadata.producerTrailId,
|
|
533
|
+
runId: input.diagnosticMetadata.runId,
|
|
534
|
+
signalId: input.signalId,
|
|
535
|
+
}),
|
|
536
|
+
input.status,
|
|
537
|
+
input.errorCategory,
|
|
538
|
+
input.traceSink
|
|
539
|
+
);
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const shouldInvokeConsumer = async (
|
|
543
|
+
activation: ConsumerActivation,
|
|
544
|
+
payload: unknown,
|
|
545
|
+
payloadSummary: SignalPayloadSummary | undefined,
|
|
546
|
+
signalId: string,
|
|
547
|
+
producerCtx: TrailContextInit | undefined,
|
|
548
|
+
diagnosticMetadata: FireDiagnosticMetadata,
|
|
549
|
+
logger: Logger | undefined,
|
|
550
|
+
traceSink: TraceSink
|
|
551
|
+
): Promise<boolean> => {
|
|
552
|
+
if (activation.wheres.length === 0) {
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
for (const where of activation.wheres) {
|
|
557
|
+
const predicate = getActivationWherePredicate(where);
|
|
558
|
+
if (predicate === undefined) {
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
const matched = await predicate(payload);
|
|
564
|
+
await recordPredicateTrace(
|
|
565
|
+
producerCtx,
|
|
566
|
+
matched
|
|
567
|
+
? 'signal.handler.predicate_matched'
|
|
568
|
+
: 'signal.handler.predicate_skipped',
|
|
569
|
+
{
|
|
570
|
+
diagnosticMetadata,
|
|
571
|
+
handlerTrailId: activation.trail.id,
|
|
572
|
+
payloadSummary,
|
|
573
|
+
signalId,
|
|
574
|
+
traceSink,
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
if (matched) {
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
} catch (error) {
|
|
581
|
+
const cause = signalDiagnosticCauseFromUnknown(error);
|
|
582
|
+
const diagnostic = createSignalPredicateFailedDiagnostic({
|
|
583
|
+
...diagnosticMetadata,
|
|
584
|
+
cause: error,
|
|
585
|
+
handlerTrailId: activation.trail.id,
|
|
586
|
+
payload,
|
|
587
|
+
signalId,
|
|
588
|
+
});
|
|
589
|
+
await recordRuntimeSignalDiagnostic(producerCtx, diagnostic);
|
|
590
|
+
await recordPredicateTrace(
|
|
591
|
+
producerCtx,
|
|
592
|
+
'signal.handler.predicate_failed',
|
|
593
|
+
{
|
|
594
|
+
diagnosticMetadata,
|
|
595
|
+
errorCategory: deriveSignalErrorCategory(error),
|
|
596
|
+
errorName: cause.name,
|
|
597
|
+
handlerTrailId: activation.trail.id,
|
|
598
|
+
payloadSummary,
|
|
599
|
+
signalId,
|
|
600
|
+
status: 'err',
|
|
601
|
+
traceSink,
|
|
602
|
+
}
|
|
603
|
+
);
|
|
604
|
+
logger?.warn('Signal activation predicate failed', {
|
|
605
|
+
consumerId: activation.trail.id,
|
|
606
|
+
error: cause.message,
|
|
607
|
+
signalId,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return false;
|
|
613
|
+
};
|
|
101
614
|
|
|
102
615
|
/**
|
|
103
616
|
* Fan out a validated signal payload to its consumer trails.
|
|
104
617
|
*
|
|
105
618
|
* @remarks
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
619
|
+
* Signal delivery is fire-and-forget notification, not ordered orchestration;
|
|
620
|
+
* if one consumer depends on another, the dependency belongs in `crosses:`
|
|
621
|
+
* instead of sibling signal sequencing.
|
|
109
622
|
*
|
|
110
|
-
* `Promise.allSettled` preserves failure isolation
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
623
|
+
* `Promise.allSettled` preserves failure isolation for the background
|
|
624
|
+
* completion task. Producer-facing `ctx.fire()` starts this task but does not
|
|
625
|
+
* wait for every consumer to complete before resolving. Each consumer gets its
|
|
626
|
+
* own derived context so sibling fan-out branches do not share mutable
|
|
627
|
+
* top-level state while they overlap. Re-entrant suppression elsewhere in this
|
|
628
|
+
* module is still based on signal-id membership in the current fire stack: it
|
|
629
|
+
* prevents infinite loops, but it can over-suppress legitimate diamond
|
|
630
|
+
* re-fires. Activation provenance records each fire boundary so downstream
|
|
631
|
+
* traces and diagnostics can still reconstruct the chain that activated a
|
|
632
|
+
* consumer.
|
|
117
633
|
*/
|
|
118
634
|
const fanOutToConsumers = async (
|
|
119
|
-
|
|
635
|
+
activations: readonly ConsumerActivation[],
|
|
120
636
|
payload: unknown,
|
|
121
637
|
signalId: string,
|
|
122
638
|
producerCtx: TrailContextInit | undefined,
|
|
639
|
+
diagnosticMetadata: FireDiagnosticMetadata,
|
|
123
640
|
bindFire: ConsumerFireBinder,
|
|
124
641
|
executor: ConsumerExecutor,
|
|
125
|
-
logger: Logger | undefined
|
|
642
|
+
logger: Logger | undefined,
|
|
643
|
+
traceSink: TraceSink
|
|
126
644
|
): Promise<void> => {
|
|
645
|
+
const payloadSummary = summarizeSignalPayloadForTrace(
|
|
646
|
+
producerCtx,
|
|
647
|
+
payload,
|
|
648
|
+
traceSink
|
|
649
|
+
);
|
|
127
650
|
const settled = await Promise.allSettled(
|
|
128
|
-
|
|
651
|
+
activations.map(async (activation) => {
|
|
652
|
+
const consumer = activation.trail;
|
|
653
|
+
const shouldInvoke = await shouldInvokeConsumer(
|
|
654
|
+
activation,
|
|
655
|
+
payload,
|
|
656
|
+
payloadSummary,
|
|
657
|
+
signalId,
|
|
658
|
+
producerCtx,
|
|
659
|
+
diagnosticMetadata,
|
|
660
|
+
logger,
|
|
661
|
+
traceSink
|
|
662
|
+
);
|
|
663
|
+
if (!shouldInvoke) {
|
|
664
|
+
return consumer.id;
|
|
665
|
+
}
|
|
129
666
|
const consumerCtx = bindFire(
|
|
130
|
-
deriveConsumerCtx(
|
|
667
|
+
deriveConsumerCtx(
|
|
668
|
+
producerCtx,
|
|
669
|
+
signalId,
|
|
670
|
+
consumer.id,
|
|
671
|
+
diagnosticMetadata.activation
|
|
672
|
+
),
|
|
673
|
+
consumer.id
|
|
131
674
|
);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
675
|
+
await recordSignalLifecycleTrace(
|
|
676
|
+
producerCtx,
|
|
677
|
+
'signal.handler.invoked',
|
|
678
|
+
buildSignalTraceAttrs({
|
|
679
|
+
activation: diagnosticMetadata.activation,
|
|
680
|
+
handlerTrailId: consumer.id,
|
|
681
|
+
payload: payloadSummary,
|
|
682
|
+
producerTrailId: diagnosticMetadata.producerTrailId,
|
|
683
|
+
runId: diagnosticMetadata.runId,
|
|
684
|
+
signalId,
|
|
685
|
+
}),
|
|
686
|
+
undefined,
|
|
687
|
+
undefined,
|
|
688
|
+
traceSink
|
|
689
|
+
);
|
|
690
|
+
try {
|
|
691
|
+
const consumerResult = await executor(consumer, payload, consumerCtx);
|
|
692
|
+
if (consumerResult.isErr()) {
|
|
693
|
+
const cause = signalDiagnosticCauseFromUnknown(consumerResult.error);
|
|
694
|
+
const diagnostic = createSignalHandlerFailedDiagnostic({
|
|
695
|
+
...diagnosticMetadata,
|
|
696
|
+
cause: consumerResult.error,
|
|
697
|
+
handlerTrailId: consumer.id,
|
|
698
|
+
payload,
|
|
699
|
+
signalId,
|
|
700
|
+
});
|
|
701
|
+
await recordRuntimeSignalDiagnostic(producerCtx, diagnostic);
|
|
702
|
+
await recordSignalLifecycleTrace(
|
|
703
|
+
producerCtx,
|
|
704
|
+
'signal.handler.failed',
|
|
705
|
+
buildSignalTraceAttrs({
|
|
706
|
+
activation: diagnosticMetadata.activation,
|
|
707
|
+
errorName: cause.name,
|
|
708
|
+
handlerTrailId: consumer.id,
|
|
709
|
+
payload: payloadSummary,
|
|
710
|
+
producerTrailId: diagnosticMetadata.producerTrailId,
|
|
711
|
+
runId: diagnosticMetadata.runId,
|
|
712
|
+
signalId,
|
|
713
|
+
}),
|
|
714
|
+
'err',
|
|
715
|
+
deriveSignalErrorCategory(consumerResult.error),
|
|
716
|
+
traceSink
|
|
717
|
+
);
|
|
718
|
+
(consumerCtx.logger ?? logger)?.warn('Signal consumer failed', {
|
|
719
|
+
consumerId: consumer.id,
|
|
720
|
+
error: consumerResult.error.message,
|
|
721
|
+
signalId,
|
|
722
|
+
});
|
|
723
|
+
return consumer.id;
|
|
724
|
+
}
|
|
725
|
+
await recordSignalLifecycleTrace(
|
|
726
|
+
producerCtx,
|
|
727
|
+
'signal.handler.completed',
|
|
728
|
+
buildSignalTraceAttrs({
|
|
729
|
+
activation: diagnosticMetadata.activation,
|
|
730
|
+
handlerTrailId: consumer.id,
|
|
731
|
+
payload: payloadSummary,
|
|
732
|
+
producerTrailId: diagnosticMetadata.producerTrailId,
|
|
733
|
+
runId: diagnosticMetadata.runId,
|
|
734
|
+
signalId,
|
|
735
|
+
}),
|
|
736
|
+
undefined,
|
|
737
|
+
undefined,
|
|
738
|
+
traceSink
|
|
739
|
+
);
|
|
740
|
+
return consumer.id;
|
|
741
|
+
} catch (error) {
|
|
742
|
+
const cause = signalDiagnosticCauseFromUnknown(error);
|
|
743
|
+
const diagnostic = createSignalHandlerRejectedDiagnostic({
|
|
744
|
+
...diagnosticMetadata,
|
|
745
|
+
cause: error,
|
|
746
|
+
handlerTrailId: consumer.id,
|
|
747
|
+
payload,
|
|
137
748
|
signalId,
|
|
138
749
|
});
|
|
750
|
+
await recordRuntimeSignalDiagnostic(producerCtx, diagnostic);
|
|
751
|
+
await recordSignalLifecycleTrace(
|
|
752
|
+
producerCtx,
|
|
753
|
+
'signal.handler.failed',
|
|
754
|
+
buildSignalTraceAttrs({
|
|
755
|
+
activation: diagnosticMetadata.activation,
|
|
756
|
+
errorName: cause.name,
|
|
757
|
+
handlerTrailId: consumer.id,
|
|
758
|
+
payload: payloadSummary,
|
|
759
|
+
producerTrailId: diagnosticMetadata.producerTrailId,
|
|
760
|
+
runId: diagnosticMetadata.runId,
|
|
761
|
+
signalId,
|
|
762
|
+
}),
|
|
763
|
+
'err',
|
|
764
|
+
deriveSignalErrorCategory(error),
|
|
765
|
+
traceSink
|
|
766
|
+
);
|
|
767
|
+
throw error;
|
|
139
768
|
}
|
|
140
|
-
return consumer.id;
|
|
141
769
|
})
|
|
142
770
|
);
|
|
143
771
|
for (const [index, entry] of settled.entries()) {
|
|
@@ -149,7 +777,7 @@ const fanOutToConsumers = async (
|
|
|
149
777
|
// unexpectedly. Log at debug to preserve provenance without propagating
|
|
150
778
|
// the failure to the producer (fire-and-forget semantics).
|
|
151
779
|
logger?.debug('Signal consumer rejected unexpectedly', {
|
|
152
|
-
consumerId:
|
|
780
|
+
consumerId: activations[index]?.trail.id,
|
|
153
781
|
error:
|
|
154
782
|
entry.reason instanceof Error
|
|
155
783
|
? entry.reason.message
|
|
@@ -159,51 +787,170 @@ const fanOutToConsumers = async (
|
|
|
159
787
|
}
|
|
160
788
|
};
|
|
161
789
|
|
|
162
|
-
const
|
|
163
|
-
|
|
790
|
+
const recordInvalidSignalDiagnostic = async (
|
|
791
|
+
producerCtx: TrailContextInit | undefined,
|
|
792
|
+
diagnostic: SignalInvalidDiagnostic
|
|
793
|
+
): Promise<boolean> =>
|
|
794
|
+
await recordRuntimeSignalDiagnostic(producerCtx, diagnostic);
|
|
795
|
+
|
|
796
|
+
const createInvalidPayloadError = (
|
|
164
797
|
signalId: string,
|
|
798
|
+
message: string,
|
|
799
|
+
diagnostic: SignalInvalidDiagnostic,
|
|
800
|
+
promoted: boolean
|
|
801
|
+
): ValidationError =>
|
|
802
|
+
new ValidationError(`Invalid payload for signal "${signalId}": ${message}`, {
|
|
803
|
+
context: {
|
|
804
|
+
diagnosticCode: diagnostic.code,
|
|
805
|
+
promoted,
|
|
806
|
+
schemaIssues: diagnostic.schemaIssues,
|
|
807
|
+
signalId,
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
const PAYLOAD_SCHEMA_READ_ERROR_MESSAGE =
|
|
812
|
+
'Payload schema validation could not read the payload safely';
|
|
813
|
+
|
|
814
|
+
type SignalPayloadParseResult =
|
|
815
|
+
| {
|
|
816
|
+
readonly data: unknown;
|
|
817
|
+
readonly success: true;
|
|
818
|
+
}
|
|
819
|
+
| {
|
|
820
|
+
readonly issues: readonly z.core.$ZodIssue[];
|
|
821
|
+
readonly message: string;
|
|
822
|
+
readonly success: false;
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const unreadablePayloadIssue = (): z.core.$ZodIssue =>
|
|
826
|
+
({
|
|
827
|
+
code: 'custom',
|
|
828
|
+
message: PAYLOAD_SCHEMA_READ_ERROR_MESSAGE,
|
|
829
|
+
path: [],
|
|
830
|
+
}) as z.core.$ZodIssue;
|
|
831
|
+
|
|
832
|
+
const safeParseSignalPayload = (
|
|
833
|
+
signal: AnySignal,
|
|
165
834
|
payload: unknown
|
|
166
|
-
):
|
|
167
|
-
{
|
|
168
|
-
|
|
835
|
+
): SignalPayloadParseResult => {
|
|
836
|
+
try {
|
|
837
|
+
const parsed = signal.payload.safeParse(payload);
|
|
838
|
+
if (parsed.success) {
|
|
839
|
+
return { data: parsed.data, success: true };
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
issues: parsed.error.issues,
|
|
843
|
+
message: parsed.error.message,
|
|
844
|
+
success: false,
|
|
845
|
+
};
|
|
846
|
+
} catch {
|
|
847
|
+
return {
|
|
848
|
+
issues: [unreadablePayloadIssue()],
|
|
849
|
+
message: PAYLOAD_SCHEMA_READ_ERROR_MESSAGE,
|
|
850
|
+
success: false,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
const resolveFireDispatch = async (
|
|
856
|
+
topo: Topo,
|
|
857
|
+
signalId: string,
|
|
858
|
+
payload: unknown,
|
|
859
|
+
producerCtx: TrailContextInit | undefined,
|
|
860
|
+
diagnosticMetadata: FireDiagnosticMetadata,
|
|
861
|
+
traceSink: TraceSink
|
|
862
|
+
): Promise<
|
|
863
|
+
Result<
|
|
864
|
+
{
|
|
865
|
+
readonly activations: readonly ConsumerActivation[];
|
|
866
|
+
readonly consumers: readonly AnyTrail[];
|
|
867
|
+
readonly payload: unknown;
|
|
868
|
+
},
|
|
869
|
+
Error
|
|
870
|
+
>
|
|
169
871
|
> => {
|
|
170
872
|
const signal = topo.signals.get(signalId);
|
|
171
873
|
if (signal === undefined) {
|
|
874
|
+
await recordRuntimeSignalDiagnostic(
|
|
875
|
+
producerCtx,
|
|
876
|
+
createSignalUnknownDiagnostic({
|
|
877
|
+
...diagnosticMetadata,
|
|
878
|
+
signalId,
|
|
879
|
+
})
|
|
880
|
+
);
|
|
172
881
|
return Result.err(
|
|
173
882
|
new NotFoundError(`Signal "${signalId}" not found in topo "${topo.name}"`)
|
|
174
883
|
);
|
|
175
884
|
}
|
|
176
|
-
const parsed = signal
|
|
885
|
+
const parsed = safeParseSignalPayload(signal, payload);
|
|
177
886
|
if (!parsed.success) {
|
|
887
|
+
const diagnostic = createSignalInvalidDiagnostic({
|
|
888
|
+
...diagnosticMetadata,
|
|
889
|
+
payload,
|
|
890
|
+
schemaIssues: parsed.issues,
|
|
891
|
+
signalId,
|
|
892
|
+
});
|
|
893
|
+
const promoted = await recordInvalidSignalDiagnostic(
|
|
894
|
+
producerCtx,
|
|
895
|
+
diagnostic
|
|
896
|
+
);
|
|
897
|
+
await recordSignalLifecycleTrace(
|
|
898
|
+
producerCtx,
|
|
899
|
+
'signal.invalid',
|
|
900
|
+
buildSignalTraceAttrs({
|
|
901
|
+
activation: diagnostic.activation,
|
|
902
|
+
payload: diagnostic.payload,
|
|
903
|
+
producerTrailId: diagnostic.producerTrailId,
|
|
904
|
+
runId: diagnostic.runId,
|
|
905
|
+
schemaIssues: diagnostic.schemaIssues,
|
|
906
|
+
signalId,
|
|
907
|
+
}),
|
|
908
|
+
'err',
|
|
909
|
+
'validation',
|
|
910
|
+
traceSink
|
|
911
|
+
);
|
|
178
912
|
return Result.err(
|
|
179
|
-
|
|
180
|
-
`Invalid payload for signal "${signalId}": ${parsed.error.message}`
|
|
181
|
-
)
|
|
913
|
+
createInvalidPayloadError(signalId, parsed.message, diagnostic, promoted)
|
|
182
914
|
);
|
|
183
915
|
}
|
|
916
|
+
const activations = listConsumerActivations(topo, signalId);
|
|
184
917
|
return Result.ok({
|
|
185
|
-
|
|
918
|
+
activations,
|
|
919
|
+
consumers: consumerTrailsFromActivations(activations),
|
|
186
920
|
payload: parsed.data,
|
|
187
921
|
});
|
|
188
922
|
};
|
|
189
923
|
|
|
190
924
|
const resolveSignalId = (signalOrId: unknown): Result<string, Error> => {
|
|
191
|
-
if (typeof signalOrId === 'string') {
|
|
192
|
-
return Result.ok(signalOrId);
|
|
193
|
-
}
|
|
194
925
|
if (
|
|
195
926
|
typeof signalOrId === 'object' &&
|
|
196
927
|
signalOrId !== null &&
|
|
928
|
+
'kind' in signalOrId &&
|
|
929
|
+
(signalOrId as { kind: unknown }).kind === 'signal' &&
|
|
197
930
|
'id' in signalOrId &&
|
|
198
931
|
typeof (signalOrId as { id: unknown }).id === 'string'
|
|
199
932
|
) {
|
|
200
|
-
return Result.ok((signalOrId as
|
|
933
|
+
return Result.ok((signalOrId as AnySignal).id);
|
|
201
934
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
935
|
+
if (typeof signalOrId === 'string') {
|
|
936
|
+
return Result.err(
|
|
937
|
+
new ValidationError(
|
|
938
|
+
'ctx.fire() requires a Signal value; string signal ids are not part of the public fire API'
|
|
939
|
+
)
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
return Result.err(new ValidationError('ctx.fire() requires a Signal value'));
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
const logFireError = (
|
|
946
|
+
logger: Logger | undefined,
|
|
947
|
+
signalId: string | undefined,
|
|
948
|
+
error: Error
|
|
949
|
+
): void => {
|
|
950
|
+
logger?.warn('Signal fire skipped', {
|
|
951
|
+
error: error.message,
|
|
952
|
+
signalId,
|
|
953
|
+
});
|
|
207
954
|
};
|
|
208
955
|
|
|
209
956
|
/**
|
|
@@ -218,9 +965,15 @@ const resolveSignalId = (signalOrId: unknown): Result<string, Error> => {
|
|
|
218
965
|
export const createFireFn = (
|
|
219
966
|
topo: Topo,
|
|
220
967
|
producerCtx: TrailContextInit | undefined,
|
|
221
|
-
executor: ConsumerExecutor
|
|
968
|
+
executor: ConsumerExecutor,
|
|
969
|
+
producerTrailId?: string | undefined
|
|
222
970
|
): FireFn => {
|
|
223
|
-
const
|
|
971
|
+
const trackedProducerCtx =
|
|
972
|
+
producerCtx === undefined
|
|
973
|
+
? undefined
|
|
974
|
+
: withFireDispatchTracking(producerCtx);
|
|
975
|
+
const traceSink = topo.observe?.trace ?? getTraceSink();
|
|
976
|
+
const bindConsumerFire: ConsumerFireBinder = (consumerCtx, consumerId) => ({
|
|
224
977
|
...consumerCtx,
|
|
225
978
|
// Pre-bind fire on the consumer ctx as a safety net for direct
|
|
226
979
|
// executeTrail calls that skip the topo-aware path. In the normal
|
|
@@ -228,50 +981,138 @@ export const createFireFn = (
|
|
|
228
981
|
// the fully-traced ctx before the blaze runs, so this assignment
|
|
229
982
|
// is superseded — but keeping it makes consumerCtx self-sufficient
|
|
230
983
|
// for any caller that inspects it pre-execution.
|
|
231
|
-
fire: createFireFn(
|
|
984
|
+
fire: createFireFn(
|
|
985
|
+
topo,
|
|
986
|
+
consumerCtx as TrailContextInit,
|
|
987
|
+
executor,
|
|
988
|
+
consumerId
|
|
989
|
+
),
|
|
232
990
|
});
|
|
233
991
|
|
|
234
992
|
const dispatchFire = async (
|
|
235
993
|
signalId: string,
|
|
236
|
-
payload: unknown
|
|
994
|
+
payload: unknown,
|
|
995
|
+
diagnosticMetadata: FireDiagnosticMetadata
|
|
237
996
|
): Promise<Result<void, Error>> => {
|
|
238
|
-
const dispatch = resolveFireDispatch(
|
|
997
|
+
const dispatch = await resolveFireDispatch(
|
|
998
|
+
topo,
|
|
999
|
+
signalId,
|
|
1000
|
+
payload,
|
|
1001
|
+
trackedProducerCtx,
|
|
1002
|
+
diagnosticMetadata,
|
|
1003
|
+
traceSink
|
|
1004
|
+
);
|
|
239
1005
|
if (dispatch.isErr()) {
|
|
240
1006
|
return Result.err(dispatch.error);
|
|
241
1007
|
}
|
|
242
|
-
await
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
1008
|
+
await recordSignalLifecycleTrace(
|
|
1009
|
+
trackedProducerCtx,
|
|
1010
|
+
'signal.fired',
|
|
1011
|
+
buildSignalTraceAttrs({
|
|
1012
|
+
activation: diagnosticMetadata.activation,
|
|
1013
|
+
consumerIds: dispatch.value.consumers.map((consumer) => consumer.id),
|
|
1014
|
+
payload: summarizeSignalPayloadForTrace(
|
|
1015
|
+
trackedProducerCtx,
|
|
1016
|
+
dispatch.value.payload,
|
|
1017
|
+
traceSink
|
|
1018
|
+
),
|
|
1019
|
+
producerTrailId: diagnosticMetadata.producerTrailId,
|
|
1020
|
+
runId: diagnosticMetadata.runId,
|
|
1021
|
+
signalId,
|
|
1022
|
+
}),
|
|
1023
|
+
undefined,
|
|
1024
|
+
undefined,
|
|
1025
|
+
traceSink
|
|
250
1026
|
);
|
|
1027
|
+
const completion = (async (): Promise<void> => {
|
|
1028
|
+
try {
|
|
1029
|
+
await fanOutToConsumers(
|
|
1030
|
+
dispatch.value.activations,
|
|
1031
|
+
dispatch.value.payload,
|
|
1032
|
+
signalId,
|
|
1033
|
+
trackedProducerCtx,
|
|
1034
|
+
diagnosticMetadata,
|
|
1035
|
+
bindConsumerFire,
|
|
1036
|
+
executor,
|
|
1037
|
+
trackedProducerCtx?.logger,
|
|
1038
|
+
traceSink
|
|
1039
|
+
);
|
|
1040
|
+
} catch (error: unknown) {
|
|
1041
|
+
trackedProducerCtx?.logger?.debug(
|
|
1042
|
+
'Signal dispatch completion failed unexpectedly',
|
|
1043
|
+
{
|
|
1044
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1045
|
+
signalId,
|
|
1046
|
+
}
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
})();
|
|
1050
|
+
trackFireDispatch(trackedProducerCtx, completion);
|
|
251
1051
|
return Result.ok();
|
|
252
1052
|
};
|
|
253
1053
|
|
|
254
1054
|
/** Return an early Result if the fire should be suppressed, or null to proceed. */
|
|
255
|
-
const guardFire = (
|
|
1055
|
+
const guardFire = async (
|
|
256
1056
|
signalId: string,
|
|
257
|
-
stack: readonly string[]
|
|
258
|
-
|
|
1057
|
+
stack: readonly string[],
|
|
1058
|
+
diagnosticMetadata: FireDiagnosticMetadata
|
|
1059
|
+
): Promise<Result<void, Error> | null> => {
|
|
259
1060
|
if (stack.length >= MAX_FIRE_DEPTH) {
|
|
260
|
-
|
|
1061
|
+
trackedProducerCtx?.logger?.warn(
|
|
261
1062
|
'Signal fan-out depth limit reached — skipping fire',
|
|
262
1063
|
{ depth: stack.length, signalId }
|
|
263
1064
|
);
|
|
1065
|
+
await recordRuntimeSignalDiagnostic(
|
|
1066
|
+
trackedProducerCtx,
|
|
1067
|
+
createSignalFireSuppressedDiagnostic({
|
|
1068
|
+
...diagnosticMetadata,
|
|
1069
|
+
fireStack: [...stack],
|
|
1070
|
+
limit: MAX_FIRE_DEPTH,
|
|
1071
|
+
reason: 'depth',
|
|
1072
|
+
signalId,
|
|
1073
|
+
})
|
|
1074
|
+
);
|
|
1075
|
+
await recordActivationGuardTrace(
|
|
1076
|
+
trackedProducerCtx,
|
|
1077
|
+
diagnosticMetadata,
|
|
1078
|
+
'depth',
|
|
1079
|
+
signalId,
|
|
1080
|
+
stack,
|
|
1081
|
+
MAX_FIRE_DEPTH,
|
|
1082
|
+
traceSink
|
|
1083
|
+
);
|
|
264
1084
|
return Result.ok();
|
|
265
1085
|
}
|
|
266
1086
|
if (stack.includes(signalId)) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
1087
|
+
trackedProducerCtx?.logger?.debug(
|
|
1088
|
+
'Signal fan-out suppressed due to cycle',
|
|
1089
|
+
{
|
|
1090
|
+
fireStack: [...stack],
|
|
1091
|
+
signalId,
|
|
1092
|
+
}
|
|
1093
|
+
);
|
|
1094
|
+
trackedProducerCtx?.logger?.warn(
|
|
272
1095
|
'Signal cycle detected — skipping re-entrant fire',
|
|
273
1096
|
{ fireStack: [...stack], signalId }
|
|
274
1097
|
);
|
|
1098
|
+
await recordRuntimeSignalDiagnostic(
|
|
1099
|
+
trackedProducerCtx,
|
|
1100
|
+
createSignalFireSuppressedDiagnostic({
|
|
1101
|
+
...diagnosticMetadata,
|
|
1102
|
+
fireStack: [...stack],
|
|
1103
|
+
reason: 'cycle',
|
|
1104
|
+
signalId,
|
|
1105
|
+
})
|
|
1106
|
+
);
|
|
1107
|
+
await recordActivationGuardTrace(
|
|
1108
|
+
trackedProducerCtx,
|
|
1109
|
+
diagnosticMetadata,
|
|
1110
|
+
'cycle',
|
|
1111
|
+
signalId,
|
|
1112
|
+
stack,
|
|
1113
|
+
undefined,
|
|
1114
|
+
traceSink
|
|
1115
|
+
);
|
|
275
1116
|
return Result.ok();
|
|
276
1117
|
}
|
|
277
1118
|
return null;
|
|
@@ -280,16 +1121,49 @@ export const createFireFn = (
|
|
|
280
1121
|
const fireImpl: FireFn = async (
|
|
281
1122
|
signalOrId: unknown,
|
|
282
1123
|
payload: unknown
|
|
283
|
-
): Promise<
|
|
1124
|
+
): Promise<void> => {
|
|
284
1125
|
const resolved = resolveSignalId(signalOrId);
|
|
285
1126
|
if (resolved.isErr()) {
|
|
286
|
-
|
|
1127
|
+
logFireError(
|
|
1128
|
+
trackedProducerCtx?.logger,
|
|
1129
|
+
typeof signalOrId === 'string' ? signalOrId : undefined,
|
|
1130
|
+
resolved.error
|
|
1131
|
+
);
|
|
1132
|
+
return;
|
|
287
1133
|
}
|
|
288
|
-
const
|
|
1134
|
+
const diagnosticMetadata = deriveFireDiagnosticMetadata(
|
|
1135
|
+
trackedProducerCtx,
|
|
1136
|
+
producerTrailId,
|
|
1137
|
+
resolved.value
|
|
1138
|
+
);
|
|
1139
|
+
const suppressed = await guardFire(
|
|
1140
|
+
resolved.value,
|
|
1141
|
+
getFireStack(trackedProducerCtx),
|
|
1142
|
+
diagnosticMetadata
|
|
1143
|
+
);
|
|
289
1144
|
if (suppressed) {
|
|
290
|
-
|
|
1145
|
+
if (suppressed.isErr()) {
|
|
1146
|
+
logFireError(
|
|
1147
|
+
trackedProducerCtx?.logger,
|
|
1148
|
+
resolved.value,
|
|
1149
|
+
suppressed.error
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const dispatched = await dispatchFire(
|
|
1155
|
+
resolved.value,
|
|
1156
|
+
payload,
|
|
1157
|
+
diagnosticMetadata
|
|
1158
|
+
);
|
|
1159
|
+
if (dispatched.isErr()) {
|
|
1160
|
+
logFireError(
|
|
1161
|
+
trackedProducerCtx?.logger,
|
|
1162
|
+
resolved.value,
|
|
1163
|
+
dispatched.error
|
|
1164
|
+
);
|
|
291
1165
|
}
|
|
292
|
-
return await dispatchFire(resolved.value, payload);
|
|
293
1166
|
};
|
|
1167
|
+
frameworkFireFns.add(fireImpl);
|
|
294
1168
|
return fireImpl;
|
|
295
1169
|
};
|