@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
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Resource resolution pipeline.
|
|
3
3
|
*
|
|
4
4
|
* Extracted from execute.ts to keep both modules under the 400 LOC ceiling.
|
|
5
5
|
* Handles config validation, singleton caching, concurrent-creation dedup,
|
|
6
|
-
* and the full resolve-or-create flow for declared
|
|
6
|
+
* and the full resolve-or-create flow for declared resources.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from './
|
|
10
|
+
AnyResource,
|
|
11
|
+
ResourceContext,
|
|
12
|
+
ResourceOverrideMap,
|
|
13
|
+
} from './resource.js';
|
|
14
14
|
import type { AnyTrail } from './trail.js';
|
|
15
15
|
import type { TrailContext } from './types.js';
|
|
16
16
|
|
|
17
17
|
import { InternalError, ValidationError } from './errors.js';
|
|
18
18
|
import { Result } from './result.js';
|
|
19
|
-
import {
|
|
19
|
+
import { createResourceLookup } from './resource.js';
|
|
20
20
|
|
|
21
21
|
type MutableTrailContext = {
|
|
22
22
|
-readonly [K in keyof TrailContext]: TrailContext[K];
|
|
@@ -28,11 +28,11 @@ type ConfigValues = Readonly<Record<string, Record<string, unknown>>>;
|
|
|
28
28
|
// Singleton caches
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
|
|
31
|
-
const
|
|
31
|
+
const singletonResources = new WeakMap<AnyResource, Map<string, unknown>>();
|
|
32
32
|
|
|
33
|
-
/** In-flight
|
|
33
|
+
/** In-flight resource creation promises, keyed by resource x context. */
|
|
34
34
|
const pendingCreations = new WeakMap<
|
|
35
|
-
|
|
35
|
+
AnyResource,
|
|
36
36
|
Map<string, Promise<Result<unknown, Error>>>
|
|
37
37
|
>();
|
|
38
38
|
|
|
@@ -40,17 +40,17 @@ const pendingCreations = new WeakMap<
|
|
|
40
40
|
// Context helpers
|
|
41
41
|
// ---------------------------------------------------------------------------
|
|
42
42
|
|
|
43
|
-
const
|
|
43
|
+
const toResourceContext = (
|
|
44
44
|
ctx: TrailContext,
|
|
45
45
|
config?: unknown
|
|
46
|
-
):
|
|
46
|
+
): ResourceContext => ({
|
|
47
47
|
config,
|
|
48
48
|
cwd: ctx.cwd,
|
|
49
49
|
env: ctx.env,
|
|
50
50
|
workspaceRoot: ctx.workspaceRoot,
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
const
|
|
53
|
+
const toResourceContextKey = (ctx: ResourceContext): string =>
|
|
54
54
|
JSON.stringify({
|
|
55
55
|
config: ctx.config,
|
|
56
56
|
cwd: ctx.cwd,
|
|
@@ -64,27 +64,27 @@ const toProvisionContextKey = (ctx: ProvisionContext): string =>
|
|
|
64
64
|
// Config validation
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
66
|
|
|
67
|
-
/** Validate and resolve a
|
|
68
|
-
const
|
|
69
|
-
|
|
67
|
+
/** Validate and resolve a resource's config from the provided configValues map. */
|
|
68
|
+
const resolveResourceConfig = (
|
|
69
|
+
declaredResource: AnyResource,
|
|
70
70
|
configValues?: ConfigValues
|
|
71
71
|
): Result<unknown, Error> => {
|
|
72
|
-
if (
|
|
72
|
+
if (declaredResource.config === undefined) {
|
|
73
73
|
return Result.ok();
|
|
74
74
|
}
|
|
75
|
-
const raw = configValues?.[
|
|
75
|
+
const raw = configValues?.[declaredResource.id];
|
|
76
76
|
if (raw === undefined) {
|
|
77
77
|
return Result.err(
|
|
78
78
|
new ValidationError(
|
|
79
|
-
`
|
|
79
|
+
`Resource "${declaredResource.id}" declares a config schema but no config was provided`
|
|
80
80
|
)
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
|
-
const parsed =
|
|
83
|
+
const parsed = declaredResource.config.safeParse(raw);
|
|
84
84
|
if (!parsed.success) {
|
|
85
85
|
return Result.err(
|
|
86
86
|
new ValidationError(
|
|
87
|
-
`
|
|
87
|
+
`Resource "${declaredResource.id}" config validation failed: ${parsed.error.message}`
|
|
88
88
|
)
|
|
89
89
|
);
|
|
90
90
|
}
|
|
@@ -95,22 +95,22 @@ const resolveProvisionConfig = (
|
|
|
95
95
|
// Override / cache lookups
|
|
96
96
|
// ---------------------------------------------------------------------------
|
|
97
97
|
|
|
98
|
-
const
|
|
99
|
-
overrides:
|
|
98
|
+
const hasOwnResourceOverride = (
|
|
99
|
+
overrides: ResourceOverrideMap | undefined,
|
|
100
100
|
id: string
|
|
101
|
-
): overrides is
|
|
101
|
+
): overrides is ResourceOverrideMap =>
|
|
102
102
|
overrides !== undefined && Object.hasOwn(overrides, id);
|
|
103
103
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
const getCachedSingletonResource = (
|
|
105
|
+
declaredResource: AnyResource,
|
|
106
|
+
resourceContext: ResourceContext
|
|
107
107
|
): { readonly found: boolean; readonly value: unknown } => {
|
|
108
|
-
const scopedCache =
|
|
108
|
+
const scopedCache = singletonResources.get(declaredResource);
|
|
109
109
|
if (scopedCache === undefined) {
|
|
110
110
|
return { found: false, value: undefined };
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
const key =
|
|
113
|
+
const key = toResourceContextKey(resourceContext);
|
|
114
114
|
if (!scopedCache.has(key)) {
|
|
115
115
|
return { found: false, value: undefined };
|
|
116
116
|
}
|
|
@@ -121,14 +121,14 @@ const getCachedSingletonProvision = (
|
|
|
121
121
|
};
|
|
122
122
|
};
|
|
123
123
|
|
|
124
|
-
const
|
|
124
|
+
const getProvidedResource = (
|
|
125
125
|
ctx: TrailContext,
|
|
126
|
-
overrides:
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
overrides: ResourceOverrideMap | undefined,
|
|
127
|
+
declaredResource: AnyResource,
|
|
128
|
+
resourceContext: ResourceContext
|
|
129
129
|
): Result<unknown, Error> | undefined => {
|
|
130
|
-
const { id } =
|
|
131
|
-
if (
|
|
130
|
+
const { id } = declaredResource;
|
|
131
|
+
if (hasOwnResourceOverride(overrides, id)) {
|
|
132
132
|
return Result.ok(overrides[id]);
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -136,10 +136,7 @@ const getProvidedProvision = (
|
|
|
136
136
|
return Result.ok(ctx.extensions?.[id]);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
const cached =
|
|
140
|
-
declaredProvision,
|
|
141
|
-
provisionContext
|
|
142
|
-
);
|
|
139
|
+
const cached = getCachedSingletonResource(declaredResource, resourceContext);
|
|
143
140
|
if (cached.found) {
|
|
144
141
|
return Result.ok(cached.value);
|
|
145
142
|
}
|
|
@@ -149,216 +146,205 @@ const getProvidedProvision = (
|
|
|
149
146
|
|
|
150
147
|
const getOverrideOrExtension = (
|
|
151
148
|
ctx: TrailContext,
|
|
152
|
-
overrides:
|
|
153
|
-
|
|
149
|
+
overrides: ResourceOverrideMap | undefined,
|
|
150
|
+
declaredResource: AnyResource
|
|
154
151
|
): Result<unknown, Error> | undefined =>
|
|
155
|
-
|
|
156
|
-
ctx,
|
|
157
|
-
overrides,
|
|
158
|
-
declaredProvision,
|
|
159
|
-
toProvisionContext(ctx)
|
|
160
|
-
);
|
|
152
|
+
getProvidedResource(ctx, overrides, declaredResource, toResourceContext(ctx));
|
|
161
153
|
|
|
162
154
|
type ConfigAwareResolution =
|
|
163
155
|
| Result<{ readonly kind: 'provided'; readonly value: unknown }, Error>
|
|
164
156
|
| Result<
|
|
165
|
-
{ readonly kind: 'context'; readonly
|
|
157
|
+
{ readonly kind: 'context'; readonly resourceContext: ResourceContext },
|
|
166
158
|
Error
|
|
167
159
|
>;
|
|
168
160
|
|
|
169
|
-
const
|
|
161
|
+
const resolveConfigAwareProvidedResource = (
|
|
170
162
|
ctx: TrailContext,
|
|
171
|
-
|
|
163
|
+
declaredResource: AnyResource,
|
|
172
164
|
configValues: ConfigValues | undefined
|
|
173
165
|
): ConfigAwareResolution => {
|
|
174
|
-
const configResult =
|
|
166
|
+
const configResult = resolveResourceConfig(declaredResource, configValues);
|
|
175
167
|
if (configResult.isErr()) {
|
|
176
168
|
return configResult;
|
|
177
169
|
}
|
|
178
170
|
|
|
179
|
-
const
|
|
180
|
-
const provided =
|
|
171
|
+
const resourceContext = toResourceContext(ctx, configResult.value);
|
|
172
|
+
const provided = getProvidedResource(
|
|
181
173
|
ctx,
|
|
182
174
|
undefined,
|
|
183
|
-
|
|
184
|
-
|
|
175
|
+
declaredResource,
|
|
176
|
+
resourceContext
|
|
185
177
|
);
|
|
186
178
|
|
|
187
179
|
return provided
|
|
188
180
|
? Result.ok({ kind: 'provided', value: provided.unwrap() })
|
|
189
|
-
: Result.ok({ kind: 'context',
|
|
181
|
+
: Result.ok({ kind: 'context', resourceContext });
|
|
190
182
|
};
|
|
191
183
|
|
|
192
184
|
// ---------------------------------------------------------------------------
|
|
193
185
|
// Instance creation
|
|
194
186
|
// ---------------------------------------------------------------------------
|
|
195
187
|
|
|
196
|
-
const
|
|
197
|
-
id: string,
|
|
198
|
-
error: unknown
|
|
199
|
-
): InternalError => {
|
|
188
|
+
const toInternalResourceError = (id: string, error: unknown): InternalError => {
|
|
200
189
|
const cause = error instanceof Error ? error : undefined;
|
|
201
190
|
const message = cause?.message ?? String(error);
|
|
202
|
-
return new InternalError(`
|
|
191
|
+
return new InternalError(`Resource "${id}" failed to resolve: ${message}`, {
|
|
203
192
|
...(cause ? { cause } : {}),
|
|
204
|
-
context: {
|
|
193
|
+
context: { resourceId: id },
|
|
205
194
|
});
|
|
206
195
|
};
|
|
207
196
|
|
|
208
|
-
const
|
|
209
|
-
|
|
197
|
+
const getSingletonResourceCache = (
|
|
198
|
+
declaredResource: AnyResource
|
|
210
199
|
): Map<string, unknown> => {
|
|
211
|
-
const existing =
|
|
200
|
+
const existing = singletonResources.get(declaredResource);
|
|
212
201
|
if (existing !== undefined) {
|
|
213
202
|
return existing;
|
|
214
203
|
}
|
|
215
204
|
|
|
216
205
|
const created = new Map<string, unknown>();
|
|
217
|
-
|
|
206
|
+
singletonResources.set(declaredResource, created);
|
|
218
207
|
return created;
|
|
219
208
|
};
|
|
220
209
|
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
210
|
+
const doCreateResourceInstance = async (
|
|
211
|
+
declaredResource: AnyResource,
|
|
212
|
+
resourceContext: ResourceContext
|
|
224
213
|
): Promise<Result<unknown, Error>> => {
|
|
225
214
|
try {
|
|
226
|
-
const created = await
|
|
215
|
+
const created = await declaredResource.create(resourceContext);
|
|
227
216
|
if (created.isErr()) {
|
|
228
217
|
return Result.err(created.error);
|
|
229
218
|
}
|
|
230
219
|
|
|
231
220
|
const instance = created.unwrap();
|
|
232
|
-
|
|
233
|
-
|
|
221
|
+
getSingletonResourceCache(declaredResource).set(
|
|
222
|
+
toResourceContextKey(resourceContext),
|
|
234
223
|
instance
|
|
235
224
|
);
|
|
236
225
|
return Result.ok(instance);
|
|
237
226
|
} catch (error: unknown) {
|
|
238
|
-
return Result.err(
|
|
227
|
+
return Result.err(toInternalResourceError(declaredResource.id, error));
|
|
239
228
|
}
|
|
240
229
|
};
|
|
241
230
|
|
|
242
231
|
const trackPendingCreation = (
|
|
243
|
-
|
|
232
|
+
declaredResource: AnyResource,
|
|
244
233
|
key: string,
|
|
245
234
|
promise: Promise<Result<unknown, Error>>
|
|
246
235
|
): void => {
|
|
247
|
-
const pending = pendingCreations.get(
|
|
236
|
+
const pending = pendingCreations.get(declaredResource);
|
|
248
237
|
if (pending) {
|
|
249
238
|
pending.set(key, promise);
|
|
250
239
|
} else {
|
|
251
|
-
pendingCreations.set(
|
|
240
|
+
pendingCreations.set(declaredResource, new Map([[key, promise]]));
|
|
252
241
|
}
|
|
253
242
|
};
|
|
254
243
|
|
|
255
244
|
/**
|
|
256
|
-
* Deduplicates concurrent creation of the same
|
|
257
|
-
* If a creation is already in flight for this
|
|
245
|
+
* Deduplicates concurrent creation of the same resource singleton.
|
|
246
|
+
* If a creation is already in flight for this resource x context key,
|
|
258
247
|
* returns the existing promise instead of spawning a second factory call.
|
|
259
248
|
*/
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
249
|
+
const createResourceInstance = async (
|
|
250
|
+
declaredResource: AnyResource,
|
|
251
|
+
resourceContext: ResourceContext
|
|
263
252
|
): Promise<Result<unknown, Error>> => {
|
|
264
|
-
const key =
|
|
265
|
-
const inflight = pendingCreations.get(
|
|
253
|
+
const key = toResourceContextKey(resourceContext);
|
|
254
|
+
const inflight = pendingCreations.get(declaredResource)?.get(key);
|
|
266
255
|
if (inflight) {
|
|
267
256
|
return inflight;
|
|
268
257
|
}
|
|
269
258
|
|
|
270
|
-
const promise =
|
|
271
|
-
|
|
272
|
-
provisionContext
|
|
273
|
-
);
|
|
274
|
-
trackPendingCreation(declaredProvision, key, promise);
|
|
259
|
+
const promise = doCreateResourceInstance(declaredResource, resourceContext);
|
|
260
|
+
trackPendingCreation(declaredResource, key, promise);
|
|
275
261
|
|
|
276
262
|
try {
|
|
277
263
|
return await promise;
|
|
278
264
|
} finally {
|
|
279
|
-
pendingCreations.get(
|
|
265
|
+
pendingCreations.get(declaredResource)?.delete(key);
|
|
280
266
|
}
|
|
281
267
|
};
|
|
282
268
|
|
|
283
|
-
/** Validate config and resolve a single declared
|
|
284
|
-
const
|
|
285
|
-
|
|
269
|
+
/** Validate config and resolve a single declared resource. */
|
|
270
|
+
const resolveDeclaredResource = async (
|
|
271
|
+
declaredResource: AnyResource,
|
|
286
272
|
ctx: TrailContext,
|
|
287
|
-
overrides:
|
|
273
|
+
overrides: ResourceOverrideMap | undefined,
|
|
288
274
|
configValues: ConfigValues | undefined
|
|
289
275
|
): Promise<Result<unknown, Error>> => {
|
|
290
276
|
// Check overrides/extensions first — skip config validation entirely when
|
|
291
|
-
// a
|
|
277
|
+
// a resource instance is already provided.
|
|
292
278
|
const overrideOrExtension = getOverrideOrExtension(
|
|
293
279
|
ctx,
|
|
294
280
|
overrides,
|
|
295
|
-
|
|
281
|
+
declaredResource
|
|
296
282
|
);
|
|
297
283
|
if (overrideOrExtension !== undefined) {
|
|
298
284
|
return overrideOrExtension;
|
|
299
285
|
}
|
|
300
286
|
|
|
301
287
|
// Resolve config before consulting the singleton cache so config-aware
|
|
302
|
-
//
|
|
303
|
-
const
|
|
288
|
+
// resources use the same canonical context for cache reads and writes.
|
|
289
|
+
const configAwareResource = resolveConfigAwareProvidedResource(
|
|
304
290
|
ctx,
|
|
305
|
-
|
|
291
|
+
declaredResource,
|
|
306
292
|
configValues
|
|
307
293
|
);
|
|
308
|
-
if (
|
|
309
|
-
return
|
|
294
|
+
if (configAwareResource.isErr()) {
|
|
295
|
+
return configAwareResource;
|
|
310
296
|
}
|
|
311
297
|
|
|
312
298
|
// No provided instance — create via factory.
|
|
313
|
-
const resolved =
|
|
299
|
+
const resolved = configAwareResource.unwrap();
|
|
314
300
|
if (resolved.kind === 'provided') {
|
|
315
301
|
return Result.ok(resolved.value);
|
|
316
302
|
}
|
|
317
303
|
|
|
318
|
-
return await
|
|
319
|
-
|
|
320
|
-
resolved.
|
|
304
|
+
return await createResourceInstance(
|
|
305
|
+
declaredResource,
|
|
306
|
+
resolved.resourceContext
|
|
321
307
|
);
|
|
322
308
|
};
|
|
323
309
|
|
|
324
310
|
// ---------------------------------------------------------------------------
|
|
325
|
-
// Full trail
|
|
311
|
+
// Full trail resource resolution
|
|
326
312
|
// ---------------------------------------------------------------------------
|
|
327
313
|
|
|
328
|
-
const
|
|
314
|
+
const withResolvedResources = (
|
|
329
315
|
ctx: TrailContext,
|
|
330
|
-
|
|
316
|
+
resolvedResources: Record<string, unknown>
|
|
331
317
|
): TrailContext => {
|
|
332
|
-
const extensions = { ...ctx.extensions, ...
|
|
318
|
+
const extensions = { ...ctx.extensions, ...resolvedResources };
|
|
333
319
|
const resolvedCtx = { ...ctx, extensions } as MutableTrailContext;
|
|
334
|
-
const lookup =
|
|
335
|
-
resolvedCtx.
|
|
320
|
+
const lookup = createResourceLookup(() => resolvedCtx);
|
|
321
|
+
resolvedCtx.resource = lookup;
|
|
336
322
|
return resolvedCtx;
|
|
337
323
|
};
|
|
338
324
|
|
|
339
325
|
/**
|
|
340
|
-
* Resolve all declared
|
|
326
|
+
* Resolve all declared resources for a trail.
|
|
341
327
|
*
|
|
342
|
-
* Validates per-
|
|
343
|
-
* new instances as needed. Returns an enriched context with all
|
|
328
|
+
* Validates per-resource config, checks overrides and caches, and creates
|
|
329
|
+
* new instances as needed. Returns an enriched context with all resource
|
|
344
330
|
* instances injected into extensions.
|
|
345
331
|
*/
|
|
346
|
-
export const
|
|
332
|
+
export const createResources = async (
|
|
347
333
|
trail: AnyTrail,
|
|
348
334
|
ctx: TrailContext,
|
|
349
|
-
overrides?:
|
|
335
|
+
overrides?: ResourceOverrideMap,
|
|
350
336
|
configValues?: ConfigValues
|
|
351
337
|
): Promise<Result<TrailContext, Error>> => {
|
|
352
|
-
const {
|
|
353
|
-
if (
|
|
338
|
+
const { resources } = trail;
|
|
339
|
+
if (resources.length === 0) {
|
|
354
340
|
return Result.ok(ctx);
|
|
355
341
|
}
|
|
356
342
|
|
|
357
|
-
const
|
|
343
|
+
const resolvedResources: Record<string, unknown> = {};
|
|
358
344
|
|
|
359
|
-
for (const
|
|
360
|
-
const resolved = await
|
|
361
|
-
|
|
345
|
+
for (const declaredResource of resources) {
|
|
346
|
+
const resolved = await resolveDeclaredResource(
|
|
347
|
+
declaredResource,
|
|
362
348
|
ctx,
|
|
363
349
|
overrides,
|
|
364
350
|
configValues
|
|
@@ -366,8 +352,8 @@ export const resolveProvisions = async (
|
|
|
366
352
|
if (resolved.isErr()) {
|
|
367
353
|
return resolved;
|
|
368
354
|
}
|
|
369
|
-
|
|
355
|
+
resolvedResources[declaredResource.id] = resolved.unwrap();
|
|
370
356
|
}
|
|
371
357
|
|
|
372
|
-
return Result.ok(
|
|
358
|
+
return Result.ok(withResolvedResources(ctx, resolvedResources));
|
|
373
359
|
};
|
package/src/resource.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { InternalError } from './errors.js';
|
|
2
|
+
import type { Result } from './result.js';
|
|
3
|
+
import type { AnySignal } from './signal.js';
|
|
4
|
+
import type { ResourceLookup, TrailContext } from './types.js';
|
|
5
|
+
import type { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Stable process-scoped fields available when constructing a resource.
|
|
9
|
+
*
|
|
10
|
+
* Resources are app-level singletons, so they intentionally do not receive
|
|
11
|
+
* the full per-request TrailContext. When a resource declares a `config` schema,
|
|
12
|
+
* the validated config is passed as `svc.config`.
|
|
13
|
+
*/
|
|
14
|
+
export type ResourceContext<C = unknown> = Pick<
|
|
15
|
+
TrailContext,
|
|
16
|
+
'cwd' | 'env' | 'workspaceRoot'
|
|
17
|
+
> & {
|
|
18
|
+
readonly config: C;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Everything needed to describe a resource before a factory is introduced.
|
|
23
|
+
*
|
|
24
|
+
* When `config` is a Zod schema, the `create` callback receives
|
|
25
|
+
* `ResourceContext<C>` with the validated config value.
|
|
26
|
+
*/
|
|
27
|
+
export interface ResourceSpec<T, C = unknown> {
|
|
28
|
+
/** Create the resource instance from stable process-scoped context. */
|
|
29
|
+
readonly create: (
|
|
30
|
+
svc: ResourceContext<C>
|
|
31
|
+
) => Result<T, Error> | Promise<Result<T, Error>>;
|
|
32
|
+
/** Config schema — when present, config is validated and passed to `create`. */
|
|
33
|
+
readonly config?: z.ZodType<C> | undefined;
|
|
34
|
+
/** Optional cleanup performed when the hosting trailhead shuts down. */
|
|
35
|
+
readonly dispose?: ((resource: T) => void | Promise<void>) | undefined;
|
|
36
|
+
/** Optional operational readiness probe for introspection tooling. */
|
|
37
|
+
readonly health?:
|
|
38
|
+
| ((
|
|
39
|
+
resource: T
|
|
40
|
+
) => Result<unknown, Error> | Promise<Result<unknown, Error>>)
|
|
41
|
+
| undefined;
|
|
42
|
+
/** Optional test factory used by higher-level helpers. */
|
|
43
|
+
readonly mock?: (() => T | Promise<T>) | undefined;
|
|
44
|
+
/** Human-readable description. */
|
|
45
|
+
readonly description?: string | undefined;
|
|
46
|
+
/** Arbitrary meta for tooling and filtering. */
|
|
47
|
+
readonly meta?: Readonly<Record<string, unknown>> | undefined;
|
|
48
|
+
/** Signals projected or owned by this resource. */
|
|
49
|
+
readonly signals?: readonly AnySignal[] | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A typed resource definition. */
|
|
53
|
+
export interface Resource<T> extends ResourceSpec<T> {
|
|
54
|
+
readonly kind: 'resource';
|
|
55
|
+
readonly id: string;
|
|
56
|
+
/** Read the resolved resource instance from a trail context. */
|
|
57
|
+
from(ctx: TrailContext): T;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Existential type for heterogeneous resource collections.
|
|
62
|
+
*
|
|
63
|
+
* `Resource<T>` includes function parameters in `dispose`/`health`, so
|
|
64
|
+
* `unknown` is too narrow for mixed resource arrays. `any` is the correct
|
|
65
|
+
* existential here.
|
|
66
|
+
*/
|
|
67
|
+
// oxlint-disable-next-line no-explicit-any -- existential type for heterogeneous resource collections
|
|
68
|
+
export type AnyResource = Resource<any>;
|
|
69
|
+
|
|
70
|
+
/** Explicit runtime overrides keyed by resource ID. */
|
|
71
|
+
export type ResourceOverrideMap = Readonly<Record<string, unknown>>;
|
|
72
|
+
|
|
73
|
+
const getResourceId = <T>(
|
|
74
|
+
resourceOrId: string | Pick<Resource<T>, 'id'>
|
|
75
|
+
): string =>
|
|
76
|
+
typeof resourceOrId === 'string' ? resourceOrId : resourceOrId.id;
|
|
77
|
+
|
|
78
|
+
const getResourceInstance = <T>(
|
|
79
|
+
ctx: Pick<TrailContext, 'extensions'>,
|
|
80
|
+
resourceOrId: string | Pick<Resource<T>, 'id'>
|
|
81
|
+
): T => {
|
|
82
|
+
const id = getResourceId(resourceOrId);
|
|
83
|
+
return ctx.extensions?.[id] as T;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const hasResourceInstance = (
|
|
87
|
+
ctx: Pick<TrailContext, 'extensions'>,
|
|
88
|
+
resourceOrId: string | Pick<AnyResource, 'id'>
|
|
89
|
+
): boolean => Object.hasOwn(ctx.extensions ?? {}, getResourceId(resourceOrId));
|
|
90
|
+
|
|
91
|
+
/** Create a `ctx.resource(...)` accessor bound to a concrete context snapshot. */
|
|
92
|
+
export const createResourceLookup = (
|
|
93
|
+
getContext: () => Pick<TrailContext, 'extensions'>
|
|
94
|
+
): ResourceLookup =>
|
|
95
|
+
((resourceOrId: string | Pick<AnyResource, 'id'>) => {
|
|
96
|
+
const id = getResourceId(resourceOrId);
|
|
97
|
+
const ctx = getContext();
|
|
98
|
+
if (!hasResourceInstance(ctx, id)) {
|
|
99
|
+
throw new InternalError(`Resource "${id}" not provisioned in context`);
|
|
100
|
+
}
|
|
101
|
+
return getResourceInstance(ctx, id);
|
|
102
|
+
}) as ResourceLookup;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create a typed resource definition.
|
|
106
|
+
*
|
|
107
|
+
* The resource object is inert until a later execution branch resolves concrete
|
|
108
|
+
* instances into TrailContext extensions.
|
|
109
|
+
*/
|
|
110
|
+
export const resource = <T>(id: string, spec: ResourceSpec<T>): Resource<T> =>
|
|
111
|
+
(() => {
|
|
112
|
+
if (id.includes(':')) {
|
|
113
|
+
throw new InternalError(
|
|
114
|
+
`Resource "${id}" is invalid because resource ids may not contain ":"`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
...spec,
|
|
120
|
+
from(ctx: TrailContext): T {
|
|
121
|
+
const lookup = ctx.resource ?? createResourceLookup(() => ctx);
|
|
122
|
+
return lookup(this);
|
|
123
|
+
},
|
|
124
|
+
id,
|
|
125
|
+
kind: 'resource' as const,
|
|
126
|
+
});
|
|
127
|
+
})();
|
|
128
|
+
|
|
129
|
+
/** Narrow unknown values to resource definitions during topo discovery. */
|
|
130
|
+
export const isResource = (value: unknown): value is AnyResource => {
|
|
131
|
+
if (typeof value !== 'object' || value === null) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const v = value as { kind?: unknown; id?: unknown };
|
|
135
|
+
return v.kind === 'resource' && typeof v.id === 'string';
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Return the first duplicate resource ID in a collection, if any.
|
|
140
|
+
*
|
|
141
|
+
* This supports later topo registration without each caller duplicating the
|
|
142
|
+
* same scan logic.
|
|
143
|
+
*/
|
|
144
|
+
export const findDuplicateResourceId = (
|
|
145
|
+
resources: readonly Pick<AnyResource, 'id'>[]
|
|
146
|
+
): string | undefined => {
|
|
147
|
+
const seen = new Set<string>();
|
|
148
|
+
for (const candidate of resources) {
|
|
149
|
+
if (seen.has(candidate.id)) {
|
|
150
|
+
return candidate.id;
|
|
151
|
+
}
|
|
152
|
+
seen.add(candidate.id);
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
};
|
package/src/run.ts
CHANGED
|
@@ -26,9 +26,10 @@ export type RunOptions = ExecuteTrailOptions;
|
|
|
26
26
|
* Execute a trail by ID from a topo without mounting a trailhead.
|
|
27
27
|
*
|
|
28
28
|
* Resolves the trail from the topo, then runs it through the standard
|
|
29
|
-
* `executeTrail` pipeline
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* `executeTrail` pipeline with `topo` threaded so `ctx.fire()` is bound
|
|
30
|
+
* to the producer's context inside `executeTrail`. Returns
|
|
31
|
+
* `Result.err(NotFoundError)` if the trail ID is not registered. Never
|
|
32
|
+
* throws — unexpected exceptions are returned as `Result.err(InternalError)`.
|
|
32
33
|
*
|
|
33
34
|
* @example
|
|
34
35
|
* ```typescript
|
|
@@ -50,5 +51,5 @@ export const run = (
|
|
|
50
51
|
)
|
|
51
52
|
);
|
|
52
53
|
}
|
|
53
|
-
return executeTrail(trail, input, options);
|
|
54
|
+
return executeTrail(trail, input, { ...options, topo });
|
|
54
55
|
};
|
package/src/serialization.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
RateLimitError,
|
|
19
19
|
NetworkError,
|
|
20
20
|
InternalError,
|
|
21
|
+
DerivationError,
|
|
21
22
|
AuthError,
|
|
22
23
|
CancelledError,
|
|
23
24
|
isTrailsError,
|
|
@@ -100,6 +101,7 @@ const errorConstructorsByName: Record<string, ErrorFactory> = {
|
|
|
100
101
|
AuthError: (msg, opts) => new AuthError(msg, opts),
|
|
101
102
|
CancelledError: (msg, opts) => new CancelledError(msg, opts),
|
|
102
103
|
ConflictError: (msg, opts) => new ConflictError(msg, opts),
|
|
104
|
+
DerivationError: (msg, opts) => new DerivationError(msg, opts),
|
|
103
105
|
InternalError: (msg, opts) => new InternalError(msg, opts),
|
|
104
106
|
NetworkError: (msg, opts) => new NetworkError(msg, opts),
|
|
105
107
|
NotFoundError: (msg, opts) => new NotFoundError(msg, opts),
|