@ontrails/core 1.0.0-beta.39 → 1.0.0-beta.41
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 +58 -0
- package/README.md +10 -10
- package/package.json +1 -1
- package/src/activation-provenance.ts +5 -0
- package/src/activation-source-projection.ts +6 -0
- package/src/activation-source.ts +2 -0
- package/src/compose-batch.ts +1 -1
- package/src/draft.ts +21 -21
- package/src/{contour.ts → entity.ts} +72 -72
- package/src/execute.ts +15 -15
- package/src/fire.ts +5 -2
- package/src/index.ts +19 -17
- package/src/layer.ts +1 -1
- package/src/observe.ts +1 -1
- package/src/queue.ts +163 -0
- package/src/signal-ref.ts +30 -6
- package/src/store/accessor-protocol.ts +4 -4
- package/src/surface-overlay.ts +1 -1
- package/src/topo.ts +47 -47
- package/src/tracing.ts +1 -0
- package/src/trail.ts +92 -87
- package/src/trails/derive-trail.ts +202 -209
- package/src/trails/ingest.ts +18 -5
- package/src/trails-db.ts +1 -1
- package/src/type-utils.ts +1 -1
- package/src/types.ts +3 -3
- package/src/validate-established-topo.ts +7 -3
- package/src/validate-topo.ts +31 -15
package/src/queue.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActivationSource,
|
|
3
|
+
ActivationSourceMeta,
|
|
4
|
+
ActivationSourceParse,
|
|
5
|
+
} from './activation-source.js';
|
|
6
|
+
import { ValidationError } from './errors.js';
|
|
7
|
+
|
|
8
|
+
export interface QueueSpec<TOutput = unknown> {
|
|
9
|
+
readonly meta?: ActivationSourceMeta | undefined;
|
|
10
|
+
readonly parse: ActivationSourceParse<TOutput>;
|
|
11
|
+
readonly payload?: ActivationSource['payload'] | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Runtime queue name. Defaults to the source id, so authored queue
|
|
14
|
+
* contracts can stay stable while host bindings choose their own names.
|
|
15
|
+
*/
|
|
16
|
+
readonly queue?: string | undefined;
|
|
17
|
+
/** Reserved for future queue-specific design; trail versioning is trail-only. */
|
|
18
|
+
readonly version?: never;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface QueueSource<TOutput = unknown> extends ActivationSource {
|
|
22
|
+
readonly kind: 'queue';
|
|
23
|
+
readonly meta?: ActivationSourceMeta | undefined;
|
|
24
|
+
readonly parse: ActivationSourceParse<TOutput>;
|
|
25
|
+
readonly payload?: ActivationSource['payload'] | undefined;
|
|
26
|
+
readonly queue: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface QueueValidationIssue {
|
|
30
|
+
readonly field: 'parse' | 'queue';
|
|
31
|
+
readonly message: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const normalizeQueueName = (queueName: string): string => queueName.trim();
|
|
35
|
+
|
|
36
|
+
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
|
37
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
38
|
+
|
|
39
|
+
const isZodSchema = (value: unknown): boolean =>
|
|
40
|
+
isObjectRecord(value) && typeof value['safeParse'] === 'function';
|
|
41
|
+
|
|
42
|
+
const validateQueueName = (queueName: unknown): QueueValidationIssue[] =>
|
|
43
|
+
typeof queueName === 'string' && queueName.trim().length > 0
|
|
44
|
+
? []
|
|
45
|
+
: [
|
|
46
|
+
{
|
|
47
|
+
field: 'queue',
|
|
48
|
+
message: 'Queue source must define a non-empty queue name',
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const validateRequiredParse = (parse: unknown): QueueValidationIssue[] =>
|
|
53
|
+
parse === undefined
|
|
54
|
+
? [
|
|
55
|
+
{
|
|
56
|
+
field: 'parse',
|
|
57
|
+
message: 'Queue sources must define parse',
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
: [];
|
|
61
|
+
|
|
62
|
+
const validateParseShape = (parse: unknown): QueueValidationIssue[] => {
|
|
63
|
+
if (parse === undefined || isZodSchema(parse)) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
if (isObjectRecord(parse) && isZodSchema(parse['output'])) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
field: 'parse',
|
|
72
|
+
message: 'Queue parse must be a Zod schema or define parse.output',
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const queueIssuesMessage = (
|
|
78
|
+
id: string,
|
|
79
|
+
issues: readonly QueueValidationIssue[]
|
|
80
|
+
): string =>
|
|
81
|
+
`queue("${id}") is invalid: ${issues.map((issue) => `${issue.field}: ${issue.message}`).join('; ')}`;
|
|
82
|
+
|
|
83
|
+
const assertQueueSpec = <TOutput>(
|
|
84
|
+
id: string,
|
|
85
|
+
spec: QueueSpec<TOutput>
|
|
86
|
+
): {
|
|
87
|
+
readonly queue: string;
|
|
88
|
+
} => {
|
|
89
|
+
const queueName = spec.queue === undefined ? id : spec.queue;
|
|
90
|
+
const issues = [
|
|
91
|
+
...validateQueueName(queueName),
|
|
92
|
+
...validateRequiredParse(spec.parse),
|
|
93
|
+
...validateParseShape(spec.parse),
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
if (issues.length > 0) {
|
|
97
|
+
throw new ValidationError(queueIssuesMessage(id, issues), {
|
|
98
|
+
context: { issues },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { queue: normalizeQueueName(queueName) };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const validateQueueSource = (
|
|
106
|
+
source: ActivationSource
|
|
107
|
+
): readonly QueueValidationIssue[] => {
|
|
108
|
+
if (source.kind !== 'queue') {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return [
|
|
113
|
+
...validateQueueName(source.queue),
|
|
114
|
+
...validateRequiredParse(source.parse),
|
|
115
|
+
...validateParseShape(source.parse),
|
|
116
|
+
];
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Define a queue activation source.
|
|
121
|
+
*
|
|
122
|
+
* Queue sources are inert contract data: they describe which runtime queue
|
|
123
|
+
* wakes a trail and how the message body becomes trail input. A host adapter
|
|
124
|
+
* such as `@ontrails/cloudflare/workers` owns delivery.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* import { queue } from '@ontrails/core';
|
|
129
|
+
* import { z } from 'zod';
|
|
130
|
+
*
|
|
131
|
+
* const source = queue('queue.email.outbox', {
|
|
132
|
+
* queue: 'email-outbox',
|
|
133
|
+
* parse: z.object({ messageId: z.string() }),
|
|
134
|
+
* });
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export function queue<TOutput>(
|
|
138
|
+
id: string,
|
|
139
|
+
spec: QueueSpec<TOutput>
|
|
140
|
+
): QueueSource<TOutput>;
|
|
141
|
+
export function queue<TOutput>(
|
|
142
|
+
spec: QueueSpec<TOutput> & { readonly id: string }
|
|
143
|
+
): QueueSource<TOutput>;
|
|
144
|
+
export function queue<TOutput>(
|
|
145
|
+
idOrSpec: string | (QueueSpec<TOutput> & { readonly id: string }),
|
|
146
|
+
maybeSpec?: QueueSpec<TOutput>
|
|
147
|
+
): QueueSource<TOutput> {
|
|
148
|
+
const id = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
|
|
149
|
+
// oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
|
|
150
|
+
const spec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
|
|
151
|
+
const normalized = assertQueueSpec(id, spec);
|
|
152
|
+
|
|
153
|
+
return Object.freeze({
|
|
154
|
+
id,
|
|
155
|
+
kind: 'queue' as const,
|
|
156
|
+
parse: spec.parse,
|
|
157
|
+
queue: normalized.queue,
|
|
158
|
+
...(spec.meta === undefined
|
|
159
|
+
? {}
|
|
160
|
+
: { meta: Object.freeze({ ...spec.meta }) }),
|
|
161
|
+
...(spec.payload === undefined ? {} : { payload: spec.payload }),
|
|
162
|
+
});
|
|
163
|
+
}
|
package/src/signal-ref.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface LateBoundSignalMarker {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const LATE_BOUND_SIGNAL_REF = Symbol('trails.late-bound-signal-ref');
|
|
14
|
+
const LATE_BOUND_SIGNAL_BOUND = Symbol('trails.late-bound-signal-bound');
|
|
14
15
|
const LATE_BOUND_SIGNAL_MARKER_PREFIX = '@@trails:late-bound-signal-ref:';
|
|
15
16
|
|
|
16
17
|
const defineLateBoundSignalRef = <T extends object>(
|
|
@@ -26,6 +27,22 @@ const defineLateBoundSignalRef = <T extends object>(
|
|
|
26
27
|
return value;
|
|
27
28
|
};
|
|
28
29
|
|
|
30
|
+
const defineLateBoundSignalBound = <T extends object>(value: T): T => {
|
|
31
|
+
Object.defineProperty(value, LATE_BOUND_SIGNAL_BOUND, {
|
|
32
|
+
configurable: false,
|
|
33
|
+
enumerable: false,
|
|
34
|
+
value: true,
|
|
35
|
+
writable: false,
|
|
36
|
+
});
|
|
37
|
+
return value;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const isBoundLateBoundSignal = (
|
|
41
|
+
signal: Pick<AnySignal, 'id'> | undefined
|
|
42
|
+
): boolean =>
|
|
43
|
+
signal !== undefined &&
|
|
44
|
+
(signal as Record<PropertyKey, unknown>)[LATE_BOUND_SIGNAL_BOUND] === true;
|
|
45
|
+
|
|
29
46
|
export const getLateBoundSignalRef = (
|
|
30
47
|
signal: Pick<AnySignal, 'id'> | undefined
|
|
31
48
|
): LateBoundSignalRef | undefined =>
|
|
@@ -58,14 +75,17 @@ export const cloneSignalWithId = <T>(
|
|
|
58
75
|
};
|
|
59
76
|
const ref = getLateBoundSignalRef(signal);
|
|
60
77
|
return Object.freeze(
|
|
61
|
-
ref
|
|
78
|
+
ref
|
|
79
|
+
? defineLateBoundSignalBound(defineLateBoundSignalRef(clone, ref))
|
|
80
|
+
: clone
|
|
62
81
|
) as Signal<T>;
|
|
63
82
|
};
|
|
64
83
|
|
|
65
84
|
export const createLateBoundSignalMarker = (
|
|
66
85
|
ref: LateBoundSignalRef,
|
|
67
86
|
displayId: string
|
|
68
|
-
): string =>
|
|
87
|
+
): string =>
|
|
88
|
+
`${LATE_BOUND_SIGNAL_MARKER_PREFIX}${encodeURIComponent(ref.token)}:${displayId}`;
|
|
69
89
|
|
|
70
90
|
export const parseLateBoundSignalMarker = (
|
|
71
91
|
value: string
|
|
@@ -80,8 +100,12 @@ export const parseLateBoundSignalMarker = (
|
|
|
80
100
|
return null;
|
|
81
101
|
}
|
|
82
102
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
token
|
|
86
|
-
|
|
103
|
+
try {
|
|
104
|
+
const token = decodeURIComponent(remainder.slice(0, separator));
|
|
105
|
+
return token.length === 0
|
|
106
|
+
? null
|
|
107
|
+
: { displayId: remainder.slice(separator + 1), token };
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
87
111
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Structural accessor protocol used by `deriveTrail()` to synthesize default
|
|
3
|
-
*
|
|
3
|
+
* implementations for standard CRUD operations without depending on `@ontrails/store`.
|
|
4
4
|
*
|
|
5
5
|
* @remarks
|
|
6
6
|
* This type is intentionally minimal and structural. `@ontrails/store`'s
|
|
@@ -35,21 +35,21 @@ export interface StoreAccessorProtocol<
|
|
|
35
35
|
remove(id: TId): Promise<{ readonly deleted: boolean }>;
|
|
36
36
|
/**
|
|
37
37
|
* Optional insert — available on tabular adapters that distinguish
|
|
38
|
-
* create from update. When absent, synthesized
|
|
38
|
+
* create from update. When absent, synthesized implementations fall back to
|
|
39
39
|
* `upsert`.
|
|
40
40
|
*/
|
|
41
41
|
insert?(input: TInput): Promise<TEntity>;
|
|
42
42
|
/**
|
|
43
43
|
* Optional patch-by-identity — available on tabular adapters. Returns
|
|
44
44
|
* `null` when no row with the given identity exists. When absent,
|
|
45
|
-
* synthesized update
|
|
45
|
+
* synthesized update implementations fall back to `get` + merge + `upsert`.
|
|
46
46
|
*/
|
|
47
47
|
update?(id: TId, patch: Partial<TInput>): Promise<TEntity | null>;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Record shape returned by a resource's `from(ctx)` call, keyed by accessor
|
|
52
|
-
* name. Used by `deriveTrail()` to resolve an accessor by
|
|
52
|
+
* name. Used by `deriveTrail()` to resolve an accessor by entity name.
|
|
53
53
|
*/
|
|
54
54
|
export type StoreAccessorRecord = Readonly<
|
|
55
55
|
Record<string, StoreAccessorProtocol<unknown, unknown, unknown, unknown>>
|
package/src/surface-overlay.ts
CHANGED
|
@@ -577,7 +577,7 @@ const assertMcpSynonymBindingName = (name: string): void => {
|
|
|
577
577
|
* Derive the deterministic default description for an MCP grouped entry
|
|
578
578
|
* projected from the `surfaces` overlay.
|
|
579
579
|
*
|
|
580
|
-
* Both the MCP surface (the runtime tool description) and
|
|
580
|
+
* Both the MCP surface (the runtime tool description) and Topography (the
|
|
581
581
|
* lock's trailhead entry description) read this helper, so the authored
|
|
582
582
|
* binding projects one description everywhere.
|
|
583
583
|
*
|
package/src/topo.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Application entry point — scans module exports to build a topology graph.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { AnyEntity } from './entity.js';
|
|
6
6
|
import { ValidationError } from './errors.js';
|
|
7
7
|
import type { ActivationEntry } from './activation-source.js';
|
|
8
8
|
import {
|
|
@@ -39,7 +39,7 @@ export interface Topo {
|
|
|
39
39
|
readonly name: string;
|
|
40
40
|
readonly version?: string;
|
|
41
41
|
readonly description?: string;
|
|
42
|
-
readonly
|
|
42
|
+
readonly entities: ReadonlyMap<string, AnyEntity>;
|
|
43
43
|
readonly trails: ReadonlyMap<string, AnyTrail>;
|
|
44
44
|
readonly signals: ReadonlyMap<string, AnySignal>;
|
|
45
45
|
readonly resources: ReadonlyMap<string, AnyResource>;
|
|
@@ -49,23 +49,23 @@ export interface Topo {
|
|
|
49
49
|
*
|
|
50
50
|
* The CLI/MCP/HTTP surfaces forward these into `executeTrail` as
|
|
51
51
|
* `topoLayers`, where they are composed outermost in the layer chain.
|
|
52
|
-
* The final composition order is `topo → surface → trail →
|
|
52
|
+
* The final composition order is `topo → surface → trail → implementation`
|
|
53
53
|
* (outermost-first).
|
|
54
54
|
*/
|
|
55
55
|
readonly layers: readonly Layer[];
|
|
56
56
|
readonly count: number;
|
|
57
|
-
readonly
|
|
57
|
+
readonly entityCount: number;
|
|
58
58
|
readonly resourceCount: number;
|
|
59
|
-
|
|
59
|
+
getEntity(name: string): AnyEntity | undefined;
|
|
60
60
|
get(id: string): AnyTrail | undefined;
|
|
61
61
|
getResource(id: string): AnyResource | undefined;
|
|
62
|
-
|
|
62
|
+
hasEntity(name: string): boolean;
|
|
63
63
|
has(id: string): boolean;
|
|
64
64
|
hasResource(id: string): boolean;
|
|
65
|
-
|
|
65
|
+
entityIds(): string[];
|
|
66
66
|
ids(): string[];
|
|
67
67
|
resourceIds(): string[];
|
|
68
|
-
|
|
68
|
+
listEntities(): AnyEntity[];
|
|
69
69
|
list(): AnyTrail[];
|
|
70
70
|
listSignals(): AnySignal[];
|
|
71
71
|
listResources(): AnyResource[];
|
|
@@ -75,14 +75,14 @@ export interface Topo {
|
|
|
75
75
|
// Kind discriminant check
|
|
76
76
|
// ---------------------------------------------------------------------------
|
|
77
77
|
|
|
78
|
-
type Registrable =
|
|
78
|
+
type Registrable = AnyEntity | AnyTrail | AnySignal | AnyResource;
|
|
79
79
|
|
|
80
80
|
const isRegistrable = (value: unknown): value is Registrable => {
|
|
81
81
|
if (typeof value !== 'object' || value === null) {
|
|
82
82
|
return false;
|
|
83
83
|
}
|
|
84
84
|
const { kind } = value as Record<string, unknown>;
|
|
85
|
-
return kind === '
|
|
85
|
+
return kind === 'entity' || kind === 'trail' || kind === 'signal';
|
|
86
86
|
};
|
|
87
87
|
|
|
88
88
|
// ---------------------------------------------------------------------------
|
|
@@ -91,24 +91,24 @@ const isRegistrable = (value: unknown): value is Registrable => {
|
|
|
91
91
|
|
|
92
92
|
const createTopo = (
|
|
93
93
|
identity: TopoIdentity,
|
|
94
|
-
|
|
94
|
+
entities: ReadonlyMap<string, AnyEntity>,
|
|
95
95
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
96
96
|
signals: ReadonlyMap<string, AnySignal>,
|
|
97
97
|
resources: ReadonlyMap<string, AnyResource>,
|
|
98
98
|
observe: ObserveConfig | undefined,
|
|
99
99
|
layers: readonly Layer[]
|
|
100
100
|
): Topo => ({
|
|
101
|
-
contourCount: contours.size,
|
|
102
|
-
contourIds(): string[] {
|
|
103
|
-
return [...contours.keys()];
|
|
104
|
-
},
|
|
105
|
-
contours,
|
|
106
101
|
count: trails.size,
|
|
102
|
+
entities,
|
|
103
|
+
entityCount: entities.size,
|
|
104
|
+
entityIds(): string[] {
|
|
105
|
+
return [...entities.keys()];
|
|
106
|
+
},
|
|
107
107
|
get(id: string): AnyTrail | undefined {
|
|
108
108
|
return trails.get(id);
|
|
109
109
|
},
|
|
110
|
-
|
|
111
|
-
return
|
|
110
|
+
getEntity(entityName: string): AnyEntity | undefined {
|
|
111
|
+
return entities.get(entityName);
|
|
112
112
|
},
|
|
113
113
|
getResource(id: string): AnyResource | undefined {
|
|
114
114
|
return resources.get(id);
|
|
@@ -116,8 +116,8 @@ const createTopo = (
|
|
|
116
116
|
has(id: string): boolean {
|
|
117
117
|
return trails.has(id);
|
|
118
118
|
},
|
|
119
|
-
|
|
120
|
-
return
|
|
119
|
+
hasEntity(entityName: string): boolean {
|
|
120
|
+
return entities.has(entityName);
|
|
121
121
|
},
|
|
122
122
|
hasResource(id: string): boolean {
|
|
123
123
|
return resources.has(id);
|
|
@@ -129,8 +129,8 @@ const createTopo = (
|
|
|
129
129
|
list(): AnyTrail[] {
|
|
130
130
|
return [...trails.values()];
|
|
131
131
|
},
|
|
132
|
-
|
|
133
|
-
return [...
|
|
132
|
+
listEntities(): AnyEntity[] {
|
|
133
|
+
return [...entities.values()];
|
|
134
134
|
},
|
|
135
135
|
listResources(): AnyResource[] {
|
|
136
136
|
return [...resources.values()];
|
|
@@ -176,15 +176,15 @@ const registerUnique = <T>(
|
|
|
176
176
|
collection.set(id, value);
|
|
177
177
|
};
|
|
178
178
|
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
const registerEntity = (
|
|
180
|
+
entity: AnyEntity,
|
|
181
|
+
entities: Map<string, AnyEntity>
|
|
182
182
|
): void => {
|
|
183
183
|
registerUnique(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
`Duplicate
|
|
184
|
+
entities,
|
|
185
|
+
entity.name,
|
|
186
|
+
entity,
|
|
187
|
+
`Duplicate entity name: "${entity.name}"`
|
|
188
188
|
);
|
|
189
189
|
};
|
|
190
190
|
|
|
@@ -395,14 +395,14 @@ const finalizeTrailSignals = (
|
|
|
395
395
|
/** Register a single registrable value into the appropriate map. */
|
|
396
396
|
const register = (
|
|
397
397
|
value: Registrable,
|
|
398
|
-
|
|
398
|
+
entities: Map<string, AnyEntity>,
|
|
399
399
|
trails: Map<string, AnyTrail>,
|
|
400
400
|
signals: Map<string, AnySignal>,
|
|
401
401
|
resources: Map<string, AnyResource>
|
|
402
402
|
): void => {
|
|
403
403
|
switch (value.kind) {
|
|
404
|
-
case '
|
|
405
|
-
|
|
404
|
+
case 'entity': {
|
|
405
|
+
registerEntity(value as AnyEntity, entities);
|
|
406
406
|
break;
|
|
407
407
|
}
|
|
408
408
|
case 'resource': {
|
|
@@ -423,15 +423,15 @@ const register = (
|
|
|
423
423
|
}
|
|
424
424
|
};
|
|
425
425
|
|
|
426
|
-
const
|
|
426
|
+
const registerTrailEntities = (
|
|
427
427
|
trail: AnyTrail,
|
|
428
|
-
|
|
428
|
+
entities: Map<string, AnyEntity>,
|
|
429
429
|
trails: Map<string, AnyTrail>,
|
|
430
430
|
signals: Map<string, AnySignal>,
|
|
431
431
|
resources: Map<string, AnyResource>
|
|
432
432
|
): void => {
|
|
433
|
-
for (const
|
|
434
|
-
register(
|
|
433
|
+
for (const entity of trail.entities ?? []) {
|
|
434
|
+
register(entity, entities, trails, signals, resources);
|
|
435
435
|
}
|
|
436
436
|
};
|
|
437
437
|
|
|
@@ -451,13 +451,13 @@ const markUniqueObject = (
|
|
|
451
451
|
|
|
452
452
|
const registerModuleValue = (
|
|
453
453
|
value: unknown,
|
|
454
|
-
|
|
454
|
+
entities: Map<string, AnyEntity>,
|
|
455
455
|
trails: Map<string, AnyTrail>,
|
|
456
456
|
signals: Map<string, AnySignal>,
|
|
457
457
|
resources: Map<string, AnyResource>
|
|
458
458
|
): void => {
|
|
459
459
|
if (isResource(value) || isRegistrable(value)) {
|
|
460
|
-
register(value,
|
|
460
|
+
register(value, entities, trails, signals, resources);
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
if (isResource(value)) {
|
|
@@ -469,9 +469,9 @@ const registerModuleValue = (
|
|
|
469
469
|
value !== null &&
|
|
470
470
|
(value as { kind?: unknown }).kind === 'trail'
|
|
471
471
|
) {
|
|
472
|
-
|
|
472
|
+
registerTrailEntities(
|
|
473
473
|
value as AnyTrail,
|
|
474
|
-
|
|
474
|
+
entities,
|
|
475
475
|
trails,
|
|
476
476
|
signals,
|
|
477
477
|
resources
|
|
@@ -481,7 +481,7 @@ const registerModuleValue = (
|
|
|
481
481
|
|
|
482
482
|
const registerModuleValues = (
|
|
483
483
|
mod: Record<string, unknown>,
|
|
484
|
-
|
|
484
|
+
entities: Map<string, AnyEntity>,
|
|
485
485
|
trails: Map<string, AnyTrail>,
|
|
486
486
|
signals: Map<string, AnySignal>,
|
|
487
487
|
resources: Map<string, AnyResource>
|
|
@@ -491,7 +491,7 @@ const registerModuleValues = (
|
|
|
491
491
|
if (!markUniqueObject(value, seenValues)) {
|
|
492
492
|
continue;
|
|
493
493
|
}
|
|
494
|
-
registerModuleValue(value,
|
|
494
|
+
registerModuleValue(value, entities, trails, signals, resources);
|
|
495
495
|
}
|
|
496
496
|
};
|
|
497
497
|
|
|
@@ -535,7 +535,7 @@ const hasRegistrableKind = (value: unknown): boolean => {
|
|
|
535
535
|
}
|
|
536
536
|
const { kind } = value as { kind?: unknown };
|
|
537
537
|
return (
|
|
538
|
-
kind === '
|
|
538
|
+
kind === 'entity' ||
|
|
539
539
|
kind === 'trail' ||
|
|
540
540
|
kind === 'signal' ||
|
|
541
541
|
kind === 'resource'
|
|
@@ -583,7 +583,7 @@ const disambiguateBrandedObserve = (options: TopoOptions): TopoOptions => {
|
|
|
583
583
|
* 2. The shape does not look like `TopoOptions` (mixed keys or no
|
|
584
584
|
* keys) → module.
|
|
585
585
|
* 3. The trailing arg is a registrable module export
|
|
586
|
-
* (`kind: 'trail' | '
|
|
586
|
+
* (`kind: 'trail' | 'entity' | …`) under a known option key →
|
|
587
587
|
* module. Preserves the "module exporting a single trail named
|
|
588
588
|
* `observe`" case that the warden and existing apps rely on.
|
|
589
589
|
* 4. The `observe` value is a bare `LogSink` or `TraceSink` (a sink
|
|
@@ -810,18 +810,18 @@ const topoImpl = (
|
|
|
810
810
|
const observe = normalizeObserve(options?.observe);
|
|
811
811
|
const layers = Object.freeze([...(options?.layers ?? [])]);
|
|
812
812
|
|
|
813
|
-
const
|
|
813
|
+
const entities = new Map<string, AnyEntity>();
|
|
814
814
|
const trails = new Map<string, AnyTrail>();
|
|
815
815
|
const signals = new Map<string, AnySignal>();
|
|
816
816
|
const resources = new Map<string, AnyResource>();
|
|
817
817
|
|
|
818
818
|
for (const mod of modules) {
|
|
819
|
-
registerModuleValues(mod,
|
|
819
|
+
registerModuleValues(mod, entities, trails, signals, resources);
|
|
820
820
|
}
|
|
821
821
|
|
|
822
822
|
return createTopo(
|
|
823
823
|
identity,
|
|
824
|
-
|
|
824
|
+
entities,
|
|
825
825
|
finalizeTrailSignals(trails, resources),
|
|
826
826
|
signals,
|
|
827
827
|
resources,
|
package/src/tracing.ts
CHANGED
|
@@ -28,6 +28,7 @@ export type SignalTraceRecordName =
|
|
|
28
28
|
/** Activation boundary records emitted by runtime materializers. */
|
|
29
29
|
export type ActivationTraceRecordName =
|
|
30
30
|
| 'activation.cycle_detected'
|
|
31
|
+
| 'activation.queue'
|
|
31
32
|
| 'activation.scheduled'
|
|
32
33
|
| 'activation.webhook'
|
|
33
34
|
| 'activation.webhook.invalid';
|