@telorun/kernel 0.61.0 → 0.63.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 +28 -2
- package/dist/bundle/module-artifact.d.ts.map +1 -1
- package/dist/bundle/module-artifact.js +36 -9
- package/dist/bundle/module-artifact.js.map +1 -1
- package/dist/controller-loader.d.ts +11 -4
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +1 -1
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-loaders/bundle-loader.d.ts +21 -0
- package/dist/controller-loaders/bundle-loader.d.ts.map +1 -1
- package/dist/controller-loaders/bundle-loader.js +106 -21
- package/dist/controller-loaders/bundle-loader.js.map +1 -1
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +62 -10
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/controller-loaders/source-bundle-builder.d.ts +30 -0
- package/dist/controller-loaders/source-bundle-builder.d.ts.map +1 -0
- package/dist/controller-loaders/source-bundle-builder.js +302 -0
- package/dist/controller-loaders/source-bundle-builder.js.map +1 -0
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +9 -2
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +9 -0
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +21 -20
- package/dist/kernel.js.map +1 -1
- package/dist/logging/file-sink.js +1 -1
- package/dist/logging/file-sink.js.map +1 -1
- package/dist/logging/index.d.ts +1 -1
- package/dist/logging/index.d.ts.map +1 -1
- package/dist/logging/index.js +1 -1
- package/dist/logging/index.js.map +1 -1
- package/dist/logging/log-sink.d.ts +1 -1
- package/dist/logging/log-sink.d.ts.map +1 -1
- package/dist/logging/log-sink.js +1 -1
- package/dist/logging/log-sink.js.map +1 -1
- package/dist/resource-context.d.ts +11 -1
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +22 -2
- package/dist/resource-context.js.map +1 -1
- package/dist/runtime-seam.d.ts +19 -0
- package/dist/runtime-seam.d.ts.map +1 -0
- package/dist/runtime-seam.js +211 -0
- package/dist/runtime-seam.js.map +1 -0
- package/dist/static-analysis-diagnostics.d.ts +19 -0
- package/dist/static-analysis-diagnostics.d.ts.map +1 -0
- package/dist/static-analysis-diagnostics.js +52 -0
- package/dist/static-analysis-diagnostics.js.map +1 -0
- package/dist/transports/oci/oci-transport.d.ts +17 -0
- package/dist/transports/oci/oci-transport.d.ts.map +1 -1
- package/dist/transports/oci/oci-transport.js +33 -8
- package/dist/transports/oci/oci-transport.js.map +1 -1
- package/dist/transports/transport-registry.d.ts +6 -4
- package/dist/transports/transport-registry.d.ts.map +1 -1
- package/dist/transports/transport-registry.js +6 -4
- package/dist/transports/transport-registry.js.map +1 -1
- package/package.json +5 -5
- package/src/bundle/module-artifact.ts +54 -12
- package/src/controller-loader.ts +12 -5
- package/src/controller-loaders/bundle-loader.ts +117 -27
- package/src/controller-loaders/npm-loader.ts +75 -13
- package/src/controller-loaders/source-bundle-builder.ts +337 -0
- package/src/controllers/resource-definition/resource-definition-controller.ts +23 -17
- package/src/index.ts +3 -1
- package/src/kernel.ts +25 -17
- package/src/logging/file-sink.ts +1 -1
- package/src/logging/index.ts +1 -1
- package/src/logging/log-sink.ts +1 -0
- package/src/resource-context.ts +23 -0
- package/src/runtime-seam.ts +230 -0
- package/src/static-analysis-diagnostics.ts +51 -0
- package/src/transports/oci/oci-transport.ts +34 -8
- package/src/transports/transport-registry.ts +6 -4
- package/dist/logging/record-buffer.d.ts +0 -24
- package/dist/logging/record-buffer.d.ts.map +0 -1
- package/dist/logging/record-buffer.js +0 -78
- package/dist/logging/record-buffer.js.map +0 -1
- package/src/logging/record-buffer.ts +0 -72
|
@@ -59,10 +59,12 @@ export function defaultTransports(registryUrl) {
|
|
|
59
59
|
}
|
|
60
60
|
const defaultRegistryCache = new Map();
|
|
61
61
|
/** A `TransportRegistry` seeded with {@link defaultTransports}, memoized per
|
|
62
|
-
* `registryUrl`. The default transports
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
62
|
+
* `registryUrl`. The default transports hold no per-call state, so one shared
|
|
63
|
+
* instance per registry URL is safe — and avoids re-instantiating the whole set
|
|
64
|
+
* on hot paths like `cachePathForCanonical`. It is also what gives
|
|
65
|
+
* `OciTransport`'s per-instance read-client pool a process-wide lifetime here,
|
|
66
|
+
* so the bearer-token cache survives across operations without the pool having
|
|
67
|
+
* to be global. */
|
|
66
68
|
export function defaultTransportRegistry(registryUrl) {
|
|
67
69
|
const key = registryUrl ?? "";
|
|
68
70
|
let cached = defaultRegistryCache.get(key);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport-registry.js","sourceRoot":"","sources":["../../src/transports/transport-registry.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAQ5D;;;;2EAI2E;AAC3E,MAAM,OAAO,iBAAiB;IAC5B,YAA6B,UAAuB;QAAvB,eAAU,GAAV,UAAU,CAAa;IAAG,CAAC;IAExD,kEAAkE;IAClE,MAAM,CAAC,GAAW;QAChB,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IACtD,CAAC;IAED;0EACsE;IACtE,OAAO;QACL,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC9C,CAAC;IAED;iEAC6D;IAC7D,WAAW,CAAC,GAAW;QACrB,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,WAAW,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;IACpD,CAAC;IAED;wEACoE;IACpE,YAAY,CAAC,GAAW;QACtB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IAC7C,CAAC;IAED;gEAC4D;IAC5D,UAAU,CAAC,GAAW,EAAE,UAAkB;QACxC,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IACvD,CAAC;IAED;kFAC8E;IAC9E,MAAM,CAAC,GAAW;QAChB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACvC,CAAC;IAED;yDACqD;IACrD,OAAO,CACL,WAAmB,EACnB,MAAqB,EACrB,IAAqB;QAErB,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IACtE,CAAC;IAEO,OAAO,CAAC,GAAW;QACzB,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,0BAA0B,GAAG,GAAG,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;CACF;AAED;;oFAEoF;AACpF,MAAM,UAAU,iBAAiB,CAAC,WAAoB;IACpD,OAAO,CAAC,IAAI,YAAY,EAAE,EAAE,IAAI,iBAAiB,CAAC,WAAW,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAA6B,CAAC;AAElE
|
|
1
|
+
{"version":3,"file":"transport-registry.js","sourceRoot":"","sources":["../../src/transports/transport-registry.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAQ5D;;;;2EAI2E;AAC3E,MAAM,OAAO,iBAAiB;IAC5B,YAA6B,UAAuB;QAAvB,eAAU,GAAV,UAAU,CAAa;IAAG,CAAC;IAExD,kEAAkE;IAClE,MAAM,CAAC,GAAW;QAChB,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IACtD,CAAC;IAED;0EACsE;IACtE,OAAO;QACL,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC9C,CAAC;IAED;iEAC6D;IAC7D,WAAW,CAAC,GAAW;QACrB,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,WAAW,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;IACpD,CAAC;IAED;wEACoE;IACpE,YAAY,CAAC,GAAW;QACtB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IAC7C,CAAC;IAED;gEAC4D;IAC5D,UAAU,CAAC,GAAW,EAAE,UAAkB;QACxC,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IACvD,CAAC;IAED;kFAC8E;IAC9E,MAAM,CAAC,GAAW;QAChB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACvC,CAAC;IAED;yDACqD;IACrD,OAAO,CACL,WAAmB,EACnB,MAAqB,EACrB,IAAqB;QAErB,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IACtE,CAAC;IAEO,OAAO,CAAC,GAAW;QACzB,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,0BAA0B,GAAG,GAAG,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;CACF;AAED;;oFAEoF;AACpF,MAAM,UAAU,iBAAiB,CAAC,WAAoB;IACpD,OAAO,CAAC,IAAI,YAAY,EAAE,EAAE,IAAI,iBAAiB,CAAC,WAAW,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAA6B,CAAC;AAElE;;;;;;oBAMoB;AACpB,MAAM,UAAU,wBAAwB,CAAC,WAAoB;IAC3D,MAAM,GAAG,GAAG,WAAW,IAAI,EAAE,CAAC;IAC9B,IAAI,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,GAAG,IAAI,iBAAiB,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC,CAAC;QAC/D,oBAAoB,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/kernel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.0",
|
|
4
4
|
"description": "Telo Runtime - A lightweight, polyglot execution host.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@marcbachmann/cel-js": "^7.6.1",
|
|
63
63
|
"@sinclair/typebox": "^0.34.48",
|
|
64
|
-
"@telorun/analyzer": "0.
|
|
64
|
+
"@telorun/analyzer": "0.50.0",
|
|
65
65
|
"@telorun/glob": "0.2.0",
|
|
66
|
-
"@telorun/templating": "0.11.
|
|
66
|
+
"@telorun/templating": "0.11.1",
|
|
67
67
|
"ajv": "^8.17.1",
|
|
68
68
|
"ajv-formats": "^3.0.1",
|
|
69
69
|
"packageurl-js": "^2.0.1",
|
|
@@ -75,10 +75,10 @@
|
|
|
75
75
|
"@types/tar-stream": "^3.1.3",
|
|
76
76
|
"typescript": "^5.0.0",
|
|
77
77
|
"vitest": "^2.1.8",
|
|
78
|
-
"@telorun/sdk": "0.
|
|
78
|
+
"@telorun/sdk": "0.63.0"
|
|
79
79
|
},
|
|
80
80
|
"optionalDependencies": {
|
|
81
|
-
"esbuild": "^0.
|
|
81
|
+
"esbuild": "^0.25.12"
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
84
|
"@telorun/sdk": "*"
|
|
@@ -26,6 +26,17 @@ export interface MaterializedLayer {
|
|
|
26
26
|
files: string[];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** A materialized controller layer plus whether reaching it cost a transfer.
|
|
30
|
+
*
|
|
31
|
+
* `transferred` is out of band rather than a field on `MaterializedLayer`
|
|
32
|
+
* because it describes THIS call, not the layer: the value is memoized and
|
|
33
|
+
* shared, so a flag on it would be meaningless to every other caller. Only the
|
|
34
|
+
* controller path reports progress, so only it asks. */
|
|
35
|
+
export interface ResolvedControllerLayer {
|
|
36
|
+
layer: MaterializedLayer;
|
|
37
|
+
transferred: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
/**
|
|
30
41
|
* Map Node's platform vocabulary onto the canonical OCI/GOOS names selectors are
|
|
31
42
|
* published with. Node says `win32`/`x64`; OCI descriptors say
|
|
@@ -123,8 +134,11 @@ export class ModuleArtifact {
|
|
|
123
134
|
private readonly transports: TransportRegistry;
|
|
124
135
|
private readonly log: Logger;
|
|
125
136
|
/** In-flight / completed materializations, keyed by blob digest. Rejections
|
|
126
|
-
* are dropped so a transient fetch failure retries on the next ask.
|
|
127
|
-
|
|
137
|
+
* are dropped so a transient fetch failure retries on the next ask. Each
|
|
138
|
+
* records whether the work it wraps transferred the layer or read it off
|
|
139
|
+
* disk — the memoized entry is shared, so a joining caller reads the flag
|
|
140
|
+
* but never claims it. */
|
|
141
|
+
private readonly inFlight = new Map<string, Promise<ResolvedControllerLayer>>();
|
|
128
142
|
|
|
129
143
|
constructor(opts: {
|
|
130
144
|
pinnedRef: string;
|
|
@@ -165,14 +179,23 @@ export class ModuleArtifact {
|
|
|
165
179
|
* Returns `undefined` when the artifact ships no layer for this selector, which
|
|
166
180
|
* is how a loader learns to fall through to the next candidate.
|
|
167
181
|
*/
|
|
168
|
-
async materializeController(
|
|
182
|
+
async materializeController(
|
|
183
|
+
selector: ArtifactSelector,
|
|
184
|
+
): Promise<ResolvedControllerLayer | undefined> {
|
|
169
185
|
const key = selectorKey(selector);
|
|
170
186
|
const layer = this.layers.find(
|
|
171
187
|
(l) => l.role === "controller" && l.selector !== undefined && selectorKey(l.selector) === key,
|
|
172
188
|
);
|
|
173
189
|
if (!layer) return undefined;
|
|
174
|
-
await this.
|
|
175
|
-
|
|
190
|
+
const common = await this.materializeCommonTracked();
|
|
191
|
+
const controller = await this.materializeTracked(layer);
|
|
192
|
+
// Either half is a transfer the caller waited on: the common layer's bytes
|
|
193
|
+
// come down on this call too, so they are as much of a wait as the
|
|
194
|
+
// controller layer's.
|
|
195
|
+
return {
|
|
196
|
+
layer: controller.layer,
|
|
197
|
+
transferred: controller.transferred || (common?.transferred ?? false),
|
|
198
|
+
};
|
|
176
199
|
}
|
|
177
200
|
|
|
178
201
|
/**
|
|
@@ -197,8 +220,15 @@ export class ModuleArtifact {
|
|
|
197
220
|
|
|
198
221
|
/** Materialize the `common` layer, if the module ships one. */
|
|
199
222
|
async materializeCommon(): Promise<MaterializedLayer | undefined> {
|
|
223
|
+
return (await this.materializeCommonTracked())?.layer;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** As `materializeCommon`, reporting whether this call transferred it — the
|
|
227
|
+
* controller path rides the common layer along and has to count its bytes as
|
|
228
|
+
* part of the wait. One lookup, so "common comes too" is encoded once. */
|
|
229
|
+
private async materializeCommonTracked(): Promise<ResolvedControllerLayer | undefined> {
|
|
200
230
|
const layer = singletonLayer(this.layers, "common");
|
|
201
|
-
return layer ? this.
|
|
231
|
+
return layer ? this.materializeTracked(layer) : undefined;
|
|
202
232
|
}
|
|
203
233
|
|
|
204
234
|
/**
|
|
@@ -226,9 +256,21 @@ export class ModuleArtifact {
|
|
|
226
256
|
.join(", ");
|
|
227
257
|
}
|
|
228
258
|
|
|
229
|
-
private materialize(layer: ArtifactLayer): Promise<MaterializedLayer> {
|
|
259
|
+
private async materialize(layer: ArtifactLayer): Promise<MaterializedLayer> {
|
|
260
|
+
return (await this.materializeTracked(layer)).layer;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* As `materialize`, but also reporting whether THIS call transferred the layer.
|
|
265
|
+
*
|
|
266
|
+
* A caller that joins work already in flight reports `false`: several
|
|
267
|
+
* controller candidates of one module share a layer, and attributing the
|
|
268
|
+
* transfer to all of them would print one progress line per candidate for a
|
|
269
|
+
* single download. The call that started the work owns the report.
|
|
270
|
+
*/
|
|
271
|
+
private materializeTracked(layer: ArtifactLayer): Promise<ResolvedControllerLayer> {
|
|
230
272
|
const pending = this.inFlight.get(layer.blob);
|
|
231
|
-
if (pending) return pending;
|
|
273
|
+
if (pending) return pending.then((r) => ({ layer: r.layer, transferred: false }));
|
|
232
274
|
const work = this.materializeUncached(layer).catch((err) => {
|
|
233
275
|
// Drop the rejection so a transient fetch failure is retried rather than
|
|
234
276
|
// cached for the lifetime of the module.
|
|
@@ -247,10 +289,10 @@ export class ModuleArtifact {
|
|
|
247
289
|
return path.join(this.dir, `.telo-layer-${layer.role}-${short}`);
|
|
248
290
|
}
|
|
249
291
|
|
|
250
|
-
private async materializeUncached(layer: ArtifactLayer): Promise<
|
|
292
|
+
private async materializeUncached(layer: ArtifactLayer): Promise<ResolvedControllerLayer> {
|
|
251
293
|
const marker = this.markerPath(layer);
|
|
252
294
|
if (existsSync(marker)) {
|
|
253
|
-
return { dir: this.dir, files: await readMarker(marker) };
|
|
295
|
+
return { layer: { dir: this.dir, files: await readMarker(marker) }, transferred: false };
|
|
254
296
|
}
|
|
255
297
|
|
|
256
298
|
return withDirectoryLock(
|
|
@@ -260,7 +302,7 @@ export class ModuleArtifact {
|
|
|
260
302
|
// Re-check inside the lock: a peer may have extracted this layer between
|
|
261
303
|
// the fast-path miss and our acquisition.
|
|
262
304
|
if (existsSync(marker)) {
|
|
263
|
-
return { dir: this.dir, files: await readMarker(marker) };
|
|
305
|
+
return { layer: { dir: this.dir, files: await readMarker(marker) }, transferred: false };
|
|
264
306
|
}
|
|
265
307
|
|
|
266
308
|
const files = await this.transports.fetchLayer(this.pinnedRef, layer.blob);
|
|
@@ -283,7 +325,7 @@ export class ModuleArtifact {
|
|
|
283
325
|
"telo.layer.role": layer.role,
|
|
284
326
|
"telo.layer.files": written.length,
|
|
285
327
|
});
|
|
286
|
-
return { dir: this.dir, files: written };
|
|
328
|
+
return { layer: { dir: this.dir, files: written }, transferred: true };
|
|
287
329
|
},
|
|
288
330
|
this.log,
|
|
289
331
|
);
|
package/src/controller-loader.ts
CHANGED
|
@@ -8,10 +8,13 @@ import { ControllerPolicy, DEFAULT_POLICY, POLICY_WILDCARD } from "./runtime-reg
|
|
|
8
8
|
export type { ControllerPolicy } from "./runtime-registry.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Which branch the per-scheme loader actually took. Cache/local hits resolve
|
|
12
|
-
*
|
|
13
|
-
* do real (network or compile) work
|
|
14
|
-
*
|
|
11
|
+
* Which branch the per-scheme loader actually took. Cache/local hits resolve in
|
|
12
|
+
* milliseconds; `npm-install`, `cargo-build` and `bundle` are the branches that
|
|
13
|
+
* do real (network or compile) work — `bundle` is reported only when the resolve
|
|
14
|
+
* fetched the module's controller layer, never when it found it already
|
|
15
|
+
* extracted. The CLI uses this to decide whether a "downloading…" line was
|
|
16
|
+
* honest or should be erased, so a source that names work no one waited for
|
|
17
|
+
* turns every warm start into noise.
|
|
15
18
|
*/
|
|
16
19
|
export type ControllerResolveSource =
|
|
17
20
|
| "local"
|
|
@@ -73,6 +76,10 @@ export interface ControllerLoaderOptions {
|
|
|
73
76
|
* single `resolveCacheRoot`. Overrides the entry-anchored default so a
|
|
74
77
|
* relocated `TELO_CACHE_DIR` is honoured. */
|
|
75
78
|
installRoot?: string;
|
|
79
|
+
/** The `.telo` cache root for this load. The bundle loader caches a dev build
|
|
80
|
+
* of a local module's controller source under it; absent simply disables that
|
|
81
|
+
* path, leaving a prebuilt `path=` to load. */
|
|
82
|
+
cacheRoot?: string;
|
|
76
83
|
/** Where the sub-loaders' diagnostics go — install-lock waits, bundle skips.
|
|
77
84
|
* Threaded from `ctx.log` so §13.1 holds (no direct `process.stderr`), and so
|
|
78
85
|
* the bundle-skip diagnostics that replaced `TELO_BUNDLE_DEBUG` actually reach
|
|
@@ -111,7 +118,7 @@ export class ControllerLoader {
|
|
|
111
118
|
});
|
|
112
119
|
if (options.log) this.npmLoader.setLogger(options.log);
|
|
113
120
|
this.napiLoader = new NapiControllerLoader();
|
|
114
|
-
this.bundleLoader = new BundleControllerLoader();
|
|
121
|
+
this.bundleLoader = new BundleControllerLoader(options.cacheRoot);
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
async load(
|
|
@@ -10,6 +10,7 @@ import { hostPlatformTarget, type ModuleArtifact } from "../bundle/module-artifa
|
|
|
10
10
|
import type { ControllerResolveSource } from "../controller-loader.js";
|
|
11
11
|
import { ControllerEnvMissingError } from "./napi-loader.js";
|
|
12
12
|
import { REALM_COLLAPSE_NAMES } from "./realm.js";
|
|
13
|
+
import { buildControllerFromSource, canBuildFromSource } from "./source-bundle-builder.js";
|
|
13
14
|
|
|
14
15
|
/** A base URI whose files are already on disk: a `file://` URL or a bare
|
|
15
16
|
* absolute path. Everything else (`oci://`, `http(s)://`, `memory://`) names a
|
|
@@ -130,7 +131,16 @@ function findPackageRoot(entryFile: string, name: string): string | null {
|
|
|
130
131
|
* (or `[pkg:telo …, pkg:npm …]`) falls through to a candidate this — or
|
|
131
132
|
* another runtime's — kernel can load.
|
|
132
133
|
* - `path` — the file in the bundle; `#export` — the named export.
|
|
134
|
+
* - `local_path` — the TypeScript source `path=` was built from. Present only
|
|
135
|
+
* while the module is a working copy; a published artifact ships no `src/`.
|
|
136
|
+
* When the module arrives with **no artifact handle** and the source resolves
|
|
137
|
+
* on disk, the loader builds it (see `source-bundle-builder.ts`) rather than
|
|
138
|
+
* importing `path=`, so editing a controller and re-running picks the edit up
|
|
139
|
+
* with no build step. The guard is the absence of an artifact, not the shape
|
|
140
|
+
* of the base URI: a published module served from the on-disk manifest cache
|
|
141
|
+
* has a local base too, and its payload is the layer regardless.
|
|
133
142
|
*
|
|
143
|
+
|
|
134
144
|
* Two separate concerns for `js` bundles importing `@telorun/sdk`:
|
|
135
145
|
* - *Resolution* — the bare specifier must point at a real file. The bundle has
|
|
136
146
|
* no node_modules, so `ensureRealmSymlinks()` symlinks the realm-collapse
|
|
@@ -146,6 +156,11 @@ function findPackageRoot(entryFile: string, name: string): string | null {
|
|
|
146
156
|
* through); a bundle that loads but is malformed is a hard `ERR_CONTROLLER_INVALID`.
|
|
147
157
|
*/
|
|
148
158
|
export class BundleControllerLoader {
|
|
159
|
+
/** Where a dev build from `local_path` is cached (`<cache-root>/controller-src`).
|
|
160
|
+
* Absent for callers that resolved no cache root, which simply disables the
|
|
161
|
+
* source path — a prebuilt `path=` still loads. */
|
|
162
|
+
constructor(private readonly cacheRoot?: string) {}
|
|
163
|
+
|
|
149
164
|
async load(
|
|
150
165
|
purl: string,
|
|
151
166
|
baseUri: string,
|
|
@@ -215,22 +230,76 @@ export class BundleControllerLoader {
|
|
|
215
230
|
);
|
|
216
231
|
}
|
|
217
232
|
|
|
233
|
+
const fragment = parsed.subpath;
|
|
234
|
+
|
|
235
|
+
// Dev path: a module that is a working copy — no artifact behind it — with a
|
|
236
|
+
// `local_path` source on disk is built from that source, because the source
|
|
237
|
+
// is what is authoritative there. An artifact means the payload IS the layer,
|
|
238
|
+
// so this never fires for a published module, including one served from the
|
|
239
|
+
// on-disk manifest cache (whose base is local but whose payload is a layer).
|
|
240
|
+
//
|
|
241
|
+
// esbuild is probed HERE rather than inside the build. It is an optional
|
|
242
|
+
// dependency so that an install skipping optionals still runs published
|
|
243
|
+
// artifacts — and a working copy that has run its build script has the same
|
|
244
|
+
// prebuilt file on disk. Deciding it lazily would turn "no bundler" into a
|
|
245
|
+
// hard failure standing next to a perfectly good bundle, and the candidate
|
|
246
|
+
// list could not rescue it: the fallback belongs to this same PURL, not to a
|
|
247
|
+
// sibling candidate.
|
|
248
|
+
const cacheRoot = this.cacheRoot;
|
|
249
|
+
const sourceFile = artifact ? undefined : this.localSourceFile(parsed, baseUri);
|
|
250
|
+
const buildFromSource =
|
|
251
|
+
sourceFile !== undefined &&
|
|
252
|
+
cacheRoot !== undefined &&
|
|
253
|
+
(await pathExists(sourceFile)) &&
|
|
254
|
+
(await canBuildFromSource());
|
|
255
|
+
if (buildFromSource) {
|
|
256
|
+
// The same file the prebuilt branch would import, kept as the fallback for
|
|
257
|
+
// an environment that stops being able to build between resolve and first
|
|
258
|
+
// instantiation.
|
|
259
|
+
const prebuilt = path.resolve(this.moduleDir(baseUri), relPath);
|
|
260
|
+
return {
|
|
261
|
+
source: "local",
|
|
262
|
+
// Deferred like every other branch: the build is paid on the kind's
|
|
263
|
+
// first instantiation, so a manifest pays only for the kinds it uses.
|
|
264
|
+
// A build FAILURE is user code and propagates unchanged; only a missing
|
|
265
|
+
// *environment* falls back, and only to a file that is actually there.
|
|
266
|
+
importInstance: async () => {
|
|
267
|
+
let built: string;
|
|
268
|
+
try {
|
|
269
|
+
built = await buildControllerFromSource(sourceFile!, cacheRoot!);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
if (!(err instanceof ControllerEnvMissingError)) throw err;
|
|
272
|
+
if (!(await pathExists(prebuilt))) throw err;
|
|
273
|
+
await ensureRealmSymlinks(path.dirname(prebuilt));
|
|
274
|
+
return importControllerModule(prebuilt, purl, fragment);
|
|
275
|
+
}
|
|
276
|
+
await ensureRealmSymlinks(path.dirname(built));
|
|
277
|
+
return importControllerModule(built, purl, fragment);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
218
282
|
// A published module's payload lives in its artifact, so materialize the one
|
|
219
283
|
// layer carrying this candidate and resolve `path=` inside it. Nothing is
|
|
220
284
|
// fetched here: the artifact handle owns the pinned ref and the verified
|
|
221
285
|
// layer index, so an `oci://` module ref never reaches this loader as a path.
|
|
222
286
|
let bundleDir: string;
|
|
287
|
+
// What this resolve actually cost. A module already on disk is `local`; an
|
|
288
|
+
// artifact layer found extracted is `cache`; only a layer this call pulled
|
|
289
|
+
// down reports `bundle`, the branch that made the user wait.
|
|
290
|
+
let source: ControllerResolveSource = "local";
|
|
223
291
|
if (artifact) {
|
|
224
292
|
// By its own selector, not by re-matching the host: this candidate IS one
|
|
225
293
|
// selector, and it is exactly the key of the layer that carries it.
|
|
226
|
-
const
|
|
227
|
-
if (!
|
|
294
|
+
const resolved = await artifact.materializeController(selector);
|
|
295
|
+
if (!resolved) {
|
|
228
296
|
throw new ControllerEnvMissingError(
|
|
229
297
|
`pkg:telo controller "${purl}": the module artifact ships no layer for ` +
|
|
230
298
|
`${describeSelector(selector)} (has: ${artifact.describeLayers()})`,
|
|
231
299
|
);
|
|
232
300
|
}
|
|
233
|
-
bundleDir = layer.dir;
|
|
301
|
+
bundleDir = resolved.layer.dir;
|
|
302
|
+
source = resolved.transferred ? "bundle" : "cache";
|
|
234
303
|
} else {
|
|
235
304
|
// No artifact: a module already on disk (local development, or a manifest
|
|
236
305
|
// served from the on-disk cache). Its files sit next to the manifest.
|
|
@@ -256,31 +325,52 @@ export class BundleControllerLoader {
|
|
|
256
325
|
// importing the bundle, so authors write normal imports.
|
|
257
326
|
await ensureRealmSymlinks(path.dirname(absFile));
|
|
258
327
|
|
|
259
|
-
const fragment = parsed.subpath;
|
|
260
328
|
return {
|
|
261
|
-
source
|
|
262
|
-
importInstance:
|
|
263
|
-
// A broken bundle (syntax / failed import) is a real user-code failure —
|
|
264
|
-
// let it propagate rather than masking it as env-missing.
|
|
265
|
-
const mod = (await import(pathToFileURL(absFile).href)) as Record<string, ControllerInstance>;
|
|
266
|
-
// Distinguish "no such export" from "export isn't a controller" so the
|
|
267
|
-
// error points at the actual problem (mirrors the napi loader's project()).
|
|
268
|
-
if (fragment && !(fragment in mod)) {
|
|
269
|
-
throw new RuntimeError(
|
|
270
|
-
"ERR_CONTROLLER_INVALID",
|
|
271
|
-
`Bundled controller "${purl}": module "${absFile}" has no export named "${fragment}"`,
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
const instance = fragment ? mod[fragment] : (mod as unknown as ControllerInstance);
|
|
275
|
-
if (!instance || (!instance.create && !instance.register)) {
|
|
276
|
-
throw new RuntimeError(
|
|
277
|
-
"ERR_CONTROLLER_INVALID",
|
|
278
|
-
`Bundled controller "${purl}" exports neither create() nor register()` +
|
|
279
|
-
(fragment ? ` at fragment "#${fragment}"` : ""),
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
return instance;
|
|
283
|
-
},
|
|
329
|
+
source,
|
|
330
|
+
importInstance: () => importControllerModule(absFile, purl, fragment),
|
|
284
331
|
};
|
|
285
332
|
}
|
|
333
|
+
|
|
334
|
+
/** The `local_path` source this candidate names, resolved against the declaring
|
|
335
|
+
* module's directory — or `undefined` when the candidate declares none, or the
|
|
336
|
+
* module is not on disk to resolve it against. */
|
|
337
|
+
private localSourceFile(parsed: PackageURL, baseUri: string): string | undefined {
|
|
338
|
+
const localPath = parsed.qualifiers?.local_path;
|
|
339
|
+
if (!localPath || !isLocalBase(baseUri)) return undefined;
|
|
340
|
+
return path.resolve(this.moduleDir(baseUri), localPath);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Directory of the declaring module's manifest. Only meaningful for a local
|
|
344
|
+
* base; callers gate on {@link isLocalBase} first. */
|
|
345
|
+
private moduleDir(baseUri: string): string {
|
|
346
|
+
return path.dirname(baseUri.startsWith("file://") ? fileURLToPath(baseUri) : baseUri);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Import a built bundle and project out the controller the fragment names. A
|
|
351
|
+
* broken bundle (syntax error, failed import) is a real user-code failure and
|
|
352
|
+
* propagates; it is never masked as env-missing. */
|
|
353
|
+
async function importControllerModule(
|
|
354
|
+
absFile: string,
|
|
355
|
+
purl: string,
|
|
356
|
+
fragment: string | undefined,
|
|
357
|
+
): Promise<ControllerInstance> {
|
|
358
|
+
const mod = (await import(pathToFileURL(absFile).href)) as Record<string, ControllerInstance>;
|
|
359
|
+
// Distinguish "no such export" from "export isn't a controller" so the error
|
|
360
|
+
// points at the actual problem (mirrors the napi loader's project()).
|
|
361
|
+
if (fragment && !(fragment in mod)) {
|
|
362
|
+
throw new RuntimeError(
|
|
363
|
+
"ERR_CONTROLLER_INVALID",
|
|
364
|
+
`Bundled controller "${purl}": module "${absFile}" has no export named "${fragment}"`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const instance = fragment ? mod[fragment] : (mod as unknown as ControllerInstance);
|
|
368
|
+
if (!instance || (!instance.create && !instance.register)) {
|
|
369
|
+
throw new RuntimeError(
|
|
370
|
+
"ERR_CONTROLLER_INVALID",
|
|
371
|
+
`Bundled controller "${purl}" exports neither create() nor register()` +
|
|
372
|
+
(fragment ? ` at fragment "#${fragment}"` : ""),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return instance;
|
|
286
376
|
}
|
|
@@ -149,7 +149,7 @@ export class NpmControllerLoader {
|
|
|
149
149
|
* same spec share one `npm install <spec>` invocation rather than each
|
|
150
150
|
* acquiring the fs-lock and reinstalling.
|
|
151
151
|
*/
|
|
152
|
-
private readonly inFlight = new Map<string, Promise<
|
|
152
|
+
private readonly inFlight = new Map<string, Promise<boolean>>();
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
155
|
* Promise that resolves when the install root has been materialized
|
|
@@ -264,15 +264,20 @@ export class NpmControllerLoader {
|
|
|
264
264
|
dependencies[name] = `file:${resolvedPkgRoot}`;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
const packageJson = {
|
|
268
|
-
name: "telo-runtime-install",
|
|
269
|
-
private: true,
|
|
270
|
-
version: "0.0.0",
|
|
271
|
-
dependencies,
|
|
272
|
-
};
|
|
273
267
|
const packageJsonPath = path.join(installRoot, "package.json");
|
|
274
268
|
const stateFile = path.join(installRoot, ".telo-state.json");
|
|
275
|
-
|
|
269
|
+
// Keyed on the realm deps alone — the only part this function owns. The
|
|
270
|
+
// controller aliases below join the file but never the identity: they are
|
|
271
|
+
// added one `--save` at a time, so hashing them would make every controller
|
|
272
|
+
// install invalidate the root and trigger another root install.
|
|
273
|
+
const newHash = sha256(
|
|
274
|
+
JSON.stringify({
|
|
275
|
+
name: "telo-runtime-install",
|
|
276
|
+
private: true,
|
|
277
|
+
version: "0.0.0",
|
|
278
|
+
dependencies,
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
276
281
|
|
|
277
282
|
await fs.mkdir(installRoot, { recursive: true });
|
|
278
283
|
|
|
@@ -301,7 +306,25 @@ export class NpmControllerLoader {
|
|
|
301
306
|
return;
|
|
302
307
|
}
|
|
303
308
|
|
|
304
|
-
|
|
309
|
+
// Rewrite the root's package.json REALM DEPS ONLY, keeping every
|
|
310
|
+
// controller alias a previous `installPackage --save` recorded. Writing a
|
|
311
|
+
// fresh file instead drops them, and the install below then prunes their
|
|
312
|
+
// `node_modules` folders — so a realm change (a different kernel checkout,
|
|
313
|
+
// e.g. a globally installed CLI and a repo one addressing the same app)
|
|
314
|
+
// evicted every controller and reinstalled the lot, on every switch.
|
|
315
|
+
const existingDeps = (await readPackageDeps(installRoot)) ?? {};
|
|
316
|
+
const merged: Record<string, string> = {
|
|
317
|
+
...(await liveControllerDeps(existingDeps, installRoot)),
|
|
318
|
+
...dependencies,
|
|
319
|
+
};
|
|
320
|
+
await fs.writeFile(
|
|
321
|
+
packageJsonPath,
|
|
322
|
+
JSON.stringify(
|
|
323
|
+
{ name: "telo-runtime-install", private: true, version: "0.0.0", dependencies: merged },
|
|
324
|
+
null,
|
|
325
|
+
2,
|
|
326
|
+
) + "\n",
|
|
327
|
+
);
|
|
305
328
|
await runPackageManager(installRoot, [
|
|
306
329
|
"install",
|
|
307
330
|
"--no-audit",
|
|
@@ -414,8 +437,11 @@ export class NpmControllerLoader {
|
|
|
414
437
|
}
|
|
415
438
|
}
|
|
416
439
|
|
|
440
|
+
// Resolves to whether the package manager actually ran: the re-check below
|
|
441
|
+
// routinely wins the race, and reporting an install that never happened
|
|
442
|
+
// makes the CLI print a "downloading…" line nobody waited for.
|
|
417
443
|
const work = (async () => {
|
|
418
|
-
|
|
444
|
+
return withDirectoryLock(installRoot, "controller install", async () => {
|
|
419
445
|
// Re-check inside the lock: a peer process may have installed the
|
|
420
446
|
// spec between the fast-path miss and our acquisition.
|
|
421
447
|
if (kind === "registry") {
|
|
@@ -424,7 +450,7 @@ export class NpmControllerLoader {
|
|
|
424
450
|
installedVersion !== null &&
|
|
425
451
|
(requestedVersion === null || requestedVersion === installedVersion)
|
|
426
452
|
) {
|
|
427
|
-
return;
|
|
453
|
+
return false;
|
|
428
454
|
}
|
|
429
455
|
} else {
|
|
430
456
|
// Normalize the on-disk record to absolute form before comparing —
|
|
@@ -436,7 +462,7 @@ export class NpmControllerLoader {
|
|
|
436
462
|
(await pathExists(targetPath))
|
|
437
463
|
) {
|
|
438
464
|
this.rootDeps[alias] = lockedSpec;
|
|
439
|
-
return;
|
|
465
|
+
return false;
|
|
440
466
|
}
|
|
441
467
|
}
|
|
442
468
|
|
|
@@ -458,16 +484,21 @@ export class NpmControllerLoader {
|
|
|
458
484
|
const written = await readDepSpec(installRoot, alias);
|
|
459
485
|
if (written !== undefined) this.rootDeps[alias] = written;
|
|
460
486
|
else this.rootDeps[alias] = spec;
|
|
487
|
+
return true;
|
|
461
488
|
}, this.log);
|
|
462
489
|
})();
|
|
463
490
|
|
|
464
491
|
this.inFlight.set(cacheKey, work);
|
|
492
|
+
let installed: boolean;
|
|
465
493
|
try {
|
|
466
|
-
await work;
|
|
494
|
+
installed = await work;
|
|
467
495
|
} finally {
|
|
468
496
|
this.inFlight.delete(cacheKey);
|
|
469
497
|
}
|
|
470
498
|
this.installedSpecs.add(cacheKey);
|
|
499
|
+
// A peer that won the lock had already put the package there, so nothing
|
|
500
|
+
// was installed — that is a cache hit, whatever the fast path thought.
|
|
501
|
+
if (!installed) return "cache";
|
|
471
502
|
// First-touch local installs report `local` (silenced in CLI progress);
|
|
472
503
|
// fresh registry installs report `npm-install` (the only branch a
|
|
473
504
|
// user-facing "downloading…" line should ever surface for).
|
|
@@ -693,6 +724,37 @@ function normalizeFileSpec(spec: string, installRoot: string): string {
|
|
|
693
724
|
* Returns null when the file is missing or unreadable; callers decide whether
|
|
694
725
|
* to seed from a synthesized map instead.
|
|
695
726
|
*/
|
|
727
|
+
/**
|
|
728
|
+
* The subset of a root's recorded dependencies still worth carrying into a
|
|
729
|
+
* rewrite: the package manager has to resolve every entry, so one dead record
|
|
730
|
+
* fails the whole install.
|
|
731
|
+
*
|
|
732
|
+
* Dropped: a `file:` spec whose target no longer exists (the app was run from a
|
|
733
|
+
* checkout that has since moved, or the app directory was copied without it),
|
|
734
|
+
* and an alias with no folder in `node_modules`. Both are free to drop —
|
|
735
|
+
* `installPackage` reinstalls a controller the moment something asks for it —
|
|
736
|
+
* and keeping them is not: a dangling `file:` dep makes the root install fail
|
|
737
|
+
* with an ENOENT that names nothing in the manifest, and since the state file is
|
|
738
|
+
* only written after a successful install, every later run repeats it. This is
|
|
739
|
+
* also what stops aliases accumulating forever now that the rewrite preserves
|
|
740
|
+
* them: a version an app has moved off drops out on the next root install.
|
|
741
|
+
*/
|
|
742
|
+
async function liveControllerDeps(
|
|
743
|
+
deps: Record<string, string>,
|
|
744
|
+
installRoot: string,
|
|
745
|
+
): Promise<Record<string, string>> {
|
|
746
|
+
const live: Record<string, string> = {};
|
|
747
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
748
|
+
if (spec.startsWith("file:")) {
|
|
749
|
+
const target = normalizeFileSpec(spec, installRoot).slice("file:".length);
|
|
750
|
+
if (!(await pathExists(target))) continue;
|
|
751
|
+
}
|
|
752
|
+
if (!(await pathExists(path.join(installRoot, "node_modules", name)))) continue;
|
|
753
|
+
live[name] = spec;
|
|
754
|
+
}
|
|
755
|
+
return live;
|
|
756
|
+
}
|
|
757
|
+
|
|
696
758
|
async function readPackageDeps(installRoot: string): Promise<Record<string, string> | null> {
|
|
697
759
|
try {
|
|
698
760
|
const text = await fs.readFile(path.join(installRoot, "package.json"), "utf8");
|