@telorun/kernel 0.77.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/bundle/tar.d.ts +3 -0
- package/dist/bundle/tar.d.ts.map +1 -1
- package/dist/bundle/tar.js +27 -1
- package/dist/bundle/tar.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/transports/oci/oci-client.d.ts +4 -0
- package/dist/transports/oci/oci-client.d.ts.map +1 -1
- package/dist/transports/oci/oci-client.js +7 -1
- package/dist/transports/oci/oci-client.js.map +1 -1
- package/dist/transports/oci/oci-transport.d.ts +8 -3
- package/dist/transports/oci/oci-transport.d.ts.map +1 -1
- package/dist/transports/oci/oci-transport.js +67 -21
- package/dist/transports/oci/oci-transport.js.map +1 -1
- package/dist/transports/registry-transport.d.ts +3 -2
- package/dist/transports/registry-transport.d.ts.map +1 -1
- package/dist/transports/registry-transport.js +10 -0
- package/dist/transports/registry-transport.js.map +1 -1
- package/dist/transports/transport.d.ts +25 -6
- package/dist/transports/transport.d.ts.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/bundle/tar.ts +28 -1
- 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/transports/oci/oci-client.ts +8 -1
- package/src/transports/oci/oci-transport.ts +76 -22
- package/src/transports/registry-transport.ts +14 -0
- package/src/transports/transport.ts +26 -5
- package/src/zone-context.ts +70 -1
|
@@ -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
|
|
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
isCompiledValue,
|
|
7
7
|
isInvokeError,
|
|
8
8
|
isCancellationError,
|
|
9
|
+
isSuspension,
|
|
9
10
|
resourceKey,
|
|
10
11
|
UNCANCELLABLE_CONTEXT,
|
|
11
12
|
type EvaluationContext as IEvaluationContext,
|
|
13
|
+
type SpanOutcome,
|
|
12
14
|
type EmitEvent,
|
|
13
15
|
type InstanceFactory,
|
|
14
16
|
type InvokeContext,
|
|
@@ -230,6 +232,121 @@ const completedInstances = new WeakSet<object>();
|
|
|
230
232
|
* `kind` is already canonical (`<module>.<Kind>`) on that path, so the resolver
|
|
231
233
|
* needs no alias table.
|
|
232
234
|
*/
|
|
235
|
+
/**
|
|
236
|
+
* The last published reading for an instance, by identity.
|
|
237
|
+
*
|
|
238
|
+
* CEL reads a resource through its PUBLISHED STATE — that is what
|
|
239
|
+
* `resources.<name>.<field>` has always meant — and a resource reached through a
|
|
240
|
+
* ref slot is no different: `self.table.table` and `resources.users.table` name
|
|
241
|
+
* the same fact and must answer the same way. A ref slot holds the live
|
|
242
|
+
* instance, which CEL cannot read a member off at all, so this is the table that
|
|
243
|
+
* makes the two agree.
|
|
244
|
+
*
|
|
245
|
+
* Keyed by instance rather than by name because a ref slot carries no name, and
|
|
246
|
+
* a `with:`-scoped resource has one published reading per scope run. Weak, so
|
|
247
|
+
* nothing here extends an instance's lifetime, and deliberately one-directional
|
|
248
|
+
* — an entry is a reading OF an instance, never a way to obtain one.
|
|
249
|
+
*/
|
|
250
|
+
const publishedByInstance = new WeakMap<object, Record<string, unknown>>();
|
|
251
|
+
|
|
252
|
+
/** The published reading for an instance, or undefined if it has never
|
|
253
|
+
* published — a resource that has not been created yet, or a foreign object. */
|
|
254
|
+
export function publishedPropsForInstance(
|
|
255
|
+
instance: unknown,
|
|
256
|
+
): Record<string, unknown> | undefined {
|
|
257
|
+
return instance && typeof instance === "object"
|
|
258
|
+
? publishedByInstance.get(instance as object)
|
|
259
|
+
: undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The CEL-facing view of a template's `self`.
|
|
264
|
+
*
|
|
265
|
+
* Ref slots hold live instances after Phase-5 injection; each is replaced by its
|
|
266
|
+
* published reading so a template body can read a scalar off a resource it
|
|
267
|
+
* references. A slot whose instance has never published is left as it is — a
|
|
268
|
+
* value CEL will reject on member access, which is the honest outcome rather
|
|
269
|
+
* than an invented empty object that would make a typo evaluate to null.
|
|
270
|
+
*/
|
|
271
|
+
/**
|
|
272
|
+
* A memoized `self` view, beside the exact readings it substituted.
|
|
273
|
+
*
|
|
274
|
+
* Invalidation is per view, checked against the reading each substituted
|
|
275
|
+
* instance currently has — `publishedPropsOf` publishes a NEW props object every
|
|
276
|
+
* time, so identity IS the version. A process-global counter was wrong twice
|
|
277
|
+
* over: every `invoke()` republishes, so under any concurrency the memo was
|
|
278
|
+
* defeated and the walk was back on the per-request path it was added for; and
|
|
279
|
+
* the counter is shared across in-process kernels, so one app's traffic
|
|
280
|
+
* invalidated another app's views.
|
|
281
|
+
*
|
|
282
|
+
* The check is O(substituted slots) — the refs, not the fields — and a view that
|
|
283
|
+
* substituted nothing has an empty source list and is therefore always valid,
|
|
284
|
+
* which is the common case.
|
|
285
|
+
*/
|
|
286
|
+
interface SelfView {
|
|
287
|
+
readonly view: Record<string, unknown>;
|
|
288
|
+
readonly sources: readonly [object, Record<string, unknown>][];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const selfViews = new WeakMap<object, SelfView>();
|
|
292
|
+
|
|
293
|
+
function viewIsCurrent(cached: SelfView): boolean {
|
|
294
|
+
for (const [instance, props] of cached.sources) {
|
|
295
|
+
if (publishedByInstance.get(instance) !== props) return false;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function celSelfView(self: Record<string, unknown>): Record<string, unknown> {
|
|
301
|
+
// A template's `invoke` expands its body on EVERY dispatch, so this sits on
|
|
302
|
+
// the per-request path: without the memo a repository rebuilt the whole view
|
|
303
|
+
// per call.
|
|
304
|
+
const cached = selfViews.get(self);
|
|
305
|
+
if (cached && viewIsCurrent(cached)) return cached.view;
|
|
306
|
+
|
|
307
|
+
const sources: [object, Record<string, unknown>][] = [];
|
|
308
|
+
let view: Record<string, unknown> | undefined;
|
|
309
|
+
for (const [key, value] of Object.entries(self)) {
|
|
310
|
+
const substituted = celValueView(value, sources);
|
|
311
|
+
if (substituted === value) continue;
|
|
312
|
+
view ??= { ...self };
|
|
313
|
+
view[key] = substituted;
|
|
314
|
+
}
|
|
315
|
+
const resolved = view ?? self;
|
|
316
|
+
selfViews.set(self, { view: resolved, sources });
|
|
317
|
+
return resolved;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* A ref slot holds one instance or a LIST of them (`tables: [!ref a, !ref b]`),
|
|
322
|
+
* and both are ref slots — so converting only the first would make
|
|
323
|
+
* `self.tables[0].name` mean something different from `self.table.name`, which
|
|
324
|
+
* is the inconsistency this rule exists to remove.
|
|
325
|
+
*
|
|
326
|
+
* Arrays only: the walk stops at any other object, because a nested plain object
|
|
327
|
+
* in a manifest is the author's own data and a resource's published reading is
|
|
328
|
+
* not something to graft into the middle of it.
|
|
329
|
+
*/
|
|
330
|
+
function celValueView(
|
|
331
|
+
value: unknown,
|
|
332
|
+
sources: [object, Record<string, unknown>][],
|
|
333
|
+
): unknown {
|
|
334
|
+
const published = publishedPropsForInstance(value);
|
|
335
|
+
if (published) {
|
|
336
|
+
sources.push([value as object, published]);
|
|
337
|
+
return published;
|
|
338
|
+
}
|
|
339
|
+
if (!Array.isArray(value)) return value;
|
|
340
|
+
let copy: unknown[] | undefined;
|
|
341
|
+
for (let i = 0; i < value.length; i++) {
|
|
342
|
+
const substituted = celValueView(value[i], sources);
|
|
343
|
+
if (substituted === value[i]) continue;
|
|
344
|
+
copy ??= [...value];
|
|
345
|
+
copy[i] = substituted;
|
|
346
|
+
}
|
|
347
|
+
return copy ?? value;
|
|
348
|
+
}
|
|
349
|
+
|
|
233
350
|
export async function publishedPropsOf(
|
|
234
351
|
kind: string,
|
|
235
352
|
name: string,
|
|
@@ -240,7 +357,7 @@ export async function publishedPropsOf(
|
|
|
240
357
|
typeof instance.snapshot === "function"
|
|
241
358
|
? ((await Promise.resolve(instance.snapshot())) as Record<string, unknown> | undefined)
|
|
242
359
|
: undefined;
|
|
243
|
-
|
|
360
|
+
const props = buildPublishedProps(snap, {
|
|
244
361
|
kind,
|
|
245
362
|
name,
|
|
246
363
|
module: kind.split(".")[0],
|
|
@@ -249,6 +366,8 @@ export async function publishedPropsOf(
|
|
|
249
366
|
started: startedInstances.has(instance),
|
|
250
367
|
completed: completedInstances.has(instance),
|
|
251
368
|
});
|
|
369
|
+
publishedByInstance.set(instance, props);
|
|
370
|
+
return props;
|
|
252
371
|
}
|
|
253
372
|
|
|
254
373
|
/**
|
|
@@ -381,6 +500,19 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
381
500
|
/** Resources queued for initialization on this context node. */
|
|
382
501
|
private pendingResources: ResourceManifest[] = [];
|
|
383
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Every manifest ever registered on this node, by name — kept after the
|
|
505
|
+
* resource has been created, unlike `pendingResources`, which drains.
|
|
506
|
+
*
|
|
507
|
+
* A DECLARATION-derived contract (`x-telo-schema-projection-from`) is resolved
|
|
508
|
+
* when the kernel binds a contract, which happens during create — so the
|
|
509
|
+
* pending queue has already given the target up by then. The declaration is
|
|
510
|
+
* what the projection reads (a table's `columns:`), not the instance, so this
|
|
511
|
+
* is the record it needs. Manifests only, one direction: nothing here yields
|
|
512
|
+
* an instance, and nothing here extends one's lifetime.
|
|
513
|
+
*/
|
|
514
|
+
protected readonly declaredManifests = new Map<string, ResourceManifest>();
|
|
515
|
+
|
|
384
516
|
/** Per-resource dependency names, captured at create() time — BEFORE Phase-5
|
|
385
517
|
* injection swaps refs for live instances, so the walk sees plain objects and
|
|
386
518
|
* cannot wander into a controller's (possibly cyclic) object graph. Read only
|
|
@@ -523,18 +655,17 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
523
655
|
| Record<string, unknown>
|
|
524
656
|
| undefined;
|
|
525
657
|
const kind = entry.resource.kind as string;
|
|
526
|
-
|
|
658
|
+
const props = buildPublishedProps(snap, {
|
|
659
|
+
kind,
|
|
527
660
|
name,
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}),
|
|
537
|
-
);
|
|
661
|
+
module: entry.resource.metadata?.module as string | undefined,
|
|
662
|
+
statusSchema: this.statusSchemaOf(kind),
|
|
663
|
+
status: reportedStatus.get(entry.instance),
|
|
664
|
+
started: startedInstances.has(entry.instance),
|
|
665
|
+
completed: completedInstances.has(entry.instance),
|
|
666
|
+
});
|
|
667
|
+
publishedByInstance.set(entry.instance, props);
|
|
668
|
+
this.onResourceSnapshotted(name, props);
|
|
538
669
|
}
|
|
539
670
|
|
|
540
671
|
get context(): Record<string, unknown> {
|
|
@@ -580,6 +711,35 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
580
711
|
throw new RuntimeError("ERR_DUPLICATE_RESOURCE", `Resource '${name}' is already registered`);
|
|
581
712
|
}
|
|
582
713
|
this.pendingResources.push(resource);
|
|
714
|
+
this.declaredManifests.set(name, resource);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* The manifest a name was DECLARED with, resolved scope-local first and then
|
|
719
|
+
* up the enclosing chain — the order `getInstance` and the CEL `resources`
|
|
720
|
+
* layering already use, so a declaration lookup and an instance lookup agree
|
|
721
|
+
* about what a name means inside a scope.
|
|
722
|
+
*
|
|
723
|
+
* `alias` routes into an import's exported instances, the one shape a bare
|
|
724
|
+
* name cannot reach. Returns undefined rather than throwing: a caller here is
|
|
725
|
+
* typing a contract, and a name that resolves to nothing is reported by the
|
|
726
|
+
* check that owns that failure, not by a lookup.
|
|
727
|
+
*/
|
|
728
|
+
resolveDeclaredManifest(name: string, alias?: string): ResourceManifest | undefined {
|
|
729
|
+
if (alias && alias !== "Self") return this.resolveImportedManifest(alias, name);
|
|
730
|
+
let node: EvaluationContext | undefined = this;
|
|
731
|
+
while (node) {
|
|
732
|
+
const found = node.declaredManifests.get(name);
|
|
733
|
+
if (found) return found;
|
|
734
|
+
node = node.parent as EvaluationContext | undefined;
|
|
735
|
+
}
|
|
736
|
+
return undefined;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** The manifest half of {@link resolveImportedInstance}. Overridden by
|
|
740
|
+
* `ModuleContext`, which is the only node that has imports. */
|
|
741
|
+
resolveImportedManifest(alias: string, name: string): ResourceManifest | undefined {
|
|
742
|
+
return undefined;
|
|
583
743
|
}
|
|
584
744
|
|
|
585
745
|
/**
|
|
@@ -625,6 +785,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
625
785
|
this.emit,
|
|
626
786
|
);
|
|
627
787
|
child.resolveImportedInstance = (alias, name) => this.resolveImportedInstance(alias, name);
|
|
788
|
+
child.resolveImportedManifest = (alias, name) => this.resolveImportedManifest(alias, name);
|
|
628
789
|
child.kindResolver = (kind) => this.resolveKindSafe(kind);
|
|
629
790
|
return this.spawnChild(child);
|
|
630
791
|
}
|
|
@@ -1138,7 +1299,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1138
1299
|
traceId: string | undefined,
|
|
1139
1300
|
capability: "invoke" | "run" | "provide" | "request",
|
|
1140
1301
|
phase: "start" | "end",
|
|
1141
|
-
outcome:
|
|
1302
|
+
outcome: SpanOutcome | undefined,
|
|
1142
1303
|
detail: Record<string, unknown>,
|
|
1143
1304
|
): Record<string, unknown> {
|
|
1144
1305
|
return {
|
|
@@ -1192,7 +1353,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1192
1353
|
const rootScope = tracing && parentInvocationId === undefined ? this.traceRootScope() : undefined;
|
|
1193
1354
|
const span = (
|
|
1194
1355
|
phase: "start" | "end",
|
|
1195
|
-
outcome:
|
|
1356
|
+
outcome: SpanOutcome | undefined,
|
|
1196
1357
|
detail: Record<string, unknown>,
|
|
1197
1358
|
) =>
|
|
1198
1359
|
this.tracePayload(
|
|
@@ -1259,6 +1420,25 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1259
1420
|
await this.emit(`${name}.InvokeCancelled`, span("end", "cancelled", { inputs, reason }));
|
|
1260
1421
|
throw err;
|
|
1261
1422
|
}
|
|
1423
|
+
// A SUSPENSION passes through untouched, exactly as a cancellation does,
|
|
1424
|
+
// and for a sharper reason. It is not a failure — it is the run leaving —
|
|
1425
|
+
// and the chokepoint is the one swallower that cannot be worked around:
|
|
1426
|
+
// EVERY dispatch between a parking kind and the workflow that owns the run
|
|
1427
|
+
// goes through here, so wrapping it once converts a park into an
|
|
1428
|
+
// `ERR_EXECUTION_FAILED` at every hop. The latch would then catch the
|
|
1429
|
+
// corruption at the boundary, which is a loud failure where nothing was
|
|
1430
|
+
// wrong.
|
|
1431
|
+
//
|
|
1432
|
+
// Reported as its own outcome rather than as a rejection: a parked
|
|
1433
|
+
// invocation neither succeeded nor failed, and a trace that recorded it
|
|
1434
|
+
// as failed would say the run broke every time it waited.
|
|
1435
|
+
if (isSuspension(err)) {
|
|
1436
|
+
await this.emit(
|
|
1437
|
+
`${name}.InvokeParked`,
|
|
1438
|
+
span("end", "parked", { inputs, path: err.path, resource: err.resource }),
|
|
1439
|
+
);
|
|
1440
|
+
throw err;
|
|
1441
|
+
}
|
|
1262
1442
|
if (isInvokeError(err)) {
|
|
1263
1443
|
const detail = { inputs, code: err.code, message: err.message, data: err.data };
|
|
1264
1444
|
await this.emit(`${name}.InvokeRejected`, span("end", "rejected", detail));
|
|
@@ -1394,7 +1574,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1394
1574
|
};
|
|
1395
1575
|
const payload = (
|
|
1396
1576
|
phase: "start" | "end",
|
|
1397
|
-
outcome:
|
|
1577
|
+
outcome: SpanOutcome | undefined,
|
|
1398
1578
|
extra: Record<string, unknown> = {},
|
|
1399
1579
|
) =>
|
|
1400
1580
|
this.tracePayload(
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,12 @@ export {
|
|
|
35
35
|
type ResolvedControllerLayer,
|
|
36
36
|
} from "./bundle/module-artifact.js";
|
|
37
37
|
export { readOwnerManifest, type OwnerManifest } from "./bundle/module-manifest.js";
|
|
38
|
+
export {
|
|
39
|
+
buildSiblingLibraries,
|
|
40
|
+
NO_SIBLING_LIBRARIES,
|
|
41
|
+
type SiblingLibraryMap,
|
|
42
|
+
type ResolvedSiblingLibrary,
|
|
43
|
+
} from "./controller-loaders/sibling-libraries.js";
|
|
38
44
|
export {
|
|
39
45
|
buildControllerBundle,
|
|
40
46
|
canBuildFromSource,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Instance → the manifest it was DECLARED with, recorded at `create()` — the
|
|
5
|
+
* kernel's single instance-production site, beside where the handle is minted
|
|
6
|
+
* and the contract is bound.
|
|
7
|
+
*
|
|
8
|
+
* A contract typed from a referenced declaration
|
|
9
|
+
* (`x-telo-schema-projection-from`) is bound AFTER Phase-5 injection has already
|
|
10
|
+
* replaced the reference in the slot with the live instance, so the reference is
|
|
11
|
+
* gone by the time the contract needs it. This is what recovers the declaration
|
|
12
|
+
* behind it — and recovering it is what lets the kernel enforce exactly the
|
|
13
|
+
* schema the analyzer checked, rather than a reopened one.
|
|
14
|
+
*
|
|
15
|
+
* Its own module rather than a second table in `resource-handle.ts`, which
|
|
16
|
+
* guards an exact export list precisely so a new direction gets looked at: the
|
|
17
|
+
* rule there is that a HANDLE must never become someone else's live instance,
|
|
18
|
+
* and this is the unrelated instance→declaration direction. Weak and one-way —
|
|
19
|
+
* a manifest is obtainable FROM an instance, never an instance from a manifest —
|
|
20
|
+
* so nothing here extends a lifetime or hands out a reference to live state.
|
|
21
|
+
*/
|
|
22
|
+
const declarations = new WeakMap<object, ResourceManifest>();
|
|
23
|
+
|
|
24
|
+
/** Record what a live instance was declared with. First record wins, matching
|
|
25
|
+
* the handle rule: a `base:` child IS its parent instance, and the parent's
|
|
26
|
+
* declaration is the one that produced it. */
|
|
27
|
+
export function recordInstanceDeclaration(instance: object, manifest: ResourceManifest): void {
|
|
28
|
+
if (!declarations.has(instance)) declarations.set(instance, manifest);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The manifest a live instance was declared with, if it is one of ours. */
|
|
32
|
+
export function declarationOfInstance(instance: unknown): ResourceManifest | undefined {
|
|
33
|
+
return instance && typeof instance === "object"
|
|
34
|
+
? declarations.get(instance as object)
|
|
35
|
+
: undefined;
|
|
36
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ContractDirection,
|
|
3
|
+
type ProjectionScope,
|
|
3
4
|
declaredScalarPaths,
|
|
4
5
|
type DeclaredScalarForm,
|
|
5
6
|
type DeclaredScalarPath,
|
|
6
7
|
defaultBearingPaths,
|
|
7
8
|
effectiveContractField,
|
|
9
|
+
describeProjectionFailure,
|
|
10
|
+
resolveSchemaProjections,
|
|
11
|
+
type ProjectionFailure,
|
|
8
12
|
type DefResolver,
|
|
9
13
|
withLiveValuesSkipped,
|
|
10
14
|
} from "@telorun/analyzer";
|
|
@@ -13,6 +17,7 @@ import {
|
|
|
13
17
|
ERR_CONTRACT_UNRESOLVABLE,
|
|
14
18
|
ERR_INPUT_INVALID,
|
|
15
19
|
ERR_OUTPUT_INVALID,
|
|
20
|
+
ERR_SCHEMA_PROJECTION_UNRESOLVED,
|
|
16
21
|
InvokeError,
|
|
17
22
|
} from "@telorun/sdk";
|
|
18
23
|
|
|
@@ -120,6 +125,19 @@ export function resolveBoundContract(
|
|
|
120
125
|
definition: ResourceDefinition | undefined,
|
|
121
126
|
resolveDef: DefResolver,
|
|
122
127
|
factory: ContractValidatorFactory,
|
|
128
|
+
/**
|
|
129
|
+
* Resolves a DECLARATION-derived slot (`x-telo-schema-projection-from`) to the
|
|
130
|
+
* shape the referenced declaration projects to.
|
|
131
|
+
*
|
|
132
|
+
* The kernel resolves it for the same reason the analyzer does, and it must be
|
|
133
|
+
* the SAME resolution: a projected contract enforced statically and not at
|
|
134
|
+
* dispatch is a contract with a hole exactly where a value is computed rather
|
|
135
|
+
* than written — `telo check` rejects a misspelled column in a literal and the
|
|
136
|
+
* identical key arriving from a CEL expression reaches the database. Omitted
|
|
137
|
+
* only by callers with no scope to resolve against, where the annotated node
|
|
138
|
+
* stays as it was.
|
|
139
|
+
*/
|
|
140
|
+
projections?: ProjectionScope,
|
|
123
141
|
): BoundContract | undefined {
|
|
124
142
|
const own = (manifest as unknown as Record<string, unknown>)[direction];
|
|
125
143
|
const declared =
|
|
@@ -146,13 +164,43 @@ export function resolveBoundContract(
|
|
|
146
164
|
`The type is not registered, so the contract cannot be enforced.`,
|
|
147
165
|
);
|
|
148
166
|
}
|
|
149
|
-
|
|
167
|
+
let projected = schema;
|
|
168
|
+
if (projections) {
|
|
169
|
+
const failures: ProjectionFailure[] = [];
|
|
170
|
+
projected = resolveSchemaProjections(
|
|
171
|
+
schema,
|
|
172
|
+
manifest as unknown as Record<string, any>,
|
|
173
|
+
projections,
|
|
174
|
+
failures,
|
|
175
|
+
) as Record<string, any>;
|
|
176
|
+
if (failures.length > 0) {
|
|
177
|
+
// A slot that opted into a projection and got none is a DEFECT, not a
|
|
178
|
+
// default: leaving it means the contract silently reopens to the slot's
|
|
179
|
+
// own schema, which for a projected slot constrains nothing — the exact
|
|
180
|
+
// reopening this mechanism exists to prevent. The analyzer reports the
|
|
181
|
+
// same set, but only for the entry's own modules, so a dependency's
|
|
182
|
+
// consumer slot would otherwise be unreported at BOTH ends. Same rule as
|
|
183
|
+
// `ERR_CONTRACT_UNRESOLVABLE` above, for the same reason.
|
|
184
|
+
throw new InvokeError(
|
|
185
|
+
ERR_SCHEMA_PROJECTION_UNRESOLVED,
|
|
186
|
+
`declared \`${direction}\` could not be projected: ` +
|
|
187
|
+
failures.map(describeProjectionFailure).join(" ") +
|
|
188
|
+
` The slot declares 'x-telo-schema-projection-from', so leaving it unresolved ` +
|
|
189
|
+
`would enforce nothing where it promises a declared shape.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const stripped = withLiveValuesSkipped(projected, factory.resolveRef);
|
|
150
194
|
paths = defaultBearingPaths(stripped, factory.resolveRef);
|
|
151
195
|
scalars = declaredScalarPaths(stripped, factory.resolveRef);
|
|
152
196
|
// Compile by NAME whenever the declaration is one, so the type's CEL
|
|
153
197
|
// `rules:` are composed in — including when a stream had to be stripped, in
|
|
154
198
|
// which case the stream-bearing properties are dropped from the schema the
|
|
155
199
|
// named validator sees rather than the reference being abandoned.
|
|
200
|
+
// A named type compiles BY NAME so its CEL `rules:` compose in — but only
|
|
201
|
+
// while the schema is untouched. Stripping a stream or resolving a
|
|
202
|
+
// projection both produce a different schema, so both take the
|
|
203
|
+
// rules-preserving path rather than the bare name.
|
|
156
204
|
compiled = !isNamedTypeReference(declared)
|
|
157
205
|
? factory(stripped)
|
|
158
206
|
: stripped === schema
|