@telorun/kernel 0.65.0 → 0.67.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/controller-loader.d.ts +7 -5
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +7 -5
- package/dist/controller-loader.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 +29 -8
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/evaluation-context.d.ts +6 -0
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +20 -8
- package/dist/evaluation-context.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +13 -2
- 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 +6 -0
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +3 -4
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts +16 -2
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +58 -3
- package/dist/resource-context.js.map +1 -1
- package/dist/resource-handle.d.ts +11 -0
- package/dist/resource-handle.d.ts.map +1 -0
- package/dist/resource-handle.js +32 -0
- package/dist/resource-handle.js.map +1 -0
- package/dist/runtime-seam.d.ts.map +1 -1
- package/dist/runtime-seam.js +29 -3
- package/dist/runtime-seam.js.map +1 -1
- package/dist/zone-context.d.ts +94 -0
- package/dist/zone-context.d.ts.map +1 -0
- package/dist/zone-context.js +272 -0
- package/dist/zone-context.js.map +1 -0
- package/package.json +4 -4
- package/src/controller-loader.ts +7 -5
- package/src/controllers/resource-definition/resource-definition-controller.ts +31 -13
- package/src/evaluation-context.ts +21 -7
- package/src/kernel.ts +17 -1
- package/src/manifest-schemas.ts +6 -0
- package/src/module-context.ts +3 -4
- package/src/resource-context.ts +98 -2
- package/src/resource-handle.ts +35 -0
- package/src/runtime-seam.ts +31 -2
- package/src/zone-context.ts +337 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/kernel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.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.54.0",
|
|
65
65
|
"@telorun/glob": "0.2.0",
|
|
66
|
-
"@telorun/templating": "0.
|
|
66
|
+
"@telorun/templating": "0.12.0",
|
|
67
67
|
"ajv": "^8.17.1",
|
|
68
68
|
"ajv-formats": "^3.0.1",
|
|
69
69
|
"packageurl-js": "^2.0.1",
|
|
@@ -75,7 +75,7 @@
|
|
|
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.67.0"
|
|
79
79
|
},
|
|
80
80
|
"optionalDependencies": {
|
|
81
81
|
"esbuild": "^0.25.12"
|
package/src/controller-loader.ts
CHANGED
|
@@ -183,14 +183,16 @@ export class ControllerLoader {
|
|
|
183
183
|
* Resolve a controller without importing it: pick the first candidate this
|
|
184
184
|
* environment can host (same ordering + env-missing fallback as {@link load}),
|
|
185
185
|
* verify it's present, and return a {@link ResolvedController} whose
|
|
186
|
-
* `importInstance` defers the actual import/eval.
|
|
187
|
-
*
|
|
188
|
-
*
|
|
186
|
+
* `importInstance` defers the actual import/eval. Lazy controller loading
|
|
187
|
+
* calls this from the kind's first instantiation — never at boot — so a
|
|
188
|
+
* definition whose candidate list nothing in this environment can host
|
|
189
|
+
* registers fine and errors only when a resource of it is declared (matching
|
|
190
|
+
* the Rust kernel's deferral).
|
|
189
191
|
*
|
|
190
192
|
* Silent by design — no lifecycle events here; the caller emits
|
|
191
193
|
* ControllerLoading/Loaded around `importInstance` so the events fire when the
|
|
192
|
-
* load actually happens. A total resolution failure throws
|
|
193
|
-
*
|
|
194
|
+
* load actually happens. A total resolution failure throws, mirroring
|
|
195
|
+
* {@link load}'s aggregated error.
|
|
194
196
|
*/
|
|
195
197
|
async resolve(
|
|
196
198
|
purlCandidates: string[],
|
|
@@ -161,32 +161,39 @@ class ResourceDefinition implements ResourceInstance {
|
|
|
161
161
|
cacheRoot: host.getCacheRoot?.(),
|
|
162
162
|
log: ctx.log,
|
|
163
163
|
});
|
|
164
|
-
// Eager resolve — verify the controller is hostable now (so a broken
|
|
165
|
-
// `controllers:` candidate fails fast at boot), but defer the expensive
|
|
166
|
-
// import/eval and the controller's `register()` to the kind's first
|
|
167
|
-
// instantiation. Definitions whose kind is never instantiated never import.
|
|
168
164
|
// The artifact of the module that DECLARED this kind — a bundled controller
|
|
169
165
|
// ships in its own module's payload, not the consumer's. It owns the pinned
|
|
170
166
|
// ref and the verified layer index, so the loader picks a candidate and asks
|
|
171
167
|
// it for that selector's directory rather than fetching anything itself.
|
|
172
168
|
const artifact = host.getModuleArtifact?.(this.resource.metadata.source);
|
|
173
|
-
const resolved = await loader.resolve(
|
|
174
|
-
this.resource.controllers,
|
|
175
|
-
this.resource.metadata.source,
|
|
176
|
-
ctx.getControllerPolicy(),
|
|
177
|
-
artifact,
|
|
178
|
-
);
|
|
179
169
|
ctx.registerDefinition(this.resource);
|
|
180
170
|
|
|
181
171
|
const moduleName = this.resource.metadata.module;
|
|
182
172
|
const kindName = this.resource.metadata.name;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
173
|
+
const controllers = this.resource.controllers;
|
|
174
|
+
const source = this.resource.metadata.source;
|
|
175
|
+
const policy = ctx.getControllerPolicy();
|
|
176
|
+
// Resolution AND import are deferred to the kind's first instantiation,
|
|
177
|
+
// matching the Rust kernel: a definition whose candidate list nothing in
|
|
178
|
+
// this environment can host registers fine and errors — naming the kind —
|
|
179
|
+
// only when a resource of it is declared. That is what lets both kernels
|
|
180
|
+
// load a partially-covered module (e.g. console's stream kinds with no Rust
|
|
181
|
+
// controller) instead of rejecting it over kinds nobody uses.
|
|
182
|
+
// Lifecycle events are emitted here (not in the loader) so ControllerLoading
|
|
183
|
+
// / ControllerLoaded / ControllerLoadFailed — and the import duration —
|
|
184
|
+
// surface when the load actually happens, with the resolved PURL + source.
|
|
186
185
|
host.registerLazyController(
|
|
187
186
|
moduleName,
|
|
188
187
|
kindName,
|
|
189
188
|
async () => {
|
|
189
|
+
const resolved = await loader
|
|
190
|
+
.resolve(controllers, source, policy, artifact)
|
|
191
|
+
.catch((err) => {
|
|
192
|
+
if (err instanceof RuntimeError) {
|
|
193
|
+
throw new RuntimeError(err.code, `kind '${moduleName}.${kindName}': ${err.message}`);
|
|
194
|
+
}
|
|
195
|
+
throw err;
|
|
196
|
+
});
|
|
190
197
|
await ctx.emit("ControllerLoading", { purl: resolved.purl });
|
|
191
198
|
const startedAt = Date.now();
|
|
192
199
|
const instance = await resolved.importInstance().catch(async (err) => {
|
|
@@ -241,6 +248,17 @@ export function register(ctx: ControllerContext): void {
|
|
|
241
248
|
}
|
|
242
249
|
|
|
243
250
|
export async function create(resource: any, ctx: ResourceContext): Promise<ResourceDefinition> {
|
|
251
|
+
// Named preflight for the one capability the schema rejects by omission: the
|
|
252
|
+
// AJV oneOf failure it produces never says WHY, and this is the mistake an
|
|
253
|
+
// author migrating a multi-kind slot is most likely to make.
|
|
254
|
+
if (resource?.capability === "Telo.Executable") {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Invalid ResourceDefinition "${resource.metadata?.name}": capability 'Telo.Executable' ` +
|
|
257
|
+
`is an x-telo-ref slot constraint (the parent Telo.Invocable and Telo.Runnable ` +
|
|
258
|
+
`extend), not a declarable lifecycle role. Declare 'Telo.Invocable' (invoke) or ` +
|
|
259
|
+
`'Telo.Runnable' (run) instead.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
244
262
|
// Validate incoming resource definition against schema
|
|
245
263
|
if (!validateResourceDefinition(resource)) {
|
|
246
264
|
throw new Error(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { formatSpanCounter } from "./logging/span-id.js";
|
|
3
3
|
import {
|
|
4
|
+
deriveContext,
|
|
4
5
|
getRefIdentity,
|
|
5
6
|
isCompiledValue,
|
|
6
7
|
isInvokeError,
|
|
@@ -263,6 +264,15 @@ export function ambientInvokeContext(): InvokeContext | undefined {
|
|
|
263
264
|
return cancellationStore.getStore();
|
|
264
265
|
}
|
|
265
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Establish `ctx` as the ambient invocation context for the duration of `fn`.
|
|
269
|
+
* The seam `withZone` uses to make an opened zone ambient without owning the
|
|
270
|
+
* store — the store itself stays kernel-internal, never on the SDK surface.
|
|
271
|
+
*/
|
|
272
|
+
export function runWithAmbientContext<T>(ctx: InvokeContext, fn: () => T): T {
|
|
273
|
+
return cancellationStore.run(ctx, fn);
|
|
274
|
+
}
|
|
275
|
+
|
|
266
276
|
/** Marks a scope built by {@link EvaluationContext.bindScope}, whose properties
|
|
267
277
|
* are getters. `expandWith` merges such a scope by descriptor rather than by
|
|
268
278
|
* value — reading the value here is what a lazy binding must not do. */
|
|
@@ -1189,11 +1199,13 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1189
1199
|
outcome,
|
|
1190
1200
|
phase === "end" && rootScope ? { ...detail, context: rootScope } : detail,
|
|
1191
1201
|
);
|
|
1192
|
-
// When tracing, a
|
|
1202
|
+
// When tracing, a derived context carries the new id down the tree so nested
|
|
1193
1203
|
// invokes read it as their parent; it is never `=== ambient`, so the call
|
|
1194
|
-
// always (re)establishes the ALS scope.
|
|
1204
|
+
// always (re)establishes the ALS scope. Derived, never a fresh literal: a
|
|
1205
|
+
// literal drops every field it does not restate (`zones`), making
|
|
1206
|
+
// propagation differ between tracing on and off.
|
|
1195
1207
|
const invokeCtx: InvokeContext = tracing
|
|
1196
|
-
? {
|
|
1208
|
+
? deriveContext(baseCtx, { invocationId, parentInvocationId, traceId })
|
|
1197
1209
|
: baseCtx;
|
|
1198
1210
|
|
|
1199
1211
|
// Pre-dispatch gate: a sub-invoke reached after the tree was cancelled is
|
|
@@ -1391,12 +1403,14 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1391
1403
|
);
|
|
1392
1404
|
|
|
1393
1405
|
await this.emit(`${opts.ref.name}.Requesting`, payload("start", undefined));
|
|
1394
|
-
|
|
1395
|
-
|
|
1406
|
+
// Derived so the span context keeps whatever `base` carries beyond the span
|
|
1407
|
+
// ids — with tracing off this method returns `base` unchanged, and tracing
|
|
1408
|
+
// must not change what propagates.
|
|
1409
|
+
const context: InvokeContext = deriveContext(ctx, {
|
|
1396
1410
|
invocationId: spanId,
|
|
1397
1411
|
parentInvocationId: parentSpanId,
|
|
1398
1412
|
traceId,
|
|
1399
|
-
};
|
|
1413
|
+
});
|
|
1400
1414
|
let settled = false;
|
|
1401
1415
|
return {
|
|
1402
1416
|
context,
|
|
@@ -1460,7 +1474,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1460
1474
|
phase === "end" && rootScope ? { ...detail, context: rootScope } : detail,
|
|
1461
1475
|
);
|
|
1462
1476
|
const invokeCtx: InvokeContext = tracing
|
|
1463
|
-
? {
|
|
1477
|
+
? deriveContext(baseCtx, { invocationId, parentInvocationId, traceId })
|
|
1464
1478
|
: baseCtx;
|
|
1465
1479
|
|
|
1466
1480
|
// Refuse a target reached after the boot run was cancelled.
|
package/src/kernel.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AnalysisRegistry,
|
|
3
3
|
buildEvalPaths,
|
|
4
|
+
collectZoneModuleDocuments,
|
|
4
5
|
flattenForAnalyzer,
|
|
5
6
|
flattenLoadedModule,
|
|
6
7
|
isModuleKind,
|
|
@@ -45,6 +46,7 @@ import { formatSpanCounter } from "./logging/span-id.js";
|
|
|
45
46
|
import { ambientInvokeContext } from "./evaluation-context.js";
|
|
46
47
|
import { ModuleContext } from "./module-context.js";
|
|
47
48
|
import { ResourceContextImpl } from "./resource-context.js";
|
|
49
|
+
import { mintResourceHandle } from "./resource-handle.js";
|
|
48
50
|
import { nodeCelHandlers } from "./cel-handlers.js";
|
|
49
51
|
import { parseRef, seedInvokeSource } from "./invoke-dispatch.js";
|
|
50
52
|
import { stripCompiledValues } from "./schema-compiled-values.js";
|
|
@@ -520,7 +522,10 @@ export class Kernel implements IKernel {
|
|
|
520
522
|
const skipValidation = stamp?.signature === analysisSignature;
|
|
521
523
|
const errors = this.analyzer.analyzeErrors(
|
|
522
524
|
staticManifests,
|
|
523
|
-
|
|
525
|
+
// Imported libraries' full documents, for the zone stage's per-library
|
|
526
|
+
// export derivation: `flattenForAnalyzer` forwards only each library's
|
|
527
|
+
// export surface, never its internal dispatch chain.
|
|
528
|
+
{ skipValidation, moduleDocuments: collectZoneModuleDocuments(analysisGraph) },
|
|
524
529
|
this.registry,
|
|
525
530
|
);
|
|
526
531
|
if (errors.length > 0) {
|
|
@@ -1286,6 +1291,17 @@ export class Kernel implements IKernel {
|
|
|
1286
1291
|
const instance = await controller.create(processedResource, ctx);
|
|
1287
1292
|
if (!instance) return null;
|
|
1288
1293
|
|
|
1294
|
+
// Mint the instance's identity here, at the single instance-production site,
|
|
1295
|
+
// so an instance is never observable without a handle — the same argument
|
|
1296
|
+
// that put contract binding here. First mint wins: a `base:` child IS the
|
|
1297
|
+
// parent instance returned verbatim and must not be re-identified.
|
|
1298
|
+
const handle = mintResourceHandle(
|
|
1299
|
+
instance,
|
|
1300
|
+
resolvedKind,
|
|
1301
|
+
(processedResource.metadata?.name as string | undefined) ?? "<unnamed>",
|
|
1302
|
+
);
|
|
1303
|
+
(ctx as ResourceContextImpl).bindResourceIdentity(handle, resolvedKind, processedResource);
|
|
1304
|
+
|
|
1289
1305
|
// Bind the resolved invocation contract to the instance, here at the kernel's
|
|
1290
1306
|
// single instance-production site — so every consumer holds an already
|
|
1291
1307
|
// enforcing instance, including the majority that read a Phase-5-injected ref
|
package/src/manifest-schemas.ts
CHANGED
|
@@ -96,6 +96,12 @@ const KNOWN_CAPABILITIES = [
|
|
|
96
96
|
// path, and dispatch emits trace events, so routing logs through it would
|
|
97
97
|
// generate telemetry from inside the telemetry path. See kernel/specs/logging.md §10.
|
|
98
98
|
"Telo.Sink",
|
|
99
|
+
// `Telo.Executable` is deliberately declarable NOWHERE: it is the slot-
|
|
100
|
+
// constraint parent of Invocable and Runnable ("control can be transferred to
|
|
101
|
+
// this"), naming no lifecycle role. Listing it here keeps the open third-party
|
|
102
|
+
// fallback branch below from accepting it — and since no branch above admits
|
|
103
|
+
// it either, `capability: Telo.Executable` fails validation outright.
|
|
104
|
+
"Telo.Executable",
|
|
99
105
|
] as const;
|
|
100
106
|
|
|
101
107
|
/** Rule 8: `throws:` is only meaningful on Telo.Invocable or Telo.Runnable.
|
package/src/module-context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { executeInvokeStep, getRefIdentity, RuntimeError } from "@telorun/sdk";
|
|
1
|
+
import { deriveContext, executeInvokeStep, getRefIdentity, RuntimeError } from "@telorun/sdk";
|
|
2
2
|
import type { ScopeConfig } from "./logging/scope-config.js";
|
|
3
3
|
import type {
|
|
4
4
|
BootTarget,
|
|
@@ -585,12 +585,11 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
585
585
|
);
|
|
586
586
|
const targetCtx: InvokeContext | undefined =
|
|
587
587
|
tracing && ctx
|
|
588
|
-
? {
|
|
589
|
-
cancellation: ctx.cancellation,
|
|
588
|
+
? deriveContext(ctx, {
|
|
590
589
|
invocationId: appSpanId,
|
|
591
590
|
parentInvocationId: undefined,
|
|
592
591
|
traceId: appTraceId,
|
|
593
|
-
}
|
|
592
|
+
})
|
|
594
593
|
: ctx;
|
|
595
594
|
|
|
596
595
|
const steps: Record<string, unknown> = {};
|
package/src/resource-context.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
|
+
InvokeError,
|
|
2
3
|
NoopValidator,
|
|
3
4
|
ResourceContext,
|
|
4
5
|
ResourceInstance,
|
|
5
6
|
ResourceManifest,
|
|
6
7
|
RuntimeError,
|
|
7
8
|
RuntimeResource,
|
|
9
|
+
UNCANCELLABLE_CONTEXT,
|
|
8
10
|
createCancellationSource,
|
|
11
|
+
deriveContext,
|
|
12
|
+
getRefIdentity,
|
|
9
13
|
resolveRefInstance,
|
|
10
14
|
type CancellationSource,
|
|
11
15
|
type ControllerPolicy,
|
|
12
16
|
type EvaluationContext as IEvaluationContext,
|
|
17
|
+
type InvokeByNameOptions,
|
|
13
18
|
type InvokeContext,
|
|
14
19
|
type LoadOptions,
|
|
15
20
|
type ModuleContext,
|
|
@@ -17,10 +22,14 @@ import {
|
|
|
17
22
|
type OpenSpan,
|
|
18
23
|
type OpenSpanOptions,
|
|
19
24
|
type ParsedArgs,
|
|
25
|
+
type ResourceDefinition,
|
|
26
|
+
type ResourceHandle,
|
|
20
27
|
type RuntimeSeam,
|
|
21
28
|
type TypeRule,
|
|
29
|
+
type ZoneEntry,
|
|
22
30
|
} from "@telorun/sdk";
|
|
23
31
|
import { isRefSentinel } from "@telorun/templating";
|
|
32
|
+
import { ZoneContext } from "./zone-context.js";
|
|
24
33
|
import * as path from "path";
|
|
25
34
|
import { pathToFileURL } from "url";
|
|
26
35
|
import type { ModuleArtifact } from "./bundle/module-artifact.js";
|
|
@@ -350,6 +359,88 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
350
359
|
return createCancellationSource();
|
|
351
360
|
}
|
|
352
361
|
|
|
362
|
+
// ── Execution zones (kernel/specs/execution-zones.md) ─────────────────────
|
|
363
|
+
//
|
|
364
|
+
// Identity is held here (it is the resource's, not the zone subsystem's);
|
|
365
|
+
// everything else delegates to `ZoneContext`, which owns the annotation
|
|
366
|
+
// resolution, correlation walk and stack matching — and memoizes them, since
|
|
367
|
+
// this sits on the dispatch path.
|
|
368
|
+
|
|
369
|
+
#self: ResourceHandle | undefined;
|
|
370
|
+
#zones: ZoneContext | undefined;
|
|
371
|
+
|
|
372
|
+
/** Kernel-internal: stamped at `create()`, the moment the instance exists. */
|
|
373
|
+
bindResourceIdentity(
|
|
374
|
+
handle: ResourceHandle,
|
|
375
|
+
resolvedKind: string,
|
|
376
|
+
manifest: Record<string, unknown>,
|
|
377
|
+
): void {
|
|
378
|
+
this.#self = handle;
|
|
379
|
+
this.#zones = new ZoneContext({
|
|
380
|
+
resourceName: (this.metadata?.name as string) ?? "<unnamed>",
|
|
381
|
+
resolvedKind,
|
|
382
|
+
self: handle,
|
|
383
|
+
// The SAME object Phase-5 injection later mutates, so a correlation
|
|
384
|
+
// pointer read at invoke time sees live instances in ref slots.
|
|
385
|
+
manifest,
|
|
386
|
+
resolveDefinition: (kind) => this.kernel.getAnalysisRegistry().resolveDefinition(kind),
|
|
387
|
+
resolveDefinitionIn: (kind, module) =>
|
|
388
|
+
this.kernel.getAnalysisRegistry().resolveDefinitionIn(kind, module),
|
|
389
|
+
resolveLocalInstance: (name) => this.resolveLocalInstance(name),
|
|
390
|
+
resolveLocalManifest: (name) =>
|
|
391
|
+
this.contextForName(name).resourceInstances.get(name)?.resource as
|
|
392
|
+
| Record<string, unknown>
|
|
393
|
+
| undefined,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
get self(): ResourceHandle {
|
|
398
|
+
if (!this.#self) {
|
|
399
|
+
throw new RuntimeError(
|
|
400
|
+
"ERR_RESOURCE_IDENTITY_UNBOUND",
|
|
401
|
+
`[${this.metadata.name}] ctx.self is unavailable inside create() — the handle is minted when create() returns`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
return this.#self;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The zone subsystem, available once `create()` has returned. */
|
|
408
|
+
private zoneContext(): ZoneContext {
|
|
409
|
+
if (!this.#zones) {
|
|
410
|
+
throw new RuntimeError(
|
|
411
|
+
"ERR_RESOURCE_IDENTITY_UNBOUND",
|
|
412
|
+
`[${this.metadata.name}] zones are unavailable inside create() — the handle is minted when create() returns`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
return this.#zones;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
withZone<T>(
|
|
419
|
+
slot: string,
|
|
420
|
+
fn: (ctx: InvokeContext, entry: ZoneEntry) => Promise<T>,
|
|
421
|
+
base?: InvokeContext,
|
|
422
|
+
): Promise<T> {
|
|
423
|
+
return this.zoneContext().withZone(slot, fn, base);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
requireZone(field: string, ctx?: InvokeContext): ZoneEntry {
|
|
427
|
+
return this.zoneContext().requireZone(field, ctx);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
findZone(field: string, ctx?: InvokeContext): ZoneEntry | undefined {
|
|
431
|
+
return this.zoneContext().findZone(field, ctx);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
zonesFor(instance: ResourceInstance, ctx?: InvokeContext): readonly ZoneEntry[] {
|
|
435
|
+
return this.zoneContext().zonesFor(instance, ctx);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** The root context for runtime-driven inbound work — inherits nothing from
|
|
439
|
+
* whatever ambient happens to be live at the registration site. */
|
|
440
|
+
rootContext(opts?: { cancellation?: CancellationSource }): InvokeContext {
|
|
441
|
+
return opts?.cancellation?.context ?? UNCANCELLABLE_CONTEXT;
|
|
442
|
+
}
|
|
443
|
+
|
|
353
444
|
/** In-flight fire-and-forget tasks this resource spawned. Owned here, not by
|
|
354
445
|
* the kernel: the resource drains them in its own teardown (see
|
|
355
446
|
* `drainDetached`), so background work is bounded by the resource's lifetime. */
|
|
@@ -401,8 +492,13 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
401
492
|
return this.owningContext.openSpan(base, opts);
|
|
402
493
|
}
|
|
403
494
|
|
|
404
|
-
invoke<TInputs>(
|
|
405
|
-
|
|
495
|
+
invoke<TInputs>(
|
|
496
|
+
kind: string,
|
|
497
|
+
name: string,
|
|
498
|
+
inputs: TInputs,
|
|
499
|
+
options?: InvokeByNameOptions,
|
|
500
|
+
): Promise<any> {
|
|
501
|
+
return this.contextForName(name).invoke(kind, name, inputs, options?.ctx);
|
|
406
502
|
}
|
|
407
503
|
|
|
408
504
|
invokeResolved<TInputs>(
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ResourceHandle, ResourceInstanceId } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Instance → handle, minted at `create()` — the kernel's single
|
|
5
|
+
* instance-production site, where the invocation contract already binds — so an
|
|
6
|
+
* instance is never observable without one. The reverse direction deliberately
|
|
7
|
+
* does not exist: nothing turns a handle back into someone else's live
|
|
8
|
+
* instance, which is what keeps the ambient zone stack from leaking instances
|
|
9
|
+
* across module boundaries.
|
|
10
|
+
*/
|
|
11
|
+
const handles = new WeakMap<object, ResourceHandle>();
|
|
12
|
+
|
|
13
|
+
let counter = 0;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Mint (or return) the handle for a live instance. Idempotent, first mint wins
|
|
17
|
+
* — a `base:` child IS its parent's instance returned verbatim, so the nested
|
|
18
|
+
* parent create stamps first and the child create must not re-identify it; one
|
|
19
|
+
* live instance, one id, exactly like `stampRefIdentity`.
|
|
20
|
+
*/
|
|
21
|
+
export function mintResourceHandle(instance: object, kind: string, name: string): ResourceHandle {
|
|
22
|
+
const existing = handles.get(instance);
|
|
23
|
+
if (existing) return existing;
|
|
24
|
+
const handle: ResourceHandle = Object.freeze({
|
|
25
|
+
id: `ri-${++counter}` as ResourceInstanceId,
|
|
26
|
+
ref: Object.freeze({ kind, name }),
|
|
27
|
+
});
|
|
28
|
+
handles.set(instance, handle);
|
|
29
|
+
return handle;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The handle minted for a live instance, if any. */
|
|
33
|
+
export function handleOfInstance(instance: object): ResourceHandle | undefined {
|
|
34
|
+
return handles.get(instance);
|
|
35
|
+
}
|
package/src/runtime-seam.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Loader,
|
|
3
3
|
StaticAnalyzer,
|
|
4
|
+
collectZoneModuleDocuments,
|
|
4
5
|
flattenForAnalyzer,
|
|
5
6
|
type AnalysisDiagnostic,
|
|
6
7
|
type ManifestSource,
|
|
8
|
+
type ZoneModuleDocuments,
|
|
7
9
|
} from "@telorun/analyzer";
|
|
8
10
|
import {
|
|
9
11
|
Stream,
|
|
@@ -207,12 +209,22 @@ export class KernelRuntimeSeam implements RuntimeSeam {
|
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
let manifests;
|
|
212
|
+
// Parse failures and version reconciliation are the loader's findings, not
|
|
213
|
+
// `analyze()`'s — carried out of the try so the checks below can see them.
|
|
214
|
+
let parseDiagnostics: AnalysisDiagnostic[] = [];
|
|
215
|
+
let versionDiagnostics: AnalysisDiagnostic[] = [];
|
|
216
|
+
let moduleDocuments: ZoneModuleDocuments[] = [];
|
|
210
217
|
try {
|
|
211
218
|
const graph = await loader.loadGraph(source, {
|
|
212
219
|
desugarImports: options?.desugarImports ?? true,
|
|
213
220
|
});
|
|
214
221
|
if (graph.errors.length > 0) throw graph.errors[0].error;
|
|
222
|
+
parseDiagnostics = graph.parseDiagnostics;
|
|
223
|
+
versionDiagnostics = graph.versionDiagnostics;
|
|
215
224
|
manifests = flattenForAnalyzer(graph);
|
|
225
|
+
// The zone stage derives each imported library's export contracts from
|
|
226
|
+
// its own full documents, which the flattened list drops.
|
|
227
|
+
moduleDocuments = collectZoneModuleDocuments(graph);
|
|
216
228
|
} catch (err) {
|
|
217
229
|
// A graph that would not load is an answer, not a failure of the call —
|
|
218
230
|
// "this manifest does not load, and here is the reason" is precisely what
|
|
@@ -224,7 +236,24 @@ export class KernelRuntimeSeam implements RuntimeSeam {
|
|
|
224
236
|
};
|
|
225
237
|
}
|
|
226
238
|
|
|
227
|
-
|
|
228
|
-
|
|
239
|
+
// A file that fails to parse is not dropped — it reaches the flattened list
|
|
240
|
+
// as a mangled `toJSON()` tree, and analyzing that buries the real error
|
|
241
|
+
// under a cascade of secondaries which exist only because the parse failed.
|
|
242
|
+
// Report the parse findings and stop, the policy `load()` already applies by
|
|
243
|
+
// treating a parse failure as fatal before analysis.
|
|
244
|
+
if (parseDiagnostics.length > 0) {
|
|
245
|
+
return {
|
|
246
|
+
diagnostics: [...parseDiagnostics, ...versionDiagnostics].map(toCheckDiagnostic),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// `analyze()` never sees version skew, so without merging these a major
|
|
251
|
+
// mismatch — which `load()` refuses to boot on — would check clean.
|
|
252
|
+
const diagnostics = new StaticAnalyzer({ celHandlers: nodeCelHandlers }).analyze(manifests, {
|
|
253
|
+
moduleDocuments,
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
diagnostics: [...versionDiagnostics, ...diagnostics].map(toCheckDiagnostic),
|
|
257
|
+
};
|
|
229
258
|
}
|
|
230
259
|
}
|