@ontrails/core 1.0.0-beta.13 → 1.0.0-beta.15
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 +17 -0
- package/README.md +19 -14
- package/dist/branded.d.ts +1 -1
- package/dist/branded.d.ts.map +1 -1
- package/dist/branded.js +1 -1
- package/dist/branded.js.map +1 -1
- package/dist/context.d.ts +13 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +19 -3
- package/dist/context.js.map +1 -1
- package/dist/contour.d.ts +81 -0
- package/dist/contour.d.ts.map +1 -0
- package/dist/contour.js +176 -0
- package/dist/contour.js.map +1 -0
- package/dist/cross-schema.d.ts +18 -0
- package/dist/cross-schema.d.ts.map +1 -0
- package/dist/cross-schema.js +29 -0
- package/dist/cross-schema.js.map +1 -0
- package/dist/derive.d.ts +6 -0
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +29 -5
- package/dist/derive.js.map +1 -1
- package/dist/draft.d.ts +28 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +157 -0
- package/dist/draft.js.map +1 -0
- package/dist/errors.d.ts +62 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +44 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +26 -9
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +441 -20
- package/dist/execute.js.map +1 -1
- package/dist/fire.d.ts +43 -0
- package/dist/fire.d.ts.map +1 -0
- package/dist/fire.js +185 -0
- package/dist/fire.js.map +1 -0
- package/dist/index.d.ts +33 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/cross-batch.d.ts +44 -0
- package/dist/internal/cross-batch.d.ts.map +1 -0
- package/dist/internal/cross-batch.js +58 -0
- package/dist/internal/cross-batch.js.map +1 -0
- package/dist/internal/fork-ctx.d.ts +41 -0
- package/dist/internal/fork-ctx.d.ts.map +1 -0
- package/dist/internal/fork-ctx.js +47 -0
- package/dist/internal/fork-ctx.js.map +1 -0
- package/dist/internal/signal-ref.d.ts +15 -0
- package/dist/internal/signal-ref.d.ts.map +1 -0
- package/dist/internal/signal-ref.js +41 -0
- package/dist/internal/signal-ref.js.map +1 -0
- package/dist/internal/topo-saves.d.ts +53 -0
- package/dist/internal/topo-saves.d.ts.map +1 -0
- package/dist/internal/topo-saves.js +449 -0
- package/dist/internal/topo-saves.js.map +1 -0
- package/dist/internal/topo-snapshots.d.ts +57 -0
- package/dist/internal/topo-snapshots.d.ts.map +1 -0
- package/dist/internal/topo-snapshots.js +372 -0
- package/dist/internal/topo-snapshots.js.map +1 -0
- package/dist/internal/topo-store-read.d.ts +70 -0
- package/dist/internal/topo-store-read.d.ts.map +1 -0
- package/dist/internal/topo-store-read.js +221 -0
- package/dist/internal/topo-store-read.js.map +1 -0
- package/dist/internal/topo-store.d.ts +26 -0
- package/dist/internal/topo-store.d.ts.map +1 -0
- package/dist/internal/topo-store.js +621 -0
- package/dist/internal/topo-store.js.map +1 -0
- package/dist/internal/tracing.d.ts +98 -0
- package/dist/internal/tracing.d.ts.map +1 -0
- package/dist/internal/tracing.js +102 -0
- package/dist/internal/tracing.js.map +1 -0
- package/dist/internal/trails-db.d.ts +16 -0
- package/dist/internal/trails-db.d.ts.map +1 -0
- package/dist/internal/trails-db.js +118 -0
- package/dist/internal/trails-db.js.map +1 -0
- package/dist/internal/zod-wrappers.d.ts +40 -0
- package/dist/internal/zod-wrappers.d.ts.map +1 -0
- package/dist/internal/zod-wrappers.js +59 -0
- package/dist/internal/zod-wrappers.js.map +1 -0
- package/dist/layer.d.ts +22 -5
- package/dist/layer.d.ts.map +1 -1
- package/dist/layer.js +20 -3
- package/dist/layer.js.map +1 -1
- package/dist/path-security.d.ts +1 -1
- package/dist/path-security.d.ts.map +1 -1
- package/dist/path-security.js +1 -1
- package/dist/path-security.js.map +1 -1
- package/dist/resilience.d.ts +1 -1
- package/dist/resilience.d.ts.map +1 -1
- package/dist/resilience.js +3 -3
- package/dist/resilience.js.map +1 -1
- package/dist/resource-config.d.ts +22 -0
- package/dist/resource-config.d.ts.map +1 -0
- package/dist/resource-config.js +210 -0
- package/dist/resource-config.js.map +1 -0
- package/dist/resource.d.ts +74 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +61 -0
- package/dist/resource.js.map +1 -0
- package/dist/run.d.ts +4 -3
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +5 -4
- package/dist/run.js.map +1 -1
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +2 -1
- package/dist/serialization.js.map +1 -1
- package/dist/store/accessor-protocol.d.ts +51 -0
- package/dist/store/accessor-protocol.d.ts.map +1 -0
- package/dist/store/accessor-protocol.js +2 -0
- package/dist/store/accessor-protocol.js.map +1 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +2 -0
- package/dist/store/index.js.map +1 -0
- package/dist/surface-filter.d.ts +13 -0
- package/dist/surface-filter.d.ts.map +1 -0
- package/dist/surface-filter.js +76 -0
- package/dist/surface-filter.js.map +1 -0
- package/dist/topo-store.d.ts +62 -0
- package/dist/topo-store.d.ts.map +1 -0
- package/dist/topo-store.js +319 -0
- package/dist/topo-store.js.map +1 -0
- package/dist/topo.d.ts +23 -9
- package/dist/topo.d.ts.map +1 -1
- package/dist/topo.js +175 -46
- package/dist/topo.js.map +1 -1
- package/dist/trail.d.ts +86 -19
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +59 -4
- package/dist/trail.js.map +1 -1
- package/dist/trails/derive-trail.d.ts +66 -0
- package/dist/trails/derive-trail.d.ts.map +1 -0
- package/dist/trails/derive-trail.js +378 -0
- package/dist/trails/derive-trail.js.map +1 -0
- package/dist/trails/index.d.ts +5 -0
- package/dist/trails/index.d.ts.map +1 -0
- package/dist/trails/index.js +3 -0
- package/dist/trails/index.js.map +1 -0
- package/dist/trails/ingest.d.ts +24 -0
- package/dist/trails/ingest.d.ts.map +1 -0
- package/dist/trails/ingest.js +63 -0
- package/dist/trails/ingest.js.map +1 -0
- package/dist/transport-error-map.d.ts +106 -0
- package/dist/transport-error-map.d.ts.map +1 -0
- package/dist/transport-error-map.js +24 -0
- package/dist/transport-error-map.js.map +1 -0
- package/dist/type-checks.test-d.d.ts +71 -0
- package/dist/type-checks.test-d.d.ts.map +1 -0
- package/dist/type-checks.test-d.js +12 -0
- package/dist/type-checks.test-d.js.map +1 -0
- package/dist/type-utils.d.ts +11 -2
- package/dist/type-utils.d.ts.map +1 -1
- package/dist/type-utils.js.map +1 -1
- package/dist/types.d.ts +113 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validate-established-topo.d.ts +76 -0
- package/dist/validate-established-topo.d.ts.map +1 -0
- package/dist/validate-established-topo.js +43 -0
- package/dist/validate-established-topo.js.map +1 -0
- package/dist/validate-topo.d.ts.map +1 -1
- package/dist/validate-topo.js +27 -8
- package/dist/validate-topo.js.map +1 -1
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +1 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +10 -1
- package/src/__tests__/branded.test.ts +5 -5
- package/src/__tests__/context.test.ts +3 -3
- package/src/__tests__/contour.test.ts +263 -0
- package/src/__tests__/derive-trail.test.ts +668 -0
- package/src/__tests__/derive.test.ts +58 -1
- package/src/__tests__/errors.test.ts +118 -0
- package/src/__tests__/execute.test.ts +1179 -141
- package/src/__tests__/fire.test.ts +1056 -0
- package/src/__tests__/fork-ctx.test.ts +83 -0
- package/src/__tests__/ingest.test.ts +191 -0
- package/src/__tests__/{gate.test.ts → layer.test.ts} +21 -21
- package/src/__tests__/path-security.test.ts +7 -7
- package/src/__tests__/resilience.test.ts +8 -8
- package/src/__tests__/resource.test.ts +204 -0
- package/src/__tests__/run.test.ts +13 -13
- package/src/__tests__/service-config.test.ts +28 -32
- package/src/__tests__/surface-filter.test.ts +196 -0
- package/src/__tests__/topo-store-read.test.ts +256 -0
- package/src/__tests__/topo-store.test.ts +869 -0
- package/src/__tests__/topo.test.ts +350 -54
- package/src/__tests__/trail.test.ts +189 -24
- package/src/__tests__/trails-db.test.ts +198 -0
- package/src/__tests__/transport-error-map.test.ts +82 -0
- package/src/__tests__/type-utils.test.ts +29 -1
- package/src/__tests__/validate-topo.test.ts +305 -16
- package/src/__tests__/workspace.test.ts +6 -6
- package/src/__tests__/zod-wrappers.test.ts +62 -0
- package/src/branded.ts +1 -1
- package/src/context.ts +24 -4
- package/src/contour.ts +344 -0
- package/src/cross-schema.ts +36 -0
- package/src/derive.ts +39 -8
- package/src/draft.ts +338 -0
- package/src/errors.ts +62 -18
- package/src/execute.ts +840 -29
- package/src/fire.ts +295 -0
- package/src/index.ts +142 -26
- package/src/internal/cross-batch.ts +77 -0
- package/src/internal/fork-ctx.ts +69 -0
- package/src/internal/signal-ref.ts +87 -0
- package/src/internal/topo-snapshots.ts +517 -0
- package/src/internal/topo-store-read.ts +481 -0
- package/src/internal/topo-store.ts +1192 -0
- package/src/internal/tracing.ts +187 -0
- package/src/internal/trails-db.ts +187 -0
- package/src/internal/zod-wrappers.ts +78 -0
- package/src/layer.ts +50 -0
- package/src/path-security.ts +1 -1
- package/src/resilience.ts +3 -3
- package/src/{provision-config.ts → resource-config.ts} +109 -123
- package/src/resource.ts +155 -0
- package/src/run.ts +5 -4
- package/src/serialization.ts +2 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/surface-filter.ts +182 -0
- package/src/topo-store.ts +510 -0
- package/src/topo.ts +328 -57
- package/src/trail.ts +199 -31
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +146 -0
- package/src/transport-error-map.ts +56 -0
- package/src/type-checks.test-d.ts +106 -0
- package/src/type-utils.ts +17 -2
- package/src/types.ts +148 -11
- package/src/validate-established-topo.ts +63 -0
- package/src/validate-topo.ts +37 -9
- package/src/workspace.ts +1 -1
- package/tsconfig.tests.json +10 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/adapters.d.ts +0 -39
- package/dist/adapters.d.ts.map +0 -1
- package/dist/adapters.js +0 -2
- package/dist/adapters.js.map +0 -1
- package/dist/dispatch.d.ts +0 -27
- package/dist/dispatch.d.ts.map +0 -1
- package/dist/dispatch.js +0 -34
- package/dist/dispatch.js.map +0 -1
- package/dist/event.d.ts +0 -8
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -7
- package/dist/event.js.map +0 -1
- package/dist/gate.d.ts +0 -17
- package/dist/gate.d.ts.map +0 -1
- package/dist/gate.js +0 -21
- package/dist/gate.js.map +0 -1
- package/dist/health.d.ts +0 -18
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -5
- package/dist/health.js.map +0 -1
- package/dist/job.d.ts +0 -24
- package/dist/job.d.ts.map +0 -1
- package/dist/job.js +0 -17
- package/dist/job.js.map +0 -1
- package/dist/provision-config.d.ts +0 -22
- package/dist/provision-config.d.ts.map +0 -1
- package/dist/provision-config.js +0 -210
- package/dist/provision-config.js.map +0 -1
- package/dist/provision.d.ts +0 -71
- package/dist/provision.d.ts.map +0 -1
- package/dist/provision.js +0 -56
- package/dist/provision.js.map +0 -1
- package/dist/service-config.d.ts +0 -22
- package/dist/service-config.d.ts.map +0 -1
- package/dist/service-config.js +0 -210
- package/dist/service-config.js.map +0 -1
- package/dist/service.d.ts +0 -71
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -56
- package/dist/service.js.map +0 -1
- package/src/__tests__/service.test.ts +0 -197
- package/src/event.ts +0 -15
- package/src/gate.ts +0 -44
- package/src/provision.ts +0 -148
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { forkCtx } from '../internal/fork-ctx';
|
|
4
|
+
import type { CrossFn, FireFn, TrailContext } from '../types';
|
|
5
|
+
|
|
6
|
+
// Sentinel values — the helper never invokes these, so their shapes are
|
|
7
|
+
// irrelevant. What matters is identity: a forked ctx must either clear
|
|
8
|
+
// them or preserve the exact reference.
|
|
9
|
+
const noopCross = {} as unknown as CrossFn;
|
|
10
|
+
const noopFire = {} as unknown as FireFn;
|
|
11
|
+
const noopResource = {} as unknown as TrailContext['resource'];
|
|
12
|
+
|
|
13
|
+
describe('forkCtx', () => {
|
|
14
|
+
test('clears cross, fire, and resource by default', () => {
|
|
15
|
+
const parent = {
|
|
16
|
+
cross: noopCross,
|
|
17
|
+
env: { NODE_ENV: 'test' },
|
|
18
|
+
extensions: { trailhead: 'cli' },
|
|
19
|
+
fire: noopFire,
|
|
20
|
+
logger: undefined,
|
|
21
|
+
resource: noopResource,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const forked = forkCtx(parent, {});
|
|
25
|
+
|
|
26
|
+
expect(forked.cross).toBeUndefined();
|
|
27
|
+
expect(forked.fire).toBeUndefined();
|
|
28
|
+
expect(forked.resource).toBeUndefined();
|
|
29
|
+
// Non-reset keys are preserved.
|
|
30
|
+
expect(forked.env).toEqual({ NODE_ENV: 'test' });
|
|
31
|
+
expect(forked.extensions).toEqual({ trailhead: 'cli' });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('applies overrides after reset so overrides win', () => {
|
|
35
|
+
const parent = {
|
|
36
|
+
cross: noopCross,
|
|
37
|
+
env: { NODE_ENV: 'test' },
|
|
38
|
+
extensions: { a: 1 },
|
|
39
|
+
fire: noopFire,
|
|
40
|
+
resource: noopResource,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const forked = forkCtx(parent, {
|
|
44
|
+
env: { NODE_ENV: 'prod' },
|
|
45
|
+
extensions: { b: 2 },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(forked.env).toEqual({ NODE_ENV: 'prod' });
|
|
49
|
+
expect(forked.extensions).toEqual({ b: 2 });
|
|
50
|
+
expect(forked.cross).toBeUndefined();
|
|
51
|
+
expect(forked.fire).toBeUndefined();
|
|
52
|
+
expect(forked.resource).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('does not mutate the parent context', () => {
|
|
56
|
+
const parent = {
|
|
57
|
+
cross: noopCross,
|
|
58
|
+
fire: noopFire,
|
|
59
|
+
resource: noopResource,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
forkCtx(parent, {});
|
|
63
|
+
|
|
64
|
+
expect(parent.cross).toBe(noopCross);
|
|
65
|
+
expect(parent.fire).toBe(noopFire);
|
|
66
|
+
expect(parent.resource).toBe(noopResource);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('honours a caller-supplied reset list', () => {
|
|
70
|
+
const parent = {
|
|
71
|
+
cross: noopCross,
|
|
72
|
+
fire: noopFire,
|
|
73
|
+
resource: noopResource,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const forked = forkCtx(parent, {}, ['fire']);
|
|
77
|
+
|
|
78
|
+
expect(forked.fire).toBeUndefined();
|
|
79
|
+
// Only `fire` is reset when caller overrides the list.
|
|
80
|
+
expect(forked.cross).toBe(noopCross);
|
|
81
|
+
expect(forked.resource).toBe(noopResource);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/* oxlint-disable require-await -- layer wrappers satisfy async interfaces without awaiting */
|
|
2
|
+
import { describe, expect, test } from 'bun:test';
|
|
3
|
+
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
import { ValidationError } from '../errors.js';
|
|
7
|
+
import type { Layer } from '../layer.js';
|
|
8
|
+
import { Result } from '../result.js';
|
|
9
|
+
import { run } from '../run.js';
|
|
10
|
+
import { signal } from '../signal.js';
|
|
11
|
+
import { topo } from '../topo.js';
|
|
12
|
+
import { trail } from '../trail.js';
|
|
13
|
+
import { ingest } from '../trails/index.js';
|
|
14
|
+
|
|
15
|
+
const withExamples = <TSchema extends z.ZodType>(
|
|
16
|
+
schema: TSchema,
|
|
17
|
+
examples: readonly z.input<TSchema>[]
|
|
18
|
+
): TSchema & { readonly examples: readonly z.input<TSchema>[] } =>
|
|
19
|
+
Object.assign(schema, { examples });
|
|
20
|
+
|
|
21
|
+
const paymentCompleted = signal('payment.completed', {
|
|
22
|
+
payload: z.object({
|
|
23
|
+
amount: z.number(),
|
|
24
|
+
paymentId: z.string(),
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const rawPaymentEvent = z.object({
|
|
29
|
+
amount: z.number(),
|
|
30
|
+
paymentId: z.string(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const stripeEvent = z.object({
|
|
34
|
+
data: z.object({
|
|
35
|
+
object: z.object({
|
|
36
|
+
amount: z.number(),
|
|
37
|
+
id: z.string(),
|
|
38
|
+
}),
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const createConsumer = (
|
|
43
|
+
id: string,
|
|
44
|
+
capture: { payloads: unknown[] },
|
|
45
|
+
source = paymentCompleted
|
|
46
|
+
) =>
|
|
47
|
+
trail(id, {
|
|
48
|
+
blaze: (input) => {
|
|
49
|
+
capture.payloads.push(input);
|
|
50
|
+
return Result.ok({ ok: true });
|
|
51
|
+
},
|
|
52
|
+
input: source.payload,
|
|
53
|
+
on: [source],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('ingest()', () => {
|
|
57
|
+
test('derives id, intent, fires, output schema, and examples from schema examples', () => {
|
|
58
|
+
const schema = withExamples(rawPaymentEvent, [
|
|
59
|
+
{ amount: 42, paymentId: 'pay_1' },
|
|
60
|
+
{ amount: 7, paymentId: 'pay_2' },
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const paymentIngest = ingest({
|
|
64
|
+
schema,
|
|
65
|
+
signal: paymentCompleted,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(paymentIngest.id).toBe('payment.completed.ingest');
|
|
69
|
+
expect(paymentIngest.intent).toBe('write');
|
|
70
|
+
expect(paymentIngest.pattern).toBe('ingest');
|
|
71
|
+
expect(paymentIngest.fires).toEqual(['payment.completed']);
|
|
72
|
+
expect(paymentIngest.output?.safeParse().success).toBe(true);
|
|
73
|
+
expect(paymentIngest.examples).toEqual([
|
|
74
|
+
{
|
|
75
|
+
input: { amount: 42, paymentId: 'pay_1' },
|
|
76
|
+
name: 'Ingest payment.completed 1',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
input: { amount: 7, paymentId: 'pay_2' },
|
|
80
|
+
name: 'Ingest payment.completed 2',
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('validates input before attempting to emit the signal', async () => {
|
|
86
|
+
const capture = { payloads: [] as unknown[] };
|
|
87
|
+
const paymentIngest = ingest({
|
|
88
|
+
schema: rawPaymentEvent,
|
|
89
|
+
signal: paymentCompleted,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const app = topo('ingest-validation', {
|
|
93
|
+
consumer: createConsumer('payments.capture', capture),
|
|
94
|
+
paymentCompleted,
|
|
95
|
+
paymentIngest,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const result = await run(app, paymentIngest.id, {
|
|
99
|
+
amount: '42',
|
|
100
|
+
paymentId: 'pay_1',
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(result.isErr()).toBe(true);
|
|
104
|
+
expect(result.error).toBeInstanceOf(ValidationError);
|
|
105
|
+
expect(capture.payloads).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('applies the verification layer when provided', async () => {
|
|
109
|
+
const wrapped: string[] = [];
|
|
110
|
+
const verify: Layer = {
|
|
111
|
+
name: 'verify',
|
|
112
|
+
wrap: (trailDef, implementation) => async (input, ctx) => {
|
|
113
|
+
wrapped.push(trailDef.id);
|
|
114
|
+
return await implementation(input, ctx);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
const capture = { payloads: [] as unknown[] };
|
|
118
|
+
const paymentIngest = ingest({
|
|
119
|
+
schema: rawPaymentEvent,
|
|
120
|
+
signal: paymentCompleted,
|
|
121
|
+
verify,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const app = topo('ingest-verify', {
|
|
125
|
+
consumer: createConsumer('payments.capture', capture),
|
|
126
|
+
paymentCompleted,
|
|
127
|
+
paymentIngest,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const result = await run(app, paymentIngest.id, {
|
|
131
|
+
amount: 42,
|
|
132
|
+
paymentId: 'pay_1',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result.isOk()).toBe(true);
|
|
136
|
+
expect(wrapped).toEqual(['payment.completed.ingest']);
|
|
137
|
+
expect(capture.payloads).toEqual([{ amount: 42, paymentId: 'pay_1' }]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('emits the declared signal after successful ingestion', async () => {
|
|
141
|
+
const capture = { payloads: [] as unknown[] };
|
|
142
|
+
const paymentIngest = ingest({
|
|
143
|
+
schema: rawPaymentEvent,
|
|
144
|
+
signal: paymentCompleted,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const app = topo('ingest-fire', {
|
|
148
|
+
consumer: createConsumer('payments.capture', capture),
|
|
149
|
+
paymentCompleted,
|
|
150
|
+
paymentIngest,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const result = await run(app, paymentIngest.id, {
|
|
154
|
+
amount: 42,
|
|
155
|
+
paymentId: 'pay_1',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.isOk()).toBe(true);
|
|
159
|
+
expect(capture.payloads).toEqual([{ amount: 42, paymentId: 'pay_1' }]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('applies the transform before emitting the signal payload', async () => {
|
|
163
|
+
const capture = { payloads: [] as unknown[] };
|
|
164
|
+
const paymentIngest = ingest({
|
|
165
|
+
schema: stripeEvent,
|
|
166
|
+
signal: paymentCompleted,
|
|
167
|
+
transform: (payload) => ({
|
|
168
|
+
amount: payload.data.object.amount,
|
|
169
|
+
paymentId: payload.data.object.id,
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const app = topo('ingest-transform', {
|
|
174
|
+
consumer: createConsumer('payments.capture', capture),
|
|
175
|
+
paymentCompleted,
|
|
176
|
+
paymentIngest,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const result = await run(app, paymentIngest.id, {
|
|
180
|
+
data: {
|
|
181
|
+
object: {
|
|
182
|
+
amount: 99,
|
|
183
|
+
id: 'pay_99',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(result.isOk()).toBe(true);
|
|
189
|
+
expect(capture.payloads).toEqual([{ amount: 99, paymentId: 'pay_99' }]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
/* oxlint-disable require-await --
|
|
1
|
+
/* oxlint-disable require-await -- layer 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 { composeLayers } from '../layer';
|
|
8
|
+
import type { Layer } from '../layer';
|
|
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
14
|
abortSignal: AbortSignal.timeout(5000),
|
|
15
|
-
requestId: 'test-
|
|
15
|
+
requestId: 'test-layer',
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
const echoTrail = trail('echo', {
|
|
@@ -23,12 +23,12 @@ const echoTrail = trail('echo', {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
|
-
//
|
|
26
|
+
// Layer tests
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
|
|
29
|
-
describe('
|
|
30
|
-
test('single
|
|
31
|
-
const prefixGate:
|
|
29
|
+
describe('Layer', () => {
|
|
30
|
+
test('single layer wraps implementation', async () => {
|
|
31
|
+
const prefixGate: Layer = {
|
|
32
32
|
name: 'prefix',
|
|
33
33
|
wrap(_trail, impl) {
|
|
34
34
|
return async (input, ctx) => {
|
|
@@ -41,17 +41,17 @@ describe('Gate', () => {
|
|
|
41
41
|
},
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
const wrapped =
|
|
44
|
+
const wrapped = composeLayers([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 layers compose in outermost-first order', async () => {
|
|
52
52
|
const log: string[] = [];
|
|
53
53
|
|
|
54
|
-
const outer:
|
|
54
|
+
const outer: Layer = {
|
|
55
55
|
name: 'outer',
|
|
56
56
|
wrap(_trail, impl) {
|
|
57
57
|
return async (input, ctx) => {
|
|
@@ -63,7 +63,7 @@ describe('Gate', () => {
|
|
|
63
63
|
},
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
const inner:
|
|
66
|
+
const inner: Layer = {
|
|
67
67
|
name: 'inner',
|
|
68
68
|
wrap(_trail, impl) {
|
|
69
69
|
return async (input, ctx) => {
|
|
@@ -75,7 +75,7 @@ describe('Gate', () => {
|
|
|
75
75
|
},
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
-
const wrapped =
|
|
78
|
+
const wrapped = composeLayers([outer, inner], echoTrail, echoTrail.blaze);
|
|
79
79
|
await wrapped({ value: 'x' }, stubCtx);
|
|
80
80
|
|
|
81
81
|
expect(log).toEqual([
|
|
@@ -86,15 +86,15 @@ describe('Gate', () => {
|
|
|
86
86
|
]);
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
test('
|
|
90
|
-
const shortCircuit:
|
|
89
|
+
test('layer can short-circuit without calling inner implementation', async () => {
|
|
90
|
+
const shortCircuit: Layer = {
|
|
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 = composeLayers([shortCircuit], echoTrail, echoTrail.blaze);
|
|
98
98
|
const result = await wrapped({ value: 'hello' }, stubCtx);
|
|
99
99
|
|
|
100
100
|
expect(result.isErr()).toBe(true);
|
|
@@ -102,10 +102,10 @@ describe('Gate', () => {
|
|
|
102
102
|
expect(err.error.message).toBe('blocked');
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
test('
|
|
105
|
+
test('layer can inspect trail meta', () => {
|
|
106
106
|
let capturedDomain: unknown;
|
|
107
107
|
|
|
108
|
-
const inspectGate:
|
|
108
|
+
const inspectGate: Layer = {
|
|
109
109
|
name: 'inspect',
|
|
110
110
|
wrap(t, impl) {
|
|
111
111
|
capturedDomain = t.meta?.['domain'];
|
|
@@ -113,13 +113,13 @@ describe('Gate', () => {
|
|
|
113
113
|
},
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
composeLayers([inspectGate], echoTrail, echoTrail.blaze);
|
|
117
117
|
|
|
118
118
|
expect(capturedDomain).toBe('test');
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
test('empty
|
|
122
|
-
const wrapped =
|
|
121
|
+
test('empty layers array returns implementation unchanged', () => {
|
|
122
|
+
const wrapped = composeLayers([], echoTrail, echoTrail.blaze);
|
|
123
123
|
expect(wrapped).toBe(echoTrail.blaze);
|
|
124
124
|
});
|
|
125
125
|
});
|
|
@@ -2,7 +2,7 @@ import { describe, test, expect } from 'bun:test';
|
|
|
2
2
|
import { resolve, join } from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { PermissionError } from '../errors.js';
|
|
5
|
-
import { securePath, isPathSafe,
|
|
5
|
+
import { securePath, isPathSafe, deriveSafePath } from '../path-security.js';
|
|
6
6
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// securePath
|
|
@@ -81,33 +81,33 @@ describe('isPathSafe', () => {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
// ---------------------------------------------------------------------------
|
|
84
|
-
//
|
|
84
|
+
// deriveSafePath
|
|
85
85
|
// ---------------------------------------------------------------------------
|
|
86
86
|
|
|
87
|
-
describe('
|
|
87
|
+
describe('deriveSafePath', () => {
|
|
88
88
|
const base = '/project/workspace';
|
|
89
89
|
|
|
90
90
|
test('joins multiple segments safely', () => {
|
|
91
|
-
const result =
|
|
91
|
+
const result = deriveSafePath(base, 'src', 'lib', 'utils.ts');
|
|
92
92
|
expect(result.isOk()).toBe(true);
|
|
93
93
|
expect(result.unwrap()).toBe(join(resolve(base), 'src', 'lib', 'utils.ts'));
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
test('rejects when segments escape the base', () => {
|
|
97
|
-
const result =
|
|
97
|
+
const result = deriveSafePath(base, 'src', '../../../etc');
|
|
98
98
|
expect(result.isErr()).toBe(true);
|
|
99
99
|
const err = result as unknown as { error: PermissionError };
|
|
100
100
|
expect(err.error).toBeInstanceOf(PermissionError);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
test('handles single segment', () => {
|
|
104
|
-
const result =
|
|
104
|
+
const result = deriveSafePath(base, 'file.txt');
|
|
105
105
|
expect(result.isOk()).toBe(true);
|
|
106
106
|
expect(result.unwrap()).toBe(resolve(base, 'file.txt'));
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
test('handles empty segments', () => {
|
|
110
|
-
const result =
|
|
110
|
+
const result = deriveSafePath(base);
|
|
111
111
|
expect(result.isOk()).toBe(true);
|
|
112
112
|
expect(result.unwrap()).toBe(resolve(base));
|
|
113
113
|
});
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
retry,
|
|
12
12
|
withTimeout,
|
|
13
13
|
shouldRetry,
|
|
14
|
-
|
|
14
|
+
deriveBackoffDelay,
|
|
15
15
|
} from '../resilience.js';
|
|
16
16
|
import { Result } from '../result.js';
|
|
17
17
|
|
|
@@ -37,20 +37,20 @@ describe('shouldRetry', () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
// ---------------------------------------------------------------------------
|
|
40
|
-
//
|
|
40
|
+
// deriveBackoffDelay
|
|
41
41
|
// ---------------------------------------------------------------------------
|
|
42
42
|
|
|
43
|
-
describe('
|
|
43
|
+
describe('deriveBackoffDelay', () => {
|
|
44
44
|
test('returns a number >= 0', () => {
|
|
45
45
|
for (let i = 0; i < 20; i += 1) {
|
|
46
|
-
const delay =
|
|
46
|
+
const delay = deriveBackoffDelay(0);
|
|
47
47
|
expect(delay).toBeGreaterThanOrEqual(0);
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
test('respects maxDelay cap', () => {
|
|
52
52
|
for (let i = 0; i < 20; i += 1) {
|
|
53
|
-
const delay =
|
|
53
|
+
const delay = deriveBackoffDelay(10, { maxDelay: 100 });
|
|
54
54
|
expect(delay).toBeLessThanOrEqual(100);
|
|
55
55
|
}
|
|
56
56
|
});
|
|
@@ -59,7 +59,7 @@ describe('getBackoffDelay', () => {
|
|
|
59
59
|
// With attempt=0, exponential = base * factor^0 = base
|
|
60
60
|
// Jitter is [0, base], so delay <= base
|
|
61
61
|
for (let i = 0; i < 20; i += 1) {
|
|
62
|
-
const delay =
|
|
62
|
+
const delay = deriveBackoffDelay(0, { backoffFactor: 3, baseDelay: 500 });
|
|
63
63
|
expect(delay).toBeLessThanOrEqual(500);
|
|
64
64
|
}
|
|
65
65
|
});
|
|
@@ -67,10 +67,10 @@ describe('getBackoffDelay', () => {
|
|
|
67
67
|
test('scales with attempt number', () => {
|
|
68
68
|
// Higher attempts should have higher caps (on average)
|
|
69
69
|
const samples0 = Array.from({ length: 100 }, () =>
|
|
70
|
-
|
|
70
|
+
deriveBackoffDelay(0, { baseDelay: 100 })
|
|
71
71
|
);
|
|
72
72
|
const samples3 = Array.from({ length: 100 }, () =>
|
|
73
|
-
|
|
73
|
+
deriveBackoffDelay(3, { baseDelay: 100 })
|
|
74
74
|
);
|
|
75
75
|
const avg0 = samples0.reduce((a, b) => a + b, 0) / samples0.length;
|
|
76
76
|
const avg3 = samples3.reduce((a, b) => a + b, 0) / samples3.length;
|