@ontrails/core 1.0.0-beta.1 → 1.0.0-beta.11
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 +121 -0
- package/README.md +54 -11
- package/dist/context.d.ts +2 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +12 -7
- package/dist/context.js.map +1 -1
- 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 +33 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +207 -0
- package/dist/execute.js.map +1 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/patterns/status.d.ts +1 -1
- 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/service.d.ts +69 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +56 -0
- package/dist/service.js.map +1 -0
- package/dist/topo.d.ts +11 -4
- package/dist/topo.d.ts.map +1 -1
- package/dist/topo.js +43 -18
- package/dist/topo.js.map +1 -1
- package/dist/trail.d.ts +21 -10
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +5 -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 +10 -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 +75 -9
- package/dist/validate-topo.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +34 -3
- package/dist/validation.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/context.test.ts +16 -5
- package/src/__tests__/derive.test.ts +44 -0
- package/src/__tests__/dispatch.test.ts +181 -0
- package/src/__tests__/event.test.ts +5 -5
- package/src/__tests__/execute.test.ts +523 -0
- package/src/__tests__/layer.test.ts +14 -113
- package/src/__tests__/serialization.test.ts +166 -1
- package/src/__tests__/service.test.ts +197 -0
- package/src/__tests__/topo.test.ts +171 -78
- package/src/__tests__/trail.test.ts +119 -37
- package/src/__tests__/type-utils.test.ts +90 -0
- package/src/__tests__/validate-topo.test.ts +140 -19
- package/src/__tests__/validation.test.ts +53 -0
- package/src/context.ts +18 -9
- package/src/derive.ts +12 -2
- package/src/dispatch.ts +54 -0
- package/src/event.ts +3 -3
- package/src/execute.ts +345 -0
- package/src/index.ts +39 -18
- package/src/result.ts +18 -4
- package/src/serialization.ts +56 -11
- package/src/service.ts +139 -0
- package/src/topo.ts +66 -27
- package/src/trail.ts +36 -13
- package/src/type-utils.ts +45 -0
- package/src/types.ts +11 -2
- package/src/validate-topo.ts +92 -10
- package/src/validation.ts +35 -3
- 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
package/src/execute.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
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 {
|
|
12
|
+
AnyService,
|
|
13
|
+
ServiceContext,
|
|
14
|
+
ServiceOverrideMap,
|
|
15
|
+
} from './service.js';
|
|
16
|
+
import type { TrailContext, TrailContextInit } from './types.js';
|
|
17
|
+
|
|
18
|
+
import { composeLayers } from './layer.js';
|
|
19
|
+
import { createTrailContext } from './context.js';
|
|
20
|
+
import { InternalError } from './errors.js';
|
|
21
|
+
import { Result } from './result.js';
|
|
22
|
+
import { createServiceLookup } from './service.js';
|
|
23
|
+
import { validateInput } from './validation.js';
|
|
24
|
+
|
|
25
|
+
type MutableTrailContext = {
|
|
26
|
+
-readonly [K in keyof TrailContext]: TrailContext[K];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Options
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/** Options for executeTrail. */
|
|
34
|
+
export interface ExecuteTrailOptions {
|
|
35
|
+
/** Partial context overrides merged on top of the base context. */
|
|
36
|
+
readonly ctx?: Partial<TrailContextInit> | undefined;
|
|
37
|
+
/** AbortSignal override (takes final precedence over ctx and factory). */
|
|
38
|
+
readonly signal?: AbortSignal | undefined;
|
|
39
|
+
/** Layers to compose around the implementation. */
|
|
40
|
+
readonly layers?: readonly Layer[] | undefined;
|
|
41
|
+
/** Factory that produces a base TrailContext (takes precedence over defaults). */
|
|
42
|
+
readonly createContext?:
|
|
43
|
+
| (() => TrailContextInit | Promise<TrailContextInit>)
|
|
44
|
+
| undefined;
|
|
45
|
+
/** Explicit service instance overrides keyed by service ID. */
|
|
46
|
+
readonly services?: ServiceOverrideMap | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Context resolution
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a TrailContext from options.
|
|
55
|
+
*
|
|
56
|
+
* Resolution order:
|
|
57
|
+
* 1. Factory (`createContext`) or `createTrailContext()` defaults.
|
|
58
|
+
* 2. Partial `ctx` overrides merged on top.
|
|
59
|
+
* 3. `signal` override takes final precedence.
|
|
60
|
+
*/
|
|
61
|
+
const resolveContext = async (
|
|
62
|
+
options?: ExecuteTrailOptions
|
|
63
|
+
): Promise<TrailContext> => {
|
|
64
|
+
const seed = options?.createContext
|
|
65
|
+
? await options.createContext()
|
|
66
|
+
: createTrailContext();
|
|
67
|
+
const base = seed.service ? seed : createTrailContext(seed);
|
|
68
|
+
const withOverrides = options?.ctx
|
|
69
|
+
? {
|
|
70
|
+
...base,
|
|
71
|
+
...options.ctx,
|
|
72
|
+
extensions: { ...base.extensions, ...options.ctx.extensions },
|
|
73
|
+
}
|
|
74
|
+
: base;
|
|
75
|
+
const resolved = options?.signal
|
|
76
|
+
? { ...withOverrides, signal: options.signal }
|
|
77
|
+
: withOverrides;
|
|
78
|
+
if (
|
|
79
|
+
options?.ctx?.extensions !== undefined ||
|
|
80
|
+
resolved.service === undefined
|
|
81
|
+
) {
|
|
82
|
+
const bound = { ...resolved } as MutableTrailContext;
|
|
83
|
+
bound.service = createServiceLookup(() => bound);
|
|
84
|
+
return bound;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return resolved as TrailContext;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const singletonServices = new WeakMap<AnyService, Map<string, unknown>>();
|
|
91
|
+
|
|
92
|
+
/** In-flight service creation promises, keyed by service × context. */
|
|
93
|
+
const pendingCreations = new WeakMap<
|
|
94
|
+
AnyService,
|
|
95
|
+
Map<string, Promise<Result<unknown, Error>>>
|
|
96
|
+
>();
|
|
97
|
+
|
|
98
|
+
const hasOwnServiceOverride = (
|
|
99
|
+
overrides: ServiceOverrideMap | undefined,
|
|
100
|
+
id: string
|
|
101
|
+
): overrides is ServiceOverrideMap =>
|
|
102
|
+
overrides !== undefined && Object.hasOwn(overrides, id);
|
|
103
|
+
|
|
104
|
+
const toServiceContext = (ctx: TrailContext): ServiceContext => ({
|
|
105
|
+
cwd: ctx.cwd,
|
|
106
|
+
env: ctx.env,
|
|
107
|
+
workspaceRoot: ctx.workspaceRoot,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const toServiceContextKey = (ctx: ServiceContext): string =>
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
cwd: ctx.cwd,
|
|
113
|
+
env: Object.entries(ctx.env ?? {}).toSorted(([left], [right]) =>
|
|
114
|
+
left.localeCompare(right)
|
|
115
|
+
),
|
|
116
|
+
workspaceRoot: ctx.workspaceRoot,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const toInternalServiceError = (id: string, error: unknown): InternalError => {
|
|
120
|
+
const cause = error instanceof Error ? error : undefined;
|
|
121
|
+
const message = cause?.message ?? String(error);
|
|
122
|
+
return new InternalError(`Service "${id}" failed to resolve: ${message}`, {
|
|
123
|
+
...(cause ? { cause } : {}),
|
|
124
|
+
context: { serviceId: id },
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const getCachedSingletonService = (
|
|
129
|
+
declaredService: AnyService,
|
|
130
|
+
serviceContext: ServiceContext
|
|
131
|
+
): { readonly found: boolean; readonly value: unknown } => {
|
|
132
|
+
const scopedCache = singletonServices.get(declaredService);
|
|
133
|
+
if (scopedCache === undefined) {
|
|
134
|
+
return { found: false, value: undefined };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const key = toServiceContextKey(serviceContext);
|
|
138
|
+
if (!scopedCache.has(key)) {
|
|
139
|
+
return { found: false, value: undefined };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
found: true,
|
|
144
|
+
value: scopedCache.get(key),
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const getProvidedService = (
|
|
149
|
+
ctx: TrailContext,
|
|
150
|
+
overrides: ServiceOverrideMap | undefined,
|
|
151
|
+
declaredService: AnyService,
|
|
152
|
+
serviceContext: ServiceContext
|
|
153
|
+
): Result<unknown, Error> | undefined => {
|
|
154
|
+
const { id } = declaredService;
|
|
155
|
+
if (hasOwnServiceOverride(overrides, id)) {
|
|
156
|
+
return Result.ok(overrides[id]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (Object.hasOwn(ctx.extensions ?? {}, id)) {
|
|
160
|
+
return Result.ok(ctx.extensions?.[id]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const cached = getCachedSingletonService(declaredService, serviceContext);
|
|
164
|
+
if (cached.found) {
|
|
165
|
+
return Result.ok(cached.value);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return undefined;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const getSingletonServiceCache = (
|
|
172
|
+
declaredService: AnyService
|
|
173
|
+
): Map<string, unknown> => {
|
|
174
|
+
const existing = singletonServices.get(declaredService);
|
|
175
|
+
if (existing !== undefined) {
|
|
176
|
+
return existing;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const created = new Map<string, unknown>();
|
|
180
|
+
singletonServices.set(declaredService, created);
|
|
181
|
+
return created;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const doCreateServiceInstance = async (
|
|
185
|
+
declaredService: AnyService,
|
|
186
|
+
serviceContext: ServiceContext
|
|
187
|
+
): Promise<Result<unknown, Error>> => {
|
|
188
|
+
try {
|
|
189
|
+
const created = await declaredService.create(serviceContext);
|
|
190
|
+
if (created.isErr()) {
|
|
191
|
+
return Result.err(created.error);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const instance = created.unwrap();
|
|
195
|
+
getSingletonServiceCache(declaredService).set(
|
|
196
|
+
toServiceContextKey(serviceContext),
|
|
197
|
+
instance
|
|
198
|
+
);
|
|
199
|
+
return Result.ok(instance);
|
|
200
|
+
} catch (error: unknown) {
|
|
201
|
+
return Result.err(toInternalServiceError(declaredService.id, error));
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const trackPendingCreation = (
|
|
206
|
+
declaredService: AnyService,
|
|
207
|
+
key: string,
|
|
208
|
+
promise: Promise<Result<unknown, Error>>
|
|
209
|
+
): void => {
|
|
210
|
+
const pending = pendingCreations.get(declaredService);
|
|
211
|
+
if (pending) {
|
|
212
|
+
pending.set(key, promise);
|
|
213
|
+
} else {
|
|
214
|
+
pendingCreations.set(declaredService, new Map([[key, promise]]));
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Deduplicates concurrent creation of the same service singleton.
|
|
220
|
+
* If a creation is already in flight for this service × context key,
|
|
221
|
+
* returns the existing promise instead of spawning a second factory call.
|
|
222
|
+
*/
|
|
223
|
+
const createServiceInstance = async (
|
|
224
|
+
declaredService: AnyService,
|
|
225
|
+
serviceContext: ServiceContext
|
|
226
|
+
): Promise<Result<unknown, Error>> => {
|
|
227
|
+
const key = toServiceContextKey(serviceContext);
|
|
228
|
+
const inflight = pendingCreations.get(declaredService)?.get(key);
|
|
229
|
+
if (inflight) {
|
|
230
|
+
return inflight;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const promise = doCreateServiceInstance(declaredService, serviceContext);
|
|
234
|
+
trackPendingCreation(declaredService, key, promise);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
return await promise;
|
|
238
|
+
} finally {
|
|
239
|
+
pendingCreations.get(declaredService)?.delete(key);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const resolveServiceInstance = async (
|
|
244
|
+
declaredService: AnyService,
|
|
245
|
+
ctx: TrailContext,
|
|
246
|
+
serviceContext: ServiceContext,
|
|
247
|
+
overrides?: ServiceOverrideMap
|
|
248
|
+
): Promise<Result<unknown, Error>> =>
|
|
249
|
+
getProvidedService(ctx, overrides, declaredService, serviceContext) ??
|
|
250
|
+
(await createServiceInstance(declaredService, serviceContext));
|
|
251
|
+
|
|
252
|
+
const withResolvedServices = (
|
|
253
|
+
ctx: TrailContext,
|
|
254
|
+
resolvedServices: Record<string, unknown>
|
|
255
|
+
): TrailContext => {
|
|
256
|
+
const extensions = { ...ctx.extensions, ...resolvedServices };
|
|
257
|
+
const resolvedCtx = { ...ctx, extensions } as MutableTrailContext;
|
|
258
|
+
resolvedCtx.service = createServiceLookup(() => resolvedCtx);
|
|
259
|
+
return resolvedCtx;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const resolveServices = async (
|
|
263
|
+
trail: AnyTrail,
|
|
264
|
+
ctx: TrailContext,
|
|
265
|
+
overrides?: ServiceOverrideMap
|
|
266
|
+
): Promise<Result<TrailContext, Error>> => {
|
|
267
|
+
if (trail.services.length === 0) {
|
|
268
|
+
return Result.ok(ctx);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const resolvedServices: Record<string, unknown> = {};
|
|
272
|
+
const serviceContext = toServiceContext(ctx);
|
|
273
|
+
|
|
274
|
+
for (const declaredService of trail.services) {
|
|
275
|
+
const resolved = await resolveServiceInstance(
|
|
276
|
+
declaredService,
|
|
277
|
+
ctx,
|
|
278
|
+
serviceContext,
|
|
279
|
+
overrides
|
|
280
|
+
);
|
|
281
|
+
if (resolved.isErr()) {
|
|
282
|
+
return resolved;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
resolvedServices[declaredService.id] = resolved.unwrap();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return Result.ok(withResolvedServices(ctx, resolvedServices));
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const prepareContext = async (
|
|
292
|
+
trail: AnyTrail,
|
|
293
|
+
options?: ExecuteTrailOptions
|
|
294
|
+
): Promise<Result<TrailContext, Error>> => {
|
|
295
|
+
const baseCtx = await resolveContext(options);
|
|
296
|
+
return await resolveServices(trail, baseCtx, options?.services);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const runTrail = async (
|
|
300
|
+
trail: AnyTrail,
|
|
301
|
+
input: unknown,
|
|
302
|
+
ctx: TrailContext,
|
|
303
|
+
layers: readonly Layer[]
|
|
304
|
+
): Promise<Result<unknown, Error>> => {
|
|
305
|
+
const impl = composeLayers([...layers], trail, trail.run);
|
|
306
|
+
return await impl(input, ctx);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Pipeline
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Execute a trail through the standard validate-context-layers-run pipeline.
|
|
315
|
+
*
|
|
316
|
+
* The function never throws -- unexpected exceptions are caught and
|
|
317
|
+
* returned as `Result.err(InternalError)`.
|
|
318
|
+
*/
|
|
319
|
+
export const executeTrail = async (
|
|
320
|
+
trail: AnyTrail,
|
|
321
|
+
rawInput: unknown,
|
|
322
|
+
options?: ExecuteTrailOptions
|
|
323
|
+
): Promise<Result<unknown, Error>> => {
|
|
324
|
+
try {
|
|
325
|
+
const validated = validateInput(trail.input, rawInput);
|
|
326
|
+
if (validated.isErr()) {
|
|
327
|
+
return validated;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const resolvedCtx = await prepareContext(trail, options);
|
|
331
|
+
if (resolvedCtx.isErr()) {
|
|
332
|
+
return resolvedCtx;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return await runTrail(
|
|
336
|
+
trail,
|
|
337
|
+
validated.value,
|
|
338
|
+
resolvedCtx.value,
|
|
339
|
+
options?.layers ?? []
|
|
340
|
+
);
|
|
341
|
+
} catch (error: unknown) {
|
|
342
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
343
|
+
return Result.err(new InternalError(message));
|
|
344
|
+
}
|
|
345
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -30,23 +30,45 @@ export type { ErrorCategory } from './errors.js';
|
|
|
30
30
|
export type {
|
|
31
31
|
Implementation,
|
|
32
32
|
TrailContext,
|
|
33
|
+
TrailContextInit,
|
|
33
34
|
FollowFn,
|
|
34
35
|
ProgressCallback,
|
|
35
36
|
ProgressEvent,
|
|
36
37
|
Logger,
|
|
37
|
-
|
|
38
|
+
ServiceLookup,
|
|
38
39
|
} from './types.js';
|
|
39
40
|
|
|
40
41
|
// Context factory
|
|
41
42
|
export { createTrailContext } from './context.js';
|
|
42
43
|
|
|
44
|
+
// Service
|
|
45
|
+
export {
|
|
46
|
+
createServiceLookup,
|
|
47
|
+
findDuplicateServiceId,
|
|
48
|
+
isService,
|
|
49
|
+
service,
|
|
50
|
+
} from './service.js';
|
|
51
|
+
export type {
|
|
52
|
+
AnyService,
|
|
53
|
+
Service,
|
|
54
|
+
ServiceContext,
|
|
55
|
+
ServiceOverrideMap,
|
|
56
|
+
ServiceSpec,
|
|
57
|
+
} from './service.js';
|
|
58
|
+
|
|
43
59
|
// Trail
|
|
44
60
|
export { trail } from './trail.js';
|
|
45
|
-
export type {
|
|
61
|
+
export type {
|
|
62
|
+
AnyTrail,
|
|
63
|
+
Intent,
|
|
64
|
+
Trail,
|
|
65
|
+
TrailSpec,
|
|
66
|
+
TrailExample,
|
|
67
|
+
} from './trail.js';
|
|
46
68
|
|
|
47
|
-
//
|
|
48
|
-
export {
|
|
49
|
-
export
|
|
69
|
+
// Type utilities
|
|
70
|
+
export type { TrailInput, TrailOutput, TrailResult } from './type-utils.js';
|
|
71
|
+
export { inputOf, outputOf } from './type-utils.js';
|
|
50
72
|
|
|
51
73
|
// Event
|
|
52
74
|
export { event } from './event.js';
|
|
@@ -64,23 +86,18 @@ export type { TopoIssue } from './validate-topo.js';
|
|
|
64
86
|
export { composeLayers } from './layer.js';
|
|
65
87
|
export type { Layer } from './layer.js';
|
|
66
88
|
|
|
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
89
|
// Derive
|
|
81
90
|
export { deriveFields } from './derive.js';
|
|
82
91
|
export type { Field, FieldOverride } from './derive.js';
|
|
83
92
|
|
|
93
|
+
// Execute
|
|
94
|
+
export { executeTrail } from './execute.js';
|
|
95
|
+
export type { ExecuteTrailOptions } from './execute.js';
|
|
96
|
+
|
|
97
|
+
// Dispatch
|
|
98
|
+
export { dispatch } from './dispatch.js';
|
|
99
|
+
export type { DispatchOptions } from './dispatch.js';
|
|
100
|
+
|
|
84
101
|
// Validation
|
|
85
102
|
export {
|
|
86
103
|
validateInput,
|
|
@@ -133,6 +150,10 @@ export {
|
|
|
133
150
|
getRelativePath,
|
|
134
151
|
} from './workspace.js';
|
|
135
152
|
|
|
153
|
+
// Blob
|
|
154
|
+
export { createBlobRef, isBlobRef } from './blob-ref.js';
|
|
155
|
+
export type { BlobRef } from './blob-ref.js';
|
|
156
|
+
|
|
136
157
|
// Guards
|
|
137
158
|
export {
|
|
138
159
|
isDefined,
|
package/src/result.ts
CHANGED
|
@@ -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
|
});
|