@telorun/kernel 0.78.0 → 0.79.0
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/dist/bundle/module-artifact.d.ts +2 -1
- package/dist/bundle/module-artifact.d.ts.map +1 -1
- package/dist/bundle/module-artifact.js +14 -10
- package/dist/bundle/module-artifact.js.map +1 -1
- package/dist/controller-loader.d.ts +62 -9
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +71 -63
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-loaders/bundle-loader.d.ts +2 -2
- package/dist/controller-loaders/bundle-loader.d.ts.map +1 -1
- package/dist/controller-loaders/bundle-loader.js +9 -3
- package/dist/controller-loaders/bundle-loader.js.map +1 -1
- package/dist/controller-loaders/napi-loader.d.ts +2 -1
- package/dist/controller-loaders/napi-loader.d.ts.map +1 -1
- package/dist/controller-loaders/napi-loader.js +4 -1
- package/dist/controller-loaders/napi-loader.js.map +1 -1
- package/dist/controller-loaders/npm-loader.d.ts +2 -1
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +13 -7
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/controller-loaders/source-bundle-builder.d.ts +2 -1
- package/dist/controller-loaders/source-bundle-builder.d.ts.map +1 -1
- package/dist/controller-loaders/source-bundle-builder.js +4 -1
- package/dist/controller-loaders/source-bundle-builder.js.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +7 -3
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.js +12 -5
- package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
- package/dist/dependency-injection.d.ts.map +1 -1
- package/dist/dependency-injection.js +7 -7
- package/dist/dependency-injection.js.map +1 -1
- package/dist/evaluation-context.d.ts +33 -13
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +145 -4
- package/dist/evaluation-context.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/instance-declaration.d.ts +8 -0
- package/dist/instance-declaration.d.ts.map +1 -0
- package/dist/instance-declaration.js +34 -0
- package/dist/instance-declaration.js.map +1 -0
- package/dist/invocation-contract-binding.d.ts +15 -2
- package/dist/invocation-contract-binding.d.ts.map +1 -1
- package/dist/invocation-contract-binding.js +39 -4
- package/dist/invocation-contract-binding.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +60 -3
- package/dist/kernel.js.map +1 -1
- package/dist/manifest-schemas.d.ts +1 -1
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +1 -1
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/module-context.d.ts +5 -1
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +13 -1
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts +12 -1
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +15 -0
- package/dist/resource-context.js.map +1 -1
- package/dist/zone-context.d.ts +16 -1
- package/dist/zone-context.d.ts.map +1 -1
- package/dist/zone-context.js +61 -1
- package/dist/zone-context.js.map +1 -1
- package/package.json +3 -3
- package/src/bundle/module-artifact.ts +25 -8
- package/src/controller-loader.ts +114 -80
- package/src/controller-loaders/bundle-loader.ts +10 -2
- package/src/controller-loaders/napi-loader.ts +6 -0
- package/src/controller-loaders/npm-loader.ts +14 -4
- package/src/controller-loaders/source-bundle-builder.ts +6 -0
- package/src/controllers/resource-definition/resource-definition-controller.ts +7 -3
- package/src/controllers/resource-definition/resource-template-controller.ts +12 -5
- package/src/dependency-injection.ts +7 -7
- package/src/evaluation-context.ts +195 -15
- package/src/index.ts +6 -0
- package/src/instance-declaration.ts +36 -0
- package/src/invocation-contract-binding.ts +49 -1
- package/src/kernel.ts +77 -2
- package/src/manifest-schemas.ts +1 -0
- package/src/module-context.ts +24 -3
- package/src/resource-context.ts +18 -0
- package/src/zone-context.ts +70 -1
package/src/controller-loader.ts
CHANGED
|
@@ -39,10 +39,44 @@ export interface ResolvedController {
|
|
|
39
39
|
purl: string;
|
|
40
40
|
source: ControllerResolveSource;
|
|
41
41
|
importInstance: () => Promise<ControllerInstance>;
|
|
42
|
+
/**
|
|
43
|
+
* How long the caller has waited for this controller, measured from the
|
|
44
|
+
* first {@link ControllerWorkKind} branch it entered — or, when it entered
|
|
45
|
+
* none, from the `importInstance()` call. Resolution is what installs,
|
|
46
|
+
* compiles and fetches, so timing only the import reports a 40-second
|
|
47
|
+
* install as a few milliseconds.
|
|
48
|
+
*/
|
|
49
|
+
waitedMs: () => number;
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
/**
|
|
53
|
+
* A branch that makes the caller wait: a registry install, a compile, or a
|
|
54
|
+
* layer transfer. Reported by the sub-loader that is about to enter it, which
|
|
55
|
+
* is the only place that knows — a resolve's `source` is a verdict available
|
|
56
|
+
* only afterwards, and a warm start enters no branch at all, so nothing is
|
|
57
|
+
* announced for it. Reported for work in the resolve *and* the import phase:
|
|
58
|
+
* a dev-mode `pkg:telo` build and a `cargo build` are both paid inside
|
|
59
|
+
* `importInstance`.
|
|
60
|
+
*/
|
|
61
|
+
export type ControllerWorkKind = "npm-install" | "cargo-build" | "source-build" | "layer-fetch";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sub-loader hook, invoked immediately before entering a {@link
|
|
65
|
+
* ControllerWorkKind} branch. Awaited, so a consumer that renders progress has
|
|
66
|
+
* its line on screen before the wait starts rather than after it.
|
|
67
|
+
*/
|
|
68
|
+
export type ControllerWorkReporter = (work: ControllerWorkKind) => void | Promise<void>;
|
|
69
|
+
|
|
44
70
|
export type ControllerLoaderEvent =
|
|
45
|
-
|
|
71
|
+
/**
|
|
72
|
+
* A resolved controller is about to be imported. `source` names the branch
|
|
73
|
+
* the resolve took, so a consumer can tell a warm start (`cache` / `local`)
|
|
74
|
+
* from work someone waited for (`npm-install`, `cargo-build`, `bundle`)
|
|
75
|
+
* *before* rendering anything. The event is emitted after resolution
|
|
76
|
+
* precisely so this is known: resolution is what fetches, installs and
|
|
77
|
+
* builds, and an event emitted ahead of it could only speculate.
|
|
78
|
+
*/
|
|
79
|
+
| { name: "ControllerLoading"; payload: { purl: string; source: ControllerResolveSource } }
|
|
46
80
|
| {
|
|
47
81
|
name: "ControllerLoaded";
|
|
48
82
|
payload: { purl: string; source: ControllerResolveSource; durationMs: number };
|
|
@@ -55,7 +89,15 @@ export type ControllerLoaderEvent =
|
|
|
55
89
|
* which is non-recoverable. Consumers that opened a UI element on the
|
|
56
90
|
* matching `ControllerLoading` should close it out here.
|
|
57
91
|
*/
|
|
58
|
-
| { name: "ControllerLoadSkipped"; payload: { purl: string; reason: string } }
|
|
92
|
+
| { name: "ControllerLoadSkipped"; payload: { purl: string; reason: string } }
|
|
93
|
+
/**
|
|
94
|
+
* Real work has started for `purl` — an install, a compile or a transfer is
|
|
95
|
+
* about to run. This is the *only* in-progress signal, and it fires solely
|
|
96
|
+
* on the branch that does the work, so a warm start emits nothing rather
|
|
97
|
+
* than something a consumer has to take back. Closed by the matching
|
|
98
|
+
* `ControllerLoaded` / `ControllerLoadFailed`.
|
|
99
|
+
*/
|
|
100
|
+
| { name: "ControllerWorkStarted"; payload: { purl: string; work: ControllerWorkKind } };
|
|
59
101
|
|
|
60
102
|
/**
|
|
61
103
|
* The dispatcher awaits each emission, so the callback may be async without
|
|
@@ -104,9 +146,13 @@ export interface ControllerLoaderOptions {
|
|
|
104
146
|
* next candidate. User-code failures (`RuntimeError("ERR_CONTROLLER_BUILD_FAILED" | "ERR_CONTROLLER_INVALID")`)
|
|
105
147
|
* fail hard regardless of remaining candidates.
|
|
106
148
|
*
|
|
107
|
-
* Lifecycle events
|
|
108
|
-
* `
|
|
109
|
-
* `
|
|
149
|
+
* Lifecycle events follow resolution: a candidate this environment cannot host
|
|
150
|
+
* produces a `ControllerLoadSkipped` and nothing else, and only the candidate
|
|
151
|
+
* that resolved gets a `ControllerLoading` (carrying its resolve `source`)
|
|
152
|
+
* followed by `ControllerLoaded` / `ControllerLoadFailed`. In between, the
|
|
153
|
+
* sub-loader announces each branch that makes the caller wait as
|
|
154
|
+
* `ControllerWorkStarted` — the only in-progress signal, and the only one that
|
|
155
|
+
* cannot be emitted for a warm start.
|
|
110
156
|
*/
|
|
111
157
|
export class ControllerLoader {
|
|
112
158
|
private readonly emit: ControllerLoaderEmit | undefined;
|
|
@@ -132,56 +178,28 @@ export class ControllerLoader {
|
|
|
132
178
|
artifact?: ModuleArtifact,
|
|
133
179
|
libraries: SiblingLibraryMap = NO_SIBLING_LIBRARIES,
|
|
134
180
|
): Promise<ControllerInstance> {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
const ordered = orderCandidates(purlCandidates, effectivePolicy);
|
|
140
|
-
if (ordered.length === 0) {
|
|
141
|
-
throw new RuntimeError(
|
|
142
|
-
"ERR_CONTROLLER_NOT_FOUND",
|
|
143
|
-
`No controllers match runtime selection [${effectivePolicy.load.join(", ")}]; declared: ${purlCandidates.join(", ")}`,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const errors: string[] = [];
|
|
148
|
-
for (const purl of ordered) {
|
|
149
|
-
await this.emit?.({ name: "ControllerLoading", payload: { purl } });
|
|
150
|
-
const startedAt = Date.now();
|
|
151
|
-
try {
|
|
152
|
-
const { instance, source } = await this.dispatchOne(purl, baseUri, artifact, libraries);
|
|
153
|
-
await this.emit?.({
|
|
154
|
-
name: "ControllerLoaded",
|
|
155
|
-
payload: { purl, source, durationMs: Date.now() - startedAt },
|
|
156
|
-
});
|
|
157
|
-
return instance;
|
|
158
|
-
} catch (err) {
|
|
159
|
-
if (err instanceof ControllerEnvMissingError) {
|
|
160
|
-
errors.push(`${purl}: ${err.message}`);
|
|
161
|
-
// Env-missing isn't a hard failure — the dispatcher will try the
|
|
162
|
-
// next candidate. We still emit a terminal event for *this* attempt
|
|
163
|
-
// so consumers (notably the CLI progress renderer) can close out
|
|
164
|
-
// the UI state opened by the matching ControllerLoading. Without
|
|
165
|
-
// this, every fallback attempt would leak a pending `⬇` line.
|
|
166
|
-
await this.emit?.({
|
|
167
|
-
name: "ControllerLoadSkipped",
|
|
168
|
-
payload: { purl, reason: err.message },
|
|
169
|
-
});
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
await this.emit?.({
|
|
173
|
-
name: "ControllerLoadFailed",
|
|
174
|
-
payload: { purl, error: err instanceof Error ? err.message : String(err) },
|
|
175
|
-
});
|
|
176
|
-
throw err;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
const aggregated = `No controller resolved. Tried ${ordered.length} candidate(s):\n${errors.join("\n")}`;
|
|
181
|
+
// Resolution — which is what installs, compiles and fetches — reports its
|
|
182
|
+
// own work and its own candidate fallthrough, so this is the import half
|
|
183
|
+
// and the announcement of what resolution decided.
|
|
184
|
+
const resolved = await this.resolve(purlCandidates, baseUri, policy, artifact, libraries);
|
|
180
185
|
await this.emit?.({
|
|
181
|
-
name: "
|
|
182
|
-
payload: { purl:
|
|
186
|
+
name: "ControllerLoading",
|
|
187
|
+
payload: { purl: resolved.purl, source: resolved.source },
|
|
183
188
|
});
|
|
184
|
-
|
|
189
|
+
try {
|
|
190
|
+
const instance = await resolved.importInstance();
|
|
191
|
+
await this.emit?.({
|
|
192
|
+
name: "ControllerLoaded",
|
|
193
|
+
payload: { purl: resolved.purl, source: resolved.source, durationMs: resolved.waitedMs() },
|
|
194
|
+
});
|
|
195
|
+
return instance;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
await this.emit?.({
|
|
198
|
+
name: "ControllerLoadFailed",
|
|
199
|
+
payload: { purl: resolved.purl, error: err instanceof Error ? err.message : String(err) },
|
|
200
|
+
});
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
185
203
|
}
|
|
186
204
|
|
|
187
205
|
/**
|
|
@@ -194,10 +212,12 @@ export class ControllerLoader {
|
|
|
194
212
|
* registers fine and errors only when a resource of it is declared (matching
|
|
195
213
|
* the Rust kernel's deferral).
|
|
196
214
|
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
215
|
+
* Emits `ControllerWorkStarted` whenever a sub-loader enters a branch that
|
|
216
|
+
* makes the caller wait, and `ControllerLoadSkipped` per candidate this
|
|
217
|
+
* environment cannot host. It does NOT announce the load itself: the caller
|
|
218
|
+
* emits ControllerLoading/Loaded around `importInstance`, so those fire when
|
|
219
|
+
* the load actually happens, with the resolved `source` already in hand. A
|
|
220
|
+
* total resolution failure throws, mirroring {@link load}'s aggregated error.
|
|
201
221
|
*/
|
|
202
222
|
async resolve(
|
|
203
223
|
purlCandidates: string[],
|
|
@@ -219,41 +239,54 @@ export class ControllerLoader {
|
|
|
219
239
|
}
|
|
220
240
|
const errors: string[] = [];
|
|
221
241
|
for (const purl of ordered) {
|
|
242
|
+
// First work only: an install followed by a compile is one wait, and the
|
|
243
|
+
// clock the caller reads has to start where that wait did. The reporter
|
|
244
|
+
// outlives resolution because the import phase does work too.
|
|
245
|
+
let workStartedAt: number | undefined;
|
|
246
|
+
const report: ControllerWorkReporter = async (work) => {
|
|
247
|
+
workStartedAt ??= Date.now();
|
|
248
|
+
await this.emit?.({ name: "ControllerWorkStarted", payload: { purl, work } });
|
|
249
|
+
};
|
|
222
250
|
try {
|
|
223
251
|
const { source, importInstance } = await this.dispatchResolveOne(
|
|
224
252
|
purl,
|
|
225
253
|
baseUri,
|
|
226
254
|
artifact,
|
|
227
255
|
libraries,
|
|
256
|
+
report,
|
|
228
257
|
);
|
|
229
|
-
|
|
258
|
+
let importStartedAt: number | undefined;
|
|
259
|
+
return {
|
|
260
|
+
purl,
|
|
261
|
+
source,
|
|
262
|
+
importInstance: () => {
|
|
263
|
+
importStartedAt ??= Date.now();
|
|
264
|
+
return importInstance();
|
|
265
|
+
},
|
|
266
|
+
waitedMs: () => Date.now() - (workStartedAt ?? importStartedAt ?? Date.now()),
|
|
267
|
+
};
|
|
230
268
|
} catch (err) {
|
|
231
269
|
if (err instanceof ControllerEnvMissingError) {
|
|
232
270
|
errors.push(`${purl}: ${err.message}`);
|
|
271
|
+
await this.emit?.({
|
|
272
|
+
name: "ControllerLoadSkipped",
|
|
273
|
+
payload: { purl, reason: err.message },
|
|
274
|
+
});
|
|
233
275
|
continue;
|
|
234
276
|
}
|
|
277
|
+
await this.emit?.({
|
|
278
|
+
name: "ControllerLoadFailed",
|
|
279
|
+
payload: { purl, error: err instanceof Error ? err.message : String(err) },
|
|
280
|
+
});
|
|
235
281
|
throw err;
|
|
236
282
|
}
|
|
237
283
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
private async dispatchOne(
|
|
245
|
-
purl: string,
|
|
246
|
-
baseUri: string,
|
|
247
|
-
artifact: ModuleArtifact | undefined,
|
|
248
|
-
libraries: SiblingLibraryMap,
|
|
249
|
-
): Promise<{ instance: ControllerInstance; source: ControllerResolveSource }> {
|
|
250
|
-
const { source, importInstance } = await this.dispatchResolveOne(
|
|
251
|
-
purl,
|
|
252
|
-
baseUri,
|
|
253
|
-
artifact,
|
|
254
|
-
libraries,
|
|
255
|
-
);
|
|
256
|
-
return { instance: await importInstance(), source };
|
|
284
|
+
const aggregated = `No controller resolved. Tried ${ordered.length} candidate(s):\n${errors.join("\n")}`;
|
|
285
|
+
await this.emit?.({
|
|
286
|
+
name: "ControllerLoadFailed",
|
|
287
|
+
payload: { purl: ordered[ordered.length - 1], error: aggregated },
|
|
288
|
+
});
|
|
289
|
+
throw new RuntimeError("ERR_CONTROLLER_NOT_FOUND", aggregated);
|
|
257
290
|
}
|
|
258
291
|
|
|
259
292
|
private async dispatchResolveOne(
|
|
@@ -261,15 +294,16 @@ export class ControllerLoader {
|
|
|
261
294
|
baseUri: string,
|
|
262
295
|
artifact: ModuleArtifact | undefined,
|
|
263
296
|
libraries: SiblingLibraryMap,
|
|
297
|
+
report: ControllerWorkReporter,
|
|
264
298
|
): Promise<{ source: ControllerResolveSource; importInstance: () => Promise<ControllerInstance> }> {
|
|
265
299
|
if (purl.startsWith("pkg:npm")) {
|
|
266
|
-
return this.npmLoader.resolve(purl, baseUri);
|
|
300
|
+
return this.npmLoader.resolve(purl, baseUri, report);
|
|
267
301
|
}
|
|
268
302
|
if (purl.startsWith("pkg:cargo")) {
|
|
269
|
-
return this.napiLoader.resolve(purl, baseUri);
|
|
303
|
+
return this.napiLoader.resolve(purl, baseUri, report);
|
|
270
304
|
}
|
|
271
305
|
if (purl.startsWith("pkg:telo")) {
|
|
272
|
-
return this.bundleLoader.resolve(purl, baseUri, artifact, libraries);
|
|
306
|
+
return this.bundleLoader.resolve(purl, baseUri, artifact, libraries, report);
|
|
273
307
|
}
|
|
274
308
|
throw new ControllerEnvMissingError(`Unsupported PURL scheme: ${purl}`);
|
|
275
309
|
}
|
|
@@ -7,7 +7,7 @@ import { PackageURL } from "packageurl-js";
|
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
9
9
|
import { hostPlatformTarget, type ModuleArtifact } from "../bundle/module-artifact.js";
|
|
10
|
-
import type { ControllerResolveSource } from "../controller-loader.js";
|
|
10
|
+
import type { ControllerResolveSource, ControllerWorkReporter } from "../controller-loader.js";
|
|
11
11
|
import { ControllerEnvMissingError } from "./napi-loader.js";
|
|
12
12
|
import { REALM_COLLAPSE_NAMES } from "./realm.js";
|
|
13
13
|
import {
|
|
@@ -485,6 +485,7 @@ export class BundleControllerLoader {
|
|
|
485
485
|
baseUri: string,
|
|
486
486
|
artifact?: ModuleArtifact,
|
|
487
487
|
libraries: SiblingLibraryMap = NO_SIBLING_LIBRARIES,
|
|
488
|
+
report?: ControllerWorkReporter,
|
|
488
489
|
): Promise<{ source: ControllerResolveSource; importInstance: () => Promise<ControllerInstance> }> {
|
|
489
490
|
let parsed: PackageURL;
|
|
490
491
|
try {
|
|
@@ -578,10 +579,17 @@ export class BundleControllerLoader {
|
|
|
578
579
|
importInstance: async () => {
|
|
579
580
|
let built: string;
|
|
580
581
|
try {
|
|
582
|
+
// A dev build is a compile the caller waits on, and it is paid here
|
|
583
|
+
// rather than at resolve — which is why the work signal is not tied
|
|
584
|
+
// to the resolve phase. The reporter goes INTO the builder rather
|
|
585
|
+
// than wrapping the call: only the builder knows whether its
|
|
586
|
+
// content-addressed cache answered, and reporting a cache hit would
|
|
587
|
+
// put a line on screen for work nobody waited for.
|
|
581
588
|
built = await buildControllerFromSource(
|
|
582
589
|
sourceFile!,
|
|
583
590
|
cacheRoot!,
|
|
584
591
|
buildExternals(libraries, format),
|
|
592
|
+
report,
|
|
585
593
|
);
|
|
586
594
|
} catch (err) {
|
|
587
595
|
if (!(err instanceof ControllerEnvMissingError)) throw err;
|
|
@@ -607,7 +615,7 @@ export class BundleControllerLoader {
|
|
|
607
615
|
if (artifact) {
|
|
608
616
|
// By its own selector, not by re-matching the host: this candidate IS one
|
|
609
617
|
// selector, and it is exactly the key of the layer that carries it.
|
|
610
|
-
const resolved = await artifact.materializeController(selector);
|
|
618
|
+
const resolved = await artifact.materializeController(selector, report);
|
|
611
619
|
if (!resolved) {
|
|
612
620
|
throw new ControllerEnvMissingError(
|
|
613
621
|
`pkg:telo controller "${purl}": the module artifact ships no layer for ` +
|
|
@@ -8,6 +8,8 @@ import { fileURLToPath } from "url";
|
|
|
8
8
|
import { promisify } from "util";
|
|
9
9
|
|
|
10
10
|
import { hostEnv } from "../host-env.js";
|
|
11
|
+
// Type-only, so the cycle with the dispatcher that imports this loader is erased.
|
|
12
|
+
import type { ControllerWorkReporter } from "../controller-loader.js";
|
|
11
13
|
|
|
12
14
|
const execFileAsync = promisify(execFile);
|
|
13
15
|
const requireFromHere = createRequire(import.meta.url);
|
|
@@ -140,6 +142,7 @@ export class NapiControllerLoader {
|
|
|
140
142
|
async resolve(
|
|
141
143
|
purl: string,
|
|
142
144
|
baseUri: string,
|
|
145
|
+
report?: ControllerWorkReporter,
|
|
143
146
|
): Promise<{ source: NapiResolveSource; importInstance: () => Promise<ControllerInstance> }> {
|
|
144
147
|
const [, , name, , qualifiers, entry] = PackageURL.parseString(purl);
|
|
145
148
|
const localPath = (qualifiers as any)?.get("local_path");
|
|
@@ -199,6 +202,9 @@ export class NapiControllerLoader {
|
|
|
199
202
|
const { rawModule } = await existingInFlight;
|
|
200
203
|
return project(rawModule, entry, cratePath);
|
|
201
204
|
}
|
|
205
|
+
// Nothing above this line compiles — the cache and the in-flight gate
|
|
206
|
+
// both return without cargo running.
|
|
207
|
+
await report?.("cargo-build");
|
|
202
208
|
const buildPromise = build(cratePath, crateName, cacheKey);
|
|
203
209
|
_napiInFlight.set(cacheKey, buildPromise);
|
|
204
210
|
let rawModule: any;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ControllerInstance, NOOP_LOGGER, type Logger } from "@telorun/sdk";
|
|
2
|
+
import type { ControllerWorkReporter } from "../controller-loader.js";
|
|
2
3
|
import { execFile } from "child_process";
|
|
3
4
|
import * as crypto from "crypto";
|
|
4
5
|
import * as fs from "fs/promises";
|
|
@@ -188,6 +189,7 @@ export class NpmControllerLoader {
|
|
|
188
189
|
async resolve(
|
|
189
190
|
purl: string,
|
|
190
191
|
baseUri: string,
|
|
192
|
+
report?: ControllerWorkReporter,
|
|
191
193
|
): Promise<{ source: NpmResolveSource; importInstance: () => Promise<ControllerInstance> }> {
|
|
192
194
|
const parsed = PackageURL.fromString(purl);
|
|
193
195
|
if (!parsed.name) {
|
|
@@ -199,7 +201,7 @@ export class NpmControllerLoader {
|
|
|
199
201
|
// versions can coexist in one flat node_modules (see installAlias).
|
|
200
202
|
const alias = installAlias(packageName, version);
|
|
201
203
|
|
|
202
|
-
const installRoot = await this.ensureInstallRoot();
|
|
204
|
+
const installRoot = await this.ensureInstallRoot(report);
|
|
203
205
|
const resolved = await resolveInstallSpec(parsed, packageName, baseUri);
|
|
204
206
|
const source = await this.installPackage(
|
|
205
207
|
installRoot,
|
|
@@ -207,6 +209,7 @@ export class NpmControllerLoader {
|
|
|
207
209
|
resolved.spec,
|
|
208
210
|
resolved.kind,
|
|
209
211
|
version,
|
|
212
|
+
report,
|
|
210
213
|
);
|
|
211
214
|
const subpath = parsed.subpath ?? null;
|
|
212
215
|
return {
|
|
@@ -221,9 +224,9 @@ export class NpmControllerLoader {
|
|
|
221
224
|
* `npm install` once, and return the absolute root path. Memoized for the
|
|
222
225
|
* lifetime of this loader instance.
|
|
223
226
|
*/
|
|
224
|
-
private ensureInstallRoot(): Promise<string> {
|
|
227
|
+
private ensureInstallRoot(report?: ControllerWorkReporter): Promise<string> {
|
|
225
228
|
if (this.rootReady) return this.rootReady;
|
|
226
|
-
this.rootReady = this.materializeInstallRoot().catch((err) => {
|
|
229
|
+
this.rootReady = this.materializeInstallRoot(report).catch((err) => {
|
|
227
230
|
// Reset on failure so a follow-up call retries rather than caches the rejection.
|
|
228
231
|
this.rootReady = undefined;
|
|
229
232
|
throw err;
|
|
@@ -231,7 +234,7 @@ export class NpmControllerLoader {
|
|
|
231
234
|
return this.rootReady;
|
|
232
235
|
}
|
|
233
236
|
|
|
234
|
-
private async materializeInstallRoot(): Promise<string> {
|
|
237
|
+
private async materializeInstallRoot(report?: ControllerWorkReporter): Promise<string> {
|
|
235
238
|
if (!this.entryUrl) {
|
|
236
239
|
// Throw the env-missing variant so a mixed candidate list (e.g.
|
|
237
240
|
// `pkg:npm/... + pkg:cargo/...`) still falls back to the next
|
|
@@ -325,6 +328,9 @@ export class NpmControllerLoader {
|
|
|
325
328
|
2,
|
|
326
329
|
) + "\n",
|
|
327
330
|
);
|
|
331
|
+
// Announced here rather than at the top of the resolve: everything above
|
|
332
|
+
// this line is a fast path that reuses the existing tree.
|
|
333
|
+
await report?.("npm-install");
|
|
328
334
|
await runPackageManager(installRoot, [
|
|
329
335
|
"install",
|
|
330
336
|
"--no-audit",
|
|
@@ -383,6 +389,7 @@ export class NpmControllerLoader {
|
|
|
383
389
|
spec: string,
|
|
384
390
|
kind: SpecKind,
|
|
385
391
|
requestedVersion: string | null,
|
|
392
|
+
report?: ControllerWorkReporter,
|
|
386
393
|
): Promise<NpmResolveSource> {
|
|
387
394
|
const cacheKey = alias;
|
|
388
395
|
if (this.installedSpecs.has(cacheKey)) return "cache";
|
|
@@ -466,6 +473,9 @@ export class NpmControllerLoader {
|
|
|
466
473
|
}
|
|
467
474
|
}
|
|
468
475
|
|
|
476
|
+
// Past every re-check: the package manager is about to run for real,
|
|
477
|
+
// which is the wait a caller wants reported.
|
|
478
|
+
await report?.("npm-install");
|
|
469
479
|
await runPackageManager(installRoot, [
|
|
470
480
|
"install",
|
|
471
481
|
"--no-audit",
|
|
@@ -4,6 +4,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import * as fs from "node:fs/promises";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
+
// Type-only, so this builder stays free of a runtime edge to the dispatcher.
|
|
8
|
+
import type { ControllerWorkReporter } from "../controller-loader.js";
|
|
7
9
|
|
|
8
10
|
import { readOwnerManifest } from "../bundle/module-manifest.js";
|
|
9
11
|
import { ControllerEnvMissingError } from "./napi-loader.js";
|
|
@@ -295,6 +297,7 @@ export async function buildControllerFromSource(
|
|
|
295
297
|
entryFile: string,
|
|
296
298
|
cacheRoot: string,
|
|
297
299
|
libraries: readonly SiblingLibrary[] = [],
|
|
300
|
+
report?: ControllerWorkReporter,
|
|
298
301
|
): Promise<string> {
|
|
299
302
|
const cacheDir = path.join(cacheRoot, CACHE_DIR);
|
|
300
303
|
const externals = externalSpecifiers(libraries);
|
|
@@ -309,6 +312,9 @@ export async function buildControllerFromSource(
|
|
|
309
312
|
|
|
310
313
|
const inFlight = buildsInFlight.get(entryFile);
|
|
311
314
|
if (inFlight) return inFlight;
|
|
315
|
+
// Below the content-addressed cache and the in-flight gate: from here esbuild
|
|
316
|
+
// really runs, which is the only branch worth reporting as a wait.
|
|
317
|
+
await report?.("source-build");
|
|
312
318
|
const work = build(entryFile, cacheDir, libraries).finally(() =>
|
|
313
319
|
buildsInFlight.delete(entryFile),
|
|
314
320
|
);
|
|
@@ -161,6 +161,11 @@ class ResourceDefinition implements ResourceInstance {
|
|
|
161
161
|
installRoot: ctx.getInstallRoot(),
|
|
162
162
|
cacheRoot: host.getCacheRoot?.(),
|
|
163
163
|
log: ctx.log,
|
|
164
|
+
// What resolution reports on its own: the work branches it enters, and
|
|
165
|
+
// the candidates it skipped. Routed through `ctx.emit` so they carry the
|
|
166
|
+
// same `<kind>.` namespace as the load events emitted below — a consumer
|
|
167
|
+
// pairs a wait with its outcome by that namespace.
|
|
168
|
+
emit: (event) => ctx.emit(event.name, event.payload as Record<string, unknown>),
|
|
164
169
|
});
|
|
165
170
|
// The artifact of the module that DECLARED this kind — a bundled controller
|
|
166
171
|
// ships in its own module's payload, not the consumer's. It owns the pinned
|
|
@@ -199,8 +204,7 @@ class ResourceDefinition implements ResourceInstance {
|
|
|
199
204
|
}
|
|
200
205
|
throw err;
|
|
201
206
|
});
|
|
202
|
-
await ctx.emit("ControllerLoading", { purl: resolved.purl });
|
|
203
|
-
const startedAt = Date.now();
|
|
207
|
+
await ctx.emit("ControllerLoading", { purl: resolved.purl, source: resolved.source });
|
|
204
208
|
const instance = await resolved.importInstance().catch(async (err) => {
|
|
205
209
|
await ctx.emit("ControllerLoadFailed", {
|
|
206
210
|
purl: resolved.purl,
|
|
@@ -212,7 +216,7 @@ class ResourceDefinition implements ResourceInstance {
|
|
|
212
216
|
await ctx.emit("ControllerLoaded", {
|
|
213
217
|
purl: resolved.purl,
|
|
214
218
|
source: resolved.source,
|
|
215
|
-
durationMs:
|
|
219
|
+
durationMs: resolved.waitedMs(),
|
|
216
220
|
});
|
|
217
221
|
},
|
|
218
222
|
);
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
} from "@telorun/sdk";
|
|
8
8
|
import { isCompiledValue } from "@telorun/sdk";
|
|
9
9
|
import { isRefSentinel } from "@telorun/templating";
|
|
10
|
+
import { celSelfView } from "../../evaluation-context.js";
|
|
10
11
|
|
|
11
12
|
/** CEL variables that are only bound at call time (request handling, step
|
|
12
13
|
* chaining, error branches) — never at a template's init(). A persistent
|
|
@@ -168,7 +169,13 @@ export function createTemplateController(definition: {
|
|
|
168
169
|
for (const key of path[1].split(".").slice(1)) cur = cur?.[key];
|
|
169
170
|
return cur;
|
|
170
171
|
}
|
|
171
|
-
|
|
172
|
+
// CEL cannot read a member off a live instance, and a ref slot holds
|
|
173
|
+
// one after Phase-5 injection. `celSelfView` replaces each with its
|
|
174
|
+
// published reading, so `self.<ref>.<field>` answers exactly as
|
|
175
|
+
// `resources.<name>.<field>` does. The pure-`self.<path>` form above
|
|
176
|
+
// is navigated directly and still yields the instance itself, which is
|
|
177
|
+
// what a ref slot passed straight through (`connection:`) needs.
|
|
178
|
+
return definingContext.expandWith(value, { self: celSelfView(getSelf()) });
|
|
172
179
|
}
|
|
173
180
|
if (isRefSentinel(value)) {
|
|
174
181
|
const source = value.source;
|
|
@@ -215,10 +222,10 @@ export function createTemplateController(definition: {
|
|
|
215
222
|
throw capabilityError(entry, invokeTarget, "invoke", "Telo.Invocable");
|
|
216
223
|
}
|
|
217
224
|
const invokeInputs =
|
|
218
|
-
definition.inputs != null ? expand(definition.inputs, { self: getSelf(), inputs }) : inputs;
|
|
225
|
+
definition.inputs != null ? expand(definition.inputs, { self: celSelfView(getSelf()), inputs }) : inputs;
|
|
219
226
|
const raw = await entry.instance.invoke(invokeInputs);
|
|
220
227
|
if (definition.result == null) return raw;
|
|
221
|
-
return expand(definition.result, { self: getSelf(), result: raw });
|
|
228
|
+
return expand(definition.result, { self: celSelfView(getSelf()), result: raw });
|
|
222
229
|
},
|
|
223
230
|
}),
|
|
224
231
|
|
|
@@ -239,10 +246,10 @@ export function createTemplateController(definition: {
|
|
|
239
246
|
throw capabilityError(entry, provideTarget, "provide", "Telo.Invocable");
|
|
240
247
|
}
|
|
241
248
|
const provideInputs: any =
|
|
242
|
-
definition.inputs != null ? expand(definition.inputs, { self: getSelf() }) : {};
|
|
249
|
+
definition.inputs != null ? expand(definition.inputs, { self: celSelfView(getSelf()) }) : {};
|
|
243
250
|
const raw = await entry.instance.invoke(provideInputs);
|
|
244
251
|
if (definition.result == null) return raw;
|
|
245
|
-
return expand(definition.result, { self: getSelf(), result: raw });
|
|
252
|
+
return expand(definition.result, { self: celSelfView(getSelf()), result: raw });
|
|
246
253
|
},
|
|
247
254
|
}),
|
|
248
255
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ResourceInstance, ResourceManifest, RuntimeError
|
|
1
|
+
import { ResourceInstance, ResourceManifest, RuntimeError } from "@telorun/sdk";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Walks `resource` following `fieldPath` (dot notation, `[]` = array traversal,
|
|
@@ -41,12 +41,12 @@ export function injectAtPath(
|
|
|
41
41
|
`Local reference '${String(ref.name)}' is registered but not initialized yet (deferring to a later init pass)`,
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
// The identity is NOT stamped here. It is stamped at `create()`, the single
|
|
45
|
+
// instance-production site, which is also the only point where an instance
|
|
46
|
+
// and the context that DECLARED it are both in hand — here the context is
|
|
47
|
+
// the CONSUMER's, so a declaration site derived at this point would name
|
|
48
|
+
// whoever referenced the resource. A second write-once stamp competing for
|
|
49
|
+
// the same property would silently decide that by init order.
|
|
50
50
|
return instance;
|
|
51
51
|
}
|
|
52
52
|
|