@ontrails/core 1.0.0-beta.15 → 1.0.0-beta.17
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/CHANGELOG.md +69 -0
- package/README.md +64 -19
- package/package.json +9 -8
- package/src/activation-provenance.ts +111 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-projection.ts +221 -0
- package/src/activation-source.ts +91 -0
- package/src/blob-ref.ts +51 -0
- package/src/context.ts +4 -0
- package/src/{internal/cross-batch.ts → cross-batch.ts} +3 -11
- package/src/detours.ts +8 -0
- package/src/draft.ts +4 -0
- package/src/error-projection.ts +51 -0
- package/src/errors.ts +192 -39
- package/src/execute.ts +443 -44
- package/src/fire.ts +965 -91
- package/src/index.ts +285 -38
- package/src/layer-projection.ts +192 -0
- package/src/layer.ts +32 -1
- package/src/observe.ts +361 -0
- package/src/permits.ts +12 -0
- package/src/redaction/patterns.ts +6 -3
- package/src/resilience.ts +8 -8
- package/src/resource-config.ts +477 -44
- package/src/resource.ts +21 -1
- package/src/result.ts +18 -0
- package/src/run.ts +2 -2
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +324 -0
- package/src/serialization.ts +90 -57
- package/src/signal-diagnostics.ts +633 -0
- package/src/{internal/signal-ref.ts → signal-ref.ts} +1 -1
- package/src/signal.ts +37 -0
- package/src/store/accessor-protocol.ts +5 -5
- package/src/structured-examples.ts +245 -0
- package/src/surface-derivation.ts +91 -0
- package/src/surface-filter.ts +7 -13
- package/src/topo.ts +386 -6
- package/src/tracing.ts +345 -0
- package/src/trail.ts +168 -22
- package/src/trails/derive-trail.ts +1 -1
- package/src/trails/ingest.ts +7 -15
- package/src/{internal/trails-db.ts → trails-db.ts} +42 -21
- package/src/transport-error-map.ts +146 -28
- package/src/types.ts +82 -28
- package/src/validate-established-topo.ts +6 -0
- package/src/validate-topo.ts +212 -5
- package/src/validation.ts +58 -1
- package/src/webhook.ts +276 -0
- package/src/{internal/zod-wrappers.ts → zod-wrappers.ts} +0 -6
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/blob-ref.d.ts +0 -20
- package/dist/blob-ref.d.ts.map +0 -1
- package/dist/blob-ref.js +0 -22
- package/dist/blob-ref.js.map +0 -1
- package/dist/branded.d.ts +0 -36
- package/dist/branded.d.ts.map +0 -1
- package/dist/branded.js +0 -89
- package/dist/branded.js.map +0 -1
- package/dist/collections.d.ts +0 -31
- package/dist/collections.d.ts.map +0 -1
- package/dist/collections.js +0 -60
- package/dist/collections.js.map +0 -1
- package/dist/context.d.ts +0 -22
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -37
- package/dist/context.js.map +0 -1
- package/dist/contour.d.ts +0 -81
- package/dist/contour.d.ts.map +0 -1
- package/dist/contour.js +0 -176
- package/dist/contour.js.map +0 -1
- package/dist/cross-schema.d.ts +0 -18
- package/dist/cross-schema.d.ts.map +0 -1
- package/dist/cross-schema.js +0 -29
- package/dist/cross-schema.js.map +0 -1
- package/dist/derive.d.ts +0 -44
- package/dist/derive.d.ts.map +0 -1
- package/dist/derive.js +0 -149
- package/dist/derive.js.map +0 -1
- package/dist/draft.d.ts +0 -28
- package/dist/draft.d.ts.map +0 -1
- package/dist/draft.js +0 -157
- package/dist/draft.js.map +0 -1
- package/dist/errors.d.ts +0 -141
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -185
- package/dist/errors.js.map +0 -1
- package/dist/execute.d.ts +0 -52
- package/dist/execute.d.ts.map +0 -1
- package/dist/execute.js +0 -511
- package/dist/execute.js.map +0 -1
- package/dist/fetch.d.ts +0 -15
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js +0 -102
- package/dist/fetch.js.map +0 -1
- package/dist/fire.d.ts +0 -43
- package/dist/fire.d.ts.map +0 -1
- package/dist/fire.js +0 -185
- package/dist/fire.js.map +0 -1
- package/dist/guards.d.ts +0 -17
- package/dist/guards.d.ts.map +0 -1
- package/dist/guards.js +0 -25
- package/dist/guards.js.map +0 -1
- package/dist/index.d.ts +0 -59
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -61
- package/dist/index.js.map +0 -1
- package/dist/internal/cross-batch.d.ts +0 -44
- package/dist/internal/cross-batch.d.ts.map +0 -1
- package/dist/internal/cross-batch.js +0 -58
- package/dist/internal/cross-batch.js.map +0 -1
- package/dist/internal/fork-ctx.d.ts +0 -41
- package/dist/internal/fork-ctx.d.ts.map +0 -1
- package/dist/internal/fork-ctx.js +0 -47
- package/dist/internal/fork-ctx.js.map +0 -1
- package/dist/internal/signal-ref.d.ts +0 -15
- package/dist/internal/signal-ref.d.ts.map +0 -1
- package/dist/internal/signal-ref.js +0 -41
- package/dist/internal/signal-ref.js.map +0 -1
- package/dist/internal/topo-saves.d.ts +0 -53
- package/dist/internal/topo-saves.d.ts.map +0 -1
- package/dist/internal/topo-saves.js +0 -449
- package/dist/internal/topo-saves.js.map +0 -1
- package/dist/internal/topo-snapshots.d.ts +0 -57
- package/dist/internal/topo-snapshots.d.ts.map +0 -1
- package/dist/internal/topo-snapshots.js +0 -372
- package/dist/internal/topo-snapshots.js.map +0 -1
- package/dist/internal/topo-store-read.d.ts +0 -70
- package/dist/internal/topo-store-read.d.ts.map +0 -1
- package/dist/internal/topo-store-read.js +0 -221
- package/dist/internal/topo-store-read.js.map +0 -1
- package/dist/internal/topo-store.d.ts +0 -26
- package/dist/internal/topo-store.d.ts.map +0 -1
- package/dist/internal/topo-store.js +0 -621
- package/dist/internal/topo-store.js.map +0 -1
- package/dist/internal/tracing.d.ts +0 -98
- package/dist/internal/tracing.d.ts.map +0 -1
- package/dist/internal/tracing.js +0 -102
- package/dist/internal/tracing.js.map +0 -1
- package/dist/internal/trails-db.d.ts +0 -16
- package/dist/internal/trails-db.d.ts.map +0 -1
- package/dist/internal/trails-db.js +0 -118
- package/dist/internal/trails-db.js.map +0 -1
- package/dist/internal/zod-wrappers.d.ts +0 -40
- package/dist/internal/zod-wrappers.d.ts.map +0 -1
- package/dist/internal/zod-wrappers.js +0 -59
- package/dist/internal/zod-wrappers.js.map +0 -1
- package/dist/layer.d.ts +0 -23
- package/dist/layer.d.ts.map +0 -1
- package/dist/layer.js +0 -21
- package/dist/layer.js.map +0 -1
- package/dist/path-security.d.ts +0 -28
- package/dist/path-security.d.ts.map +0 -1
- package/dist/path-security.js +0 -63
- package/dist/path-security.js.map +0 -1
- package/dist/patterns/bulk.d.ts +0 -15
- package/dist/patterns/bulk.d.ts.map +0 -1
- package/dist/patterns/bulk.js +0 -14
- package/dist/patterns/bulk.js.map +0 -1
- package/dist/patterns/change.d.ts +0 -10
- package/dist/patterns/change.d.ts.map +0 -1
- package/dist/patterns/change.js +0 -10
- package/dist/patterns/change.js.map +0 -1
- package/dist/patterns/date-range.d.ts +0 -10
- package/dist/patterns/date-range.d.ts.map +0 -1
- package/dist/patterns/date-range.js +0 -10
- package/dist/patterns/date-range.js.map +0 -1
- package/dist/patterns/index.d.ts +0 -9
- package/dist/patterns/index.d.ts.map +0 -1
- package/dist/patterns/index.js +0 -9
- package/dist/patterns/index.js.map +0 -1
- package/dist/patterns/pagination.d.ts +0 -18
- package/dist/patterns/pagination.d.ts.map +0 -1
- package/dist/patterns/pagination.js +0 -18
- package/dist/patterns/pagination.js.map +0 -1
- package/dist/patterns/progress.d.ts +0 -11
- package/dist/patterns/progress.d.ts.map +0 -1
- package/dist/patterns/progress.js +0 -11
- package/dist/patterns/progress.js.map +0 -1
- package/dist/patterns/sorting.d.ts +0 -13
- package/dist/patterns/sorting.d.ts.map +0 -1
- package/dist/patterns/sorting.js +0 -10
- package/dist/patterns/sorting.js.map +0 -1
- package/dist/patterns/status.d.ts +0 -15
- package/dist/patterns/status.d.ts.map +0 -1
- package/dist/patterns/status.js +0 -9
- package/dist/patterns/status.js.map +0 -1
- package/dist/patterns/timestamps.d.ts +0 -10
- package/dist/patterns/timestamps.d.ts.map +0 -1
- package/dist/patterns/timestamps.js +0 -10
- package/dist/patterns/timestamps.js.map +0 -1
- package/dist/redaction/index.d.ts +0 -4
- package/dist/redaction/index.d.ts.map +0 -1
- package/dist/redaction/index.js +0 -3
- package/dist/redaction/index.js.map +0 -1
- package/dist/redaction/patterns.d.ts +0 -9
- package/dist/redaction/patterns.d.ts.map +0 -1
- package/dist/redaction/patterns.js +0 -39
- package/dist/redaction/patterns.js.map +0 -1
- package/dist/redaction/redactor.d.ts +0 -27
- package/dist/redaction/redactor.d.ts.map +0 -1
- package/dist/redaction/redactor.js +0 -89
- package/dist/redaction/redactor.js.map +0 -1
- package/dist/resilience.d.ts +0 -34
- package/dist/resilience.d.ts.map +0 -1
- package/dist/resilience.js +0 -164
- package/dist/resilience.js.map +0 -1
- package/dist/resource-config.d.ts +0 -22
- package/dist/resource-config.d.ts.map +0 -1
- package/dist/resource-config.js +0 -210
- package/dist/resource-config.js.map +0 -1
- package/dist/resource.d.ts +0 -74
- package/dist/resource.d.ts.map +0 -1
- package/dist/resource.js +0 -61
- package/dist/resource.js.map +0 -1
- package/dist/result.d.ts +0 -57
- package/dist/result.d.ts.map +0 -1
- package/dist/result.js +0 -156
- package/dist/result.js.map +0 -1
- package/dist/run.d.ts +0 -28
- package/dist/run.d.ts.map +0 -1
- package/dist/run.js +0 -35
- package/dist/run.js.map +0 -1
- package/dist/serialization.d.ts +0 -27
- package/dist/serialization.d.ts.map +0 -1
- package/dist/serialization.js +0 -154
- package/dist/serialization.js.map +0 -1
- package/dist/signal.d.ts +0 -33
- package/dist/signal.d.ts.map +0 -1
- package/dist/signal.js +0 -17
- package/dist/signal.js.map +0 -1
- package/dist/store/accessor-protocol.d.ts +0 -51
- package/dist/store/accessor-protocol.d.ts.map +0 -1
- package/dist/store/accessor-protocol.js +0 -2
- package/dist/store/accessor-protocol.js.map +0 -1
- package/dist/store/index.d.ts +0 -2
- package/dist/store/index.d.ts.map +0 -1
- package/dist/store/index.js +0 -2
- package/dist/store/index.js.map +0 -1
- package/dist/surface-filter.d.ts +0 -13
- package/dist/surface-filter.d.ts.map +0 -1
- package/dist/surface-filter.js +0 -76
- package/dist/surface-filter.js.map +0 -1
- package/dist/topo-store.d.ts +0 -62
- package/dist/topo-store.d.ts.map +0 -1
- package/dist/topo-store.js +0 -319
- package/dist/topo-store.js.map +0 -1
- package/dist/topo.d.ts +0 -39
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -241
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -163
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -75
- package/dist/trail.js.map +0 -1
- package/dist/trails/derive-trail.d.ts +0 -66
- package/dist/trails/derive-trail.d.ts.map +0 -1
- package/dist/trails/derive-trail.js +0 -378
- package/dist/trails/derive-trail.js.map +0 -1
- package/dist/trails/index.d.ts +0 -5
- package/dist/trails/index.d.ts.map +0 -1
- package/dist/trails/index.js +0 -3
- package/dist/trails/index.js.map +0 -1
- package/dist/trails/ingest.d.ts +0 -24
- package/dist/trails/ingest.d.ts.map +0 -1
- package/dist/trails/ingest.js +0 -63
- package/dist/trails/ingest.js.map +0 -1
- package/dist/transport-error-map.d.ts +0 -106
- package/dist/transport-error-map.d.ts.map +0 -1
- package/dist/transport-error-map.js +0 -24
- package/dist/transport-error-map.js.map +0 -1
- package/dist/type-checks.test-d.d.ts +0 -71
- package/dist/type-checks.test-d.d.ts.map +0 -1
- package/dist/type-checks.test-d.js +0 -12
- package/dist/type-checks.test-d.js.map +0 -1
- package/dist/type-utils.d.ts +0 -33
- package/dist/type-utils.d.ts.map +0 -1
- package/dist/type-utils.js +0 -12
- package/dist/type-utils.js.map +0 -1
- package/dist/types.d.ts +0 -177
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/validate-established-topo.d.ts +0 -76
- package/dist/validate-established-topo.d.ts.map +0 -1
- package/dist/validate-established-topo.js +0 -43
- package/dist/validate-established-topo.js.map +0 -1
- package/dist/validate-topo.d.ts +0 -24
- package/dist/validate-topo.d.ts.map +0 -1
- package/dist/validate-topo.js +0 -193
- package/dist/validate-topo.js.map +0 -1
- package/dist/validation.d.ts +0 -27
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -165
- package/dist/validation.js.map +0 -1
- package/dist/workspace.d.ts +0 -25
- package/dist/workspace.d.ts.map +0 -1
- package/dist/workspace.js +0 -57
- package/dist/workspace.js.map +0 -1
- package/src/__tests__/blob-ref.test.ts +0 -103
- package/src/__tests__/branded.test.ts +0 -148
- package/src/__tests__/collections.test.ts +0 -126
- package/src/__tests__/context.test.ts +0 -77
- package/src/__tests__/contour.test.ts +0 -263
- package/src/__tests__/derive-trail.test.ts +0 -668
- package/src/__tests__/derive.test.ts +0 -260
- package/src/__tests__/errors.test.ts +0 -427
- package/src/__tests__/execute.test.ts +0 -1563
- package/src/__tests__/fetch.test.ts +0 -217
- package/src/__tests__/fire.test.ts +0 -1056
- package/src/__tests__/fork-ctx.test.ts +0 -83
- package/src/__tests__/guards.test.ts +0 -102
- package/src/__tests__/ingest.test.ts +0 -191
- package/src/__tests__/layer.test.ts +0 -125
- package/src/__tests__/path-security.test.ts +0 -114
- package/src/__tests__/patterns.test.ts +0 -273
- package/src/__tests__/redaction.test.ts +0 -244
- package/src/__tests__/resilience.test.ts +0 -246
- package/src/__tests__/resource.test.ts +0 -204
- package/src/__tests__/result.test.ts +0 -155
- package/src/__tests__/run.test.ts +0 -181
- package/src/__tests__/serialization.test.ts +0 -401
- package/src/__tests__/service-config.test.ts +0 -224
- package/src/__tests__/signal.test.ts +0 -82
- package/src/__tests__/surface-filter.test.ts +0 -196
- package/src/__tests__/topo-store-read.test.ts +0 -256
- package/src/__tests__/topo-store.test.ts +0 -869
- package/src/__tests__/topo.test.ts +0 -573
- package/src/__tests__/trail-permit.test.ts +0 -60
- package/src/__tests__/trail.test.ts +0 -426
- package/src/__tests__/trails-db.test.ts +0 -198
- package/src/__tests__/transport-error-map.test.ts +0 -82
- package/src/__tests__/type-utils.test.ts +0 -118
- package/src/__tests__/validate-topo.test.ts +0 -611
- package/src/__tests__/validation.test.ts +0 -336
- package/src/__tests__/workspace.test.ts +0 -183
- package/src/__tests__/zod-wrappers.test.ts +0 -62
- package/src/internal/topo-snapshots.ts +0 -517
- package/src/internal/topo-store-read.ts +0 -481
- package/src/internal/topo-store.ts +0 -1192
- package/src/internal/tracing.ts +0 -187
- package/src/topo-store.ts +0 -510
- package/src/type-checks.test-d.ts +0 -106
- package/tsconfig.json +0 -9
- package/tsconfig.tests.json +0 -10
- package/tsconfig.tsbuildinfo +0 -1
package/src/resource-config.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
ResourceContext,
|
|
12
12
|
ResourceOverrideMap,
|
|
13
13
|
} from './resource.js';
|
|
14
|
+
import type { SurfaceConfigValues } from './surface-derivation.js';
|
|
14
15
|
import type { AnyTrail } from './trail.js';
|
|
15
16
|
import type { TrailContext } from './types.js';
|
|
16
17
|
|
|
@@ -22,18 +23,39 @@ type MutableTrailContext = {
|
|
|
22
23
|
-readonly [K in keyof TrailContext]: TrailContext[K];
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
type
|
|
26
|
+
export type ResourceConfigValues = SurfaceConfigValues;
|
|
27
|
+
|
|
28
|
+
type ConfigValues = ResourceConfigValues;
|
|
26
29
|
|
|
27
30
|
// ---------------------------------------------------------------------------
|
|
28
31
|
// Singleton caches
|
|
29
32
|
// ---------------------------------------------------------------------------
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
interface SingletonResourceEntry {
|
|
35
|
+
readonly value: unknown;
|
|
36
|
+
activeLeases: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ResourceLease {
|
|
40
|
+
readonly key: string;
|
|
41
|
+
readonly resource: AnyResource;
|
|
42
|
+
readonly value: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const singletonResources = new WeakMap<
|
|
46
|
+
AnyResource,
|
|
47
|
+
Map<string, SingletonResourceEntry>
|
|
48
|
+
>();
|
|
49
|
+
|
|
50
|
+
interface PendingCreation {
|
|
51
|
+
readonly promise: Promise<Result<unknown, Error>>;
|
|
52
|
+
waiters: number;
|
|
53
|
+
}
|
|
32
54
|
|
|
33
55
|
/** In-flight resource creation promises, keyed by resource x context. */
|
|
34
56
|
const pendingCreations = new WeakMap<
|
|
35
57
|
AnyResource,
|
|
36
|
-
Map<string,
|
|
58
|
+
Map<string, PendingCreation>
|
|
37
59
|
>();
|
|
38
60
|
|
|
39
61
|
// ---------------------------------------------------------------------------
|
|
@@ -65,7 +87,7 @@ const toResourceContextKey = (ctx: ResourceContext): string =>
|
|
|
65
87
|
// ---------------------------------------------------------------------------
|
|
66
88
|
|
|
67
89
|
/** Validate and resolve a resource's config from the provided configValues map. */
|
|
68
|
-
const resolveResourceConfig = (
|
|
90
|
+
export const resolveResourceConfig = (
|
|
69
91
|
declaredResource: AnyResource,
|
|
70
92
|
configValues?: ConfigValues
|
|
71
93
|
): Result<unknown, Error> => {
|
|
@@ -74,6 +96,10 @@ const resolveResourceConfig = (
|
|
|
74
96
|
}
|
|
75
97
|
const raw = configValues?.[declaredResource.id];
|
|
76
98
|
if (raw === undefined) {
|
|
99
|
+
const parsed = declaredResource.config.safeParse(raw);
|
|
100
|
+
if (parsed.success) {
|
|
101
|
+
return Result.ok(parsed.data);
|
|
102
|
+
}
|
|
77
103
|
return Result.err(
|
|
78
104
|
new ValidationError(
|
|
79
105
|
`Resource "${declaredResource.id}" declares a config schema but no config was provided`
|
|
@@ -104,20 +130,24 @@ const hasOwnResourceOverride = (
|
|
|
104
130
|
const getCachedSingletonResource = (
|
|
105
131
|
declaredResource: AnyResource,
|
|
106
132
|
resourceContext: ResourceContext
|
|
107
|
-
):
|
|
133
|
+
):
|
|
134
|
+
| { readonly found: false }
|
|
135
|
+
| { readonly found: true; readonly lease: ResourceLease } => {
|
|
108
136
|
const scopedCache = singletonResources.get(declaredResource);
|
|
109
137
|
if (scopedCache === undefined) {
|
|
110
|
-
return { found: false
|
|
138
|
+
return { found: false };
|
|
111
139
|
}
|
|
112
140
|
|
|
113
141
|
const key = toResourceContextKey(resourceContext);
|
|
114
|
-
|
|
115
|
-
|
|
142
|
+
const entry = scopedCache.get(key);
|
|
143
|
+
if (entry === undefined) {
|
|
144
|
+
return { found: false };
|
|
116
145
|
}
|
|
117
146
|
|
|
147
|
+
entry.activeLeases += 1;
|
|
118
148
|
return {
|
|
119
149
|
found: true,
|
|
120
|
-
value:
|
|
150
|
+
lease: { key, resource: declaredResource, value: entry.value },
|
|
121
151
|
};
|
|
122
152
|
};
|
|
123
153
|
|
|
@@ -126,19 +156,24 @@ const getProvidedResource = (
|
|
|
126
156
|
overrides: ResourceOverrideMap | undefined,
|
|
127
157
|
declaredResource: AnyResource,
|
|
128
158
|
resourceContext: ResourceContext
|
|
129
|
-
):
|
|
159
|
+
):
|
|
160
|
+
| Result<
|
|
161
|
+
{ readonly lease?: ResourceLease | undefined; readonly value: unknown },
|
|
162
|
+
Error
|
|
163
|
+
>
|
|
164
|
+
| undefined => {
|
|
130
165
|
const { id } = declaredResource;
|
|
131
166
|
if (hasOwnResourceOverride(overrides, id)) {
|
|
132
|
-
return Result.ok(overrides[id]);
|
|
167
|
+
return Result.ok({ value: overrides[id] });
|
|
133
168
|
}
|
|
134
169
|
|
|
135
170
|
if (Object.hasOwn(ctx.extensions ?? {}, id)) {
|
|
136
|
-
return Result.ok(ctx.extensions?.[id]);
|
|
171
|
+
return Result.ok({ value: ctx.extensions?.[id] });
|
|
137
172
|
}
|
|
138
173
|
|
|
139
174
|
const cached = getCachedSingletonResource(declaredResource, resourceContext);
|
|
140
175
|
if (cached.found) {
|
|
141
|
-
return Result.ok(cached.value);
|
|
176
|
+
return Result.ok({ lease: cached.lease, value: cached.lease?.value });
|
|
142
177
|
}
|
|
143
178
|
|
|
144
179
|
return undefined;
|
|
@@ -148,16 +183,61 @@ const getOverrideOrExtension = (
|
|
|
148
183
|
ctx: TrailContext,
|
|
149
184
|
overrides: ResourceOverrideMap | undefined,
|
|
150
185
|
declaredResource: AnyResource
|
|
151
|
-
):
|
|
186
|
+
):
|
|
187
|
+
| Result<
|
|
188
|
+
{ readonly lease?: ResourceLease | undefined; readonly value: unknown },
|
|
189
|
+
Error
|
|
190
|
+
>
|
|
191
|
+
| undefined =>
|
|
152
192
|
getProvidedResource(ctx, overrides, declaredResource, toResourceContext(ctx));
|
|
153
193
|
|
|
154
194
|
type ConfigAwareResolution =
|
|
155
|
-
| Result<
|
|
195
|
+
| Result<
|
|
196
|
+
{
|
|
197
|
+
readonly kind: 'provided';
|
|
198
|
+
readonly lease?: ResourceLease | undefined;
|
|
199
|
+
readonly value: unknown;
|
|
200
|
+
},
|
|
201
|
+
Error
|
|
202
|
+
>
|
|
156
203
|
| Result<
|
|
157
204
|
{ readonly kind: 'context'; readonly resourceContext: ResourceContext },
|
|
158
205
|
Error
|
|
159
206
|
>;
|
|
160
207
|
|
|
208
|
+
interface CreatedResourceInstance {
|
|
209
|
+
readonly key: string;
|
|
210
|
+
readonly lease: ResourceLease;
|
|
211
|
+
readonly resource: AnyResource;
|
|
212
|
+
readonly sharedDuringCreation: boolean;
|
|
213
|
+
readonly value: unknown;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
interface ResourceResolution {
|
|
217
|
+
readonly created?: CreatedResourceInstance | undefined;
|
|
218
|
+
readonly lease?: ResourceLease | undefined;
|
|
219
|
+
readonly value: unknown;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Outcome from draining cached resource singletons. */
|
|
223
|
+
export interface ResourceDrainReport {
|
|
224
|
+
/** Resource IDs whose cached singleton entries were disposed successfully. */
|
|
225
|
+
readonly disposed: readonly string[];
|
|
226
|
+
/** Resource IDs removed from the singleton cache before disposal. */
|
|
227
|
+
readonly evicted: readonly string[];
|
|
228
|
+
/**
|
|
229
|
+
* Resource IDs that had cached or pending singleton entries, but not for the
|
|
230
|
+
* stable context/config key supplied to `drainResources`.
|
|
231
|
+
*/
|
|
232
|
+
readonly missed?: readonly string[] | undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface MutableResourceDrainReport {
|
|
236
|
+
readonly disposed: string[];
|
|
237
|
+
readonly evicted: string[];
|
|
238
|
+
missed?: string[] | undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
161
241
|
const resolveConfigAwareProvidedResource = (
|
|
162
242
|
ctx: TrailContext,
|
|
163
243
|
declaredResource: AnyResource,
|
|
@@ -176,9 +256,18 @@ const resolveConfigAwareProvidedResource = (
|
|
|
176
256
|
resourceContext
|
|
177
257
|
);
|
|
178
258
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
259
|
+
if (provided === undefined) {
|
|
260
|
+
return Result.ok({ kind: 'context', resourceContext });
|
|
261
|
+
}
|
|
262
|
+
if (provided.isErr()) {
|
|
263
|
+
return provided;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return Result.ok({
|
|
267
|
+
kind: 'provided',
|
|
268
|
+
lease: provided.value.lease,
|
|
269
|
+
value: provided.value.value,
|
|
270
|
+
});
|
|
182
271
|
};
|
|
183
272
|
|
|
184
273
|
// ---------------------------------------------------------------------------
|
|
@@ -196,20 +285,118 @@ const toInternalResourceError = (id: string, error: unknown): InternalError => {
|
|
|
196
285
|
|
|
197
286
|
const getSingletonResourceCache = (
|
|
198
287
|
declaredResource: AnyResource
|
|
199
|
-
): Map<string,
|
|
288
|
+
): Map<string, SingletonResourceEntry> => {
|
|
200
289
|
const existing = singletonResources.get(declaredResource);
|
|
201
290
|
if (existing !== undefined) {
|
|
202
291
|
return existing;
|
|
203
292
|
}
|
|
204
293
|
|
|
205
|
-
const created = new Map<string,
|
|
294
|
+
const created = new Map<string, SingletonResourceEntry>();
|
|
206
295
|
singletonResources.set(declaredResource, created);
|
|
207
296
|
return created;
|
|
208
297
|
};
|
|
209
298
|
|
|
299
|
+
const acquireCachedSingletonResourceByKey = (
|
|
300
|
+
declaredResource: AnyResource,
|
|
301
|
+
key: string
|
|
302
|
+
): ResourceLease | undefined => {
|
|
303
|
+
const entry = singletonResources.get(declaredResource)?.get(key);
|
|
304
|
+
if (entry === undefined) {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
entry.activeLeases += 1;
|
|
309
|
+
return { key, resource: declaredResource, value: entry.value };
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const releaseResourceLease = (lease: ResourceLease): number => {
|
|
313
|
+
const entry = singletonResources.get(lease.resource)?.get(lease.key);
|
|
314
|
+
if (entry === undefined || entry.value !== lease.value) {
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
entry.activeLeases = Math.max(0, entry.activeLeases - 1);
|
|
319
|
+
return entry.activeLeases;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const countActiveResourceLeases = (lease: ResourceLease): number => {
|
|
323
|
+
const entry = singletonResources.get(lease.resource)?.get(lease.key);
|
|
324
|
+
return entry === undefined || entry.value !== lease.value
|
|
325
|
+
? 0
|
|
326
|
+
: entry.activeLeases;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const releaseResourceLeases = (leases: readonly ResourceLease[]): void => {
|
|
330
|
+
for (const lease of leases.toReversed()) {
|
|
331
|
+
releaseResourceLease(lease);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const deleteCachedSingletonResource = (
|
|
336
|
+
declaredResource: AnyResource,
|
|
337
|
+
key: string,
|
|
338
|
+
value: unknown
|
|
339
|
+
): boolean => {
|
|
340
|
+
const cache = singletonResources.get(declaredResource);
|
|
341
|
+
const entry = cache?.get(key);
|
|
342
|
+
if (entry === undefined || entry.value !== value) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
cache?.delete(key);
|
|
347
|
+
return true;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const disposeResourceInstance = async (
|
|
351
|
+
resource: AnyResource,
|
|
352
|
+
value: unknown
|
|
353
|
+
): Promise<Result<void, Error>> => {
|
|
354
|
+
if (resource.dispose === undefined) {
|
|
355
|
+
return Result.ok();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
await resource.dispose(value);
|
|
360
|
+
return Result.ok();
|
|
361
|
+
} catch (error: unknown) {
|
|
362
|
+
const cause = error instanceof Error ? error : undefined;
|
|
363
|
+
const message = cause?.message ?? String(error);
|
|
364
|
+
return Result.err(
|
|
365
|
+
new InternalError(
|
|
366
|
+
`Resource "${resource.id}" failed to dispose: ${message}`,
|
|
367
|
+
{
|
|
368
|
+
...(cause ? { cause } : {}),
|
|
369
|
+
context: { resourceId: resource.id },
|
|
370
|
+
}
|
|
371
|
+
)
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const toResourceLifecycleError = (
|
|
377
|
+
message: string,
|
|
378
|
+
errors: readonly Error[],
|
|
379
|
+
cause?: Error,
|
|
380
|
+
context?: Record<string, unknown>
|
|
381
|
+
): InternalError =>
|
|
382
|
+
new InternalError(message, {
|
|
383
|
+
...(cause ? { cause } : {}),
|
|
384
|
+
context: {
|
|
385
|
+
...context,
|
|
386
|
+
failures: errors.map((error) => ({
|
|
387
|
+
message: error.message,
|
|
388
|
+
name: error.name,
|
|
389
|
+
...(error instanceof InternalError && error.context !== undefined
|
|
390
|
+
? { context: error.context }
|
|
391
|
+
: {}),
|
|
392
|
+
})),
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
|
|
210
396
|
const doCreateResourceInstance = async (
|
|
211
397
|
declaredResource: AnyResource,
|
|
212
|
-
resourceContext: ResourceContext
|
|
398
|
+
resourceContext: ResourceContext,
|
|
399
|
+
key: string
|
|
213
400
|
): Promise<Result<unknown, Error>> => {
|
|
214
401
|
try {
|
|
215
402
|
const created = await declaredResource.create(resourceContext);
|
|
@@ -218,10 +405,10 @@ const doCreateResourceInstance = async (
|
|
|
218
405
|
}
|
|
219
406
|
|
|
220
407
|
const instance = created.unwrap();
|
|
221
|
-
getSingletonResourceCache(declaredResource).set(
|
|
222
|
-
|
|
223
|
-
instance
|
|
224
|
-
);
|
|
408
|
+
getSingletonResourceCache(declaredResource).set(key, {
|
|
409
|
+
activeLeases: 1,
|
|
410
|
+
value: instance,
|
|
411
|
+
});
|
|
225
412
|
return Result.ok(instance);
|
|
226
413
|
} catch (error: unknown) {
|
|
227
414
|
return Result.err(toInternalResourceError(declaredResource.id, error));
|
|
@@ -232,13 +419,15 @@ const trackPendingCreation = (
|
|
|
232
419
|
declaredResource: AnyResource,
|
|
233
420
|
key: string,
|
|
234
421
|
promise: Promise<Result<unknown, Error>>
|
|
235
|
-
):
|
|
422
|
+
): PendingCreation => {
|
|
423
|
+
const entry: PendingCreation = { promise, waiters: 0 };
|
|
236
424
|
const pending = pendingCreations.get(declaredResource);
|
|
237
425
|
if (pending) {
|
|
238
|
-
pending.set(key,
|
|
426
|
+
pending.set(key, entry);
|
|
239
427
|
} else {
|
|
240
|
-
pendingCreations.set(declaredResource, new Map([[key,
|
|
428
|
+
pendingCreations.set(declaredResource, new Map([[key, entry]]));
|
|
241
429
|
}
|
|
430
|
+
return entry;
|
|
242
431
|
};
|
|
243
432
|
|
|
244
433
|
/**
|
|
@@ -249,30 +438,96 @@ const trackPendingCreation = (
|
|
|
249
438
|
const createResourceInstance = async (
|
|
250
439
|
declaredResource: AnyResource,
|
|
251
440
|
resourceContext: ResourceContext
|
|
252
|
-
): Promise<Result<
|
|
441
|
+
): Promise<Result<ResourceResolution, Error>> => {
|
|
253
442
|
const key = toResourceContextKey(resourceContext);
|
|
254
443
|
const inflight = pendingCreations.get(declaredResource)?.get(key);
|
|
255
444
|
if (inflight) {
|
|
256
|
-
|
|
445
|
+
inflight.waiters += 1;
|
|
446
|
+
const resolved = await inflight.promise;
|
|
447
|
+
if (resolved.isErr()) {
|
|
448
|
+
return resolved;
|
|
449
|
+
}
|
|
450
|
+
const lease = acquireCachedSingletonResourceByKey(declaredResource, key);
|
|
451
|
+
return lease === undefined
|
|
452
|
+
? Result.err(
|
|
453
|
+
new InternalError(
|
|
454
|
+
`Resource "${declaredResource.id}" was created but is no longer cached`,
|
|
455
|
+
{ context: { resourceId: declaredResource.id } }
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
: Result.ok({ lease, value: lease.value });
|
|
257
459
|
}
|
|
258
460
|
|
|
259
|
-
const promise = doCreateResourceInstance(
|
|
260
|
-
|
|
461
|
+
const promise = doCreateResourceInstance(
|
|
462
|
+
declaredResource,
|
|
463
|
+
resourceContext,
|
|
464
|
+
key
|
|
465
|
+
);
|
|
466
|
+
const pending = trackPendingCreation(declaredResource, key, promise);
|
|
261
467
|
|
|
262
468
|
try {
|
|
263
|
-
|
|
469
|
+
const resolved = await promise;
|
|
470
|
+
if (resolved.isErr()) {
|
|
471
|
+
return resolved;
|
|
472
|
+
}
|
|
473
|
+
const { value } = resolved;
|
|
474
|
+
const lease = { key, resource: declaredResource, value };
|
|
475
|
+
return Result.ok({
|
|
476
|
+
created: {
|
|
477
|
+
key,
|
|
478
|
+
lease,
|
|
479
|
+
resource: declaredResource,
|
|
480
|
+
sharedDuringCreation: pending.waiters > 0,
|
|
481
|
+
value,
|
|
482
|
+
},
|
|
483
|
+
lease,
|
|
484
|
+
value,
|
|
485
|
+
});
|
|
264
486
|
} finally {
|
|
265
487
|
pendingCreations.get(declaredResource)?.delete(key);
|
|
266
488
|
}
|
|
267
489
|
};
|
|
268
490
|
|
|
269
|
-
|
|
270
|
-
|
|
491
|
+
const rollbackCreatedResources = async (
|
|
492
|
+
created: readonly CreatedResourceInstance[]
|
|
493
|
+
): Promise<Result<void, Error>> => {
|
|
494
|
+
const failures: Error[] = [];
|
|
495
|
+
|
|
496
|
+
for (const entry of created.toReversed()) {
|
|
497
|
+
const activeLeases = countActiveResourceLeases(entry.lease);
|
|
498
|
+
if (activeLeases > 0 || entry.sharedDuringCreation) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const deleted = deleteCachedSingletonResource(
|
|
502
|
+
entry.resource,
|
|
503
|
+
entry.key,
|
|
504
|
+
entry.value
|
|
505
|
+
);
|
|
506
|
+
if (!deleted) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const disposed = await disposeResourceInstance(entry.resource, entry.value);
|
|
510
|
+
if (disposed.isErr()) {
|
|
511
|
+
failures.push(disposed.error);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return failures.length === 0
|
|
516
|
+
? Result.ok()
|
|
517
|
+
: Result.err(
|
|
518
|
+
toResourceLifecycleError(
|
|
519
|
+
'Resource rollback failed during resource resolution',
|
|
520
|
+
failures
|
|
521
|
+
)
|
|
522
|
+
);
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const resolveDeclaredResourceForCreation = async (
|
|
271
526
|
declaredResource: AnyResource,
|
|
272
527
|
ctx: TrailContext,
|
|
273
528
|
overrides: ResourceOverrideMap | undefined,
|
|
274
529
|
configValues: ConfigValues | undefined
|
|
275
|
-
): Promise<Result<
|
|
530
|
+
): Promise<Result<ResourceResolution, Error>> => {
|
|
276
531
|
// Check overrides/extensions first — skip config validation entirely when
|
|
277
532
|
// a resource instance is already provided.
|
|
278
533
|
const overrideOrExtension = getOverrideOrExtension(
|
|
@@ -281,7 +536,12 @@ const resolveDeclaredResource = async (
|
|
|
281
536
|
declaredResource
|
|
282
537
|
);
|
|
283
538
|
if (overrideOrExtension !== undefined) {
|
|
284
|
-
return overrideOrExtension
|
|
539
|
+
return overrideOrExtension.isErr()
|
|
540
|
+
? overrideOrExtension
|
|
541
|
+
: Result.ok({
|
|
542
|
+
lease: overrideOrExtension.value.lease,
|
|
543
|
+
value: overrideOrExtension.value.value,
|
|
544
|
+
});
|
|
285
545
|
}
|
|
286
546
|
|
|
287
547
|
// Resolve config before consulting the singleton cache so config-aware
|
|
@@ -298,7 +558,7 @@ const resolveDeclaredResource = async (
|
|
|
298
558
|
// No provided instance — create via factory.
|
|
299
559
|
const resolved = configAwareResource.unwrap();
|
|
300
560
|
if (resolved.kind === 'provided') {
|
|
301
|
-
return Result.ok(resolved.value);
|
|
561
|
+
return Result.ok({ lease: resolved.lease, value: resolved.value });
|
|
302
562
|
}
|
|
303
563
|
|
|
304
564
|
return await createResourceInstance(
|
|
@@ -322,6 +582,16 @@ const withResolvedResources = (
|
|
|
322
582
|
return resolvedCtx;
|
|
323
583
|
};
|
|
324
584
|
|
|
585
|
+
/**
|
|
586
|
+
* Resolved trail context plus lease release for resources used by the run.
|
|
587
|
+
*/
|
|
588
|
+
interface ResolvedResourceScope {
|
|
589
|
+
readonly ctx: TrailContext;
|
|
590
|
+
release(): void;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const releaseNoResources = (): undefined => undefined;
|
|
594
|
+
|
|
325
595
|
/**
|
|
326
596
|
* Resolve all declared resources for a trail.
|
|
327
597
|
*
|
|
@@ -334,26 +604,189 @@ export const createResources = async (
|
|
|
334
604
|
ctx: TrailContext,
|
|
335
605
|
overrides?: ResourceOverrideMap,
|
|
336
606
|
configValues?: ConfigValues
|
|
337
|
-
): Promise<Result<
|
|
338
|
-
const
|
|
607
|
+
): Promise<Result<ResolvedResourceScope, Error>> => {
|
|
608
|
+
const resources = [...new Set(trail.resources)];
|
|
339
609
|
if (resources.length === 0) {
|
|
340
|
-
return Result.ok(ctx);
|
|
610
|
+
return Result.ok({ ctx, release: releaseNoResources });
|
|
341
611
|
}
|
|
342
612
|
|
|
343
613
|
const resolvedResources: Record<string, unknown> = {};
|
|
614
|
+
const acquiredLeases: ResourceLease[] = [];
|
|
615
|
+
const createdResources: CreatedResourceInstance[] = [];
|
|
344
616
|
|
|
345
617
|
for (const declaredResource of resources) {
|
|
346
|
-
const resolved = await
|
|
618
|
+
const resolved = await resolveDeclaredResourceForCreation(
|
|
347
619
|
declaredResource,
|
|
348
620
|
ctx,
|
|
349
621
|
overrides,
|
|
350
622
|
configValues
|
|
351
623
|
);
|
|
352
624
|
if (resolved.isErr()) {
|
|
625
|
+
releaseResourceLeases(acquiredLeases);
|
|
626
|
+
const rolledBack = await rollbackCreatedResources(createdResources);
|
|
627
|
+
if (rolledBack.isErr()) {
|
|
628
|
+
return Result.err(
|
|
629
|
+
toResourceLifecycleError(
|
|
630
|
+
`Resource resolution failed for "${declaredResource.id}" and rollback also failed`,
|
|
631
|
+
[rolledBack.error],
|
|
632
|
+
resolved.error
|
|
633
|
+
)
|
|
634
|
+
);
|
|
635
|
+
}
|
|
353
636
|
return resolved;
|
|
354
637
|
}
|
|
355
|
-
|
|
638
|
+
const resolution = resolved.unwrap();
|
|
639
|
+
if (resolution.lease !== undefined) {
|
|
640
|
+
acquiredLeases.push(resolution.lease);
|
|
641
|
+
}
|
|
642
|
+
if (resolution.created !== undefined) {
|
|
643
|
+
createdResources.push(resolution.created);
|
|
644
|
+
}
|
|
645
|
+
resolvedResources[declaredResource.id] = resolution.value;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return Result.ok({
|
|
649
|
+
ctx: withResolvedResources(ctx, resolvedResources),
|
|
650
|
+
release: () => releaseResourceLeases(acquiredLeases),
|
|
651
|
+
});
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const toResourceInUseError = (
|
|
655
|
+
resource: AnyResource,
|
|
656
|
+
activeLeases: number
|
|
657
|
+
): InternalError =>
|
|
658
|
+
new InternalError(`Resource "${resource.id}" is still in use`, {
|
|
659
|
+
context: { activeLeases, resourceId: resource.id },
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
const drainCachedEntry = async (
|
|
663
|
+
resource: AnyResource,
|
|
664
|
+
key: string,
|
|
665
|
+
entry: SingletonResourceEntry,
|
|
666
|
+
report: MutableResourceDrainReport,
|
|
667
|
+
failures: Error[]
|
|
668
|
+
): Promise<void> => {
|
|
669
|
+
if (entry.activeLeases > 0) {
|
|
670
|
+
failures.push(toResourceInUseError(resource, entry.activeLeases));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const deleted = deleteCachedSingletonResource(resource, key, entry.value);
|
|
675
|
+
if (!deleted) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
report.evicted.push(resource.id);
|
|
679
|
+
|
|
680
|
+
const result = await disposeResourceInstance(resource, entry.value);
|
|
681
|
+
if (result.isErr()) {
|
|
682
|
+
failures.push(result.error);
|
|
683
|
+
} else if (resource.dispose !== undefined) {
|
|
684
|
+
report.disposed.push(resource.id);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const recordDrainMiss = (
|
|
689
|
+
resource: AnyResource,
|
|
690
|
+
report: MutableResourceDrainReport
|
|
691
|
+
): void => {
|
|
692
|
+
report.missed ??= [];
|
|
693
|
+
if (report.missed.includes(resource.id)) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
report.missed.push(resource.id);
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const hasKeyOtherThan = <T>(
|
|
700
|
+
entries: ReadonlyMap<string, T> | undefined,
|
|
701
|
+
key: string
|
|
702
|
+
): boolean => {
|
|
703
|
+
if (entries === undefined) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
for (const existingKey of entries.keys()) {
|
|
707
|
+
if (existingKey !== key) {
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return false;
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const hasOtherSingletonKeys = (
|
|
715
|
+
key: string,
|
|
716
|
+
cache: ReadonlyMap<string, SingletonResourceEntry> | undefined,
|
|
717
|
+
pending: ReadonlyMap<string, PendingCreation> | undefined
|
|
718
|
+
): boolean => hasKeyOtherThan(cache, key) || hasKeyOtherThan(pending, key);
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Evict and dispose cached resource singletons for a stable resource context.
|
|
722
|
+
*
|
|
723
|
+
* Call this from surface or test shutdown paths with the same `ctx` and
|
|
724
|
+
* `configValues` used for execution. The returned report lists resources
|
|
725
|
+
* removed from the singleton cache and resources whose `dispose` hook ran
|
|
726
|
+
* successfully. On partial failure, the `InternalError` context still includes
|
|
727
|
+
* the report arrays so callers can tell what cleanup already happened.
|
|
728
|
+
*/
|
|
729
|
+
export const drainResources = async (
|
|
730
|
+
resources: readonly AnyResource[],
|
|
731
|
+
ctx: TrailContext,
|
|
732
|
+
configValues?: ConfigValues
|
|
733
|
+
): Promise<Result<ResourceDrainReport, Error>> => {
|
|
734
|
+
const report: MutableResourceDrainReport = { disposed: [], evicted: [] };
|
|
735
|
+
const failures: Error[] = [];
|
|
736
|
+
|
|
737
|
+
for (const resource of resources.toReversed()) {
|
|
738
|
+
const cache = singletonResources.get(resource);
|
|
739
|
+
const pending = pendingCreations.get(resource);
|
|
740
|
+
if (
|
|
741
|
+
(cache === undefined || cache.size === 0) &&
|
|
742
|
+
(pending === undefined || pending.size === 0)
|
|
743
|
+
) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const configResult = resolveResourceConfig(resource, configValues);
|
|
748
|
+
if (configResult.isErr()) {
|
|
749
|
+
failures.push(configResult.error);
|
|
750
|
+
if (cache !== undefined) {
|
|
751
|
+
for (const [key, entry] of [...cache.entries()].toReversed()) {
|
|
752
|
+
await drainCachedEntry(resource, key, entry, report, failures);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const key = toResourceContextKey(
|
|
759
|
+
toResourceContext(ctx, configResult.value)
|
|
760
|
+
);
|
|
761
|
+
const hasAdditionalKeys = hasOtherSingletonKeys(key, cache, pending);
|
|
762
|
+
const pendingForKey = pending?.get(key);
|
|
763
|
+
if (pendingForKey !== undefined) {
|
|
764
|
+
if (hasAdditionalKeys) {
|
|
765
|
+
recordDrainMiss(resource, report);
|
|
766
|
+
}
|
|
767
|
+
failures.push(toResourceInUseError(resource, pendingForKey.waiters + 1));
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const entry = cache?.get(key);
|
|
772
|
+
if (entry === undefined) {
|
|
773
|
+
recordDrainMiss(resource, report);
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (hasAdditionalKeys) {
|
|
778
|
+
recordDrainMiss(resource, report);
|
|
779
|
+
}
|
|
780
|
+
await drainCachedEntry(resource, key, entry, report, failures);
|
|
356
781
|
}
|
|
357
782
|
|
|
358
|
-
return
|
|
783
|
+
return failures.length === 0
|
|
784
|
+
? Result.ok(report)
|
|
785
|
+
: Result.err(
|
|
786
|
+
toResourceLifecycleError('Resource drain failed', failures, undefined, {
|
|
787
|
+
disposed: report.disposed,
|
|
788
|
+
evicted: report.evicted,
|
|
789
|
+
...(report.missed === undefined ? {} : { missed: report.missed }),
|
|
790
|
+
})
|
|
791
|
+
);
|
|
359
792
|
};
|