@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
|
@@ -2,37 +2,66 @@ import { describe, test, expect } from 'bun:test';
|
|
|
2
2
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
|
+
import { contour } from '../contour.js';
|
|
5
6
|
import { ValidationError } from '../errors.js';
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
attachLateBoundSignalRef,
|
|
9
|
+
cloneSignalWithId,
|
|
10
|
+
} from '../internal/signal-ref.js';
|
|
11
|
+
import { resource } from '../resource.js';
|
|
7
12
|
import { Result } from '../result.js';
|
|
13
|
+
import { signal } from '../signal.js';
|
|
14
|
+
import { trail } from '../trail.js';
|
|
8
15
|
import { topo } from '../topo.js';
|
|
9
16
|
|
|
10
17
|
// ---------------------------------------------------------------------------
|
|
11
18
|
// Mock factories
|
|
12
19
|
// ---------------------------------------------------------------------------
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
const mockContour = (name: string) =>
|
|
22
|
+
contour(
|
|
23
|
+
name,
|
|
24
|
+
{
|
|
25
|
+
id: z.string().uuid(),
|
|
26
|
+
value: z.string(),
|
|
27
|
+
},
|
|
28
|
+
{ identity: 'id' }
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const mockTrail = (
|
|
32
|
+
id: string,
|
|
33
|
+
crosses?: readonly string[],
|
|
34
|
+
contours?: readonly ReturnType<typeof mockContour>[]
|
|
35
|
+
) =>
|
|
36
|
+
trail(id, {
|
|
37
|
+
blaze: () => Result.ok({ y: 0 }),
|
|
38
|
+
contours,
|
|
39
|
+
crosses,
|
|
40
|
+
input: z.object({ x: z.number() }),
|
|
41
|
+
output: z.object({ y: z.number() }),
|
|
42
|
+
});
|
|
16
43
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
input: z.object({ x: z.number() }),
|
|
22
|
-
kind: 'trail' as const,
|
|
23
|
-
output: z.object({ y: z.number() }),
|
|
24
|
-
});
|
|
44
|
+
const mockEvent = (id: string) =>
|
|
45
|
+
signal(id, {
|
|
46
|
+
payload: z.object({ payload: z.string() }),
|
|
47
|
+
});
|
|
25
48
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
49
|
+
const mockSignalConsumer = (on: readonly ReturnType<typeof mockEvent>[]) =>
|
|
50
|
+
trail('notify.users', {
|
|
51
|
+
blaze: () => Result.ok({ ok: true }),
|
|
52
|
+
input: z.object({ payload: z.string() }),
|
|
53
|
+
on,
|
|
54
|
+
output: z.object({ ok: z.boolean() }),
|
|
55
|
+
});
|
|
31
56
|
|
|
32
|
-
const
|
|
33
|
-
|
|
57
|
+
const mockResource = (
|
|
58
|
+
id: string,
|
|
59
|
+
signals?: readonly ReturnType<typeof mockEvent>[]
|
|
60
|
+
) =>
|
|
61
|
+
resource(id, {
|
|
34
62
|
create: () => Result.ok({ id }),
|
|
35
|
-
description: `${id}
|
|
63
|
+
description: `${id} resource`,
|
|
64
|
+
signals,
|
|
36
65
|
});
|
|
37
66
|
|
|
38
67
|
// ---------------------------------------------------------------------------
|
|
@@ -40,7 +69,21 @@ const mockProvision = (id: string) =>
|
|
|
40
69
|
// ---------------------------------------------------------------------------
|
|
41
70
|
|
|
42
71
|
describe('topo', () => {
|
|
43
|
-
describe('
|
|
72
|
+
describe('identity', () => {
|
|
73
|
+
test('preserves object identity metadata', () => {
|
|
74
|
+
const t = topo({
|
|
75
|
+
description: 'Demo topo',
|
|
76
|
+
name: 'my-app',
|
|
77
|
+
version: '1.2.3',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(t.name).toBe('my-app');
|
|
81
|
+
expect(t.version).toBe('1.2.3');
|
|
82
|
+
expect(t.description).toBe('Demo topo');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('trail collection', () => {
|
|
44
87
|
test('returns Topo with name', () => {
|
|
45
88
|
const t = topo('my-app');
|
|
46
89
|
expect(t.name).toBe('my-app');
|
|
@@ -57,7 +100,7 @@ describe('topo', () => {
|
|
|
57
100
|
test('auto-scans exports by kind discriminant', () => {
|
|
58
101
|
const mod = {
|
|
59
102
|
event1: mockEvent('e1'),
|
|
60
|
-
|
|
103
|
+
resource1: mockResource('s1'),
|
|
61
104
|
trail1: mockTrail('t1'),
|
|
62
105
|
trail2: mockTrail('t2', ['t1']),
|
|
63
106
|
};
|
|
@@ -65,7 +108,7 @@ describe('topo', () => {
|
|
|
65
108
|
|
|
66
109
|
expect(t.trails.size).toBe(2);
|
|
67
110
|
expect(t.signals.size).toBe(1);
|
|
68
|
-
expect(t.
|
|
111
|
+
expect(t.resources.size).toBe(1);
|
|
69
112
|
});
|
|
70
113
|
|
|
71
114
|
test('collects from multiple modules', () => {
|
|
@@ -90,7 +133,7 @@ describe('topo', () => {
|
|
|
90
133
|
|
|
91
134
|
expect(t.trails.size).toBe(1);
|
|
92
135
|
expect(t.signals.size).toBe(0);
|
|
93
|
-
expect(t.
|
|
136
|
+
expect(t.resources.size).toBe(0);
|
|
94
137
|
});
|
|
95
138
|
|
|
96
139
|
test('trail with crossings registers correctly', () => {
|
|
@@ -101,13 +144,212 @@ describe('topo', () => {
|
|
|
101
144
|
const registered = t.trails.get('trail-1');
|
|
102
145
|
expect(registered?.crosses).toEqual(['trail-2']);
|
|
103
146
|
});
|
|
147
|
+
});
|
|
104
148
|
|
|
105
|
-
|
|
106
|
-
|
|
149
|
+
describe('resource and signal collection', () => {
|
|
150
|
+
test('collects resources from modules', () => {
|
|
151
|
+
const mod = { db: mockResource('db.main') };
|
|
107
152
|
const t = topo('app', mod);
|
|
108
153
|
|
|
109
|
-
expect(t.
|
|
110
|
-
expect(t.
|
|
154
|
+
expect(t.resources.size).toBe(1);
|
|
155
|
+
expect(t.resources.get('db.main')).toBe(mod.db);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('registers signals declared on resources into the topo graph', () => {
|
|
159
|
+
const usersCreated = mockEvent('users.created');
|
|
160
|
+
const t = topo('app', {
|
|
161
|
+
db: mockResource('db.main', [usersCreated]),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(t.signals.size).toBe(1);
|
|
165
|
+
expect(t.listSignals()).toContain(usersCreated);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('resolves late-bound store signal refs from resource-scoped signals', () => {
|
|
169
|
+
const authored = attachLateBoundSignalRef(mockEvent('users.created'), {
|
|
170
|
+
kind: 'store-derived',
|
|
171
|
+
token: 'users-created',
|
|
172
|
+
});
|
|
173
|
+
const consumer = mockSignalConsumer([authored]);
|
|
174
|
+
const scoped = cloneSignalWithId(authored, 'identity:users.created');
|
|
175
|
+
const t = topo('app', {
|
|
176
|
+
consumer,
|
|
177
|
+
identity: mockResource('identity', [scoped]),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(t.get('notify.users')?.on).toEqual(['identity:users.created']);
|
|
181
|
+
expect(t.listSignals().map((s) => s.id)).toContain(
|
|
182
|
+
'identity:users.created'
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('preserves canonical scoped signal ids across multi-binding stores', () => {
|
|
187
|
+
// Regression test: the same store definition bound under two resources
|
|
188
|
+
// (identity + billing). A trail that registers under one resource and
|
|
189
|
+
// another that registers under the other must resolve to the distinct
|
|
190
|
+
// canonical scoped ids, not collide on a shared late-bound token.
|
|
191
|
+
const authored = attachLateBoundSignalRef(mockEvent('users.created'), {
|
|
192
|
+
kind: 'store-derived',
|
|
193
|
+
token: 'users-created-multi',
|
|
194
|
+
});
|
|
195
|
+
const identityScoped = cloneSignalWithId(
|
|
196
|
+
authored,
|
|
197
|
+
'identity:users.created'
|
|
198
|
+
);
|
|
199
|
+
const billingScoped = cloneSignalWithId(
|
|
200
|
+
authored,
|
|
201
|
+
'billing:users.created'
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const identityConsumer = trail('notify.identity-users', {
|
|
205
|
+
blaze: () => Result.ok({ ok: true }),
|
|
206
|
+
input: z.object({ payload: z.string() }),
|
|
207
|
+
on: [identityScoped],
|
|
208
|
+
output: z.object({ ok: z.boolean() }),
|
|
209
|
+
});
|
|
210
|
+
const billingConsumer = trail('notify.billing-users', {
|
|
211
|
+
blaze: () => Result.ok({ ok: true }),
|
|
212
|
+
input: z.object({ payload: z.string() }),
|
|
213
|
+
on: [billingScoped],
|
|
214
|
+
output: z.object({ ok: z.boolean() }),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const t = topo('app', {
|
|
218
|
+
billing: mockResource('billing', [billingScoped]),
|
|
219
|
+
billingConsumer,
|
|
220
|
+
identity: mockResource('identity', [identityScoped]),
|
|
221
|
+
identityConsumer,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(t.get('notify.identity-users')?.on).toEqual([
|
|
225
|
+
'identity:users.created',
|
|
226
|
+
]);
|
|
227
|
+
expect(t.get('notify.billing-users')?.on).toEqual([
|
|
228
|
+
'billing:users.created',
|
|
229
|
+
]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('preserves canonical scoped signal ids when resource ids contain dots', () => {
|
|
233
|
+
// Regression test: the canonical-scope predicate previously disallowed
|
|
234
|
+
// dots inside the scope segment, which collapsed dotted resource ids
|
|
235
|
+
// like `demo.store` and `other.store` back to their shared late-bound
|
|
236
|
+
// token. With `:` prohibited inside resource ids, the scope is
|
|
237
|
+
// unambiguously everything before the first `:`, so dotted scopes must
|
|
238
|
+
// resolve to distinct canonical ids.
|
|
239
|
+
const authored = attachLateBoundSignalRef(mockEvent('gists.created'), {
|
|
240
|
+
kind: 'store-derived',
|
|
241
|
+
token: 'gists-created-dotted',
|
|
242
|
+
});
|
|
243
|
+
const demoScoped = cloneSignalWithId(
|
|
244
|
+
authored,
|
|
245
|
+
'demo.store:gists.created'
|
|
246
|
+
);
|
|
247
|
+
const otherScoped = cloneSignalWithId(
|
|
248
|
+
authored,
|
|
249
|
+
'other.store:gists.created'
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const demoConsumer = trail('notify.demo-gists', {
|
|
253
|
+
blaze: () => Result.ok({ ok: true }),
|
|
254
|
+
input: z.object({ payload: z.string() }),
|
|
255
|
+
on: [demoScoped],
|
|
256
|
+
output: z.object({ ok: z.boolean() }),
|
|
257
|
+
});
|
|
258
|
+
const otherConsumer = trail('notify.other-gists', {
|
|
259
|
+
blaze: () => Result.ok({ ok: true }),
|
|
260
|
+
input: z.object({ payload: z.string() }),
|
|
261
|
+
on: [otherScoped],
|
|
262
|
+
output: z.object({ ok: z.boolean() }),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const t = topo('app', {
|
|
266
|
+
'demo.store': mockResource('demo.store', [demoScoped]),
|
|
267
|
+
demoConsumer,
|
|
268
|
+
'other.store': mockResource('other.store', [otherScoped]),
|
|
269
|
+
otherConsumer,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(t.get('notify.demo-gists')?.on).toEqual([
|
|
273
|
+
'demo.store:gists.created',
|
|
274
|
+
]);
|
|
275
|
+
expect(t.get('notify.other-gists')?.on).toEqual([
|
|
276
|
+
'other.store:gists.created',
|
|
277
|
+
]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('markerizes late-bound ids containing `:` but not in canonical scoped form', () => {
|
|
281
|
+
// Guard regression: `normalizeSignalRef` previously passed any id
|
|
282
|
+
// containing `:` through unchanged, which would let a non-canonical
|
|
283
|
+
// late-bound id like `foo:bar` slip past markerization and then fail
|
|
284
|
+
// to resolve during topo finalization. The strict predicate should
|
|
285
|
+
// still rewrite such ids onto the late-bound marker path, while
|
|
286
|
+
// leaving canonical `<scope>:<table>.<event>` ids alone.
|
|
287
|
+
const nonCanonical = attachLateBoundSignalRef(mockEvent('foo:bar'), {
|
|
288
|
+
kind: 'store-derived',
|
|
289
|
+
token: 'non-canonical',
|
|
290
|
+
});
|
|
291
|
+
const nonCanonicalConsumer = trail('notify.non-canonical', {
|
|
292
|
+
blaze: () => Result.ok({ ok: true }),
|
|
293
|
+
input: z.object({ payload: z.string() }),
|
|
294
|
+
on: [nonCanonical],
|
|
295
|
+
output: z.object({ ok: z.boolean() }),
|
|
296
|
+
});
|
|
297
|
+
expect(nonCanonicalConsumer.on).not.toEqual(['foo:bar']);
|
|
298
|
+
|
|
299
|
+
const canonical = attachLateBoundSignalRef(mockEvent('users.created'), {
|
|
300
|
+
kind: 'store-derived',
|
|
301
|
+
token: 'canonical',
|
|
302
|
+
});
|
|
303
|
+
const scoped = cloneSignalWithId(canonical, 'identity:users.created');
|
|
304
|
+
const canonicalConsumer = trail('notify.canonical', {
|
|
305
|
+
blaze: () => Result.ok({ ok: true }),
|
|
306
|
+
input: z.object({ payload: z.string() }),
|
|
307
|
+
on: [scoped],
|
|
308
|
+
output: z.object({ ok: z.boolean() }),
|
|
309
|
+
});
|
|
310
|
+
expect(canonicalConsumer.on).toEqual(['identity:users.created']);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('rejects ambiguous late-bound store signal refs', () => {
|
|
314
|
+
const authored = attachLateBoundSignalRef(mockEvent('users.created'), {
|
|
315
|
+
kind: 'store-derived',
|
|
316
|
+
token: 'users-created',
|
|
317
|
+
});
|
|
318
|
+
const consumer = mockSignalConsumer([authored]);
|
|
319
|
+
|
|
320
|
+
expect(() =>
|
|
321
|
+
topo('app', {
|
|
322
|
+
billing: mockResource('billing', [
|
|
323
|
+
cloneSignalWithId(authored, 'billing:users.created'),
|
|
324
|
+
]),
|
|
325
|
+
consumer,
|
|
326
|
+
identity: mockResource('identity', [
|
|
327
|
+
cloneSignalWithId(authored, 'identity:users.created'),
|
|
328
|
+
]),
|
|
329
|
+
})
|
|
330
|
+
).toThrow(
|
|
331
|
+
'Trail "notify.users" references late-bound signal "users.created" but it resolves to multiple bound resource signals'
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('contour collection', () => {
|
|
337
|
+
test('collects contours exported directly from modules', () => {
|
|
338
|
+
const user = mockContour('user');
|
|
339
|
+
const t = topo('app', { user });
|
|
340
|
+
|
|
341
|
+
expect(t.contours.size).toBe(1);
|
|
342
|
+
expect(t.getContour('user')).toBe(user);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('registers contours declared on trails into the topo graph', () => {
|
|
346
|
+
const user = mockContour('user');
|
|
347
|
+
const t = topo('app', {
|
|
348
|
+
createUser: mockTrail('user.create', [], [user]),
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(t.contours.size).toBe(1);
|
|
352
|
+
expect(t.getContour('user')).toBe(user);
|
|
111
353
|
});
|
|
112
354
|
});
|
|
113
355
|
|
|
@@ -132,13 +374,23 @@ describe('topo', () => {
|
|
|
132
374
|
);
|
|
133
375
|
});
|
|
134
376
|
|
|
135
|
-
test('rejects duplicate
|
|
136
|
-
const mod1 = { a:
|
|
137
|
-
const mod2 = { b:
|
|
377
|
+
test('rejects duplicate resource IDs', () => {
|
|
378
|
+
const mod1 = { a: mockResource('dup') };
|
|
379
|
+
const mod2 = { b: mockResource('dup') };
|
|
380
|
+
|
|
381
|
+
expect(() => topo('app', mod1, mod2)).toThrow(ValidationError);
|
|
382
|
+
expect(() => topo('app', mod1, mod2)).toThrow(
|
|
383
|
+
'Duplicate resource ID: "dup"'
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test('rejects duplicate contour names', () => {
|
|
388
|
+
const mod1 = { a: mockContour('user') };
|
|
389
|
+
const mod2 = { b: mockContour('user') };
|
|
138
390
|
|
|
139
391
|
expect(() => topo('app', mod1, mod2)).toThrow(ValidationError);
|
|
140
392
|
expect(() => topo('app', mod1, mod2)).toThrow(
|
|
141
|
-
'Duplicate
|
|
393
|
+
'Duplicate contour name: "user"'
|
|
142
394
|
);
|
|
143
395
|
});
|
|
144
396
|
});
|
|
@@ -162,26 +414,42 @@ describe('topo accessors', () => {
|
|
|
162
414
|
expect(app.count).toBe(1);
|
|
163
415
|
});
|
|
164
416
|
|
|
165
|
-
test('
|
|
166
|
-
const db =
|
|
167
|
-
const cache =
|
|
417
|
+
test('resourceCount returns number of resources', () => {
|
|
418
|
+
const db = mockResource('db.main');
|
|
419
|
+
const cache = mockResource('cache.main');
|
|
168
420
|
const app = topo('test', { cache, db });
|
|
169
|
-
expect(app.
|
|
421
|
+
expect(app.resourceCount).toBe(2);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test('contourCount returns number of contours', () => {
|
|
425
|
+
const gist = mockContour('gist');
|
|
426
|
+
const user = mockContour('user');
|
|
427
|
+
const app = topo('test', { gist, user });
|
|
428
|
+
expect(app.contourCount).toBe(2);
|
|
170
429
|
});
|
|
171
430
|
|
|
172
431
|
test('empty topo has zero count and empty ids', () => {
|
|
173
432
|
const app = topo('empty');
|
|
174
433
|
expect(app.count).toBe(0);
|
|
434
|
+
expect(app.contourCount).toBe(0);
|
|
435
|
+
expect(app.contourIds()).toEqual([]);
|
|
175
436
|
expect(app.ids()).toEqual([]);
|
|
176
|
-
expect(app.
|
|
177
|
-
expect(app.
|
|
437
|
+
expect(app.resourceCount).toBe(0);
|
|
438
|
+
expect(app.resourceIds()).toEqual([]);
|
|
178
439
|
});
|
|
179
440
|
|
|
180
|
-
test('
|
|
181
|
-
const db =
|
|
182
|
-
const cache =
|
|
441
|
+
test('resourceIds() returns all resource IDs', () => {
|
|
442
|
+
const db = mockResource('db.main');
|
|
443
|
+
const cache = mockResource('cache.main');
|
|
183
444
|
const app = topo('test', { cache, db });
|
|
184
|
-
expect(app.
|
|
445
|
+
expect(app.resourceIds().toSorted()).toEqual(['cache.main', 'db.main']);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test('contourIds() returns all contour names', () => {
|
|
449
|
+
const gist = mockContour('gist');
|
|
450
|
+
const user = mockContour('user');
|
|
451
|
+
const app = topo('test', { gist, user });
|
|
452
|
+
expect(app.contourIds().toSorted()).toEqual(['gist', 'user']);
|
|
185
453
|
});
|
|
186
454
|
});
|
|
187
455
|
|
|
@@ -192,7 +460,7 @@ describe('topo accessors', () => {
|
|
|
192
460
|
describe('Topo', () => {
|
|
193
461
|
const mod = {
|
|
194
462
|
e1: mockEvent('event-1'),
|
|
195
|
-
p1:
|
|
463
|
+
p1: mockResource('resource-1'),
|
|
196
464
|
t1: mockTrail('trail-1'),
|
|
197
465
|
t2: mockTrail('trail-2'),
|
|
198
466
|
t3: mockTrail('trail-3', ['trail-1']),
|
|
@@ -202,6 +470,13 @@ describe('Topo', () => {
|
|
|
202
470
|
const app = topo('app', mod);
|
|
203
471
|
|
|
204
472
|
describe('get()', () => {
|
|
473
|
+
test('retrieves contour by name', () => {
|
|
474
|
+
const user = mockContour('user');
|
|
475
|
+
const contourApp = topo('app', { user });
|
|
476
|
+
|
|
477
|
+
expect(contourApp.getContour('user')).toBe(user);
|
|
478
|
+
});
|
|
479
|
+
|
|
205
480
|
test('retrieves trail by ID', () => {
|
|
206
481
|
expect(app.get('trail-1')).toBe(mod.t1);
|
|
207
482
|
});
|
|
@@ -216,6 +491,13 @@ describe('Topo', () => {
|
|
|
216
491
|
});
|
|
217
492
|
|
|
218
493
|
describe('has()', () => {
|
|
494
|
+
test('returns true for known contour', () => {
|
|
495
|
+
const user = mockContour('user');
|
|
496
|
+
const contourApp = topo('app', { user });
|
|
497
|
+
|
|
498
|
+
expect(contourApp.hasContour('user')).toBe(true);
|
|
499
|
+
});
|
|
500
|
+
|
|
219
501
|
test('returns true for known trail', () => {
|
|
220
502
|
expect(app.has('trail-1')).toBe(true);
|
|
221
503
|
});
|
|
@@ -228,28 +510,34 @@ describe('Topo', () => {
|
|
|
228
510
|
expect(app.has('nope')).toBe(false);
|
|
229
511
|
});
|
|
230
512
|
|
|
513
|
+
test('returns false for unknown contour', () => {
|
|
514
|
+
const contourApp = topo('app');
|
|
515
|
+
|
|
516
|
+
expect(contourApp.hasContour('missing')).toBe(false);
|
|
517
|
+
});
|
|
518
|
+
|
|
231
519
|
test('returns false for event ID (signals are not trails)', () => {
|
|
232
520
|
expect(app.has('event-1')).toBe(false);
|
|
233
521
|
});
|
|
234
522
|
});
|
|
235
523
|
|
|
236
|
-
describe('
|
|
237
|
-
test('retrieves
|
|
238
|
-
expect(app.
|
|
524
|
+
describe('getResource()', () => {
|
|
525
|
+
test('retrieves resource by ID', () => {
|
|
526
|
+
expect(app.getResource('resource-1')).toBe(mod.p1);
|
|
239
527
|
});
|
|
240
528
|
|
|
241
|
-
test('returns undefined for unknown
|
|
242
|
-
expect(app.
|
|
529
|
+
test('returns undefined for unknown resource ID', () => {
|
|
530
|
+
expect(app.getResource('missing-resource')).toBeUndefined();
|
|
243
531
|
});
|
|
244
532
|
});
|
|
245
533
|
|
|
246
|
-
describe('
|
|
247
|
-
test('returns true for known
|
|
248
|
-
expect(app.
|
|
534
|
+
describe('hasResource()', () => {
|
|
535
|
+
test('returns true for known resource', () => {
|
|
536
|
+
expect(app.hasResource('resource-1')).toBe(true);
|
|
249
537
|
});
|
|
250
538
|
|
|
251
|
-
test('returns false for unknown
|
|
252
|
-
expect(app.
|
|
539
|
+
test('returns false for unknown resource', () => {
|
|
540
|
+
expect(app.hasResource('missing-resource')).toBe(false);
|
|
253
541
|
});
|
|
254
542
|
});
|
|
255
543
|
|
|
@@ -268,10 +556,18 @@ describe('Topo', () => {
|
|
|
268
556
|
expect(items).toContain(mod.e1);
|
|
269
557
|
});
|
|
270
558
|
|
|
271
|
-
test('
|
|
272
|
-
const items = app.
|
|
559
|
+
test('listResources() returns all resources', () => {
|
|
560
|
+
const items = app.listResources();
|
|
273
561
|
expect(items).toHaveLength(1);
|
|
274
562
|
expect(items).toContain(mod.p1);
|
|
275
563
|
});
|
|
564
|
+
|
|
565
|
+
test('listContours() returns all contours', () => {
|
|
566
|
+
const gist = mockContour('gist');
|
|
567
|
+
const user = mockContour('user');
|
|
568
|
+
const contourApp = topo('app', { gist, user });
|
|
569
|
+
|
|
570
|
+
expect(contourApp.listContours()).toEqual([gist, user]);
|
|
571
|
+
});
|
|
276
572
|
});
|
|
277
573
|
});
|