@telorun/record-stream 0.3.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/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ # SUSTAINABLE USE LICENSE (Fair-code)
2
+
3
+ Copyright (c) 2026 DiglyAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, and distribute the Software for any purpose—including commercial purposes—subject to the following conditions:
6
+
7
+ 1. ANTI-COMPETITION RESTRICTION: The Software may not be provided to third parties as a managed service, commercial SaaS (Software-as-a-Service), PaaS (Platform-as-a-Service), BaaS (Backend-as-a-Service), or similar offering where the primary value provided to the user is the functionality of the Software itself, without a separate commercial license from the copyright holder.
8
+
9
+ 2. PERMITTED COMMERCIAL USE: You are free to use the Software to build, host, and monetize your own commercial applications, products, and services, provided such use does not violate Clause 1.
10
+
11
+ 3. ATTRIBUTION: This copyright notice and license must be included in all copies or substantial portions of the Software.
12
+
13
+ 4. CONTRIBUTIONS: Contributions to the Software are welcome and encouraged. By contributing, you agree that your contributions may be incorporated into the Software and distributed under this license.
14
+
15
+ 5. DISCLAIMER: The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
16
+
17
+ For commercial licensing, managed hosting exemptions, or enterprise inquiries, please contact DiglyAI.
package/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # ⚡ Telo
2
+
3
+ Runtime for declarative backends.
4
+
5
+ Telo is an execution engine (Micro-Kernel) that runs logic defined entirely in YAML manifests. Instead of writing imperative backend code, you define your routes, databases, schemas, and AI workflows as atomic, interconnected YAML documents. Telo takes those manifests and runs them.
6
+
7
+ Built to be language-agnostic and infinitely extensible.
8
+
9
+ ```bash
10
+ # Reconcile your manifest into a running backend
11
+ $ telo ./examples/hello-api.yaml
12
+
13
+ {"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8844"}
14
+ ```
15
+
16
+ ## Why use Telo?
17
+
18
+ - **Open Standards:** Built on YAML, JSON Schema, and CEL — no proprietary DSL.
19
+ - **Static Analysis:** CEL type checking, reference validation, and IDE diagnostics catch errors before runtime.
20
+ - **Micro-Kernel Architecture:** Telo itself knows nothing about HTTP or SQL. Everything is a module you import, scope, and compose with typed variable and secret contracts.
21
+ - **Language Agnostic:** Available as a Node.js runtime today, with a shared YAML runtime contract that allows for future Rust or Go implementations without changing your manifests.
22
+
23
+ ## What It Does
24
+
25
+ - **Loads** YAML resources and compiles CEL expressions (`${{ }}`) into an in-memory registry.
26
+ - **Resolves** resource dependencies via a multi-pass init loop, handling ordering automatically.
27
+ - **Indexes** resources by Kind and Name for constant-time lookup.
28
+ - **Dispatches** execution to the controller that owns each Kind.
29
+
30
+ Manifests also support directives for dynamic generation: `$let`, `$if`, `$for`, `$eval`, and `$include`. See [CEL-YAML Templating](./yaml-cel-templating/README.md) for documentation.
31
+
32
+ ## Example manifest
33
+
34
+ Here is an example Telo application that defines a simple HTTP API:
35
+
36
+ ```yaml
37
+ kind: Telo.Application
38
+ metadata:
39
+ name: feedback
40
+ version: 1.0.0
41
+ description: |
42
+ A complete feedback collection REST API — no code, pure YAML.
43
+ Persists entries to SQLite and serves them over HTTP.
44
+ targets:
45
+ - Migrations
46
+ - Server
47
+ ---
48
+ kind: Telo.Import
49
+ metadata:
50
+ name: Http
51
+ source: ../modules/http-server
52
+ ---
53
+ kind: Telo.Import
54
+ metadata:
55
+ name: Sql
56
+ source: ../modules/sql
57
+ ---
58
+ # SQLite database — swap driver/host/database for PostgreSQL with zero YAML changes
59
+ kind: Sql.Connection
60
+ metadata:
61
+ name: Db
62
+ driver: sqlite
63
+ file: ./tmp/feedback.db
64
+ ---
65
+ # Migrations: applied automatically before the server starts
66
+ kind: Sql.Migrations
67
+ metadata:
68
+ name: Migrations
69
+ connection:
70
+ kind: Sql.Connection
71
+ name: Db
72
+ ---
73
+ kind: Sql.Migration
74
+ metadata:
75
+ name: Migration_20260413_182154_CreateFeedback
76
+ version: 20260413_182154_CreateFeedback
77
+ sql: |
78
+ CREATE TABLE IF NOT EXISTS feedback (
79
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
80
+ text TEXT NOT NULL,
81
+ source TEXT,
82
+ score INTEGER NOT NULL DEFAULT 0,
83
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
84
+ )
85
+ ---
86
+ kind: Http.Server
87
+ metadata:
88
+ name: Server
89
+ baseUrl: http://localhost:8844
90
+ port: 8844
91
+ logger: true
92
+ openapi:
93
+ info:
94
+ title: Feedback API
95
+ version: 1.0.0
96
+ mounts:
97
+ - path: /v1
98
+ type: Http.Api.FeedbackRoutes
99
+ ---
100
+ kind: Http.Api
101
+ metadata:
102
+ name: FeedbackRoutes
103
+ routes:
104
+ # POST /v1/feedback — insert a new entry, score derived from body length heuristic
105
+ - request:
106
+ path: /feedback
107
+ method: POST
108
+ schema:
109
+ body:
110
+ type: object
111
+ properties:
112
+ text:
113
+ type: string
114
+ minLength: 1
115
+ source:
116
+ type: string
117
+ required: [text]
118
+ handler:
119
+ kind: Sql.Exec
120
+ connection:
121
+ kind: Sql.Connection
122
+ name: Db
123
+ inputs:
124
+ sql: "INSERT INTO feedback (text, source, score) VALUES (?, ?, ?)"
125
+ bindings:
126
+ - "${{ request.body.text }}"
127
+ - "${{ request.body.source }}"
128
+ - "${{ size(request.body.text) }}"
129
+ response:
130
+ - status: 201
131
+ headers:
132
+ Content-Type: application/json
133
+ body:
134
+ ok: true
135
+ message: Feedback received
136
+
137
+ # GET /v1/feedback — list all entries, newest first
138
+ - request:
139
+ path: /feedback
140
+ method: GET
141
+ handler:
142
+ kind: Sql.Select
143
+ connection:
144
+ kind: Sql.Connection
145
+ name: Db
146
+ from: feedback
147
+ columns: [id, text, source, score, created_at]
148
+ orderBy:
149
+ - { column: created_at, direction: desc }
150
+ response:
151
+ - status: 200
152
+ headers:
153
+ Content-Type: application/json
154
+ body: "${{ result.rows }}"
155
+
156
+ # GET /v1/feedback/{id} — fetch a single entry
157
+ - request:
158
+ path: /feedback/{id}
159
+ method: GET
160
+ schema:
161
+ params:
162
+ type: object
163
+ properties:
164
+ id:
165
+ type: integer
166
+ required: [id]
167
+ handler:
168
+ kind: Sql.Select
169
+ connection:
170
+ kind: Sql.Connection
171
+ name: Db
172
+ from: feedback
173
+ columns: [id, text, source, score, created_at]
174
+ where:
175
+ - { column: id, op: "=", value: "${{ request.params.id }}" }
176
+ response:
177
+ - status: 200
178
+ when: "size(result.rows) > 0"
179
+ headers:
180
+ Content-Type: application/json
181
+ body: "${{ result.rows[0] }}"
182
+ - status: 404
183
+ headers:
184
+ Content-Type: application/json
185
+ body:
186
+ ok: false
187
+ message: Not found
188
+ ```
189
+
190
+ ## Status
191
+
192
+ Telo is under **active development**. The core runtime, module system, and standard library are functional, but the API surface — including YAML shapes — may change without notice. Not yet recommended for production use.
193
+
194
+ ## The Meaning of Telo
195
+
196
+ The name Telo is derived from the Greek root Telos - meaning the "end goal", "purpose", or "final state". That is exactly the philosophy behind this runtime. In standard imperative programming, you have to write thousands of lines of code to tell a server exactly how to start. With Telo, you simply declare your desired final state.
197
+
198
+ You define the end state. Telo makes it real.
199
+
200
+ ## Philosophy
201
+
202
+ Modern platforms often spend disproportionate effort on technical mechanics-wiring frameworks, managing infrastructure, and negotiating toolchains-while the original business problem gets delayed or diluted. Telo pushes in the opposite direction: it treats kernel execution as a stable, predictable host so teams can concentrate on the **business logic and outcomes** instead of the plumbing.
203
+
204
+ By separating "what the system should do" from "how it is hosted", the runtime reduces friction for domain‑level changes. Teams can move faster on product requirements, experiment more safely, and keep conversations centered on value delivered rather than implementation trivia.
205
+
206
+ Telo also aims to **join forces across all programming language communities**, so the best ideas, patterns, and implementations can converge into a shared kernel truth without forcing everyone into a single stack.
207
+
208
+ YAML also makes the system more **AI‑friendly** than traditional programming languages: it is explicit, structured, and easier for tools to generate, review, and transform without losing intent.
209
+
210
+ ## Modularity
211
+
212
+ Telo is built around **modules** that own specific resource kinds. A module is loaded from a manifest, declares which kinds it implements, and then receives only the resources of those kinds. This keeps concerns isolated and lets teams compose systems from focused building blocks rather than monolithic services.
213
+
214
+ At kernel execution time, execution is always routed by **Kind.Name**. The kernel resolves the Kind to its owning module and hands off execution. Modules can call back into the kernel to execute other resources, enabling composition without tight coupling.
215
+
216
+ ## Architecture
217
+
218
+ The architecture is inspired by Kubernetes-style manifests: declarative resources, explicit kinds, and a control plane that routes work based on those definitions.
219
+ Those manifests were taken to the next level by allowing them to run inside a standalone runtime host.
220
+
221
+ ## See more at
222
+
223
+ - [Telo Kernel](./kernel/README.md)
224
+ - [CEL-YAML Templating](./yaml-cel-templating/README.md)
225
+ - [Telo SDK for module authors](sdk/README.md)
226
+ - [Modules](modules/README.md)
227
+
228
+ ## License
229
+
230
+ See [LICENSE](https://github.com/telorun/telo/blob/main/LICENSE).
231
+
232
+ ## Contribution Note
233
+
234
+ By contributing, you agree that code and examples in this repository may be translated or re‑implemented in other programming languages (including by AI systems) to support the project’s polyglot goals.
@@ -0,0 +1,36 @@
1
+ import type { ControllerContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
2
+ import { Stream } from "@telorun/sdk";
3
+ type Action = {
4
+ do: "emit";
5
+ field: string;
6
+ } | {
7
+ do: "drop";
8
+ } | {
9
+ do: "throw";
10
+ field: string;
11
+ };
12
+ interface ExtractTextResource {
13
+ metadata: {
14
+ name: string;
15
+ module?: string;
16
+ };
17
+ discriminator?: string;
18
+ records: Record<string, Action>;
19
+ }
20
+ interface ExtractTextInputs {
21
+ input: AsyncIterable<unknown>;
22
+ }
23
+ interface ExtractTextOutputs {
24
+ output: Stream<string>;
25
+ }
26
+ declare class ExtractText implements ResourceInstance<ExtractTextInputs, ExtractTextOutputs> {
27
+ private readonly resource;
28
+ private readonly discriminator;
29
+ private readonly records;
30
+ constructor(resource: ExtractTextResource);
31
+ invoke(inputs: ExtractTextInputs): Promise<ExtractTextOutputs>;
32
+ snapshot(): Record<string, unknown>;
33
+ }
34
+ export declare function register(_ctx: ControllerContext): void;
35
+ export declare function create(resource: ExtractTextResource, _ctx: ResourceContext): Promise<ExtractText>;
36
+ export {};
@@ -0,0 +1,66 @@
1
+ import { InvokeError, Stream } from "@telorun/sdk";
2
+ class ExtractText {
3
+ resource;
4
+ discriminator;
5
+ records;
6
+ constructor(resource) {
7
+ this.resource = resource;
8
+ this.discriminator = resource.discriminator ?? "type";
9
+ this.records = resource.records;
10
+ validateRecordsConfig(this.resource.metadata.name, this.records);
11
+ }
12
+ async invoke(inputs) {
13
+ const name = this.resource.metadata.name;
14
+ const input = inputs?.input;
15
+ if (!input || typeof input[Symbol.asyncIterator] !== "function") {
16
+ throw new InvokeError("ERR_INVALID_INPUT", `RecordStream.ExtractText "${name}": 'input' must be an AsyncIterable.`);
17
+ }
18
+ return { output: new Stream(project(input, name, this.discriminator, this.records)) };
19
+ }
20
+ snapshot() {
21
+ return { discriminator: this.discriminator, records: this.records };
22
+ }
23
+ }
24
+ function validateRecordsConfig(name, records) {
25
+ for (const [tag, action] of Object.entries(records)) {
26
+ if (action.do === "emit" || action.do === "throw") {
27
+ if (typeof action.field !== "string") {
28
+ throw new InvokeError("ERR_INVALID_CONFIG", `RecordStream.ExtractText "${name}": records[${JSON.stringify(tag)}] action "${action.do}" requires \`field\`.`);
29
+ }
30
+ }
31
+ }
32
+ }
33
+ async function* project(input, name, discriminator, records) {
34
+ for await (const item of input) {
35
+ if (!item || typeof item !== "object") {
36
+ throw new InvokeError("ERR_INVALID_INPUT", `RecordStream.ExtractText "${name}": items must be objects; got ${typeof item}.`);
37
+ }
38
+ const tag = item[discriminator];
39
+ if (typeof tag !== "string") {
40
+ throw new InvokeError("ERR_INVALID_INPUT", `RecordStream.ExtractText "${name}": record is missing string discriminator field "${discriminator}".`);
41
+ }
42
+ const action = records[tag];
43
+ if (!action) {
44
+ throw new InvokeError("ERR_UNKNOWN_RECORD", `RecordStream.ExtractText "${name}": no entry in \`records\` for ${discriminator}=${JSON.stringify(tag)}.`);
45
+ }
46
+ if (action.do === "drop")
47
+ continue;
48
+ const value = item[action.field];
49
+ if (action.do === "emit") {
50
+ if (typeof value !== "string") {
51
+ throw new InvokeError("ERR_INVALID_INPUT", `RecordStream.ExtractText "${name}": record ${discriminator}=${JSON.stringify(tag)} field "${action.field}" must be a string; got ${typeof value}.`);
52
+ }
53
+ yield value;
54
+ continue;
55
+ }
56
+ // action.do === "throw"
57
+ const message = value && typeof value === "object" && typeof value.message === "string"
58
+ ? value.message
59
+ : String(value);
60
+ throw new Error(message);
61
+ }
62
+ }
63
+ export function register(_ctx) { }
64
+ export async function create(resource, _ctx) {
65
+ return new ExtractText(resource);
66
+ }
@@ -0,0 +1,5 @@
1
+ export {};
2
+ /**
3
+ * record-stream — generic stream operations on structured records.
4
+ * First inhabitant: ExtractText (records → strings via discriminator + per-variant action map).
5
+ */
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export {};
2
+ /**
3
+ * record-stream — generic stream operations on structured records.
4
+ * First inhabitant: ExtractText (records → strings via discriminator + per-variant action map).
5
+ */
@@ -0,0 +1,24 @@
1
+ import type { ControllerContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
2
+ import { Stream } from "@telorun/sdk";
3
+ interface TeeResource {
4
+ metadata: {
5
+ name: string;
6
+ module?: string;
7
+ };
8
+ }
9
+ interface TeeInputs {
10
+ input: AsyncIterable<unknown>;
11
+ }
12
+ interface TeeOutputs {
13
+ outputA: Stream<unknown>;
14
+ outputB: Stream<unknown>;
15
+ }
16
+ declare class Tee implements ResourceInstance<TeeInputs, TeeOutputs> {
17
+ private readonly resource;
18
+ constructor(resource: TeeResource);
19
+ invoke(inputs: TeeInputs): Promise<TeeOutputs>;
20
+ snapshot(): Record<string, unknown>;
21
+ }
22
+ export declare function register(_ctx: ControllerContext): void;
23
+ export declare function create(resource: TeeResource, _ctx: ResourceContext): Promise<Tee>;
24
+ export {};
@@ -0,0 +1,174 @@
1
+ import { InvokeError, Stream } from "@telorun/sdk";
2
+ /**
3
+ * Tiny FIFO with O(1) push/dequeue. Backed by an array + head pointer; the
4
+ * array is sliced down once the head has wasted more than half the storage.
5
+ * Avoids the O(n) reindexing cost of `Array.prototype.shift` so the Tee's
6
+ * total drain stays O(n) rather than O(n²) for long streams.
7
+ */
8
+ class Queue {
9
+ items = [];
10
+ head = 0;
11
+ push(value) {
12
+ this.items.push(value);
13
+ }
14
+ shift() {
15
+ if (this.head >= this.items.length)
16
+ return undefined;
17
+ const value = this.items[this.head];
18
+ this.items[this.head++] = undefined;
19
+ if (this.head > 32 && this.head * 2 >= this.items.length) {
20
+ this.items = this.items.slice(this.head);
21
+ this.head = 0;
22
+ }
23
+ return value;
24
+ }
25
+ get length() {
26
+ return this.items.length - this.head;
27
+ }
28
+ clear() {
29
+ this.items.length = 0;
30
+ this.head = 0;
31
+ }
32
+ }
33
+ /**
34
+ * Fan one async iterable out to two consumers. Each output sees every item
35
+ * from the source. Source is pulled lazily — at most one source `next()` is
36
+ * in flight at any time (concurrent consumer pulls are serialized via an
37
+ * internal lock). Items are buffered in memory for the consumer that's
38
+ * iterating slower; buffer is bounded by the source stream's length.
39
+ *
40
+ * Early-cancellation semantics: consumers that close early (`break` out of
41
+ * `for await`, abort an HTTP response, etc.) trigger the iterator's
42
+ * `return()`/`throw()`. The closed side's buffer is cleared and subsequent
43
+ * source items skip its buffer entirely, so a stalled consumer never causes
44
+ * unbounded memory growth on the running side. Once both sides are closed,
45
+ * the source iterator's own `return()` / `throw()` is called to propagate
46
+ * cancellation upstream.
47
+ */
48
+ class TeeFanout {
49
+ bufferA = new Queue();
50
+ bufferB = new Queue();
51
+ sourceIter;
52
+ done = false;
53
+ error = null;
54
+ pulling = null;
55
+ closedA = false;
56
+ closedB = false;
57
+ constructor(source) {
58
+ this.sourceIter = source[Symbol.asyncIterator]();
59
+ }
60
+ async next(side) {
61
+ while (true) {
62
+ if (this.isClosed(side)) {
63
+ return { value: undefined, done: true };
64
+ }
65
+ const buf = side === "A" ? this.bufferA : this.bufferB;
66
+ if (buf.length > 0)
67
+ return { value: buf.shift(), done: false };
68
+ if (this.error !== null)
69
+ throw this.error;
70
+ if (this.done)
71
+ return { value: undefined, done: true };
72
+ if (this.pulling) {
73
+ await this.pulling;
74
+ continue;
75
+ }
76
+ let release;
77
+ this.pulling = new Promise((r) => {
78
+ release = r;
79
+ });
80
+ try {
81
+ const result = await this.sourceIter.next();
82
+ if (result.done) {
83
+ this.done = true;
84
+ }
85
+ else {
86
+ if (!this.closedA)
87
+ this.bufferA.push(result.value);
88
+ if (!this.closedB)
89
+ this.bufferB.push(result.value);
90
+ }
91
+ }
92
+ catch (err) {
93
+ this.error = err;
94
+ }
95
+ finally {
96
+ this.pulling = null;
97
+ release();
98
+ }
99
+ }
100
+ }
101
+ async return(side) {
102
+ this.markClosed(side);
103
+ if (this.bothClosed() && !this.done) {
104
+ this.done = true;
105
+ if (this.sourceIter.return) {
106
+ return (await this.sourceIter.return());
107
+ }
108
+ }
109
+ return { value: undefined, done: true };
110
+ }
111
+ async throw(side, err) {
112
+ this.markClosed(side);
113
+ if (this.bothClosed() && !this.done) {
114
+ this.done = true;
115
+ if (this.sourceIter.throw) {
116
+ return (await this.sourceIter.throw(err));
117
+ }
118
+ }
119
+ throw err;
120
+ }
121
+ isClosed(side) {
122
+ return side === "A" ? this.closedA : this.closedB;
123
+ }
124
+ bothClosed() {
125
+ return this.closedA && this.closedB;
126
+ }
127
+ markClosed(side) {
128
+ if (side === "A") {
129
+ this.closedA = true;
130
+ this.bufferA.clear();
131
+ }
132
+ else {
133
+ this.closedB = true;
134
+ this.bufferB.clear();
135
+ }
136
+ }
137
+ iterable(side) {
138
+ const self = this;
139
+ return {
140
+ [Symbol.asyncIterator]() {
141
+ return {
142
+ next: () => self.next(side),
143
+ return: () => self.return(side),
144
+ throw: (err) => self.throw(side, err),
145
+ };
146
+ },
147
+ };
148
+ }
149
+ }
150
+ class Tee {
151
+ resource;
152
+ constructor(resource) {
153
+ this.resource = resource;
154
+ }
155
+ async invoke(inputs) {
156
+ const name = this.resource.metadata.name;
157
+ const input = inputs?.input;
158
+ if (!input || typeof input[Symbol.asyncIterator] !== "function") {
159
+ throw new InvokeError("ERR_INVALID_INPUT", `RecordStream.Tee "${name}": 'input' must be an AsyncIterable.`);
160
+ }
161
+ const fanout = new TeeFanout(input);
162
+ return {
163
+ outputA: new Stream(fanout.iterable("A")),
164
+ outputB: new Stream(fanout.iterable("B")),
165
+ };
166
+ }
167
+ snapshot() {
168
+ return {};
169
+ }
170
+ }
171
+ export function register(_ctx) { }
172
+ export async function create(resource, _ctx) {
173
+ return new Tee(resource);
174
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@telorun/record-stream",
3
+ "version": "0.3.0",
4
+ "description": "Stream operations on structured records — generic, format-neutral transformers.",
5
+ "keywords": [
6
+ "telo",
7
+ "stream",
8
+ "records",
9
+ "transform"
10
+ ],
11
+ "author": "Bartosz Pasiński <bartosz.pasinski@codenet.pl>",
12
+ "license": "SEE LICENSE IN LICENSE",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/telorun/telo.git",
16
+ "directory": "modules/record-stream/nodejs"
17
+ },
18
+ "homepage": "https://github.com/telorun/telo#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/telorun/telo/issues"
21
+ },
22
+ "type": "module",
23
+ "main": "./dist/index.js",
24
+ "module": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "bun": "./src/index.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./extract-text": {
33
+ "types": "./dist/extract-text-controller.d.ts",
34
+ "bun": "./src/extract-text-controller.ts",
35
+ "import": "./dist/extract-text-controller.js"
36
+ },
37
+ "./tee": {
38
+ "types": "./dist/tee-controller.d.ts",
39
+ "bun": "./src/tee-controller.ts",
40
+ "import": "./dist/tee-controller.js"
41
+ }
42
+ },
43
+ "files": [
44
+ "dist/**",
45
+ "src/**"
46
+ ],
47
+ "dependencies": {
48
+ "@telorun/sdk": "0.7.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^20.0.0",
52
+ "typescript": "^5.0.0"
53
+ },
54
+ "scripts": {
55
+ "build": "tsc -p tsconfig.lib.json"
56
+ }
57
+ }
@@ -0,0 +1,118 @@
1
+ import type { ControllerContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
2
+ import { InvokeError, Stream } from "@telorun/sdk";
3
+
4
+ type Action =
5
+ | { do: "emit"; field: string }
6
+ | { do: "drop" }
7
+ | { do: "throw"; field: string };
8
+
9
+ interface ExtractTextResource {
10
+ metadata: { name: string; module?: string };
11
+ discriminator?: string;
12
+ records: Record<string, Action>;
13
+ }
14
+
15
+ interface ExtractTextInputs {
16
+ input: AsyncIterable<unknown>;
17
+ }
18
+
19
+ interface ExtractTextOutputs {
20
+ output: Stream<string>;
21
+ }
22
+
23
+ class ExtractText implements ResourceInstance<ExtractTextInputs, ExtractTextOutputs> {
24
+ private readonly discriminator: string;
25
+ private readonly records: Record<string, Action>;
26
+
27
+ constructor(private readonly resource: ExtractTextResource) {
28
+ this.discriminator = resource.discriminator ?? "type";
29
+ this.records = resource.records;
30
+ validateRecordsConfig(this.resource.metadata.name, this.records);
31
+ }
32
+
33
+ async invoke(inputs: ExtractTextInputs): Promise<ExtractTextOutputs> {
34
+ const name = this.resource.metadata.name;
35
+ const input = inputs?.input;
36
+ if (!input || typeof (input as any)[Symbol.asyncIterator] !== "function") {
37
+ throw new InvokeError(
38
+ "ERR_INVALID_INPUT",
39
+ `RecordStream.ExtractText "${name}": 'input' must be an AsyncIterable.`,
40
+ );
41
+ }
42
+ return { output: new Stream(project(input, name, this.discriminator, this.records)) };
43
+ }
44
+
45
+ snapshot(): Record<string, unknown> {
46
+ return { discriminator: this.discriminator, records: this.records };
47
+ }
48
+ }
49
+
50
+ function validateRecordsConfig(name: string, records: Record<string, Action>): void {
51
+ for (const [tag, action] of Object.entries(records)) {
52
+ if (action.do === "emit" || action.do === "throw") {
53
+ if (typeof (action as { field?: unknown }).field !== "string") {
54
+ throw new InvokeError(
55
+ "ERR_INVALID_CONFIG",
56
+ `RecordStream.ExtractText "${name}": records[${JSON.stringify(tag)}] action "${action.do}" requires \`field\`.`,
57
+ );
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ async function* project(
64
+ input: AsyncIterable<unknown>,
65
+ name: string,
66
+ discriminator: string,
67
+ records: Record<string, Action>,
68
+ ): AsyncIterable<string> {
69
+ for await (const item of input) {
70
+ if (!item || typeof item !== "object") {
71
+ throw new InvokeError(
72
+ "ERR_INVALID_INPUT",
73
+ `RecordStream.ExtractText "${name}": items must be objects; got ${typeof item}.`,
74
+ );
75
+ }
76
+ const tag = (item as Record<string, unknown>)[discriminator];
77
+ if (typeof tag !== "string") {
78
+ throw new InvokeError(
79
+ "ERR_INVALID_INPUT",
80
+ `RecordStream.ExtractText "${name}": record is missing string discriminator field "${discriminator}".`,
81
+ );
82
+ }
83
+ const action = records[tag];
84
+ if (!action) {
85
+ throw new InvokeError(
86
+ "ERR_UNKNOWN_RECORD",
87
+ `RecordStream.ExtractText "${name}": no entry in \`records\` for ${discriminator}=${JSON.stringify(tag)}.`,
88
+ );
89
+ }
90
+ if (action.do === "drop") continue;
91
+ const value = (item as Record<string, unknown>)[action.field];
92
+ if (action.do === "emit") {
93
+ if (typeof value !== "string") {
94
+ throw new InvokeError(
95
+ "ERR_INVALID_INPUT",
96
+ `RecordStream.ExtractText "${name}": record ${discriminator}=${JSON.stringify(tag)} field "${action.field}" must be a string; got ${typeof value}.`,
97
+ );
98
+ }
99
+ yield value;
100
+ continue;
101
+ }
102
+ // action.do === "throw"
103
+ const message =
104
+ value && typeof value === "object" && typeof (value as { message?: unknown }).message === "string"
105
+ ? (value as { message: string }).message
106
+ : String(value);
107
+ throw new Error(message);
108
+ }
109
+ }
110
+
111
+ export function register(_ctx: ControllerContext): void {}
112
+
113
+ export async function create(
114
+ resource: ExtractTextResource,
115
+ _ctx: ResourceContext,
116
+ ): Promise<ExtractText> {
117
+ return new ExtractText(resource);
118
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * record-stream — generic stream operations on structured records.
3
+ * First inhabitant: ExtractText (records → strings via discriminator + per-variant action map).
4
+ */
@@ -0,0 +1,202 @@
1
+ import type { ControllerContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
2
+ import { InvokeError, Stream } from "@telorun/sdk";
3
+
4
+ interface TeeResource {
5
+ metadata: { name: string; module?: string };
6
+ }
7
+
8
+ interface TeeInputs {
9
+ input: AsyncIterable<unknown>;
10
+ }
11
+
12
+ interface TeeOutputs {
13
+ outputA: Stream<unknown>;
14
+ outputB: Stream<unknown>;
15
+ }
16
+
17
+ /**
18
+ * Tiny FIFO with O(1) push/dequeue. Backed by an array + head pointer; the
19
+ * array is sliced down once the head has wasted more than half the storage.
20
+ * Avoids the O(n) reindexing cost of `Array.prototype.shift` so the Tee's
21
+ * total drain stays O(n) rather than O(n²) for long streams.
22
+ */
23
+ class Queue<T> {
24
+ private items: T[] = [];
25
+ private head = 0;
26
+
27
+ push(value: T): void {
28
+ this.items.push(value);
29
+ }
30
+
31
+ shift(): T | undefined {
32
+ if (this.head >= this.items.length) return undefined;
33
+ const value = this.items[this.head] as T;
34
+ this.items[this.head++] = undefined as unknown as T;
35
+ if (this.head > 32 && this.head * 2 >= this.items.length) {
36
+ this.items = this.items.slice(this.head);
37
+ this.head = 0;
38
+ }
39
+ return value;
40
+ }
41
+
42
+ get length(): number {
43
+ return this.items.length - this.head;
44
+ }
45
+
46
+ clear(): void {
47
+ this.items.length = 0;
48
+ this.head = 0;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Fan one async iterable out to two consumers. Each output sees every item
54
+ * from the source. Source is pulled lazily — at most one source `next()` is
55
+ * in flight at any time (concurrent consumer pulls are serialized via an
56
+ * internal lock). Items are buffered in memory for the consumer that's
57
+ * iterating slower; buffer is bounded by the source stream's length.
58
+ *
59
+ * Early-cancellation semantics: consumers that close early (`break` out of
60
+ * `for await`, abort an HTTP response, etc.) trigger the iterator's
61
+ * `return()`/`throw()`. The closed side's buffer is cleared and subsequent
62
+ * source items skip its buffer entirely, so a stalled consumer never causes
63
+ * unbounded memory growth on the running side. Once both sides are closed,
64
+ * the source iterator's own `return()` / `throw()` is called to propagate
65
+ * cancellation upstream.
66
+ */
67
+ class TeeFanout<T> {
68
+ private readonly bufferA = new Queue<T>();
69
+ private readonly bufferB = new Queue<T>();
70
+ private readonly sourceIter: AsyncIterator<T>;
71
+ private done = false;
72
+ private error: unknown = null;
73
+ private pulling: Promise<void> | null = null;
74
+ private closedA = false;
75
+ private closedB = false;
76
+
77
+ constructor(source: AsyncIterable<T>) {
78
+ this.sourceIter = source[Symbol.asyncIterator]();
79
+ }
80
+
81
+ async next(side: "A" | "B"): Promise<IteratorResult<T>> {
82
+ while (true) {
83
+ if (this.isClosed(side)) {
84
+ return { value: undefined as unknown as T, done: true };
85
+ }
86
+ const buf = side === "A" ? this.bufferA : this.bufferB;
87
+ if (buf.length > 0) return { value: buf.shift() as T, done: false };
88
+ if (this.error !== null) throw this.error;
89
+ if (this.done) return { value: undefined as unknown as T, done: true };
90
+
91
+ if (this.pulling) {
92
+ await this.pulling;
93
+ continue;
94
+ }
95
+
96
+ let release!: () => void;
97
+ this.pulling = new Promise<void>((r) => {
98
+ release = r;
99
+ });
100
+ try {
101
+ const result = await this.sourceIter.next();
102
+ if (result.done) {
103
+ this.done = true;
104
+ } else {
105
+ if (!this.closedA) this.bufferA.push(result.value);
106
+ if (!this.closedB) this.bufferB.push(result.value);
107
+ }
108
+ } catch (err) {
109
+ this.error = err;
110
+ } finally {
111
+ this.pulling = null;
112
+ release();
113
+ }
114
+ }
115
+ }
116
+
117
+ async return(side: "A" | "B"): Promise<IteratorResult<T>> {
118
+ this.markClosed(side);
119
+ if (this.bothClosed() && !this.done) {
120
+ this.done = true;
121
+ if (this.sourceIter.return) {
122
+ return (await this.sourceIter.return()) as IteratorResult<T>;
123
+ }
124
+ }
125
+ return { value: undefined as unknown as T, done: true };
126
+ }
127
+
128
+ async throw(side: "A" | "B", err: unknown): Promise<IteratorResult<T>> {
129
+ this.markClosed(side);
130
+ if (this.bothClosed() && !this.done) {
131
+ this.done = true;
132
+ if (this.sourceIter.throw) {
133
+ return (await this.sourceIter.throw(err)) as IteratorResult<T>;
134
+ }
135
+ }
136
+ throw err;
137
+ }
138
+
139
+ private isClosed(side: "A" | "B"): boolean {
140
+ return side === "A" ? this.closedA : this.closedB;
141
+ }
142
+
143
+ private bothClosed(): boolean {
144
+ return this.closedA && this.closedB;
145
+ }
146
+
147
+ private markClosed(side: "A" | "B"): void {
148
+ if (side === "A") {
149
+ this.closedA = true;
150
+ this.bufferA.clear();
151
+ } else {
152
+ this.closedB = true;
153
+ this.bufferB.clear();
154
+ }
155
+ }
156
+
157
+ iterable(side: "A" | "B"): AsyncIterable<T> {
158
+ const self = this;
159
+ return {
160
+ [Symbol.asyncIterator](): AsyncIterator<T> {
161
+ return {
162
+ next: () => self.next(side),
163
+ return: () => self.return(side),
164
+ throw: (err: unknown) => self.throw(side, err),
165
+ };
166
+ },
167
+ };
168
+ }
169
+ }
170
+
171
+ class Tee implements ResourceInstance<TeeInputs, TeeOutputs> {
172
+ constructor(private readonly resource: TeeResource) {}
173
+
174
+ async invoke(inputs: TeeInputs): Promise<TeeOutputs> {
175
+ const name = this.resource.metadata.name;
176
+ const input = inputs?.input;
177
+ if (!input || typeof (input as any)[Symbol.asyncIterator] !== "function") {
178
+ throw new InvokeError(
179
+ "ERR_INVALID_INPUT",
180
+ `RecordStream.Tee "${name}": 'input' must be an AsyncIterable.`,
181
+ );
182
+ }
183
+ const fanout = new TeeFanout<unknown>(input);
184
+ return {
185
+ outputA: new Stream(fanout.iterable("A")),
186
+ outputB: new Stream(fanout.iterable("B")),
187
+ };
188
+ }
189
+
190
+ snapshot(): Record<string, unknown> {
191
+ return {};
192
+ }
193
+ }
194
+
195
+ export function register(_ctx: ControllerContext): void {}
196
+
197
+ export async function create(
198
+ resource: TeeResource,
199
+ _ctx: ResourceContext,
200
+ ): Promise<Tee> {
201
+ return new Tee(resource);
202
+ }