@ontrails/core 1.0.0-beta.0 → 1.0.0-beta.10
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 +109 -0
- package/README.md +101 -131
- package/dist/derive.d.ts +1 -1
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +4 -1
- package/dist/derive.js.map +1 -1
- package/dist/dispatch.d.ts +27 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +34 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/event.d.ts +2 -2
- package/dist/event.d.ts.map +1 -1
- package/dist/event.js +1 -1
- package/dist/event.js.map +1 -1
- package/dist/execute.d.ts +30 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +64 -0
- package/dist/execute.js.map +1 -0
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/patterns/status.d.ts +1 -1
- package/dist/result.d.ts +11 -11
- package/dist/result.d.ts.map +1 -1
- package/dist/result.js +15 -4
- package/dist/result.js.map +1 -1
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +45 -7
- package/dist/serialization.js.map +1 -1
- package/dist/topo.d.ts +4 -4
- package/dist/topo.d.ts.map +1 -1
- package/dist/topo.js +12 -16
- package/dist/topo.js.map +1 -1
- package/dist/trail.d.ts +16 -10
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +4 -2
- package/dist/trail.js.map +1 -1
- package/dist/type-utils.d.ts +24 -0
- package/dist/type-utils.d.ts.map +1 -0
- package/dist/type-utils.js +12 -0
- package/dist/type-utils.js.map +1 -0
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-topo.d.ts +2 -2
- package/dist/validate-topo.d.ts.map +1 -1
- package/dist/validate-topo.js +59 -9
- package/dist/validate-topo.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/context.test.ts +4 -5
- package/src/__tests__/derive.test.ts +44 -0
- package/src/__tests__/dispatch.test.ts +154 -0
- package/src/__tests__/event.test.ts +5 -5
- package/src/__tests__/execute.test.ts +208 -0
- package/src/__tests__/layer.test.ts +11 -111
- package/src/__tests__/serialization.test.ts +166 -1
- package/src/__tests__/topo.test.ts +101 -79
- package/src/__tests__/trail.test.ts +73 -35
- package/src/__tests__/type-utils.test.ts +90 -0
- package/src/__tests__/validate-topo.test.ts +97 -20
- package/src/derive.ts +12 -2
- package/src/dispatch.ts +54 -0
- package/src/event.ts +3 -3
- package/src/execute.ts +96 -0
- package/src/index.ts +22 -18
- package/src/result.ts +29 -15
- package/src/serialization.ts +56 -11
- package/src/topo.ts +18 -23
- package/src/trail.ts +24 -13
- package/src/type-utils.ts +45 -0
- package/src/types.ts +1 -3
- package/src/validate-topo.ts +70 -10
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/hike.d.ts +0 -36
- package/dist/hike.d.ts.map +0 -1
- package/dist/hike.js +0 -20
- package/dist/hike.js.map +0 -1
- package/src/__tests__/hike.test.ts +0 -117
- package/src/__tests__/job.test.ts +0 -98
- package/src/adapters.ts +0 -68
- package/src/health.ts +0 -23
- package/src/hike.ts +0 -77
- package/src/job.ts +0 -20
|
@@ -17,6 +17,7 @@ const noop = async () => Result.ok();
|
|
|
17
17
|
const mockTrail = (
|
|
18
18
|
id: string,
|
|
19
19
|
overrides?: {
|
|
20
|
+
follow?: readonly string[];
|
|
20
21
|
examples?: readonly {
|
|
21
22
|
name: string;
|
|
22
23
|
input: unknown;
|
|
@@ -26,21 +27,14 @@ const mockTrail = (
|
|
|
26
27
|
output?: z.ZodType;
|
|
27
28
|
}
|
|
28
29
|
) => ({
|
|
30
|
+
follow: Object.freeze([...(overrides?.follow ?? [])]),
|
|
29
31
|
id,
|
|
30
|
-
implementation: noop,
|
|
31
32
|
input: z.object({ name: z.string() }),
|
|
32
33
|
kind: 'trail' as const,
|
|
34
|
+
run: noop,
|
|
33
35
|
...overrides,
|
|
34
36
|
});
|
|
35
37
|
|
|
36
|
-
const mockHike = (id: string, follows: readonly string[]) => ({
|
|
37
|
-
follows,
|
|
38
|
-
id,
|
|
39
|
-
implementation: noop,
|
|
40
|
-
input: z.object({ q: z.string() }),
|
|
41
|
-
kind: 'hike' as const,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
38
|
const mockEvent = (id: string, from?: readonly string[]) => ({
|
|
45
39
|
from,
|
|
46
40
|
id,
|
|
@@ -65,7 +59,9 @@ describe('validateTopo', () => {
|
|
|
65
59
|
test('valid topo passes', () => {
|
|
66
60
|
const app = topo('app', {
|
|
67
61
|
add: mockTrail('entity.add'),
|
|
68
|
-
onboard:
|
|
62
|
+
onboard: mockTrail('entity.onboard', {
|
|
63
|
+
follow: ['entity.add'],
|
|
64
|
+
}),
|
|
69
65
|
updated: mockEvent('entity.updated', ['entity.add']),
|
|
70
66
|
});
|
|
71
67
|
|
|
@@ -73,10 +69,12 @@ describe('validateTopo', () => {
|
|
|
73
69
|
expect(result.isOk()).toBe(true);
|
|
74
70
|
});
|
|
75
71
|
|
|
76
|
-
describe('
|
|
77
|
-
test('
|
|
72
|
+
describe('trail follow', () => {
|
|
73
|
+
test('trail following non-existent trail fails', () => {
|
|
78
74
|
const app = topo('app', {
|
|
79
|
-
onboard:
|
|
75
|
+
onboard: mockTrail('entity.onboard', {
|
|
76
|
+
follow: ['entity.missing'],
|
|
77
|
+
}),
|
|
80
78
|
});
|
|
81
79
|
|
|
82
80
|
const result = validateTopo(app);
|
|
@@ -84,21 +82,62 @@ describe('validateTopo', () => {
|
|
|
84
82
|
|
|
85
83
|
const issues = extractIssues(result);
|
|
86
84
|
expect(issues).toHaveLength(1);
|
|
87
|
-
expect(issues[0]?.rule).toBe('
|
|
85
|
+
expect(issues[0]?.rule).toBe('follow-exists');
|
|
88
86
|
expect(issues[0]?.message).toContain('entity.missing');
|
|
89
87
|
});
|
|
90
88
|
|
|
91
|
-
test('
|
|
89
|
+
test('trail following itself fails', () => {
|
|
92
90
|
const app = topo('app', {
|
|
93
|
-
loop:
|
|
91
|
+
loop: mockTrail('entity.loop', { follow: ['entity.loop'] }),
|
|
94
92
|
});
|
|
95
93
|
|
|
96
94
|
const result = validateTopo(app);
|
|
97
95
|
expect(result.isErr()).toBe(true);
|
|
98
96
|
|
|
99
97
|
const issues = extractIssues(result);
|
|
100
|
-
expect(issues).
|
|
101
|
-
|
|
98
|
+
expect(issues.some((i) => i.rule === 'no-self-follow')).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('two-node cycle (a→b→a) is detected', () => {
|
|
102
|
+
const app = topo('app', {
|
|
103
|
+
a: mockTrail('a', { follow: ['b'] }),
|
|
104
|
+
b: mockTrail('b', { follow: ['a'] }),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const result = validateTopo(app);
|
|
108
|
+
expect(result.isErr()).toBe(true);
|
|
109
|
+
|
|
110
|
+
const issues = extractIssues(result);
|
|
111
|
+
const cycleIssues = issues.filter((i) => i.rule === 'follow-cycle');
|
|
112
|
+
expect(cycleIssues.length).toBeGreaterThanOrEqual(1);
|
|
113
|
+
expect(cycleIssues[0]?.message).toContain('Cycle detected');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('three-node cycle (a→b→c→a) is detected', () => {
|
|
117
|
+
const app = topo('app', {
|
|
118
|
+
a: mockTrail('a', { follow: ['b'] }),
|
|
119
|
+
b: mockTrail('b', { follow: ['c'] }),
|
|
120
|
+
c: mockTrail('c', { follow: ['a'] }),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const result = validateTopo(app);
|
|
124
|
+
expect(result.isErr()).toBe(true);
|
|
125
|
+
|
|
126
|
+
const issues = extractIssues(result);
|
|
127
|
+
const cycleIssues = issues.filter((i) => i.rule === 'follow-cycle');
|
|
128
|
+
expect(cycleIssues.length).toBeGreaterThanOrEqual(1);
|
|
129
|
+
expect(cycleIssues[0]?.message).toContain('Cycle detected');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('valid DAG with shared targets is not flagged', () => {
|
|
133
|
+
const app = topo('app', {
|
|
134
|
+
a: mockTrail('a', { follow: ['c'] }),
|
|
135
|
+
b: mockTrail('b', { follow: ['c'] }),
|
|
136
|
+
c: mockTrail('c'),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const result = validateTopo(app);
|
|
140
|
+
expect(result.isOk()).toBe(true);
|
|
102
141
|
});
|
|
103
142
|
});
|
|
104
143
|
|
|
@@ -140,7 +179,7 @@ describe('validateTopo', () => {
|
|
|
140
179
|
expect(issues[0]?.rule).toBe('output-schema-present');
|
|
141
180
|
});
|
|
142
181
|
|
|
143
|
-
test('
|
|
182
|
+
test('ValidationError example with invalid input is allowed', () => {
|
|
144
183
|
const app = topo('app', {
|
|
145
184
|
show: mockTrail('entity.show', {
|
|
146
185
|
examples: [
|
|
@@ -156,6 +195,44 @@ describe('validateTopo', () => {
|
|
|
156
195
|
const result = validateTopo(app);
|
|
157
196
|
expect(result.isOk()).toBe(true);
|
|
158
197
|
});
|
|
198
|
+
|
|
199
|
+
test('NotFoundError example with invalid input fails', () => {
|
|
200
|
+
const app = topo('app', {
|
|
201
|
+
show: mockTrail('entity.show', {
|
|
202
|
+
examples: [
|
|
203
|
+
{
|
|
204
|
+
error: 'NotFoundError',
|
|
205
|
+
input: { name: 123 },
|
|
206
|
+
name: 'Not found case',
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const result = validateTopo(app);
|
|
213
|
+
expect(result.isErr()).toBe(true);
|
|
214
|
+
|
|
215
|
+
const issues = extractIssues(result);
|
|
216
|
+
expect(issues).toHaveLength(1);
|
|
217
|
+
expect(issues[0]?.rule).toBe('example-input-valid');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('NotFoundError example with valid input passes', () => {
|
|
221
|
+
const app = topo('app', {
|
|
222
|
+
show: mockTrail('entity.show', {
|
|
223
|
+
examples: [
|
|
224
|
+
{
|
|
225
|
+
error: 'NotFoundError',
|
|
226
|
+
input: { name: 'test' },
|
|
227
|
+
name: 'Not found case',
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const result = validateTopo(app);
|
|
234
|
+
expect(result.isOk()).toBe(true);
|
|
235
|
+
});
|
|
159
236
|
});
|
|
160
237
|
|
|
161
238
|
describe('event origins', () => {
|
|
@@ -185,7 +262,7 @@ describe('validateTopo', () => {
|
|
|
185
262
|
|
|
186
263
|
test('collects multiple issues', () => {
|
|
187
264
|
const app = topo('app', {
|
|
188
|
-
broken:
|
|
265
|
+
broken: mockTrail('entity.broken', { follow: ['entity.missing'] }),
|
|
189
266
|
show: mockTrail('entity.show', {
|
|
190
267
|
examples: [{ input: { name: 123 }, name: 'Bad' }],
|
|
191
268
|
}),
|
package/src/derive.ts
CHANGED
|
@@ -14,7 +14,14 @@ import type { z } from 'zod';
|
|
|
14
14
|
/** A surface-agnostic field descriptor derived from a Zod schema. */
|
|
15
15
|
export interface Field {
|
|
16
16
|
readonly name: string;
|
|
17
|
-
readonly type:
|
|
17
|
+
readonly type:
|
|
18
|
+
| 'string'
|
|
19
|
+
| 'number'
|
|
20
|
+
| 'boolean'
|
|
21
|
+
| 'enum'
|
|
22
|
+
| 'multiselect'
|
|
23
|
+
| 'string[]'
|
|
24
|
+
| 'number[]';
|
|
18
25
|
readonly label: string;
|
|
19
26
|
readonly required: boolean;
|
|
20
27
|
readonly default?: unknown | undefined;
|
|
@@ -141,7 +148,10 @@ const fieldTypeByDef: Record<string, (s: ZodInternals) => DerivedFieldType> = {
|
|
|
141
148
|
const entries = element._zod.def['entries'] as Record<string, string>;
|
|
142
149
|
return { options: Object.values(entries), type: 'multiselect' };
|
|
143
150
|
}
|
|
144
|
-
return {
|
|
151
|
+
return {
|
|
152
|
+
options: undefined,
|
|
153
|
+
type: elementType === 'number' ? 'number[]' : 'string[]',
|
|
154
|
+
};
|
|
145
155
|
},
|
|
146
156
|
boolean: () => ({ options: undefined, type: 'boolean' }),
|
|
147
157
|
enum: (s) => {
|
package/src/dispatch.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless trail execution — the "no-surface" surface.
|
|
3
|
+
*
|
|
4
|
+
* Looks up a trail by ID in a topo, then delegates to `executeTrail`.
|
|
5
|
+
* Returns a `Result` and never throws.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Topo } from './topo.js';
|
|
9
|
+
import { executeTrail } from './execute.js';
|
|
10
|
+
import type { ExecuteTrailOptions } from './execute.js';
|
|
11
|
+
import { NotFoundError } from './errors.js';
|
|
12
|
+
import { Result } from './result.js';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Options
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** Options forwarded to `executeTrail` from `dispatch`. */
|
|
19
|
+
export type DispatchOptions = ExecuteTrailOptions;
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// dispatch()
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Execute a trail by ID from a topo without mounting a surface.
|
|
27
|
+
*
|
|
28
|
+
* Resolves the trail from the topo, then runs it through the standard
|
|
29
|
+
* `executeTrail` pipeline. Returns `Result.err(NotFoundError)` if the
|
|
30
|
+
* trail ID is not registered. Never throws — unexpected exceptions are
|
|
31
|
+
* returned as `Result.err(InternalError)`.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const result = await dispatch(myTopo, 'greet', { name: 'Alice' });
|
|
36
|
+
* if (result.isOk()) console.log(result.value);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export const dispatch = (
|
|
40
|
+
topo: Topo,
|
|
41
|
+
id: string,
|
|
42
|
+
input: unknown,
|
|
43
|
+
options?: DispatchOptions
|
|
44
|
+
): Promise<Result<unknown, Error>> => {
|
|
45
|
+
const trail = topo.get(id);
|
|
46
|
+
if (trail === undefined) {
|
|
47
|
+
return Promise.resolve(
|
|
48
|
+
Result.err(
|
|
49
|
+
new NotFoundError(`Trail "${id}" not found in topo "${topo.name}"`)
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return executeTrail(trail, input, options);
|
|
54
|
+
};
|
package/src/event.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { z } from 'zod';
|
|
|
11
11
|
export interface EventSpec<T> {
|
|
12
12
|
readonly payload: z.ZodType<T>;
|
|
13
13
|
readonly description?: string | undefined;
|
|
14
|
-
readonly
|
|
14
|
+
readonly metadata?: Readonly<Record<string, unknown>> | undefined;
|
|
15
15
|
/** Trail IDs that produce this event (e.g. the trails it originates from). */
|
|
16
16
|
readonly from?: readonly string[] | undefined;
|
|
17
17
|
}
|
|
@@ -25,7 +25,7 @@ export interface Event<T> {
|
|
|
25
25
|
readonly kind: 'event';
|
|
26
26
|
readonly payload: z.ZodType<T>;
|
|
27
27
|
readonly description?: string | undefined;
|
|
28
|
-
readonly
|
|
28
|
+
readonly metadata?: Readonly<Record<string, unknown>> | undefined;
|
|
29
29
|
/** Trail IDs that produce this event (e.g. the trails it originates from). */
|
|
30
30
|
readonly from?: readonly string[] | undefined;
|
|
31
31
|
}
|
|
@@ -68,7 +68,7 @@ export function event<T>(
|
|
|
68
68
|
from: resolvedSpec.from ? Object.freeze([...resolvedSpec.from]) : undefined,
|
|
69
69
|
id: resolvedId,
|
|
70
70
|
kind: 'event' as const,
|
|
71
|
-
|
|
71
|
+
metadata: resolvedSpec.metadata,
|
|
72
72
|
payload: resolvedSpec.payload,
|
|
73
73
|
});
|
|
74
74
|
}
|
package/src/execute.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized trail execution pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Validates input, builds context, composes layers, and runs the
|
|
5
|
+
* implementation. Surfaces (CLI, MCP, HTTP) delegate here instead
|
|
6
|
+
* of reimplementing the pipeline.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AnyTrail } from './trail.js';
|
|
10
|
+
import type { Layer } from './layer.js';
|
|
11
|
+
import type { TrailContext } from './types.js';
|
|
12
|
+
|
|
13
|
+
import { composeLayers } from './layer.js';
|
|
14
|
+
import { createTrailContext } from './context.js';
|
|
15
|
+
import { InternalError } from './errors.js';
|
|
16
|
+
import { Result } from './result.js';
|
|
17
|
+
import { validateInput } from './validation.js';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Options
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** Options for executeTrail. */
|
|
24
|
+
export interface ExecuteTrailOptions {
|
|
25
|
+
/** Partial context overrides merged on top of the base context. */
|
|
26
|
+
readonly ctx?: Partial<TrailContext> | undefined;
|
|
27
|
+
/** AbortSignal override (takes final precedence over ctx and factory). */
|
|
28
|
+
readonly signal?: AbortSignal | undefined;
|
|
29
|
+
/** Layers to compose around the implementation. */
|
|
30
|
+
readonly layers?: readonly Layer[] | undefined;
|
|
31
|
+
/** Factory that produces a base TrailContext (takes precedence over defaults). */
|
|
32
|
+
readonly createContext?:
|
|
33
|
+
| (() => TrailContext | Promise<TrailContext>)
|
|
34
|
+
| undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Context resolution
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build a TrailContext from options.
|
|
43
|
+
*
|
|
44
|
+
* Resolution order:
|
|
45
|
+
* 1. Factory (`createContext`) or `createTrailContext()` defaults.
|
|
46
|
+
* 2. Partial `ctx` overrides merged on top.
|
|
47
|
+
* 3. `signal` override takes final precedence.
|
|
48
|
+
*/
|
|
49
|
+
const resolveContext = async (
|
|
50
|
+
options?: ExecuteTrailOptions
|
|
51
|
+
): Promise<TrailContext> => {
|
|
52
|
+
const base = options?.createContext
|
|
53
|
+
? await options.createContext()
|
|
54
|
+
: createTrailContext();
|
|
55
|
+
const withOverrides = options?.ctx
|
|
56
|
+
? {
|
|
57
|
+
...base,
|
|
58
|
+
...options.ctx,
|
|
59
|
+
extensions: { ...base.extensions, ...options.ctx.extensions },
|
|
60
|
+
}
|
|
61
|
+
: base;
|
|
62
|
+
return options?.signal
|
|
63
|
+
? { ...withOverrides, signal: options.signal }
|
|
64
|
+
: withOverrides;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Pipeline
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Execute a trail through the standard validate-context-layers-run pipeline.
|
|
73
|
+
*
|
|
74
|
+
* The function never throws -- unexpected exceptions are caught and
|
|
75
|
+
* returned as `Result.err(InternalError)`.
|
|
76
|
+
*/
|
|
77
|
+
export const executeTrail = async (
|
|
78
|
+
trail: AnyTrail,
|
|
79
|
+
rawInput: unknown,
|
|
80
|
+
options?: ExecuteTrailOptions
|
|
81
|
+
): Promise<Result<unknown, Error>> => {
|
|
82
|
+
try {
|
|
83
|
+
const validated = validateInput(trail.input, rawInput);
|
|
84
|
+
if (validated.isErr()) {
|
|
85
|
+
return validated;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const ctx = await resolveContext(options);
|
|
89
|
+
const layers = options?.layers ?? [];
|
|
90
|
+
const impl = composeLayers([...layers], trail, trail.run);
|
|
91
|
+
return await impl(validated.value, ctx);
|
|
92
|
+
} catch (error: unknown) {
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
94
|
+
return Result.err(new InternalError(message));
|
|
95
|
+
}
|
|
96
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -34,7 +34,6 @@ export type {
|
|
|
34
34
|
ProgressCallback,
|
|
35
35
|
ProgressEvent,
|
|
36
36
|
Logger,
|
|
37
|
-
Surface,
|
|
38
37
|
} from './types.js';
|
|
39
38
|
|
|
40
39
|
// Context factory
|
|
@@ -42,11 +41,17 @@ export { createTrailContext } from './context.js';
|
|
|
42
41
|
|
|
43
42
|
// Trail
|
|
44
43
|
export { trail } from './trail.js';
|
|
45
|
-
export type {
|
|
44
|
+
export type {
|
|
45
|
+
AnyTrail,
|
|
46
|
+
Intent,
|
|
47
|
+
Trail,
|
|
48
|
+
TrailSpec,
|
|
49
|
+
TrailExample,
|
|
50
|
+
} from './trail.js';
|
|
46
51
|
|
|
47
|
-
//
|
|
48
|
-
export {
|
|
49
|
-
export
|
|
52
|
+
// Type utilities
|
|
53
|
+
export type { TrailInput, TrailOutput, TrailResult } from './type-utils.js';
|
|
54
|
+
export { inputOf, outputOf } from './type-utils.js';
|
|
50
55
|
|
|
51
56
|
// Event
|
|
52
57
|
export { event } from './event.js';
|
|
@@ -64,23 +69,18 @@ export type { TopoIssue } from './validate-topo.js';
|
|
|
64
69
|
export { composeLayers } from './layer.js';
|
|
65
70
|
export type { Layer } from './layer.js';
|
|
66
71
|
|
|
67
|
-
// Health
|
|
68
|
-
export type { HealthStatus, HealthResult } from './health.js';
|
|
69
|
-
|
|
70
|
-
// Adapters
|
|
71
|
-
export type {
|
|
72
|
-
IndexAdapter,
|
|
73
|
-
StorageAdapter,
|
|
74
|
-
CacheAdapter,
|
|
75
|
-
SearchOptions,
|
|
76
|
-
SearchResult,
|
|
77
|
-
StorageOptions,
|
|
78
|
-
} from './adapters.js';
|
|
79
|
-
|
|
80
72
|
// Derive
|
|
81
73
|
export { deriveFields } from './derive.js';
|
|
82
74
|
export type { Field, FieldOverride } from './derive.js';
|
|
83
75
|
|
|
76
|
+
// Execute
|
|
77
|
+
export { executeTrail } from './execute.js';
|
|
78
|
+
export type { ExecuteTrailOptions } from './execute.js';
|
|
79
|
+
|
|
80
|
+
// Dispatch
|
|
81
|
+
export { dispatch } from './dispatch.js';
|
|
82
|
+
export type { DispatchOptions } from './dispatch.js';
|
|
83
|
+
|
|
84
84
|
// Validation
|
|
85
85
|
export {
|
|
86
86
|
validateInput,
|
|
@@ -133,6 +133,10 @@ export {
|
|
|
133
133
|
getRelativePath,
|
|
134
134
|
} from './workspace.js';
|
|
135
135
|
|
|
136
|
+
// Blob
|
|
137
|
+
export { createBlobRef, isBlobRef } from './blob-ref.js';
|
|
138
|
+
export type { BlobRef } from './blob-ref.js';
|
|
139
|
+
|
|
136
140
|
// Guards
|
|
137
141
|
export {
|
|
138
142
|
isDefined,
|
package/src/result.ts
CHANGED
|
@@ -17,7 +17,7 @@ class Ok<T, E> {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// oxlint-disable-next-line class-methods-use-this -- type guard for Result discriminated union
|
|
20
|
-
isErr(): this is Err<
|
|
20
|
+
isErr(): this is Err<E> {
|
|
21
21
|
return false;
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -47,7 +47,7 @@ class Ok<T, E> {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// oxlint-disable-next-line max-classes-per-file -- Result monad requires paired Ok/Err classes
|
|
50
|
-
class Err<
|
|
50
|
+
class Err<E> {
|
|
51
51
|
readonly error: E;
|
|
52
52
|
|
|
53
53
|
constructor(error: E) {
|
|
@@ -55,28 +55,28 @@ class Err<T, E> {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// oxlint-disable-next-line class-methods-use-this -- type guard for Result discriminated union
|
|
58
|
-
isOk(): this is Ok<
|
|
58
|
+
isOk(): this is Ok<never, E> {
|
|
59
59
|
return false;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// oxlint-disable-next-line class-methods-use-this -- type guard for Result discriminated union
|
|
63
|
-
isErr(): this is Err<
|
|
63
|
+
isErr(): this is Err<E> {
|
|
64
64
|
return true;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
map<U>(_fn: (value:
|
|
67
|
+
map<U>(_fn: (value: never) => U): Result<U, E> {
|
|
68
68
|
return new Err(this.error);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
flatMap<U, F = E>(_fn: (value:
|
|
71
|
+
flatMap<U, F = E>(_fn: (value: never) => Result<U, F>): Result<U, E | F> {
|
|
72
72
|
return new Err(this.error);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
mapErr<F>(fn: (error: E) => F): Result<
|
|
75
|
+
mapErr<F>(fn: (error: E) => F): Result<never, F> {
|
|
76
76
|
return new Err(fn(this.error));
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
match<U>(handlers: { ok: (value:
|
|
79
|
+
match<U>(handlers: { ok: (value: never) => U; err: (error: E) => U }): U {
|
|
80
80
|
return handlers.err(this.error);
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -87,12 +87,12 @@ class Err<T, E> {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// oxlint-disable-next-line class-methods-use-this -- symmetric API with Ok.unwrapOr
|
|
90
|
-
unwrapOr(fallback: T): T {
|
|
90
|
+
unwrapOr<T>(fallback: T): T {
|
|
91
91
|
return fallback;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export type Result<T, E = Error> = Ok<T, E> | Err<
|
|
95
|
+
export type Result<T, E = Error> = Ok<T, E> | Err<E>;
|
|
96
96
|
|
|
97
97
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
98
98
|
export const Result = {
|
|
@@ -107,7 +107,7 @@ export const Result = {
|
|
|
107
107
|
return new Ok(values);
|
|
108
108
|
},
|
|
109
109
|
|
|
110
|
-
err<E>(error: E):
|
|
110
|
+
err<E>(error: E): Err<E> {
|
|
111
111
|
return new Err(error);
|
|
112
112
|
},
|
|
113
113
|
|
|
@@ -151,16 +151,30 @@ export const Result = {
|
|
|
151
151
|
*/
|
|
152
152
|
toJson(value: unknown): Result<string, InternalError> {
|
|
153
153
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
// Track the current ancestor chain, not every object ever visited.
|
|
155
|
+
// This allows shared references in a DAG while still detecting cycles.
|
|
156
|
+
const stack: unknown[] = [];
|
|
157
|
+
const keys: string[] = [];
|
|
158
|
+
|
|
159
|
+
const json = JSON.stringify(value, function json(key, val: unknown) {
|
|
160
|
+
if (stack.length > 0) {
|
|
161
|
+
// `this` is the object that contains `key`. Trim the stack back
|
|
162
|
+
// to `this` so we only track the current ancestor path.
|
|
163
|
+
const thisIndex = stack.lastIndexOf(this as unknown);
|
|
164
|
+
stack.splice(thisIndex + 1);
|
|
165
|
+
keys.splice(thisIndex);
|
|
166
|
+
}
|
|
167
|
+
|
|
156
168
|
if (typeof val === 'object' && val !== null) {
|
|
157
|
-
if (
|
|
169
|
+
if (stack.includes(val)) {
|
|
158
170
|
return '[Circular]';
|
|
159
171
|
}
|
|
160
|
-
|
|
172
|
+
stack.push(val);
|
|
173
|
+
keys.push(key);
|
|
161
174
|
}
|
|
162
175
|
return val;
|
|
163
176
|
});
|
|
177
|
+
|
|
164
178
|
if (json === undefined) {
|
|
165
179
|
return new Err(
|
|
166
180
|
new InternalError('Value is not JSON-serializable', {
|
package/src/serialization.ts
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
import type { ErrorCategory, TrailsError } from './errors.js';
|
|
9
9
|
import {
|
|
10
10
|
ValidationError,
|
|
11
|
+
AmbiguousError,
|
|
12
|
+
AssertionError,
|
|
11
13
|
NotFoundError,
|
|
14
|
+
AlreadyExistsError,
|
|
12
15
|
ConflictError,
|
|
13
16
|
PermissionError,
|
|
14
17
|
TimeoutError,
|
|
@@ -89,6 +92,31 @@ const createErrorByCategory = (
|
|
|
89
92
|
return factory(message, opts, retryAfter);
|
|
90
93
|
};
|
|
91
94
|
|
|
95
|
+
/** Map error class names to their constructors for precise round-tripping. */
|
|
96
|
+
const errorConstructorsByName: Record<string, ErrorFactory> = {
|
|
97
|
+
AlreadyExistsError: (msg, opts) => new AlreadyExistsError(msg, opts),
|
|
98
|
+
AmbiguousError: (msg, opts) => new AmbiguousError(msg, opts),
|
|
99
|
+
AssertionError: (msg, opts) => new AssertionError(msg, opts),
|
|
100
|
+
AuthError: (msg, opts) => new AuthError(msg, opts),
|
|
101
|
+
CancelledError: (msg, opts) => new CancelledError(msg, opts),
|
|
102
|
+
ConflictError: (msg, opts) => new ConflictError(msg, opts),
|
|
103
|
+
InternalError: (msg, opts) => new InternalError(msg, opts),
|
|
104
|
+
NetworkError: (msg, opts) => new NetworkError(msg, opts),
|
|
105
|
+
NotFoundError: (msg, opts) => new NotFoundError(msg, opts),
|
|
106
|
+
PermissionError: (msg, opts) => new PermissionError(msg, opts),
|
|
107
|
+
RateLimitError: (msg, opts, retryAfter) => {
|
|
108
|
+
const rlOpts: { context?: Record<string, unknown>; retryAfter?: number } = {
|
|
109
|
+
...opts,
|
|
110
|
+
};
|
|
111
|
+
if (retryAfter !== undefined) {
|
|
112
|
+
rlOpts.retryAfter = retryAfter;
|
|
113
|
+
}
|
|
114
|
+
return new RateLimitError(msg, rlOpts);
|
|
115
|
+
},
|
|
116
|
+
TimeoutError: (msg, opts) => new TimeoutError(msg, opts),
|
|
117
|
+
ValidationError: (msg, opts) => new ValidationError(msg, opts),
|
|
118
|
+
};
|
|
119
|
+
|
|
92
120
|
// ---------------------------------------------------------------------------
|
|
93
121
|
// Error serialization
|
|
94
122
|
// ---------------------------------------------------------------------------
|
|
@@ -117,13 +145,17 @@ export const serializeError = (error: Error): SerializedError => {
|
|
|
117
145
|
|
|
118
146
|
/** Reconstruct a TrailsError from serialized data. */
|
|
119
147
|
export const deserializeError = (data: SerializedError): TrailsError => {
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
data.
|
|
125
|
-
|
|
126
|
-
|
|
148
|
+
const opts = buildOpts(data.context);
|
|
149
|
+
const nameFactory = errorConstructorsByName[data.name];
|
|
150
|
+
|
|
151
|
+
const error = nameFactory
|
|
152
|
+
? nameFactory(data.message, opts, data.retryAfter)
|
|
153
|
+
: createErrorByCategory(
|
|
154
|
+
data.category ?? 'internal',
|
|
155
|
+
data.message,
|
|
156
|
+
data.context,
|
|
157
|
+
data.retryAfter
|
|
158
|
+
);
|
|
127
159
|
|
|
128
160
|
if (data.stack) {
|
|
129
161
|
error.stack = data.stack;
|
|
@@ -155,13 +187,26 @@ export const safeStringify = (
|
|
|
155
187
|
value: unknown
|
|
156
188
|
): Result<string, InternalError> => {
|
|
157
189
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
190
|
+
// Track the current ancestor chain, not every object ever visited.
|
|
191
|
+
// This allows shared references in a DAG while still detecting cycles.
|
|
192
|
+
const stack: unknown[] = [];
|
|
193
|
+
const keys: string[] = [];
|
|
194
|
+
|
|
195
|
+
const json = JSON.stringify(value, function json(key, val: unknown) {
|
|
196
|
+
if (stack.length > 0) {
|
|
197
|
+
// `this` is the object that contains `key`. Trim the stack back
|
|
198
|
+
// to `this` so we only track the current ancestor path.
|
|
199
|
+
const thisIndex = stack.lastIndexOf(this as unknown);
|
|
200
|
+
stack.splice(thisIndex + 1);
|
|
201
|
+
keys.splice(thisIndex);
|
|
202
|
+
}
|
|
203
|
+
|
|
160
204
|
if (typeof val === 'object' && val !== null) {
|
|
161
|
-
if (
|
|
205
|
+
if (stack.includes(val)) {
|
|
162
206
|
return '[Circular]';
|
|
163
207
|
}
|
|
164
|
-
|
|
208
|
+
stack.push(val);
|
|
209
|
+
keys.push(key);
|
|
165
210
|
}
|
|
166
211
|
return val;
|
|
167
212
|
});
|