@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.
Files changed (82) hide show
  1. package/dist/bundle/module-artifact.d.ts +28 -2
  2. package/dist/bundle/module-artifact.d.ts.map +1 -1
  3. package/dist/bundle/module-artifact.js +36 -9
  4. package/dist/bundle/module-artifact.js.map +1 -1
  5. package/dist/controller-loader.d.ts +11 -4
  6. package/dist/controller-loader.d.ts.map +1 -1
  7. package/dist/controller-loader.js +1 -1
  8. package/dist/controller-loader.js.map +1 -1
  9. package/dist/controller-loaders/bundle-loader.d.ts +21 -0
  10. package/dist/controller-loaders/bundle-loader.d.ts.map +1 -1
  11. package/dist/controller-loaders/bundle-loader.js +106 -21
  12. package/dist/controller-loaders/bundle-loader.js.map +1 -1
  13. package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
  14. package/dist/controller-loaders/npm-loader.js +62 -10
  15. package/dist/controller-loaders/npm-loader.js.map +1 -1
  16. package/dist/controller-loaders/source-bundle-builder.d.ts +30 -0
  17. package/dist/controller-loaders/source-bundle-builder.d.ts.map +1 -0
  18. package/dist/controller-loaders/source-bundle-builder.js +302 -0
  19. package/dist/controller-loaders/source-bundle-builder.js.map +1 -0
  20. package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
  21. package/dist/controllers/resource-definition/resource-definition-controller.js +9 -2
  22. package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
  23. package/dist/index.d.ts +3 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/kernel.d.ts +9 -0
  28. package/dist/kernel.d.ts.map +1 -1
  29. package/dist/kernel.js +21 -20
  30. package/dist/kernel.js.map +1 -1
  31. package/dist/logging/file-sink.js +1 -1
  32. package/dist/logging/file-sink.js.map +1 -1
  33. package/dist/logging/index.d.ts +1 -1
  34. package/dist/logging/index.d.ts.map +1 -1
  35. package/dist/logging/index.js +1 -1
  36. package/dist/logging/index.js.map +1 -1
  37. package/dist/logging/log-sink.d.ts +1 -1
  38. package/dist/logging/log-sink.d.ts.map +1 -1
  39. package/dist/logging/log-sink.js +1 -1
  40. package/dist/logging/log-sink.js.map +1 -1
  41. package/dist/resource-context.d.ts +11 -1
  42. package/dist/resource-context.d.ts.map +1 -1
  43. package/dist/resource-context.js +22 -2
  44. package/dist/resource-context.js.map +1 -1
  45. package/dist/runtime-seam.d.ts +19 -0
  46. package/dist/runtime-seam.d.ts.map +1 -0
  47. package/dist/runtime-seam.js +211 -0
  48. package/dist/runtime-seam.js.map +1 -0
  49. package/dist/static-analysis-diagnostics.d.ts +19 -0
  50. package/dist/static-analysis-diagnostics.d.ts.map +1 -0
  51. package/dist/static-analysis-diagnostics.js +52 -0
  52. package/dist/static-analysis-diagnostics.js.map +1 -0
  53. package/dist/transports/oci/oci-transport.d.ts +17 -0
  54. package/dist/transports/oci/oci-transport.d.ts.map +1 -1
  55. package/dist/transports/oci/oci-transport.js +33 -8
  56. package/dist/transports/oci/oci-transport.js.map +1 -1
  57. package/dist/transports/transport-registry.d.ts +6 -4
  58. package/dist/transports/transport-registry.d.ts.map +1 -1
  59. package/dist/transports/transport-registry.js +6 -4
  60. package/dist/transports/transport-registry.js.map +1 -1
  61. package/package.json +5 -5
  62. package/src/bundle/module-artifact.ts +54 -12
  63. package/src/controller-loader.ts +12 -5
  64. package/src/controller-loaders/bundle-loader.ts +117 -27
  65. package/src/controller-loaders/npm-loader.ts +75 -13
  66. package/src/controller-loaders/source-bundle-builder.ts +337 -0
  67. package/src/controllers/resource-definition/resource-definition-controller.ts +23 -17
  68. package/src/index.ts +3 -1
  69. package/src/kernel.ts +25 -17
  70. package/src/logging/file-sink.ts +1 -1
  71. package/src/logging/index.ts +1 -1
  72. package/src/logging/log-sink.ts +1 -0
  73. package/src/resource-context.ts +23 -0
  74. package/src/runtime-seam.ts +230 -0
  75. package/src/static-analysis-diagnostics.ts +51 -0
  76. package/src/transports/oci/oci-transport.ts +34 -8
  77. package/src/transports/transport-registry.ts +6 -4
  78. package/dist/logging/record-buffer.d.ts +0 -24
  79. package/dist/logging/record-buffer.d.ts.map +0 -1
  80. package/dist/logging/record-buffer.js +0 -78
  81. package/dist/logging/record-buffer.js.map +0 -1
  82. 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 { host, repo, reference, integrity } = parseOciRef(ref);
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 new OciClient(host, repo).listTags();
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 new OciClient(host, repo).headManifest(reference);
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 new OciClient(host, repo).pullBlob(blobDigest);
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 manifest = await pullManifestLayer(ref);
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 are stateless config (a fresh
86
- * `OciClient` with its own token cache is created per OCI operation), so one
87
- * shared instance per registry URL is safe and avoids re-instantiating the
88
- * whole set on hot paths like `cachePathForCanonical`. */
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
- }