@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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Loader,
|
|
3
|
+
StaticAnalyzer,
|
|
4
|
+
flattenForAnalyzer,
|
|
5
|
+
type AnalysisDiagnostic,
|
|
6
|
+
type ManifestSource,
|
|
7
|
+
} from "@telorun/analyzer";
|
|
8
|
+
import {
|
|
9
|
+
Stream,
|
|
10
|
+
type RuntimeCheckOptions,
|
|
11
|
+
type RuntimeCheckResult,
|
|
12
|
+
type CheckDiagnostic,
|
|
13
|
+
type CheckDiagnosticSeverity,
|
|
14
|
+
type RuntimeRun,
|
|
15
|
+
type RuntimeRunOptions,
|
|
16
|
+
type RuntimeSeam,
|
|
17
|
+
} from "@telorun/sdk";
|
|
18
|
+
import { Writable } from "stream";
|
|
19
|
+
import { nodeCelHandlers } from "./cel-handlers.js";
|
|
20
|
+
import type { Kernel } from "./kernel.js";
|
|
21
|
+
import { defaultTransportRegistry } from "./transports/transport-registry.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* How many unconsumed chunks a channel holds before it stops acknowledging the
|
|
25
|
+
* child's writes.
|
|
26
|
+
*
|
|
27
|
+
* This is a courtesy, **not a memory bound**, and the distinction matters enough
|
|
28
|
+
* to state: withholding a `Writable`'s `_write` callback only moves subsequent
|
|
29
|
+
* chunks into the stream's own internal buffer, which is unbounded, and a
|
|
30
|
+
* controller writing to `ctx.stdout` does not await `drain`. So a child whose
|
|
31
|
+
* output nobody reads still grows the host's memory. What actually bounds it is
|
|
32
|
+
* `RuntimeRun.cancel()` — stopping the child, rather than hoping it stops
|
|
33
|
+
* producing. What the high-water mark does buy is that a *slow* consumer sees
|
|
34
|
+
* chunks handed over in order without the channel racing ahead of it.
|
|
35
|
+
*/
|
|
36
|
+
const CHANNEL_HIGH_WATER_MARK = 256;
|
|
37
|
+
|
|
38
|
+
interface PendingChunk {
|
|
39
|
+
text: string;
|
|
40
|
+
/** The child's `_write` callback, held back once the channel is over its
|
|
41
|
+
* high-water mark and released when a consumer takes the chunk. */
|
|
42
|
+
release?: () => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One direction of a child's output: a `Writable` the child kernel writes to,
|
|
47
|
+
* and the {@link Stream} the caller reads. Bridges Node's stream model to the
|
|
48
|
+
* SDK's without either side seeing the other's — `RuntimeSeam` has no
|
|
49
|
+
* `Writable` in it, and the child kernel has no `Stream` in it.
|
|
50
|
+
*/
|
|
51
|
+
class OutputChannel {
|
|
52
|
+
#queue: PendingChunk[] = [];
|
|
53
|
+
#waiters: Array<(result: IteratorResult<string>) => void> = [];
|
|
54
|
+
#ended = false;
|
|
55
|
+
|
|
56
|
+
readonly writable: Writable = new Writable({
|
|
57
|
+
write: (chunk: Buffer | string, _encoding, callback) => {
|
|
58
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
59
|
+
this.#push(text, callback);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
readonly stream: Stream<string> = new Stream<string>({
|
|
64
|
+
[Symbol.asyncIterator]: () => ({
|
|
65
|
+
next: () => this.#next(),
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** No more output is coming. Wakes every waiter with a terminal result, so a
|
|
70
|
+
* `for await` over the stream completes rather than hanging on a child that
|
|
71
|
+
* has already exited. */
|
|
72
|
+
end(): void {
|
|
73
|
+
this.#ended = true;
|
|
74
|
+
while (this.#waiters.length > 0) {
|
|
75
|
+
this.#waiters.shift()!({ value: undefined, done: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#push(text: string, callback: () => void): void {
|
|
80
|
+
const waiter = this.#waiters.shift();
|
|
81
|
+
if (waiter) {
|
|
82
|
+
waiter({ value: text, done: false });
|
|
83
|
+
callback();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (this.#queue.length >= CHANNEL_HIGH_WATER_MARK) {
|
|
87
|
+
this.#queue.push({ text, release: callback });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.#queue.push({ text });
|
|
91
|
+
callback();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async #next(): Promise<IteratorResult<string>> {
|
|
95
|
+
const pending = this.#queue.shift();
|
|
96
|
+
if (pending) {
|
|
97
|
+
pending.release?.();
|
|
98
|
+
return { value: pending.text, done: false };
|
|
99
|
+
}
|
|
100
|
+
if (this.#ended) return { value: undefined, done: true };
|
|
101
|
+
return new Promise<IteratorResult<string>>((resolve) => this.#waiters.push(resolve));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const SEVERITY_NAMES: Record<number, CheckDiagnosticSeverity> = {
|
|
106
|
+
1: "error",
|
|
107
|
+
2: "warning",
|
|
108
|
+
3: "info",
|
|
109
|
+
4: "hint",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Flatten an analyzer finding to the seam's plain-data shape. Severity becomes
|
|
113
|
+
* a name because the integer is an LSP protocol detail, and this contract is
|
|
114
|
+
* read by kernels that speak no LSP. An unlabelled severity is an error: the
|
|
115
|
+
* analyzer's own default, and the safe reading for a caller gating on it. */
|
|
116
|
+
function toCheckDiagnostic(diagnostic: AnalysisDiagnostic): CheckDiagnostic {
|
|
117
|
+
return {
|
|
118
|
+
code: String(diagnostic.code ?? ""),
|
|
119
|
+
message: diagnostic.message,
|
|
120
|
+
severity: SEVERITY_NAMES[diagnostic.severity ?? 1] ?? "error",
|
|
121
|
+
source: diagnostic.source,
|
|
122
|
+
line: diagnostic.range?.start?.line,
|
|
123
|
+
column: diagnostic.range?.start?.character,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The kernel's implementation of the SDK's {@link RuntimeSeam} — run a manifest,
|
|
129
|
+
* analyze a manifest — built over the `Kernel`, `Loader` and `StaticAnalyzer`
|
|
130
|
+
* this kernel already owns.
|
|
131
|
+
*
|
|
132
|
+
* Isolation is this class's choice, not the caller's: today a child `Kernel` in
|
|
133
|
+
* the same process, later a subprocess. That is what keeps process-global host
|
|
134
|
+
* state (the `process.env` guardrail is additive across in-process kernels) an
|
|
135
|
+
* implementation detail rather than something a module author has to know.
|
|
136
|
+
*/
|
|
137
|
+
export class KernelRuntimeSeam implements RuntimeSeam {
|
|
138
|
+
constructor(private readonly kernel: Kernel) {}
|
|
139
|
+
|
|
140
|
+
async run(source: string, options?: RuntimeRunOptions): Promise<RuntimeRun> {
|
|
141
|
+
// Import here rather than at module scope: `resource-context.ts` imports this
|
|
142
|
+
// file to construct the seam and `kernel.ts` imports *that*, so a top-level
|
|
143
|
+
// import of `Kernel` here would close the cycle. The dynamic import resolves
|
|
144
|
+
// after both modules are evaluated.
|
|
145
|
+
const { Kernel: ChildKernel } = await import("./kernel.js");
|
|
146
|
+
const stdout = new OutputChannel();
|
|
147
|
+
const stderr = new OutputChannel();
|
|
148
|
+
|
|
149
|
+
const child = new ChildKernel({
|
|
150
|
+
env: options?.env ?? this.kernel.env,
|
|
151
|
+
stdout: stdout.writable,
|
|
152
|
+
stderr: stderr.writable,
|
|
153
|
+
sources: [...this.kernel.injectedSources],
|
|
154
|
+
registryUrl: this.kernel.registryUrl,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// A child that fails to load is not an exception on this side: the caller
|
|
158
|
+
// asked to run a manifest and the answer is "it exited non-zero, here is
|
|
159
|
+
// why", which keeps both failure modes on one path. `start()` tears the
|
|
160
|
+
// child down in its own `finally`, so this only has to close the channels.
|
|
161
|
+
const exitCode = (async () => {
|
|
162
|
+
try {
|
|
163
|
+
await child.load(source);
|
|
164
|
+
await child.start();
|
|
165
|
+
return child.exitCode;
|
|
166
|
+
} catch (err) {
|
|
167
|
+
stderr.writable.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
168
|
+
return child.exitCode === 0 ? 1 : child.exitCode;
|
|
169
|
+
} finally {
|
|
170
|
+
stdout.end();
|
|
171
|
+
stderr.end();
|
|
172
|
+
}
|
|
173
|
+
})();
|
|
174
|
+
|
|
175
|
+
let cancelled: Promise<void> | undefined;
|
|
176
|
+
const cancel = (reason = "cancelled"): Promise<void> => {
|
|
177
|
+
// Idempotent: repeated calls await the first teardown rather than starting
|
|
178
|
+
// another. `teardown()` is itself idempotent, but a second `forceIdle()`
|
|
179
|
+
// against a kernel already unwinding buys nothing.
|
|
180
|
+
cancelled ??= (async () => {
|
|
181
|
+
// Cancel the boot run first so an in-flight target observes it, then
|
|
182
|
+
// release `waitForIdle` so `start()`'s own `finally` performs the
|
|
183
|
+
// teardown — the same path a SIGINT takes. Awaiting `exitCode` is what
|
|
184
|
+
// makes "resolves when teardown has completed" true.
|
|
185
|
+
child.cancel(reason);
|
|
186
|
+
child.forceIdle();
|
|
187
|
+
await exitCode;
|
|
188
|
+
})();
|
|
189
|
+
return cancelled;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return { stdout: stdout.stream, stderr: stderr.stream, exitCode, cancel };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async check(source: string, options?: RuntimeCheckOptions): Promise<RuntimeCheckResult> {
|
|
196
|
+
// A fresh loader over this kernel's own resolution chain — the transports it
|
|
197
|
+
// was configured with plus whatever sources were injected — so `check()`
|
|
198
|
+
// resolves an import exactly as a run of the same manifest would. Fresh
|
|
199
|
+
// rather than the kernel's own loader because a checked manifest is often
|
|
200
|
+
// deliberately broken and has no business entering the running kernel's
|
|
201
|
+
// parse cache.
|
|
202
|
+
const loader = new Loader(defaultTransportRegistry(this.kernel.registryUrl).sources(), {
|
|
203
|
+
celHandlers: nodeCelHandlers,
|
|
204
|
+
});
|
|
205
|
+
for (const injected of this.kernel.injectedSources) {
|
|
206
|
+
loader.register(injected as ManifestSource);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let manifests;
|
|
210
|
+
try {
|
|
211
|
+
const graph = await loader.loadGraph(source, {
|
|
212
|
+
desugarImports: options?.desugarImports ?? true,
|
|
213
|
+
});
|
|
214
|
+
if (graph.errors.length > 0) throw graph.errors[0].error;
|
|
215
|
+
manifests = flattenForAnalyzer(graph);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
// A graph that would not load is an answer, not a failure of the call —
|
|
218
|
+
// "this manifest does not load, and here is the reason" is precisely what
|
|
219
|
+
// a caller asked for. Throwing would force every caller to re-express the
|
|
220
|
+
// distinction in a catch.
|
|
221
|
+
return {
|
|
222
|
+
diagnostics: [],
|
|
223
|
+
loadError: err instanceof Error ? err.message : String(err),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const diagnostics = new StaticAnalyzer({ celHandlers: nodeCelHandlers }).analyze(manifests);
|
|
228
|
+
return { diagnostics: diagnostics.map(toCheckDiagnostic) };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DiagnosticSeverity, type AnalysisDiagnostic } from "@telorun/analyzer";
|
|
2
|
+
import type { DiagnosticOrigin, RuntimeDiagnostic } from "@telorun/sdk";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Carry a static-analysis diagnostic into the runtime failure set.
|
|
6
|
+
*
|
|
7
|
+
* The analyzer's `data` — the file, the field path within it, and the owning
|
|
8
|
+
* resource — travels as `origin`, alongside the diagnostic's own `range` for
|
|
9
|
+
* the failures that have no field path to look up (a YAML parse error knows
|
|
10
|
+
* where the syntax broke but has no parsed tree to index). That is what lets a
|
|
11
|
+
* renderer resolve the same `file:line:col` `telo check` prints; flattening it
|
|
12
|
+
* into `message` leaves `telo run` pointing at nothing, which is the whole
|
|
13
|
+
* reason the two commands used to disagree about one error.
|
|
14
|
+
*
|
|
15
|
+
* The sibling of `init-failure-diagnostics.ts`: both turn a kernel failure set
|
|
16
|
+
* into `RuntimeDiagnostic[]`, one for what static analysis rejected and one for
|
|
17
|
+
* what failed to initialize.
|
|
18
|
+
*/
|
|
19
|
+
export function staticDiagnosticToRuntime(d: AnalysisDiagnostic): RuntimeDiagnostic {
|
|
20
|
+
const data = d.data as DiagnosticOrigin | undefined;
|
|
21
|
+
const origin: DiagnosticOrigin = {};
|
|
22
|
+
if (data?.filePath !== undefined) origin.filePath = data.filePath;
|
|
23
|
+
if (data?.path !== undefined) origin.path = data.path;
|
|
24
|
+
if (data?.resource !== undefined) origin.resource = data.resource;
|
|
25
|
+
if (d.range !== undefined) origin.range = d.range;
|
|
26
|
+
return {
|
|
27
|
+
// Mapped, not assumed: every current caller passes a pre-filtered error set,
|
|
28
|
+
// but a warning routed through here must not be rendered in red and counted
|
|
29
|
+
// toward the exit code. `AnalysisDiagnostic.severity` is optional and its
|
|
30
|
+
// scale runs Error(1) → Hint(4), so anything looser than Error is a warning.
|
|
31
|
+
severity: (d.severity ?? DiagnosticSeverity.Warning) <= DiagnosticSeverity.Error
|
|
32
|
+
? "error"
|
|
33
|
+
: "warning",
|
|
34
|
+
message: d.message,
|
|
35
|
+
code: d.code !== undefined ? String(d.code) : undefined,
|
|
36
|
+
resource: describeResource(data?.resource),
|
|
37
|
+
// Only when something is actually set, so `origin` stays usable as the
|
|
38
|
+
// "this came from static analysis" predicate its contract promises.
|
|
39
|
+
...(Object.keys(origin).length > 0 ? { origin } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `Kind.name`, or whichever half is present — a diagnostic carrying only one
|
|
44
|
+
* of them used to render as `undefined.foo`. */
|
|
45
|
+
function describeResource(
|
|
46
|
+
resource: { kind?: string; name?: string } | undefined,
|
|
47
|
+
): string | undefined {
|
|
48
|
+
if (!resource) return undefined;
|
|
49
|
+
const parts = [resource.kind, resource.name].filter((p): p is string => p !== undefined);
|
|
50
|
+
return parts.length > 0 ? parts.join(".") : undefined;
|
|
51
|
+
}
|
|
@@ -62,9 +62,8 @@ import {
|
|
|
62
62
|
* payload gets a clear "republish" failure at the controller instead, while the
|
|
63
63
|
* npm-backed majority, which ships none, is unaffected.
|
|
64
64
|
*/
|
|
65
|
-
async function pullManifestLayer(ref: string): Promise<string> {
|
|
66
|
-
const {
|
|
67
|
-
const client = new OciClient(host, repo);
|
|
65
|
+
async function pullManifestLayer(ref: string, client: OciClient): Promise<string> {
|
|
66
|
+
const { reference, integrity } = parseOciRef(ref);
|
|
68
67
|
const manifest = await client.pullManifest(reference);
|
|
69
68
|
const layer =
|
|
70
69
|
manifest.layers.find((l) => l.mediaType === TELO_MANIFEST_LAYER_MEDIA_TYPE) ??
|
|
@@ -108,18 +107,44 @@ async function pullManifestLayer(ref: string): Promise<string> {
|
|
|
108
107
|
export class OciTransport implements Transport {
|
|
109
108
|
readonly source: ManifestSource;
|
|
110
109
|
|
|
110
|
+
/** One read-side `OciClient` per `(host, repo)`, for this transport's lifetime.
|
|
111
|
+
*
|
|
112
|
+
* The client caches bearer tokens per scope, but a client built per operation
|
|
113
|
+
* discards that cache immediately — so every manifest and every blob paid its
|
|
114
|
+
* own 401→challenge→token round trip, and with it a `~/.docker/config.json`
|
|
115
|
+
* read and possibly a credential-helper subprocess. Pooling collapses those
|
|
116
|
+
* to one handshake per repository. An expired token still self-heals:
|
|
117
|
+
* `authedFetch` re-runs the challenge on a 401 and replaces the entry.
|
|
118
|
+
*
|
|
119
|
+
* Owned by the instance rather than the module, so a second transport — a
|
|
120
|
+
* test, or a second in-process kernel — never inherits another's credentials.
|
|
121
|
+
* `defaultTransportRegistry` memoizes per registry URL, so the production
|
|
122
|
+
* lifetime is unchanged. Publishing keeps its own client: it already reuses
|
|
123
|
+
* one across the whole push, and a push-scoped token has no reason to
|
|
124
|
+
* outlive the command. */
|
|
125
|
+
private readonly readClients = new Map<string, OciClient>();
|
|
126
|
+
|
|
111
127
|
constructor() {
|
|
112
128
|
this.source = {
|
|
113
129
|
supports: (url) => this.supports(url),
|
|
114
130
|
read: async (url) => {
|
|
115
|
-
const manifest = await pullManifestLayer(url);
|
|
116
131
|
const { host, repo, reference } = parseOciRef(url);
|
|
132
|
+
const manifest = await pullManifestLayer(url, this.readClient(host, repo));
|
|
117
133
|
return { text: manifest, source: `${OCI_SCHEME}${host}/${repo}@${reference}` };
|
|
118
134
|
},
|
|
119
135
|
resolveRelative: (base, relative) => this.resolveRelative(base, relative),
|
|
120
136
|
};
|
|
121
137
|
}
|
|
122
138
|
|
|
139
|
+
private readClient(host: string, repo: string): OciClient {
|
|
140
|
+
const key = `${host}/${repo}`;
|
|
141
|
+
const existing = this.readClients.get(key);
|
|
142
|
+
if (existing) return existing;
|
|
143
|
+
const client = new OciClient(host, repo);
|
|
144
|
+
this.readClients.set(key, client);
|
|
145
|
+
return client;
|
|
146
|
+
}
|
|
147
|
+
|
|
123
148
|
supports(ref: string): boolean {
|
|
124
149
|
return isOciRef(ref);
|
|
125
150
|
}
|
|
@@ -159,7 +184,7 @@ export class OciTransport implements Transport {
|
|
|
159
184
|
|
|
160
185
|
async listVersions(ref: string): Promise<string[] | null> {
|
|
161
186
|
const { host, repo } = parseOciRef(ref);
|
|
162
|
-
const tags = await
|
|
187
|
+
const tags = await this.readClient(host, repo).listTags();
|
|
163
188
|
return tags;
|
|
164
189
|
}
|
|
165
190
|
|
|
@@ -178,7 +203,7 @@ export class OciTransport implements Transport {
|
|
|
178
203
|
|
|
179
204
|
async digest(ref: string): Promise<string | null> {
|
|
180
205
|
const { host, repo, reference } = parseOciRef(ref);
|
|
181
|
-
return
|
|
206
|
+
return this.readClient(host, repo).headManifest(reference);
|
|
182
207
|
}
|
|
183
208
|
|
|
184
209
|
/** Pull one payload layer by the `blob` digest the pinned index supplies. The
|
|
@@ -188,7 +213,7 @@ export class OciTransport implements Transport {
|
|
|
188
213
|
* expected `integrity`. */
|
|
189
214
|
async fetchLayer(ref: string, blobDigest: string): Promise<PayloadFile[]> {
|
|
190
215
|
const { host, repo } = parseOciRef(ref);
|
|
191
|
-
const tar = await
|
|
216
|
+
const tar = await this.readClient(host, repo).pullBlob(blobDigest);
|
|
192
217
|
// Verify the transfer against the digest that addressed it. A registry is
|
|
193
218
|
// not trusted to return the blob that was asked for, and this is the only
|
|
194
219
|
// place the pushed bytes exist — the content digest checked after extraction
|
|
@@ -212,7 +237,8 @@ export class OciTransport implements Transport {
|
|
|
212
237
|
* blob per import rather than a full artifact pull, and a corrupt payload
|
|
213
238
|
* upstream no longer surfaces here as a pinning failure. */
|
|
214
239
|
async manifestHash(ref: string): Promise<string> {
|
|
215
|
-
const
|
|
240
|
+
const { host, repo } = parseOciRef(ref);
|
|
241
|
+
const manifest = await pullManifestLayer(ref, this.readClient(host, repo));
|
|
216
242
|
return `sha256-${await sha256Base64Url(new TextEncoder().encode(manifest))}`;
|
|
217
243
|
}
|
|
218
244
|
|
|
@@ -82,10 +82,12 @@ export function defaultTransports(registryUrl?: string): Transport[] {
|
|
|
82
82
|
const defaultRegistryCache = new Map<string, TransportRegistry>();
|
|
83
83
|
|
|
84
84
|
/** A `TransportRegistry` seeded with {@link defaultTransports}, memoized per
|
|
85
|
-
* `registryUrl`. The default transports
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
85
|
+
* `registryUrl`. The default transports hold no per-call state, so one shared
|
|
86
|
+
* instance per registry URL is safe — and avoids re-instantiating the whole set
|
|
87
|
+
* on hot paths like `cachePathForCanonical`. It is also what gives
|
|
88
|
+
* `OciTransport`'s per-instance read-client pool a process-wide lifetime here,
|
|
89
|
+
* so the bearer-token cache survives across operations without the pool having
|
|
90
|
+
* to be global. */
|
|
89
91
|
export function defaultTransportRegistry(registryUrl?: string): TransportRegistry {
|
|
90
92
|
const key = registryUrl ?? "";
|
|
91
93
|
let cached = defaultRegistryCache.get(key);
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { LogRecord } from "@telorun/sdk";
|
|
2
|
-
import type { SinkBufferPolicy } from "./log-sink.js";
|
|
3
|
-
/**
|
|
4
|
-
* The bounded buffer every asynchronous sink composes — `kernel/specs/logging.md`
|
|
5
|
-
* §10.3.
|
|
6
|
-
*
|
|
7
|
-
* The ecosystems disagree by default: Rust's `tracing-appender` drops, zap
|
|
8
|
-
* buffers 256 kB / 30 s, and pino has no bound at all. Telo therefore makes the
|
|
9
|
-
* policy explicit and required, and the buffer is never unbounded.
|
|
10
|
-
*
|
|
11
|
-
* `drop_old` has no precedent among the surveyed libraries, so it is implemented
|
|
12
|
-
* as specified — true ring-buffer semantics, evicting the oldest record — rather
|
|
13
|
-
* than by analogy to something else.
|
|
14
|
-
*/
|
|
15
|
-
export declare class RecordBuffer {
|
|
16
|
-
#private;
|
|
17
|
-
constructor(policy: SinkBufferPolicy, onDrop: () => void);
|
|
18
|
-
get size(): number;
|
|
19
|
-
get isFull(): boolean;
|
|
20
|
-
push(record: LogRecord): void;
|
|
21
|
-
/** Take everything buffered, leaving the buffer empty. */
|
|
22
|
-
drain(): LogRecord[];
|
|
23
|
-
}
|
|
24
|
-
//# sourceMappingURL=record-buffer.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"record-buffer.d.ts","sourceRoot":"","sources":["../../src/logging/record-buffer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,qBAAa,YAAY;;gBASX,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,IAAI;IAMxD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,IAAI,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IAiB7B,0DAA0D;IAC1D,KAAK,IAAI,SAAS,EAAE;CAerB"}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
-
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
-
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
-
};
|
|
7
|
-
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
-
};
|
|
12
|
-
var _RecordBuffer_instances, _RecordBuffer_capacity, _RecordBuffer_policy, _RecordBuffer_onDrop, _RecordBuffer_items, _RecordBuffer_head, _RecordBuffer_compact;
|
|
13
|
-
/**
|
|
14
|
-
* The bounded buffer every asynchronous sink composes — `kernel/specs/logging.md`
|
|
15
|
-
* §10.3.
|
|
16
|
-
*
|
|
17
|
-
* The ecosystems disagree by default: Rust's `tracing-appender` drops, zap
|
|
18
|
-
* buffers 256 kB / 30 s, and pino has no bound at all. Telo therefore makes the
|
|
19
|
-
* policy explicit and required, and the buffer is never unbounded.
|
|
20
|
-
*
|
|
21
|
-
* `drop_old` has no precedent among the surveyed libraries, so it is implemented
|
|
22
|
-
* as specified — true ring-buffer semantics, evicting the oldest record — rather
|
|
23
|
-
* than by analogy to something else.
|
|
24
|
-
*/
|
|
25
|
-
export class RecordBuffer {
|
|
26
|
-
constructor(policy, onDrop) {
|
|
27
|
-
_RecordBuffer_instances.add(this);
|
|
28
|
-
_RecordBuffer_capacity.set(this, void 0);
|
|
29
|
-
_RecordBuffer_policy.set(this, void 0);
|
|
30
|
-
_RecordBuffer_onDrop.set(this, void 0);
|
|
31
|
-
_RecordBuffer_items.set(this, []);
|
|
32
|
-
/** Index of the oldest record, so `drop_old` evicts in O(1) instead of
|
|
33
|
-
* shifting the whole array on every overflow. */
|
|
34
|
-
_RecordBuffer_head.set(this, 0);
|
|
35
|
-
__classPrivateFieldSet(this, _RecordBuffer_capacity, Math.max(1, policy.buffer), "f");
|
|
36
|
-
__classPrivateFieldSet(this, _RecordBuffer_policy, policy, "f");
|
|
37
|
-
__classPrivateFieldSet(this, _RecordBuffer_onDrop, onDrop, "f");
|
|
38
|
-
}
|
|
39
|
-
get size() {
|
|
40
|
-
return __classPrivateFieldGet(this, _RecordBuffer_items, "f").length - __classPrivateFieldGet(this, _RecordBuffer_head, "f");
|
|
41
|
-
}
|
|
42
|
-
get isFull() {
|
|
43
|
-
return this.size >= __classPrivateFieldGet(this, _RecordBuffer_capacity, "f");
|
|
44
|
-
}
|
|
45
|
-
push(record) {
|
|
46
|
-
if (this.isFull) {
|
|
47
|
-
if (__classPrivateFieldGet(this, _RecordBuffer_policy, "f").onFull === "drop_old") {
|
|
48
|
-
__classPrivateFieldSet(this, _RecordBuffer_head, __classPrivateFieldGet(this, _RecordBuffer_head, "f") + 1, "f");
|
|
49
|
-
__classPrivateFieldGet(this, _RecordBuffer_instances, "m", _RecordBuffer_compact).call(this);
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
// `drop_new` — and `block`, which never reaches here because a runtime
|
|
53
|
-
// that cannot honour it rejects the manifest at load rather than
|
|
54
|
-
// silently degrading to a dropping policy.
|
|
55
|
-
__classPrivateFieldGet(this, _RecordBuffer_onDrop, "f").call(this);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
__classPrivateFieldGet(this, _RecordBuffer_onDrop, "f").call(this);
|
|
59
|
-
}
|
|
60
|
-
__classPrivateFieldGet(this, _RecordBuffer_items, "f").push(record);
|
|
61
|
-
}
|
|
62
|
-
/** Take everything buffered, leaving the buffer empty. */
|
|
63
|
-
drain() {
|
|
64
|
-
const drained = __classPrivateFieldGet(this, _RecordBuffer_head, "f") === 0 ? __classPrivateFieldGet(this, _RecordBuffer_items, "f") : __classPrivateFieldGet(this, _RecordBuffer_items, "f").slice(__classPrivateFieldGet(this, _RecordBuffer_head, "f"));
|
|
65
|
-
__classPrivateFieldSet(this, _RecordBuffer_items, [], "f");
|
|
66
|
-
__classPrivateFieldSet(this, _RecordBuffer_head, 0, "f");
|
|
67
|
-
return drained;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
_RecordBuffer_capacity = new WeakMap(), _RecordBuffer_policy = new WeakMap(), _RecordBuffer_onDrop = new WeakMap(), _RecordBuffer_items = new WeakMap(), _RecordBuffer_head = new WeakMap(), _RecordBuffer_instances = new WeakSet(), _RecordBuffer_compact = function _RecordBuffer_compact() {
|
|
71
|
-
// Reclaim the consumed prefix once it dominates the array, so a long-lived
|
|
72
|
-
// ring does not grow its backing store without bound.
|
|
73
|
-
if (__classPrivateFieldGet(this, _RecordBuffer_head, "f") > 32 && __classPrivateFieldGet(this, _RecordBuffer_head, "f") * 2 >= __classPrivateFieldGet(this, _RecordBuffer_items, "f").length) {
|
|
74
|
-
__classPrivateFieldSet(this, _RecordBuffer_items, __classPrivateFieldGet(this, _RecordBuffer_items, "f").slice(__classPrivateFieldGet(this, _RecordBuffer_head, "f")), "f");
|
|
75
|
-
__classPrivateFieldSet(this, _RecordBuffer_head, 0, "f");
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
//# sourceMappingURL=record-buffer.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"record-buffer.js","sourceRoot":"","sources":["../../src/logging/record-buffer.ts"],"names":[],"mappings":";;;;;;;;;;;;AAGA;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,YAAY;IASvB,YAAY,MAAwB,EAAE,MAAkB;;QAR/C,yCAAkB;QAClB,uCAA0B;QAC1B,uCAAoB;QAC7B,8BAAsB,EAAE,EAAC;QACzB;0DACkD;QAClD,6BAAQ,CAAC,EAAC;QAGR,uBAAA,IAAI,0BAAa,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,MAAA,CAAC;QAC5C,uBAAA,IAAI,wBAAW,MAAM,MAAA,CAAC;QACtB,uBAAA,IAAI,wBAAW,MAAM,MAAA,CAAC;IACxB,CAAC;IAED,IAAI,IAAI;QACN,OAAO,uBAAA,IAAI,2BAAO,CAAC,MAAM,GAAG,uBAAA,IAAI,0BAAM,CAAC;IACzC,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,IAAI,IAAI,uBAAA,IAAI,8BAAU,CAAC;IACrC,CAAC;IAED,IAAI,CAAC,MAAiB;QACpB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,uBAAA,IAAI,4BAAQ,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;gBACvC,yGAAc,CAAC,MAAA,CAAC;gBAChB,uBAAA,IAAI,sDAAS,MAAb,IAAI,CAAW,CAAC;YAClB,CAAC;iBAAM,CAAC;gBACN,uEAAuE;gBACvE,iEAAiE;gBACjE,2CAA2C;gBAC3C,uBAAA,IAAI,4BAAQ,MAAZ,IAAI,CAAU,CAAC;gBACf,OAAO;YACT,CAAC;YACD,uBAAA,IAAI,4BAAQ,MAAZ,IAAI,CAAU,CAAC;QACjB,CAAC;QACD,uBAAA,IAAI,2BAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAED,0DAA0D;IAC1D,KAAK;QACH,MAAM,OAAO,GAAG,uBAAA,IAAI,0BAAM,KAAK,CAAC,CAAC,CAAC,CAAC,uBAAA,IAAI,2BAAO,CAAC,CAAC,CAAC,uBAAA,IAAI,2BAAO,CAAC,KAAK,CAAC,uBAAA,IAAI,0BAAM,CAAC,CAAC;QAC/E,uBAAA,IAAI,uBAAU,EAAE,MAAA,CAAC;QACjB,uBAAA,IAAI,sBAAS,CAAC,MAAA,CAAC;QACf,OAAO,OAAO,CAAC;IACjB,CAAC;CAUF;;IAPG,2EAA2E;IAC3E,sDAAsD;IACtD,IAAI,uBAAA,IAAI,0BAAM,GAAG,EAAE,IAAI,uBAAA,IAAI,0BAAM,GAAG,CAAC,IAAI,uBAAA,IAAI,2BAAO,CAAC,MAAM,EAAE,CAAC;QAC5D,uBAAA,IAAI,uBAAU,uBAAA,IAAI,2BAAO,CAAC,KAAK,CAAC,uBAAA,IAAI,0BAAM,CAAC,MAAA,CAAC;QAC5C,uBAAA,IAAI,sBAAS,CAAC,MAAA,CAAC;IACjB,CAAC;AACH,CAAC"}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import type { LogRecord } from "@telorun/sdk";
|
|
2
|
-
import type { SinkBufferPolicy } from "./log-sink.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The bounded buffer every asynchronous sink composes — `kernel/specs/logging.md`
|
|
6
|
-
* §10.3.
|
|
7
|
-
*
|
|
8
|
-
* The ecosystems disagree by default: Rust's `tracing-appender` drops, zap
|
|
9
|
-
* buffers 256 kB / 30 s, and pino has no bound at all. Telo therefore makes the
|
|
10
|
-
* policy explicit and required, and the buffer is never unbounded.
|
|
11
|
-
*
|
|
12
|
-
* `drop_old` has no precedent among the surveyed libraries, so it is implemented
|
|
13
|
-
* as specified — true ring-buffer semantics, evicting the oldest record — rather
|
|
14
|
-
* than by analogy to something else.
|
|
15
|
-
*/
|
|
16
|
-
export class RecordBuffer {
|
|
17
|
-
readonly #capacity: number;
|
|
18
|
-
readonly #policy: SinkBufferPolicy;
|
|
19
|
-
readonly #onDrop: () => void;
|
|
20
|
-
#items: LogRecord[] = [];
|
|
21
|
-
/** Index of the oldest record, so `drop_old` evicts in O(1) instead of
|
|
22
|
-
* shifting the whole array on every overflow. */
|
|
23
|
-
#head = 0;
|
|
24
|
-
|
|
25
|
-
constructor(policy: SinkBufferPolicy, onDrop: () => void) {
|
|
26
|
-
this.#capacity = Math.max(1, policy.buffer);
|
|
27
|
-
this.#policy = policy;
|
|
28
|
-
this.#onDrop = onDrop;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
get size(): number {
|
|
32
|
-
return this.#items.length - this.#head;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
get isFull(): boolean {
|
|
36
|
-
return this.size >= this.#capacity;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
push(record: LogRecord): void {
|
|
40
|
-
if (this.isFull) {
|
|
41
|
-
if (this.#policy.onFull === "drop_old") {
|
|
42
|
-
this.#head += 1;
|
|
43
|
-
this.#compact();
|
|
44
|
-
} else {
|
|
45
|
-
// `drop_new` — and `block`, which never reaches here because a runtime
|
|
46
|
-
// that cannot honour it rejects the manifest at load rather than
|
|
47
|
-
// silently degrading to a dropping policy.
|
|
48
|
-
this.#onDrop();
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
this.#onDrop();
|
|
52
|
-
}
|
|
53
|
-
this.#items.push(record);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Take everything buffered, leaving the buffer empty. */
|
|
57
|
-
drain(): LogRecord[] {
|
|
58
|
-
const drained = this.#head === 0 ? this.#items : this.#items.slice(this.#head);
|
|
59
|
-
this.#items = [];
|
|
60
|
-
this.#head = 0;
|
|
61
|
-
return drained;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
#compact(): void {
|
|
65
|
-
// Reclaim the consumed prefix once it dominates the array, so a long-lived
|
|
66
|
-
// ring does not grow its backing store without bound.
|
|
67
|
-
if (this.#head > 32 && this.#head * 2 >= this.#items.length) {
|
|
68
|
-
this.#items = this.#items.slice(this.#head);
|
|
69
|
-
this.#head = 0;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|