@inferencesh/app 0.1.12 → 0.1.13

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/README.md CHANGED
@@ -22,6 +22,7 @@ npm install @inferencesh/app
22
22
  | `StorageDir` | standard storage directory constants (`DATA`, `TEMP`, `CACHE`) |
23
23
  | `download()` | download a url to a directory with caching |
24
24
  | `textMeta`, `imageMeta`, `videoMeta`, `audioMeta`, `rawMeta` | output metadata factories for usage-based pricing |
25
+ | `createStreamSchema`, `pcm16`, `media`, `Live` | live fields for stream functions — values that flow over the socket while the task runs |
25
26
 
26
27
  ## file handling
27
28
 
@@ -219,6 +220,42 @@ export class App {
219
220
  }
220
221
  ```
221
222
 
223
+ ## stream functions
224
+
225
+ a stream function takes its input and a socket (`async talk(inputData, socket)`), runs until it returns, and its return value is the result. some input and output fields are **live**: their values travel over the socket while the task runs instead of in the request body. declare them with `createStreamSchema`; in the json schema they become `{"type": "array", "format": "stream", "items": ...}` and are never required.
226
+
227
+ ```javascript
228
+ import { z } from "zod";
229
+ import { createStreamSchema, pcm16, Live } from "@inferencesh/app";
230
+
231
+ export const TalkInput = z.object({
232
+ effect: z.enum(["none", "robot"]).default("none"), // ordinary: set at the start, may change live
233
+ audio: createStreamSchema(z, pcm16(z, 16000)), // live media: binary frames
234
+ });
235
+
236
+ export const TalkOutput = z.object({
237
+ audio: createStreamSchema(z, pcm16(z, 16000)),
238
+ frames: z.number().default(0),
239
+ });
240
+
241
+ export class App {
242
+ async talk(input, socket) {
243
+ const live = new Live(socket, input, TalkInput, TalkOutput);
244
+ let n = 0;
245
+ for await (const { field, value } of live) {
246
+ if (field === "audio") { // value is a Buffer of PCM samples
247
+ n++;
248
+ await live.send({ audio: value });
249
+ }
250
+ // an ordinary field (effect) is already set on input when it changes
251
+ }
252
+ return { frames: n };
253
+ }
254
+ }
255
+ ```
256
+
257
+ on the wire, a binary frame is one item of the schema's single binary live field (`pcm16(z, rate, channels)` or `media(z, contentType)`); a json text frame is a partial object keyed by field name: an item of a live field (`{"events": {"type": "interrupt"}}`) or a new value for an ordinary field (`{"effect": "robot"}`). frames that do not fit the input schema are answered with `{"error": {"field", "message"}}` and skipped. `live.send({...})` sends the output binary field as a binary frame and everything else as one json frame; unknown output fields throw.
258
+
222
259
  ## requirements
223
260
 
224
261
  - node.js 18.0.0 or higher
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export { File } from "./file.js";
2
2
  export type { FileOptions, FileData } from "./file.js";
3
3
  export { createFileSchema, isFileSchema, FILE_SCHEMA_MARKER } from "./schema.js";
4
+ export type { Socket, Frame } from "./socket.js";
5
+ export { createStreamSchema, media, pcm16, Live, liveFields, binaryField, STREAM_SCHEMA_MARKER, BINARY_SCHEMA_MARKER, } from "./stream.js";
6
+ export type { Update } from "./stream.js";
4
7
  export { StorageDir, ensureDir } from "./storage.js";
5
8
  export type { StorageDirValue } from "./storage.js";
6
9
  export { download } from "./download.js";
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  export { File } from "./file.js";
3
3
  // Zod schema utilities
4
4
  export { createFileSchema, isFileSchema, FILE_SCHEMA_MARKER } from "./schema.js";
5
+ export { createStreamSchema, media, pcm16, Live, liveFields, binaryField, STREAM_SCHEMA_MARKER, BINARY_SCHEMA_MARKER, } from "./stream.js";
5
6
  // Storage directories
6
7
  export { StorageDir, ensureDir } from "./storage.js";
7
8
  // Download utility
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The socket a stream function talks to.
3
+ *
4
+ * An app declares a stream function by taking a `socket` parameter after its
5
+ * input:
6
+ *
7
+ * ```js
8
+ * export class App {
9
+ * async stream(inputData, socket) {
10
+ * for await (const frame of socket) { // Buffer or string, in order
11
+ * await socket.send(frame); // Buffer -> binary, string -> text,
12
+ * } // anything else -> JSON text
13
+ * return { ... };
14
+ * }
15
+ * }
16
+ * ```
17
+ *
18
+ * The function runs until it returns; its return value is the result the
19
+ * platform records. Frames from the client arrive in order; the iterator ends
20
+ * (and `recv()` resolves to null) when the client closes. Returning ends the
21
+ * stream for the client. This is a different thing from the platform's app
22
+ * session (a worker leased across several calls): a stream function runs as
23
+ * one call, inside an app session or on its own.
24
+ *
25
+ * The object the kernel passes in implements this interface; the interface
26
+ * here is for typing.
27
+ */
28
+ /** One frame on the wire: binary (Buffer) or text (string). */
29
+ export type Frame = Buffer | string;
30
+ /** One live stream between a client and a stream function. */
31
+ export interface Socket {
32
+ readonly id: string;
33
+ metadata: Record<string, unknown>;
34
+ /** True once the client is gone or the app closed the socket. */
35
+ readonly closed: boolean;
36
+ /** The next frame from the client, or null once it is gone. */
37
+ recv(): Promise<Frame | null>;
38
+ [Symbol.asyncIterator](): AsyncIterator<Frame>;
39
+ /** Send a frame: Buffer/Uint8Array/ArrayBuffer as binary, string as text, anything else as JSON text. */
40
+ send(data: Buffer | Uint8Array | ArrayBuffer | string | object): Promise<void>;
41
+ /** Stop reading from the client; the stream ends when the function returns. */
42
+ close(): Promise<void>;
43
+ }
package/dist/socket.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The socket a stream function talks to.
3
+ *
4
+ * An app declares a stream function by taking a `socket` parameter after its
5
+ * input:
6
+ *
7
+ * ```js
8
+ * export class App {
9
+ * async stream(inputData, socket) {
10
+ * for await (const frame of socket) { // Buffer or string, in order
11
+ * await socket.send(frame); // Buffer -> binary, string -> text,
12
+ * } // anything else -> JSON text
13
+ * return { ... };
14
+ * }
15
+ * }
16
+ * ```
17
+ *
18
+ * The function runs until it returns; its return value is the result the
19
+ * platform records. Frames from the client arrive in order; the iterator ends
20
+ * (and `recv()` resolves to null) when the client closes. Returning ends the
21
+ * stream for the client. This is a different thing from the platform's app
22
+ * session (a worker leased across several calls): a stream function runs as
23
+ * one call, inside an app session or on its own.
24
+ *
25
+ * The object the kernel passes in implements this interface; the interface
26
+ * here is for typing.
27
+ */
28
+ export {};
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Live fields: input and output that flow over a stream function's socket.
3
+ *
4
+ * A stream function's input is still its input schema. Some of its fields are
5
+ * live: their values arrive over the socket while the task runs, instead of in
6
+ * the request body. The output schema works the same way in the other
7
+ * direction:
8
+ *
9
+ * ```js
10
+ * const TalkInput = z.object({
11
+ * voice: z.enum(["ara", "eve"]).default("ara"), // ordinary: set at the start
12
+ * audio: createStreamSchema(z, pcm16(z, 16000)), // live media
13
+ * events: createStreamSchema(z, z.union([Interrupt, UserText])), // live typed messages
14
+ * });
15
+ *
16
+ * const TalkOutput = z.object({
17
+ * audio: createStreamSchema(z, pcm16(z, 24000)),
18
+ * seconds: z.number().default(0),
19
+ * });
20
+ * ```
21
+ *
22
+ * In the JSON schema a live field is an array delivered over time, marked
23
+ * `"format": "stream"` the way a file field is marked `"format": "file"`:
24
+ *
25
+ * ```json
26
+ * "audio": {"type": "array", "format": "stream",
27
+ * "items": {"type": "string", "format": "binary",
28
+ * "contentMediaType": "audio/pcm;format=s16le;rate=16000;channels=1"}}
29
+ * ```
30
+ *
31
+ * Live fields are never required, so the request body is the ordinary fields
32
+ * only. File and stream fields are siblings: the same media, by reference or
33
+ * live.
34
+ *
35
+ * On the wire:
36
+ *
37
+ * - a binary frame is one item of the schema's binary live field (a schema has
38
+ * at most one per direction, because a binary frame carries no field name);
39
+ * - a JSON text frame is a partial object of the schema keyed by field name:
40
+ * `{"events": {"type": "interrupt"}}` is one item of a live field, and
41
+ * `{"voice": "eve"}` changes an ordinary field while the task runs.
42
+ *
43
+ * `Live` applies that mapping to a socket, with the schemas as the contract.
44
+ *
45
+ * Zod is never imported: the app's own zod (v3 or v4) is passed in, and the
46
+ * schemas are read by duck-typing `_def`, the same way the kernel does.
47
+ */
48
+ import type { Socket } from "./socket.js";
49
+ /**
50
+ * Marks a live field's schema. The value on `_def` is the item schema.
51
+ * The kernel's zodToJsonSchema reads this and emits `format: "stream"`.
52
+ */
53
+ export declare const STREAM_SCHEMA_MARKER: unique symbol;
54
+ /**
55
+ * Marks a binary item schema. The value on `_def` is the content type.
56
+ * The kernel's zodToJsonSchema reads this and emits
57
+ * `{type: "string", format: "binary", contentMediaType}`.
58
+ */
59
+ export declare const BINARY_SCHEMA_MARKER: unique symbol;
60
+ /**
61
+ * A field whose values travel over the socket while the task runs.
62
+ *
63
+ * Parsing always yields the placeholder `[]`, whatever the caller put in the
64
+ * request body (items come over the socket), and a missing key parses too.
65
+ * Read and write the items with `Live`.
66
+ *
67
+ * @param z the app's zod instance
68
+ * @param item the schema of one item (`pcm16(z)`, `media(z, "image/jpeg")`, or any zod schema)
69
+ */
70
+ export declare function createStreamSchema(z: any, item: any): any;
71
+ /**
72
+ * Binary items of a media type, e.g. `createStreamSchema(z, media(z, "image/jpeg"))`.
73
+ * Accepts a Buffer or any Uint8Array.
74
+ */
75
+ export declare function media(z: any, contentType: string): any;
76
+ /**
77
+ * Raw 16-bit little-endian PCM audio. One frame is one chunk of samples;
78
+ * 20 ms (`rate / 50` samples) is the usual size for live audio.
79
+ */
80
+ export declare function pcm16(z: any, rate?: number, channels?: number): any;
81
+ /**
82
+ * One thing the caller sent: an item of a live field, or a new value for an
83
+ * ordinary field (already applied to the input data).
84
+ */
85
+ export interface Update {
86
+ field: string;
87
+ value: unknown;
88
+ }
89
+ /** The object schema's live fields and their item schemas. */
90
+ export declare function liveFields(objectSchema: any): Record<string, any>;
91
+ /**
92
+ * The object schema's binary live field, or null. More than one is an error:
93
+ * a binary frame carries no field name.
94
+ */
95
+ export declare function binaryField(objectSchema: any): string | null;
96
+ /**
97
+ * A socket read and written through the function's schemas.
98
+ *
99
+ * ```js
100
+ * async talk(inputData, socket) {
101
+ * const live = new Live(socket, inputData, TalkInput, TalkOutput);
102
+ * for await (const update of live) {
103
+ * if (update.field === "audio") {
104
+ * await live.send({ audio: process(update.value, inputData.voice) });
105
+ * } else if (update.field === "events") {
106
+ * ... // update.value is an Interrupt or a UserText
107
+ * }
108
+ * // an ordinary field (voice) is already set on inputData
109
+ * }
110
+ * return { seconds: ... };
111
+ * }
112
+ * ```
113
+ *
114
+ * A frame that does not fit the input schema is answered with
115
+ * `{"error": {"field": ..., "message": ...}}` and skipped; the stream goes on.
116
+ */
117
+ export declare class Live {
118
+ readonly socket: Socket;
119
+ readonly input: Record<string, unknown>;
120
+ private readonly inShape;
121
+ private readonly inLive;
122
+ private readonly inBinary;
123
+ private readonly outShape;
124
+ private readonly outBinary;
125
+ constructor(socket: Socket, inputData: Record<string, unknown>, inputSchema: any, outputSchema: any);
126
+ [Symbol.asyncIterator](): AsyncGenerator<Update, void, undefined>;
127
+ private frames;
128
+ private read;
129
+ private refuse;
130
+ /**
131
+ * Send items of live output fields, or new values of ordinary ones.
132
+ *
133
+ * `await live.send({ audio: pcm })` is a binary frame;
134
+ * `await live.send({ transcript: word })` and
135
+ * `await live.send({ effect: "echo" })` are JSON frames. Keys in one call
136
+ * other than the binary field go out together as one JSON frame.
137
+ */
138
+ send(fields: Record<string, unknown>): Promise<void>;
139
+ }
package/dist/stream.js ADDED
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Live fields: input and output that flow over a stream function's socket.
3
+ *
4
+ * A stream function's input is still its input schema. Some of its fields are
5
+ * live: their values arrive over the socket while the task runs, instead of in
6
+ * the request body. The output schema works the same way in the other
7
+ * direction:
8
+ *
9
+ * ```js
10
+ * const TalkInput = z.object({
11
+ * voice: z.enum(["ara", "eve"]).default("ara"), // ordinary: set at the start
12
+ * audio: createStreamSchema(z, pcm16(z, 16000)), // live media
13
+ * events: createStreamSchema(z, z.union([Interrupt, UserText])), // live typed messages
14
+ * });
15
+ *
16
+ * const TalkOutput = z.object({
17
+ * audio: createStreamSchema(z, pcm16(z, 24000)),
18
+ * seconds: z.number().default(0),
19
+ * });
20
+ * ```
21
+ *
22
+ * In the JSON schema a live field is an array delivered over time, marked
23
+ * `"format": "stream"` the way a file field is marked `"format": "file"`:
24
+ *
25
+ * ```json
26
+ * "audio": {"type": "array", "format": "stream",
27
+ * "items": {"type": "string", "format": "binary",
28
+ * "contentMediaType": "audio/pcm;format=s16le;rate=16000;channels=1"}}
29
+ * ```
30
+ *
31
+ * Live fields are never required, so the request body is the ordinary fields
32
+ * only. File and stream fields are siblings: the same media, by reference or
33
+ * live.
34
+ *
35
+ * On the wire:
36
+ *
37
+ * - a binary frame is one item of the schema's binary live field (a schema has
38
+ * at most one per direction, because a binary frame carries no field name);
39
+ * - a JSON text frame is a partial object of the schema keyed by field name:
40
+ * `{"events": {"type": "interrupt"}}` is one item of a live field, and
41
+ * `{"voice": "eve"}` changes an ordinary field while the task runs.
42
+ *
43
+ * `Live` applies that mapping to a socket, with the schemas as the contract.
44
+ *
45
+ * Zod is never imported: the app's own zod (v3 or v4) is passed in, and the
46
+ * schemas are read by duck-typing `_def`, the same way the kernel does.
47
+ */
48
+ /**
49
+ * Marks a live field's schema. The value on `_def` is the item schema.
50
+ * The kernel's zodToJsonSchema reads this and emits `format: "stream"`.
51
+ */
52
+ export const STREAM_SCHEMA_MARKER = Symbol.for("inferencesh.streamSchema");
53
+ /**
54
+ * Marks a binary item schema. The value on `_def` is the content type.
55
+ * The kernel's zodToJsonSchema reads this and emits
56
+ * `{type: "string", format: "binary", contentMediaType}`.
57
+ */
58
+ export const BINARY_SCHEMA_MARKER = Symbol.for("inferencesh.binarySchema");
59
+ /**
60
+ * A field whose values travel over the socket while the task runs.
61
+ *
62
+ * Parsing always yields the placeholder `[]`, whatever the caller put in the
63
+ * request body (items come over the socket), and a missing key parses too.
64
+ * Read and write the items with `Live`.
65
+ *
66
+ * @param z the app's zod instance
67
+ * @param item the schema of one item (`pcm16(z)`, `media(z, "image/jpeg")`, or any zod schema)
68
+ */
69
+ export function createStreamSchema(z, item) {
70
+ const schema = z.preprocess(() => [], z.array(item));
71
+ schema._def[STREAM_SCHEMA_MARKER] = item;
72
+ return schema;
73
+ }
74
+ /**
75
+ * Binary items of a media type, e.g. `createStreamSchema(z, media(z, "image/jpeg"))`.
76
+ * Accepts a Buffer or any Uint8Array.
77
+ */
78
+ export function media(z, contentType) {
79
+ const schema = z.custom((v) => v instanceof Uint8Array, {
80
+ message: "expected binary data",
81
+ });
82
+ schema._def[BINARY_SCHEMA_MARKER] = contentType;
83
+ return schema;
84
+ }
85
+ /**
86
+ * Raw 16-bit little-endian PCM audio. One frame is one chunk of samples;
87
+ * 20 ms (`rate / 50` samples) is the usual size for live audio.
88
+ */
89
+ export function pcm16(z, rate = 16000, channels = 1) {
90
+ return media(z, `audio/pcm;format=s16le;rate=${rate};channels=${channels}`);
91
+ }
92
+ // --- schema walking (zod v3 and v4 `_def` shapes) ---------------------------
93
+ /** Inner schemas of a wrapper def, in the order worth checking. */
94
+ function innerSchemas(def) {
95
+ const out = [];
96
+ // v3 + v4: optional / default / nullable / catch / readonly
97
+ if (def.innerType)
98
+ out.push(def.innerType);
99
+ // v3: effects (preprocess / transform / refine), v4: some wrappers
100
+ if (def.schema)
101
+ out.push(def.schema);
102
+ if (def.inner)
103
+ out.push(def.inner);
104
+ // v4: pipe (preprocess, transform) — in, then out
105
+ if (def.in)
106
+ out.push(def.in);
107
+ if (def.out)
108
+ out.push(def.out);
109
+ return out;
110
+ }
111
+ /** The marker's value on the schema or any schema it wraps, or undefined. */
112
+ function findMarker(schema, marker, seen = new Set()) {
113
+ if (!schema || typeof schema !== "object" || seen.has(schema))
114
+ return undefined;
115
+ seen.add(schema);
116
+ const def = schema._def;
117
+ if (!def || typeof def !== "object")
118
+ return undefined;
119
+ if (Object.prototype.hasOwnProperty.call(def, marker))
120
+ return def[marker];
121
+ for (const inner of innerSchemas(def)) {
122
+ const found = findMarker(inner, marker, seen);
123
+ if (found !== undefined)
124
+ return found;
125
+ }
126
+ return undefined;
127
+ }
128
+ /** The fields of a zod object schema. v3: `_def.shape()`, v4: `_def.shape`. */
129
+ function shapeOf(objectSchema) {
130
+ const def = objectSchema?._def;
131
+ if (!def)
132
+ throw new TypeError("expected a zod object schema");
133
+ const shape = typeof def.shape === "function" ? def.shape() : def.shape;
134
+ if (!shape || typeof shape !== "object")
135
+ throw new TypeError("expected a zod object schema");
136
+ return shape;
137
+ }
138
+ function isBinaryItem(item) {
139
+ return typeof findMarker(item, BINARY_SCHEMA_MARKER) === "string";
140
+ }
141
+ /** The object schema's live fields and their item schemas. */
142
+ export function liveFields(objectSchema) {
143
+ const found = {};
144
+ for (const [name, field] of Object.entries(shapeOf(objectSchema))) {
145
+ const item = findMarker(field, STREAM_SCHEMA_MARKER);
146
+ if (item !== undefined)
147
+ found[name] = item;
148
+ }
149
+ return found;
150
+ }
151
+ /**
152
+ * The object schema's binary live field, or null. More than one is an error:
153
+ * a binary frame carries no field name.
154
+ */
155
+ export function binaryField(objectSchema) {
156
+ const names = Object.entries(liveFields(objectSchema))
157
+ .filter(([, item]) => isBinaryItem(item))
158
+ .map(([name]) => name);
159
+ if (names.length > 1) {
160
+ throw new TypeError(`the schema has ${names.length} binary live fields (${names.join(", ")}); ` +
161
+ "a schema can have one per direction because a binary frame carries no field name");
162
+ }
163
+ return names[0] ?? null;
164
+ }
165
+ // --- frames -----------------------------------------------------------------
166
+ function isBinaryFrame(frame) {
167
+ return frame instanceof Uint8Array || frame instanceof ArrayBuffer;
168
+ }
169
+ function toBuffer(value) {
170
+ if (Buffer.isBuffer(value))
171
+ return value;
172
+ if (value instanceof Uint8Array)
173
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
174
+ if (value instanceof ArrayBuffer)
175
+ return Buffer.from(value);
176
+ throw new TypeError("a binary field takes a Buffer, Uint8Array or ArrayBuffer");
177
+ }
178
+ function firstIssue(result) {
179
+ const issue = result?.error?.issues?.[0];
180
+ return (issue && issue.message) || String(result?.error ?? "invalid value");
181
+ }
182
+ /**
183
+ * A socket read and written through the function's schemas.
184
+ *
185
+ * ```js
186
+ * async talk(inputData, socket) {
187
+ * const live = new Live(socket, inputData, TalkInput, TalkOutput);
188
+ * for await (const update of live) {
189
+ * if (update.field === "audio") {
190
+ * await live.send({ audio: process(update.value, inputData.voice) });
191
+ * } else if (update.field === "events") {
192
+ * ... // update.value is an Interrupt or a UserText
193
+ * }
194
+ * // an ordinary field (voice) is already set on inputData
195
+ * }
196
+ * return { seconds: ... };
197
+ * }
198
+ * ```
199
+ *
200
+ * A frame that does not fit the input schema is answered with
201
+ * `{"error": {"field": ..., "message": ...}}` and skipped; the stream goes on.
202
+ */
203
+ export class Live {
204
+ socket;
205
+ input;
206
+ inShape;
207
+ inLive;
208
+ inBinary;
209
+ outShape;
210
+ outBinary;
211
+ constructor(socket, inputData, inputSchema, outputSchema) {
212
+ this.socket = socket;
213
+ this.input = inputData;
214
+ this.inShape = shapeOf(inputSchema);
215
+ this.inLive = liveFields(inputSchema);
216
+ this.inBinary = binaryField(inputSchema);
217
+ this.outShape = shapeOf(outputSchema);
218
+ this.outBinary = binaryField(outputSchema);
219
+ }
220
+ async *[Symbol.asyncIterator]() {
221
+ for await (const frame of this.frames()) {
222
+ for (const update of await this.read(frame))
223
+ yield update;
224
+ }
225
+ }
226
+ async *frames() {
227
+ const socket = this.socket;
228
+ if (typeof socket[Symbol.asyncIterator] === "function") {
229
+ for await (const frame of socket)
230
+ yield frame;
231
+ return;
232
+ }
233
+ for (;;) {
234
+ const frame = await socket.recv();
235
+ if (frame == null)
236
+ return;
237
+ yield frame;
238
+ }
239
+ }
240
+ async read(frame) {
241
+ if (isBinaryFrame(frame)) {
242
+ if (this.inBinary === null) {
243
+ await this.refuse(null, "this function takes no binary frames");
244
+ return [];
245
+ }
246
+ return [{ field: this.inBinary, value: toBuffer(frame) }];
247
+ }
248
+ let patch;
249
+ try {
250
+ patch = typeof frame === "string" ? JSON.parse(frame) : frame;
251
+ }
252
+ catch {
253
+ patch = undefined;
254
+ }
255
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
256
+ await this.refuse(null, "a text frame is a JSON object keyed by field name");
257
+ return [];
258
+ }
259
+ const updates = [];
260
+ for (const [key, raw] of Object.entries(patch)) {
261
+ if (Object.prototype.hasOwnProperty.call(this.inLive, key)) {
262
+ if (key === this.inBinary) {
263
+ await this.refuse(key, "send this field's items as binary frames");
264
+ continue;
265
+ }
266
+ const result = this.inLive[key].safeParse(raw);
267
+ if (!result.success) {
268
+ await this.refuse(key, firstIssue(result));
269
+ continue;
270
+ }
271
+ updates.push({ field: key, value: result.data });
272
+ }
273
+ else if (Object.prototype.hasOwnProperty.call(this.inShape, key)) {
274
+ const result = this.inShape[key].safeParse(raw);
275
+ if (!result.success) {
276
+ await this.refuse(key, firstIssue(result));
277
+ continue;
278
+ }
279
+ this.input[key] = result.data;
280
+ updates.push({ field: key, value: result.data });
281
+ }
282
+ else {
283
+ await this.refuse(key, "no such field");
284
+ }
285
+ }
286
+ return updates;
287
+ }
288
+ async refuse(field, message) {
289
+ await this.socket.send({ error: { field, message } });
290
+ }
291
+ /**
292
+ * Send items of live output fields, or new values of ordinary ones.
293
+ *
294
+ * `await live.send({ audio: pcm })` is a binary frame;
295
+ * `await live.send({ transcript: word })` and
296
+ * `await live.send({ effect: "echo" })` are JSON frames. Keys in one call
297
+ * other than the binary field go out together as one JSON frame.
298
+ */
299
+ async send(fields) {
300
+ const patch = {};
301
+ for (const [key, value] of Object.entries(fields)) {
302
+ if (!Object.prototype.hasOwnProperty.call(this.outShape, key)) {
303
+ throw new Error(`the output schema has no field "${key}"`);
304
+ }
305
+ if (key === this.outBinary) {
306
+ await this.socket.send(toBuffer(value));
307
+ continue;
308
+ }
309
+ patch[key] = value;
310
+ }
311
+ if (Object.keys(patch).length > 0) {
312
+ await this.socket.send(patch);
313
+ }
314
+ }
315
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferencesh/app",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "App framework for building inference.sh apps — File handling, output metadata, storage utilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,7 +37,8 @@
37
37
  "@types/node": "^22.0.0",
38
38
  "rimraf": "^6.0.1",
39
39
  "typescript": "~6.0.3",
40
- "zod": "^4.3.6"
40
+ "zod": "^4.3.6",
41
+ "zod3": "npm:zod@^3.25.76"
41
42
  },
42
43
  "files": [
43
44
  "dist",