@ontrails/core 1.0.0-beta.11 → 1.0.0-beta.13
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 +67 -30
- package/README.md +19 -19
- package/dist/context.d.ts +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +5 -4
- package/dist/context.js.map +1 -1
- package/dist/derive.d.ts +8 -3
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +7 -7
- package/dist/derive.js.map +1 -1
- package/dist/event.d.ts +4 -41
- package/dist/event.d.ts.map +1 -1
- package/dist/event.js +4 -14
- package/dist/event.js.map +1 -1
- package/dist/execute.d.ts +11 -9
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +34 -151
- package/dist/execute.js.map +1 -1
- package/dist/gate.d.ts +17 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +21 -0
- package/dist/gate.js.map +1 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -7
- package/dist/index.js.map +1 -1
- package/dist/layer.d.ts +5 -16
- package/dist/layer.d.ts.map +1 -1
- package/dist/layer.js +3 -20
- package/dist/layer.js.map +1 -1
- package/dist/provision-config.d.ts +22 -0
- package/dist/provision-config.d.ts.map +1 -0
- package/dist/provision-config.js +210 -0
- package/dist/provision-config.js.map +1 -0
- package/dist/provision.d.ts +71 -0
- package/dist/provision.d.ts.map +1 -0
- package/dist/provision.js +56 -0
- package/dist/provision.js.map +1 -0
- package/dist/run.d.ts +27 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +34 -0
- package/dist/run.js.map +1 -0
- package/dist/service-config.d.ts +22 -0
- package/dist/service-config.d.ts.map +1 -0
- package/dist/service-config.js +210 -0
- package/dist/service-config.js.map +1 -0
- package/dist/service.d.ts +39 -37
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +21 -21
- package/dist/service.js.map +1 -1
- package/dist/signal.d.ts +33 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +17 -0
- package/dist/signal.js.map +1 -0
- package/dist/topo.d.ts +10 -10
- package/dist/topo.d.ts.map +1 -1
- package/dist/topo.js +48 -35
- package/dist/topo.js.map +1 -1
- package/dist/trail.d.ts +19 -17
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +5 -4
- package/dist/trail.js.map +1 -1
- package/dist/types.d.ts +26 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -1
- package/dist/types.js.map +1 -1
- package/dist/validate-topo.d.ts +2 -2
- package/dist/validate-topo.js +31 -31
- package/dist/validate-topo.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/context.test.ts +8 -8
- package/src/__tests__/execute.test.ts +124 -122
- package/src/__tests__/{layer.test.ts → gate.test.ts} +26 -26
- package/src/__tests__/{dispatch.test.ts → run.test.ts} +39 -39
- package/src/__tests__/service-config.test.ts +228 -0
- package/src/__tests__/service.test.ts +71 -71
- package/src/__tests__/{event.test.ts → signal.test.ts} +15 -15
- package/src/__tests__/topo.test.ts +54 -54
- package/src/__tests__/trail-permit.test.ts +60 -0
- package/src/__tests__/trail.test.ts +58 -58
- package/src/__tests__/type-utils.test.ts +3 -3
- package/src/__tests__/validate-topo.test.ts +38 -38
- package/src/context.ts +5 -4
- package/src/derive.ts +8 -8
- package/src/event.ts +11 -73
- package/src/execute.ts +59 -238
- package/src/{layer.ts → gate.ts} +13 -13
- package/src/index.ts +26 -21
- package/src/provision-config.ts +373 -0
- package/src/provision.ts +148 -0
- package/src/{dispatch.ts → run.ts} +8 -8
- package/src/signal.ts +65 -0
- package/src/topo.ts +74 -52
- package/src/trail.ts +30 -23
- package/src/types.ts +30 -10
- package/src/validate-topo.ts +33 -33
- package/tsconfig.tsbuildinfo +1 -1
- package/src/service.ts +0 -139
|
@@ -6,9 +6,9 @@ import { z } from 'zod';
|
|
|
6
6
|
import { InternalError, ValidationError } from '../errors';
|
|
7
7
|
import { executeTrail } from '../execute';
|
|
8
8
|
import { createTrailContext } from '../context';
|
|
9
|
-
import type {
|
|
9
|
+
import type { Gate } from '../gate';
|
|
10
10
|
import { Result } from '../result';
|
|
11
|
-
import {
|
|
11
|
+
import { provision } from '../provision';
|
|
12
12
|
import { trail } from '../trail';
|
|
13
13
|
import type { TrailContext, TrailContextInit } from '../types';
|
|
14
14
|
|
|
@@ -17,84 +17,84 @@ import type { TrailContext, TrailContextInit } from '../types';
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
|
|
19
19
|
const echoTrail = trail('echo', {
|
|
20
|
+
blaze: (input) => Result.ok({ value: input.value }),
|
|
20
21
|
input: z.object({ value: z.string() }),
|
|
21
22
|
output: z.object({ value: z.string() }),
|
|
22
|
-
run: (input) => Result.ok({ value: input.value }),
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
const failingTrail = trail('fails', {
|
|
26
|
+
blaze: () => Result.err(new ValidationError('bad input')),
|
|
26
27
|
input: z.object({}),
|
|
27
|
-
run: () => Result.err(new ValidationError('bad input')),
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
const throwingTrail = trail('throws', {
|
|
31
|
-
|
|
32
|
-
run: () => {
|
|
31
|
+
blaze: () => {
|
|
33
32
|
throw new Error('kaboom');
|
|
34
33
|
},
|
|
34
|
+
input: z.object({}),
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
const
|
|
38
|
-
`test.
|
|
37
|
+
const nextProvisionId = (name: string): string =>
|
|
38
|
+
`test.provision.${name}.${Bun.randomUUIDv7()}`;
|
|
39
39
|
|
|
40
|
-
const
|
|
40
|
+
const createResolvedValueProvision = (
|
|
41
41
|
id: string,
|
|
42
42
|
onCreate: () => void,
|
|
43
43
|
value: number
|
|
44
44
|
) =>
|
|
45
|
-
|
|
45
|
+
provision(id, {
|
|
46
46
|
create: () => {
|
|
47
47
|
onCreate();
|
|
48
48
|
return Result.ok({ value });
|
|
49
49
|
},
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
const
|
|
52
|
+
const createEagerProvisionTrail = (
|
|
53
53
|
id: string,
|
|
54
|
-
counter: ReturnType<typeof
|
|
54
|
+
counter: ReturnType<typeof createResolvedValueProvision>,
|
|
55
55
|
onRun: (value: number) => void
|
|
56
56
|
) =>
|
|
57
|
-
trail('
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
run: (_input, ctx) => {
|
|
61
|
-
const fromAccessor = ctx.service<{ value: number }>(id);
|
|
57
|
+
trail('provision.eager', {
|
|
58
|
+
blaze: (_input, ctx) => {
|
|
59
|
+
const fromAccessor = ctx.provision<{ value: number }>(id);
|
|
62
60
|
const fromDefinition = counter.from(ctx);
|
|
63
61
|
onRun(fromDefinition.value);
|
|
64
62
|
return Result.ok({ total: fromAccessor.value + 1 });
|
|
65
63
|
},
|
|
66
|
-
|
|
64
|
+
input: z.object({}),
|
|
65
|
+
output: z.object({ total: z.number() }),
|
|
66
|
+
provisions: [counter],
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
const
|
|
70
|
-
counter: ReturnType<typeof
|
|
69
|
+
const createProvisionProbeGate = (
|
|
70
|
+
counter: ReturnType<typeof createResolvedValueProvision>,
|
|
71
71
|
onResolve: (value: number) => void
|
|
72
|
-
):
|
|
73
|
-
name: 'uses-
|
|
72
|
+
): Gate => ({
|
|
73
|
+
name: 'uses-provision',
|
|
74
74
|
wrap(_trail, impl) {
|
|
75
75
|
return async (input, ctx) => {
|
|
76
|
-
onResolve(ctx.
|
|
76
|
+
onResolve(ctx.provision<{ value: number }>(counter).value);
|
|
77
77
|
return await impl(input, ctx);
|
|
78
78
|
};
|
|
79
79
|
},
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
const
|
|
83
|
-
|
|
82
|
+
const createSingletonProvision = (id: string, onCreate: () => number) =>
|
|
83
|
+
provision(id, {
|
|
84
84
|
create: () => Result.ok({ createdAtCall: onCreate() }),
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
const createSingletonTrail = (
|
|
88
|
-
singleton: ReturnType<typeof
|
|
88
|
+
singleton: ReturnType<typeof createSingletonProvision>
|
|
89
89
|
) =>
|
|
90
|
-
trail('
|
|
91
|
-
|
|
92
|
-
output: z.object({ createdAtCall: z.number() }),
|
|
93
|
-
run: (_input, ctx) =>
|
|
90
|
+
trail('provision.singleton', {
|
|
91
|
+
blaze: (_input, ctx) =>
|
|
94
92
|
Result.ok({
|
|
95
93
|
createdAtCall: singleton.from(ctx).createdAtCall,
|
|
96
94
|
}),
|
|
97
|
-
|
|
95
|
+
input: z.object({}),
|
|
96
|
+
output: z.object({ createdAtCall: z.number() }),
|
|
97
|
+
provisions: [singleton],
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
const unwrapExecution = async (
|
|
@@ -117,60 +117,60 @@ describe('executeTrail', () => {
|
|
|
117
117
|
expect(result.unwrap()).toEqual({ value: 'hello' });
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
test('eagerly resolves declared
|
|
121
|
-
const id =
|
|
120
|
+
test('eagerly resolves declared provisions before gates and implementation run', async () => {
|
|
121
|
+
const id = nextProvisionId('eager');
|
|
122
122
|
const captures = {
|
|
123
123
|
createCalls: 0,
|
|
124
|
-
|
|
124
|
+
gateResolved: undefined as number | undefined,
|
|
125
125
|
runResolved: undefined as number | undefined,
|
|
126
126
|
};
|
|
127
|
-
const counter =
|
|
127
|
+
const counter = createResolvedValueProvision(
|
|
128
128
|
id,
|
|
129
129
|
() => {
|
|
130
130
|
captures.createCalls += 1;
|
|
131
131
|
},
|
|
132
132
|
41
|
|
133
133
|
);
|
|
134
|
-
const layeredTrail =
|
|
134
|
+
const layeredTrail = createEagerProvisionTrail(id, counter, (value) => {
|
|
135
135
|
captures.runResolved = value;
|
|
136
136
|
});
|
|
137
|
-
const
|
|
138
|
-
captures.
|
|
137
|
+
const gate = createProvisionProbeGate(counter, (value) => {
|
|
138
|
+
captures.gateResolved = value;
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
-
const result = await executeTrail(layeredTrail, {}, {
|
|
141
|
+
const result = await executeTrail(layeredTrail, {}, { gates: [gate] });
|
|
142
142
|
|
|
143
143
|
expect(result.unwrap()).toEqual({ total: 42 });
|
|
144
144
|
expect(captures.createCalls).toBe(1);
|
|
145
|
-
expect(captures.
|
|
145
|
+
expect(captures.gateResolved).toBe(41);
|
|
146
146
|
expect(captures.runResolved).toBe(41);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
test('awaits async
|
|
150
|
-
const db =
|
|
149
|
+
test('awaits async provision factories before running the trail', async () => {
|
|
150
|
+
const db = provision(nextProvisionId('async-factory'), {
|
|
151
151
|
create: async () => {
|
|
152
152
|
await Bun.sleep(0);
|
|
153
153
|
return Result.ok({ source: 'async-factory' });
|
|
154
154
|
},
|
|
155
155
|
});
|
|
156
|
-
const
|
|
156
|
+
const provisionTrail = trail('provision.async-factory', {
|
|
157
|
+
blaze: (_input, ctx) =>
|
|
158
|
+
Result.ok({ source: db.from(ctx).source as string }),
|
|
157
159
|
input: z.object({}),
|
|
158
160
|
output: z.object({ source: z.string() }),
|
|
159
|
-
|
|
160
|
-
Result.ok({ source: db.from(ctx).source as string }),
|
|
161
|
-
services: [db],
|
|
161
|
+
provisions: [db],
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
const result = await executeTrail(
|
|
164
|
+
const result = await executeTrail(provisionTrail, {});
|
|
165
165
|
|
|
166
166
|
expect(result.isOk()).toBe(true);
|
|
167
167
|
expect(result.unwrap()).toEqual({ source: 'async-factory' });
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
-
test('reuses cached singleton
|
|
171
|
-
const id =
|
|
170
|
+
test('reuses cached singleton provisions across executions', async () => {
|
|
171
|
+
const id = nextProvisionId('singleton');
|
|
172
172
|
const captures = { createCalls: 0 };
|
|
173
|
-
const singleton =
|
|
173
|
+
const singleton = createSingletonProvision(id, () => {
|
|
174
174
|
captures.createCalls += 1;
|
|
175
175
|
return captures.createCalls;
|
|
176
176
|
});
|
|
@@ -185,17 +185,17 @@ describe('executeTrail', () => {
|
|
|
185
185
|
expect(captures.createCalls).toBe(1);
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
-
test('scopes cached singleton
|
|
189
|
-
const id =
|
|
190
|
-
const
|
|
188
|
+
test('scopes cached singleton provisions to compatible provision contexts', async () => {
|
|
189
|
+
const id = nextProvisionId('singleton-context');
|
|
190
|
+
const envAwareProvision = provision(id, {
|
|
191
191
|
create: (ctx) => Result.ok({ value: String(ctx.env?.VAL) }),
|
|
192
192
|
});
|
|
193
|
-
const envAwareTrail = trail('
|
|
193
|
+
const envAwareTrail = trail('provision.singleton-context', {
|
|
194
|
+
blaze: (_input, ctx) =>
|
|
195
|
+
Result.ok({ value: envAwareProvision.from(ctx).value }),
|
|
194
196
|
input: z.object({}),
|
|
195
197
|
output: z.object({ value: z.string() }),
|
|
196
|
-
|
|
197
|
-
Result.ok({ value: envAwareService.from(ctx).value }),
|
|
198
|
-
services: [envAwareService],
|
|
198
|
+
provisions: [envAwareProvision],
|
|
199
199
|
});
|
|
200
200
|
|
|
201
201
|
const first = await executeTrail(
|
|
@@ -233,11 +233,11 @@ describe('executeTrail', () => {
|
|
|
233
233
|
});
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
-
describe('
|
|
237
|
-
test('composes
|
|
236
|
+
describe('gates', () => {
|
|
237
|
+
test('composes gates around execution', async () => {
|
|
238
238
|
const log: string[] = [];
|
|
239
|
-
const
|
|
240
|
-
name: 'test-
|
|
239
|
+
const gate: Gate = {
|
|
240
|
+
name: 'test-gate',
|
|
241
241
|
wrap(_trail, impl) {
|
|
242
242
|
return async (input, ctx) => {
|
|
243
243
|
log.push('before');
|
|
@@ -251,7 +251,7 @@ describe('executeTrail', () => {
|
|
|
251
251
|
const result = await executeTrail(
|
|
252
252
|
echoTrail,
|
|
253
253
|
{ value: 'x' },
|
|
254
|
-
{
|
|
254
|
+
{ gates: [gate] }
|
|
255
255
|
);
|
|
256
256
|
|
|
257
257
|
expect(result.isOk()).toBe(true);
|
|
@@ -263,11 +263,11 @@ describe('executeTrail', () => {
|
|
|
263
263
|
test('accepts context overrides', async () => {
|
|
264
264
|
let capturedCtx: TrailContext | undefined;
|
|
265
265
|
const ctxTrail = trail('ctx-test', {
|
|
266
|
-
|
|
267
|
-
run: (_input, ctx) => {
|
|
266
|
+
blaze: (_input, ctx) => {
|
|
268
267
|
capturedCtx = ctx;
|
|
269
268
|
return Result.ok(null);
|
|
270
269
|
},
|
|
270
|
+
input: z.object({}),
|
|
271
271
|
});
|
|
272
272
|
|
|
273
273
|
await executeTrail(ctxTrail, {}, { ctx: { requestId: 'override-id' } });
|
|
@@ -275,36 +275,36 @@ describe('executeTrail', () => {
|
|
|
275
275
|
expect(capturedCtx?.requestId).toBe('override-id');
|
|
276
276
|
});
|
|
277
277
|
|
|
278
|
-
test('accepts
|
|
278
|
+
test('accepts abortSignal override', async () => {
|
|
279
279
|
let capturedSignal: AbortSignal | undefined;
|
|
280
280
|
const sigTrail = trail('sig-test', {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
capturedSignal = ctx.signal;
|
|
281
|
+
blaze: (_input, ctx) => {
|
|
282
|
+
capturedSignal = ctx.abortSignal;
|
|
284
283
|
return Result.ok(null);
|
|
285
284
|
},
|
|
285
|
+
input: z.object({}),
|
|
286
286
|
});
|
|
287
287
|
|
|
288
|
-
const
|
|
289
|
-
await executeTrail(sigTrail, {}, {
|
|
288
|
+
const abortSignal = AbortSignal.timeout(9999);
|
|
289
|
+
await executeTrail(sigTrail, {}, { abortSignal });
|
|
290
290
|
|
|
291
|
-
expect(capturedSignal).toBe(
|
|
291
|
+
expect(capturedSignal).toBe(abortSignal);
|
|
292
292
|
});
|
|
293
293
|
|
|
294
294
|
test('accepts context factory', async () => {
|
|
295
295
|
let capturedCtx: TrailContext | undefined;
|
|
296
296
|
const ctxTrail = trail('factory-test', {
|
|
297
|
-
|
|
298
|
-
run: (_input, ctx) => {
|
|
297
|
+
blaze: (_input, ctx) => {
|
|
299
298
|
capturedCtx = ctx;
|
|
300
299
|
return Result.ok(null);
|
|
301
300
|
},
|
|
301
|
+
input: z.object({}),
|
|
302
302
|
});
|
|
303
303
|
|
|
304
304
|
const customCtx: TrailContextInit = {
|
|
305
|
+
abortSignal: new AbortController().signal,
|
|
305
306
|
cwd: '/custom',
|
|
306
307
|
requestId: 'factory-id',
|
|
307
|
-
signal: new AbortController().signal,
|
|
308
308
|
};
|
|
309
309
|
|
|
310
310
|
await executeTrail(ctxTrail, {}, { createContext: () => customCtx });
|
|
@@ -316,17 +316,17 @@ describe('executeTrail', () => {
|
|
|
316
316
|
test('context factory + ctx overrides merge correctly', async () => {
|
|
317
317
|
let capturedCtx: TrailContext | undefined;
|
|
318
318
|
const ctxTrail = trail('merge-test', {
|
|
319
|
-
|
|
320
|
-
run: (_input, ctx) => {
|
|
319
|
+
blaze: (_input, ctx) => {
|
|
321
320
|
capturedCtx = ctx;
|
|
322
321
|
return Result.ok(null);
|
|
323
322
|
},
|
|
323
|
+
input: z.object({}),
|
|
324
324
|
});
|
|
325
325
|
|
|
326
326
|
const baseCtx: TrailContextInit = {
|
|
327
|
+
abortSignal: new AbortController().signal,
|
|
327
328
|
cwd: '/factory',
|
|
328
329
|
requestId: 'factory-id',
|
|
329
|
-
signal: new AbortController().signal,
|
|
330
330
|
};
|
|
331
331
|
|
|
332
332
|
await executeTrail(
|
|
@@ -345,12 +345,12 @@ describe('executeTrail', () => {
|
|
|
345
345
|
test('deep-merges extensions from factory and overrides', async () => {
|
|
346
346
|
let captured: TrailContext | undefined;
|
|
347
347
|
const t = trail('ext.test', {
|
|
348
|
-
|
|
349
|
-
output: z.object({}),
|
|
350
|
-
run: (_input, ctx) => {
|
|
348
|
+
blaze: (_input, ctx) => {
|
|
351
349
|
captured = ctx;
|
|
352
350
|
return Result.ok({});
|
|
353
351
|
},
|
|
352
|
+
input: z.object({}),
|
|
353
|
+
output: z.object({}),
|
|
354
354
|
});
|
|
355
355
|
await executeTrail(
|
|
356
356
|
t,
|
|
@@ -364,23 +364,23 @@ describe('executeTrail', () => {
|
|
|
364
364
|
expect(captured?.extensions).toEqual({ store: 'db', userId: '123' });
|
|
365
365
|
});
|
|
366
366
|
|
|
367
|
-
test('rebinds ctx.
|
|
367
|
+
test('rebinds ctx.provision after merging extension overrides from createContext', async () => {
|
|
368
368
|
let resolvedSource: string | undefined;
|
|
369
|
-
const db =
|
|
369
|
+
const db = provision(nextProvisionId('context-override'), {
|
|
370
370
|
create: () => Result.ok({ source: 'factory' }),
|
|
371
371
|
});
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
run: (_input, ctx) => {
|
|
376
|
-
resolvedSource = ctx.service<{ source: string }>(db).source;
|
|
372
|
+
const provisionTrail = trail('ctx.provision.override', {
|
|
373
|
+
blaze: (_input, ctx) => {
|
|
374
|
+
resolvedSource = ctx.provision<{ source: string }>(db).source;
|
|
377
375
|
return Result.ok({ source: resolvedSource });
|
|
378
376
|
},
|
|
379
|
-
|
|
377
|
+
input: z.object({}),
|
|
378
|
+
output: z.object({ source: z.string() }),
|
|
379
|
+
provisions: [db],
|
|
380
380
|
});
|
|
381
381
|
|
|
382
382
|
const result = await executeTrail(
|
|
383
|
-
|
|
383
|
+
provisionTrail,
|
|
384
384
|
{},
|
|
385
385
|
{
|
|
386
386
|
createContext: () =>
|
|
@@ -401,17 +401,17 @@ describe('executeTrail', () => {
|
|
|
401
401
|
expect(resolvedSource).toBe('override-context');
|
|
402
402
|
});
|
|
403
403
|
|
|
404
|
-
test('context factory seeds the
|
|
404
|
+
test('context factory seeds the provision accessor when omitted', async () => {
|
|
405
405
|
let capturedCtx: TrailContext | undefined;
|
|
406
|
-
const id =
|
|
406
|
+
const id = nextProvisionId('factory-seed');
|
|
407
407
|
const widget = { id: 'widget-1' };
|
|
408
|
-
const widgetTrail = trail('
|
|
409
|
-
|
|
410
|
-
run: (_input, ctx) => {
|
|
408
|
+
const widgetTrail = trail('provision.factory-seed', {
|
|
409
|
+
blaze: (_input, ctx) => {
|
|
411
410
|
capturedCtx = ctx;
|
|
412
411
|
return Result.ok(null);
|
|
413
412
|
},
|
|
414
|
-
|
|
413
|
+
input: z.object({}),
|
|
414
|
+
provisions: [],
|
|
415
415
|
});
|
|
416
416
|
|
|
417
417
|
await executeTrail(
|
|
@@ -419,14 +419,14 @@ describe('executeTrail', () => {
|
|
|
419
419
|
{},
|
|
420
420
|
{
|
|
421
421
|
createContext: () => ({
|
|
422
|
+
abortSignal: new AbortController().signal,
|
|
422
423
|
extensions: { [id]: widget },
|
|
423
|
-
requestId: 'seeded-
|
|
424
|
-
signal: new AbortController().signal,
|
|
424
|
+
requestId: 'seeded-provision',
|
|
425
425
|
}),
|
|
426
426
|
}
|
|
427
427
|
);
|
|
428
428
|
|
|
429
|
-
expect(capturedCtx?.
|
|
429
|
+
expect(capturedCtx?.provision(id)).toBe(widget);
|
|
430
430
|
});
|
|
431
431
|
});
|
|
432
432
|
|
|
@@ -447,22 +447,22 @@ describe('executeTrail', () => {
|
|
|
447
447
|
expect(result.error.message).toBe('kaboom');
|
|
448
448
|
});
|
|
449
449
|
|
|
450
|
-
test('short-circuits when a
|
|
451
|
-
const
|
|
450
|
+
test('short-circuits when a provision factory returns Result.err', async () => {
|
|
451
|
+
const failingProvision = provision(nextProvisionId('factory-error'), {
|
|
452
452
|
create: () =>
|
|
453
453
|
Result.err(new ValidationError('DATABASE_URL is required')),
|
|
454
454
|
});
|
|
455
455
|
let ran = false;
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
run: () => {
|
|
456
|
+
const provisionTrail = trail('provision.factory-error', {
|
|
457
|
+
blaze: () => {
|
|
459
458
|
ran = true;
|
|
460
459
|
return Result.ok(null);
|
|
461
460
|
},
|
|
462
|
-
|
|
461
|
+
input: z.object({}),
|
|
462
|
+
provisions: [failingProvision],
|
|
463
463
|
});
|
|
464
464
|
|
|
465
|
-
const result = await executeTrail(
|
|
465
|
+
const result = await executeTrail(provisionTrail, {});
|
|
466
466
|
|
|
467
467
|
expect(result.isErr()).toBe(true);
|
|
468
468
|
expect(result.error).toBeInstanceOf(ValidationError);
|
|
@@ -470,48 +470,50 @@ describe('executeTrail', () => {
|
|
|
470
470
|
expect(ran).toBe(false);
|
|
471
471
|
});
|
|
472
472
|
|
|
473
|
-
test('wraps thrown
|
|
474
|
-
const
|
|
473
|
+
test('wraps thrown provision factory exceptions with the provision ID in context', async () => {
|
|
474
|
+
const explodingProvision = provision(nextProvisionId('factory-throw'), {
|
|
475
475
|
create: () => {
|
|
476
476
|
throw new Error('boom');
|
|
477
477
|
},
|
|
478
478
|
});
|
|
479
|
-
const
|
|
479
|
+
const provisionTrail = trail('provision.factory-throw', {
|
|
480
|
+
blaze: () => Result.ok(null),
|
|
480
481
|
input: z.object({}),
|
|
481
|
-
|
|
482
|
-
services: [explodingService],
|
|
482
|
+
provisions: [explodingProvision],
|
|
483
483
|
});
|
|
484
484
|
|
|
485
|
-
const result = await executeTrail(
|
|
485
|
+
const result = await executeTrail(provisionTrail, {});
|
|
486
486
|
|
|
487
487
|
expect(result.isErr()).toBe(true);
|
|
488
488
|
expect(result.error).toBeInstanceOf(InternalError);
|
|
489
|
-
expect(result.error.message).toContain(
|
|
490
|
-
expect(result.error.context).toEqual({
|
|
489
|
+
expect(result.error.message).toContain(explodingProvision.id);
|
|
490
|
+
expect(result.error.context).toEqual({
|
|
491
|
+
provisionId: explodingProvision.id,
|
|
492
|
+
});
|
|
491
493
|
});
|
|
492
494
|
|
|
493
|
-
test('prefers explicit
|
|
494
|
-
const id =
|
|
495
|
+
test('prefers explicit provision overrides over cached or created instances', async () => {
|
|
496
|
+
const id = nextProvisionId('override');
|
|
495
497
|
let createCalls = 0;
|
|
496
|
-
const db =
|
|
498
|
+
const db = provision(id, {
|
|
497
499
|
create: () => {
|
|
498
500
|
createCalls += 1;
|
|
499
501
|
return Result.ok({ source: 'factory' });
|
|
500
502
|
},
|
|
501
503
|
});
|
|
502
|
-
const
|
|
504
|
+
const provisionTrail = trail('provision.override', {
|
|
505
|
+
blaze: (_input, ctx) =>
|
|
506
|
+
Result.ok({ source: db.from(ctx).source as string }),
|
|
503
507
|
input: z.object({}),
|
|
504
508
|
output: z.object({ source: z.string() }),
|
|
505
|
-
|
|
506
|
-
Result.ok({ source: db.from(ctx).source as string }),
|
|
507
|
-
services: [db],
|
|
509
|
+
provisions: [db],
|
|
508
510
|
});
|
|
509
511
|
|
|
510
512
|
const result = await executeTrail(
|
|
511
|
-
|
|
513
|
+
provisionTrail,
|
|
512
514
|
{},
|
|
513
515
|
{
|
|
514
|
-
|
|
516
|
+
provisions: { [id]: { source: 'override' } },
|
|
515
517
|
}
|
|
516
518
|
);
|
|
517
519
|
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
/* oxlint-disable require-await --
|
|
1
|
+
/* oxlint-disable require-await -- gate wrappers satisfy async interfaces without awaiting */
|
|
2
2
|
import { describe, test, expect } from 'bun:test';
|
|
3
3
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
6
6
|
import { createTrailContext } from '../context';
|
|
7
|
-
import {
|
|
8
|
-
import type {
|
|
7
|
+
import { composeGates } from '../gate';
|
|
8
|
+
import type { Gate } from '../gate';
|
|
9
9
|
import { Result } from '../result';
|
|
10
10
|
import { trail } from '../trail';
|
|
11
11
|
import type { TrailContext } from '../types';
|
|
12
12
|
|
|
13
13
|
const stubCtx: TrailContext = createTrailContext({
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
abortSignal: AbortSignal.timeout(5000),
|
|
15
|
+
requestId: 'test-gate',
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
const echoTrail = trail('echo', {
|
|
19
|
+
blaze: (input) => Result.ok({ value: input.value }),
|
|
19
20
|
input: z.object({ value: z.string() }),
|
|
20
|
-
|
|
21
|
+
meta: { domain: 'test' },
|
|
21
22
|
output: z.object({ value: z.string() }),
|
|
22
|
-
run: (input) => Result.ok({ value: input.value }),
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
|
-
//
|
|
26
|
+
// Gate tests
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
|
|
29
|
-
describe('
|
|
30
|
-
test('single
|
|
31
|
-
const
|
|
29
|
+
describe('Gate', () => {
|
|
30
|
+
test('single gate wraps implementation', async () => {
|
|
31
|
+
const prefixGate: Gate = {
|
|
32
32
|
name: 'prefix',
|
|
33
33
|
wrap(_trail, impl) {
|
|
34
34
|
return async (input, ctx) => {
|
|
@@ -41,17 +41,17 @@ describe('Layer', () => {
|
|
|
41
41
|
},
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
const wrapped =
|
|
44
|
+
const wrapped = composeGates([prefixGate], echoTrail, echoTrail.blaze);
|
|
45
45
|
const result = await wrapped({ value: 'hello' }, stubCtx);
|
|
46
46
|
|
|
47
47
|
expect(result.isOk()).toBe(true);
|
|
48
48
|
expect(result.unwrap()).toEqual({ value: 'prefixed:hello' });
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
test('multiple
|
|
51
|
+
test('multiple gates compose in outermost-first order', async () => {
|
|
52
52
|
const log: string[] = [];
|
|
53
53
|
|
|
54
|
-
const outer:
|
|
54
|
+
const outer: Gate = {
|
|
55
55
|
name: 'outer',
|
|
56
56
|
wrap(_trail, impl) {
|
|
57
57
|
return async (input, ctx) => {
|
|
@@ -63,7 +63,7 @@ describe('Layer', () => {
|
|
|
63
63
|
},
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
const inner:
|
|
66
|
+
const inner: Gate = {
|
|
67
67
|
name: 'inner',
|
|
68
68
|
wrap(_trail, impl) {
|
|
69
69
|
return async (input, ctx) => {
|
|
@@ -75,7 +75,7 @@ describe('Layer', () => {
|
|
|
75
75
|
},
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
-
const wrapped =
|
|
78
|
+
const wrapped = composeGates([outer, inner], echoTrail, echoTrail.blaze);
|
|
79
79
|
await wrapped({ value: 'x' }, stubCtx);
|
|
80
80
|
|
|
81
81
|
expect(log).toEqual([
|
|
@@ -86,15 +86,15 @@ describe('Layer', () => {
|
|
|
86
86
|
]);
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
test('
|
|
90
|
-
const shortCircuit:
|
|
89
|
+
test('gate can short-circuit without calling inner implementation', async () => {
|
|
90
|
+
const shortCircuit: Gate = {
|
|
91
91
|
name: 'short-circuit',
|
|
92
92
|
wrap() {
|
|
93
93
|
return async () => Result.err(new Error('blocked'));
|
|
94
94
|
},
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
-
const wrapped =
|
|
97
|
+
const wrapped = composeGates([shortCircuit], echoTrail, echoTrail.blaze);
|
|
98
98
|
const result = await wrapped({ value: 'hello' }, stubCtx);
|
|
99
99
|
|
|
100
100
|
expect(result.isErr()).toBe(true);
|
|
@@ -102,24 +102,24 @@ describe('Layer', () => {
|
|
|
102
102
|
expect(err.error.message).toBe('blocked');
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
test('
|
|
105
|
+
test('gate can inspect trail meta', () => {
|
|
106
106
|
let capturedDomain: unknown;
|
|
107
107
|
|
|
108
|
-
const
|
|
108
|
+
const inspectGate: Gate = {
|
|
109
109
|
name: 'inspect',
|
|
110
110
|
wrap(t, impl) {
|
|
111
|
-
capturedDomain = t.
|
|
111
|
+
capturedDomain = t.meta?.['domain'];
|
|
112
112
|
return impl;
|
|
113
113
|
},
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
composeGates([inspectGate], echoTrail, echoTrail.blaze);
|
|
117
117
|
|
|
118
118
|
expect(capturedDomain).toBe('test');
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
test('empty
|
|
122
|
-
const wrapped =
|
|
123
|
-
expect(wrapped).toBe(echoTrail.
|
|
121
|
+
test('empty gates array returns implementation unchanged', () => {
|
|
122
|
+
const wrapped = composeGates([], echoTrail, echoTrail.blaze);
|
|
123
|
+
expect(wrapped).toBe(echoTrail.blaze);
|
|
124
124
|
});
|
|
125
125
|
});
|