iii-sdk 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/helpers.cjs +30 -0
- package/dist/helpers.cjs.map +1 -0
- package/dist/helpers.d.cts +20 -0
- package/dist/helpers.d.cts.map +1 -0
- package/dist/helpers.d.mts +20 -0
- package/dist/helpers.d.mts.map +1 -0
- package/dist/helpers.mjs +24 -0
- package/dist/helpers.mjs.map +1 -0
- package/dist/index.cjs +11 -301
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -718
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1 -718
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -264
- package/dist/index.mjs.map +1 -1
- package/dist/state.d.cts +1 -1
- package/dist/state.d.mts +1 -1
- package/dist/stream.d.cts +267 -2
- package/dist/stream.d.cts.map +1 -0
- package/dist/stream.d.mts +267 -2
- package/dist/stream.d.mts.map +1 -0
- package/dist/utils-CuS1Knym.cjs +401 -0
- package/dist/utils-CuS1Knym.cjs.map +1 -0
- package/dist/utils-DXL7JI0q.mjs +319 -0
- package/dist/utils-DXL7JI0q.mjs.map +1 -0
- package/dist/utils-DcbZmefT.d.cts +707 -0
- package/dist/utils-DcbZmefT.d.cts.map +1 -0
- package/dist/utils-tcJ0Rzg-.d.mts +707 -0
- package/dist/utils-tcJ0Rzg-.d.mts.map +1 -0
- package/package.json +7 -2
- package/dist/stream-CNxp3Hhu.d.cts +0 -267
- package/dist/stream-CNxp3Hhu.d.cts.map +0 -1
- package/dist/stream-CPQKQv-x.d.mts +0 -267
- package/dist/stream-CPQKQv-x.d.mts.map +0 -1
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { Readable, Writable } from "node:stream";
|
|
2
|
+
import { WebSocket } from "ws";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/channels.ts
|
|
7
|
+
/**
|
|
8
|
+
* Direction of a streaming channel endpoint. Mirrors the Rust SDK's
|
|
9
|
+
* `ChannelDirection` enum and matches the literal values used by
|
|
10
|
+
* {@link StreamChannelRef.direction}.
|
|
11
|
+
*/
|
|
12
|
+
const ChannelDirection = {
|
|
13
|
+
Read: "read",
|
|
14
|
+
Write: "write"
|
|
15
|
+
};
|
|
16
|
+
const ChannelItem = {
|
|
17
|
+
Text(value) {
|
|
18
|
+
return {
|
|
19
|
+
type: "text",
|
|
20
|
+
value
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
Binary(value) {
|
|
24
|
+
return {
|
|
25
|
+
type: "binary",
|
|
26
|
+
value
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Write end of a streaming channel. Provides both a Node.js `Writable` stream
|
|
32
|
+
* and a `sendMessage` method for sending structured text messages.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { createChannel } from 'iii-sdk/helpers'
|
|
37
|
+
* const channel = await createChannel(iii)
|
|
38
|
+
*
|
|
39
|
+
* // Stream binary data
|
|
40
|
+
* channel.writer.stream.write(Buffer.from('hello'))
|
|
41
|
+
* channel.writer.stream.end()
|
|
42
|
+
*
|
|
43
|
+
* // Or send text messages
|
|
44
|
+
* channel.writer.sendMessage(JSON.stringify({ type: 'event', data: 'test' }))
|
|
45
|
+
* channel.writer.close()
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
var ChannelWriter = class ChannelWriter {
|
|
49
|
+
static {
|
|
50
|
+
this.FRAME_SIZE = 64 * 1024;
|
|
51
|
+
}
|
|
52
|
+
constructor(engineWsBase, ref) {
|
|
53
|
+
this.ws = null;
|
|
54
|
+
this.wsReady = false;
|
|
55
|
+
this.pendingMessages = [];
|
|
56
|
+
this.url = buildChannelUrl(engineWsBase, ref.channel_id, ref.access_key, "write");
|
|
57
|
+
this.stream = new Writable({
|
|
58
|
+
write: (chunk, _encoding, callback) => {
|
|
59
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
60
|
+
this.sendChunked(buf, callback);
|
|
61
|
+
},
|
|
62
|
+
final: (callback) => {
|
|
63
|
+
if (!this.ws) {
|
|
64
|
+
callback();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const doClose = () => {
|
|
68
|
+
if (this.ws) this.ws.close(1e3, "stream_complete");
|
|
69
|
+
callback();
|
|
70
|
+
};
|
|
71
|
+
if (this.wsReady) setTimeout(doClose, 10);
|
|
72
|
+
else this.ws.on("open", () => setTimeout(doClose, 10));
|
|
73
|
+
},
|
|
74
|
+
destroy: (err, callback) => {
|
|
75
|
+
if (this.ws) this.ws.terminate();
|
|
76
|
+
callback(err);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
ensureConnected() {
|
|
81
|
+
if (this.ws) return;
|
|
82
|
+
this.ws = new WebSocket(this.url);
|
|
83
|
+
this.ws.on("open", () => {
|
|
84
|
+
this.wsReady = true;
|
|
85
|
+
for (const { data, callback } of this.pendingMessages) this.ws?.send(data, callback);
|
|
86
|
+
this.pendingMessages.length = 0;
|
|
87
|
+
});
|
|
88
|
+
this.ws.on("error", (err) => {
|
|
89
|
+
this.stream.destroy(err);
|
|
90
|
+
});
|
|
91
|
+
this.ws.on("close", () => {
|
|
92
|
+
if (!this.stream.destroyed) this.stream.destroy();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Send a text message through the channel. */
|
|
96
|
+
sendMessage(msg) {
|
|
97
|
+
this.ensureConnected();
|
|
98
|
+
this.sendRaw(msg, (err) => {
|
|
99
|
+
if (err) this.stream.destroy(err);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** Close the channel writer. */
|
|
103
|
+
close() {
|
|
104
|
+
if (!this.ws) return;
|
|
105
|
+
const doClose = () => {
|
|
106
|
+
if (this.ws) this.ws.close(1e3, "channel_close");
|
|
107
|
+
};
|
|
108
|
+
if (this.wsReady) doClose();
|
|
109
|
+
else this.ws.on("open", () => doClose());
|
|
110
|
+
}
|
|
111
|
+
sendChunked(data, callback) {
|
|
112
|
+
let offset = 0;
|
|
113
|
+
const sendNext = (err) => {
|
|
114
|
+
if (err) {
|
|
115
|
+
callback(err);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (offset >= data.length) {
|
|
119
|
+
callback(null);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const end = Math.min(offset + ChannelWriter.FRAME_SIZE, data.length);
|
|
123
|
+
const part = data.subarray(offset, end);
|
|
124
|
+
offset = end;
|
|
125
|
+
this.sendRaw(part, sendNext);
|
|
126
|
+
};
|
|
127
|
+
sendNext(null);
|
|
128
|
+
}
|
|
129
|
+
sendRaw(data, callback) {
|
|
130
|
+
this.ensureConnected();
|
|
131
|
+
if (this.wsReady && this.ws) this.ws.send(data, (err) => callback(err ?? null));
|
|
132
|
+
else this.pendingMessages.push({
|
|
133
|
+
data,
|
|
134
|
+
callback
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Read end of a streaming channel. Provides both a Node.js `Readable` stream
|
|
140
|
+
* for binary data and an `onMessage` callback for structured text messages.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* import { createChannel } from 'iii-sdk/helpers'
|
|
145
|
+
* const channel = await createChannel(iii)
|
|
146
|
+
*
|
|
147
|
+
* // Stream binary data
|
|
148
|
+
* channel.reader.stream.on('data', (chunk) => console.log(chunk))
|
|
149
|
+
*
|
|
150
|
+
* // Or receive text messages
|
|
151
|
+
* channel.reader.onMessage((msg) => console.log('Got:', msg))
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
var ChannelReader = class {
|
|
155
|
+
constructor(engineWsBase, ref) {
|
|
156
|
+
this.ws = null;
|
|
157
|
+
this.connected = false;
|
|
158
|
+
this.messageCallbacks = [];
|
|
159
|
+
this.url = buildChannelUrl(engineWsBase, ref.channel_id, ref.access_key, "read");
|
|
160
|
+
const self = this;
|
|
161
|
+
this.stream = new Readable({
|
|
162
|
+
read() {
|
|
163
|
+
self.ensureConnected();
|
|
164
|
+
if (self.ws) self.ws.resume();
|
|
165
|
+
},
|
|
166
|
+
destroy(err, callback) {
|
|
167
|
+
if (self.ws && self.ws.readyState !== WebSocket.CLOSED) self.ws.terminate();
|
|
168
|
+
self.ws = null;
|
|
169
|
+
callback(err);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
ensureConnected() {
|
|
174
|
+
if (this.connected) return;
|
|
175
|
+
this.connected = true;
|
|
176
|
+
this.ws = new WebSocket(this.url);
|
|
177
|
+
this.ws.on("open", () => {
|
|
178
|
+
this.ws.binaryType = "nodebuffer";
|
|
179
|
+
});
|
|
180
|
+
this.ws.on("message", (data, isBinary) => {
|
|
181
|
+
if (isBinary) {
|
|
182
|
+
if (!this.stream.push(data)) this.ws?.pause();
|
|
183
|
+
} else {
|
|
184
|
+
const msg = data.toString("utf-8");
|
|
185
|
+
for (const cb of this.messageCallbacks) cb(msg);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
this.ws.on("close", () => {
|
|
189
|
+
this.ws = null;
|
|
190
|
+
if (!this.stream.destroyed) this.stream.push(null);
|
|
191
|
+
});
|
|
192
|
+
this.ws.on("error", (err) => {
|
|
193
|
+
this.stream.destroy(err);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/** Register a callback to receive text messages from the channel. */
|
|
197
|
+
onMessage(callback) {
|
|
198
|
+
this.messageCallbacks.push(callback);
|
|
199
|
+
}
|
|
200
|
+
async readAll() {
|
|
201
|
+
this.ensureConnected();
|
|
202
|
+
const chunks = [];
|
|
203
|
+
for await (const chunk of this.stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
204
|
+
return Buffer.concat(chunks);
|
|
205
|
+
}
|
|
206
|
+
close() {
|
|
207
|
+
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) this.ws.close(1e3, "channel_close");
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
function buildChannelUrl(engineWsBase, channelId, accessKey, direction) {
|
|
211
|
+
return `${engineWsBase.replace(/\/$/, "")}/ws/channels/${channelId}?key=${encodeURIComponent(accessKey)}&dir=${direction}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/utils.ts
|
|
216
|
+
/**
|
|
217
|
+
* Returns a project identifier for telemetry, derived from the current working
|
|
218
|
+
* directory. Reads `package.json` `name` if present at `cwd`; otherwise falls
|
|
219
|
+
* back to the basename of `cwd`. Returns `undefined` only when both signals
|
|
220
|
+
* are unavailable (e.g. cwd is the filesystem root).
|
|
221
|
+
*
|
|
222
|
+
* No directory walking — only inspects `cwd` itself, so the SDK never reads
|
|
223
|
+
* files outside the user's explicit working directory.
|
|
224
|
+
*/
|
|
225
|
+
function detectProjectName(cwd = process.cwd()) {
|
|
226
|
+
try {
|
|
227
|
+
const manifest = path.join(cwd, "package.json");
|
|
228
|
+
if (fs.existsSync(manifest)) {
|
|
229
|
+
const parsed = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
230
|
+
if (typeof parsed.name === "string") {
|
|
231
|
+
const trimmed = parsed.name.trim();
|
|
232
|
+
if (trimmed) return trimmed;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch {}
|
|
236
|
+
return path.basename(cwd).trim() || void 0;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Helper that wraps an HTTP-style handler (with separate `req`/`res` arguments)
|
|
240
|
+
* into the function handler format expected by the SDK.
|
|
241
|
+
*
|
|
242
|
+
* @param callback - Async handler receiving an {@link HttpRequest} and {@link HttpResponse}.
|
|
243
|
+
* @returns A function handler compatible with {@link ISdk.registerFunction}.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* import { http } from 'iii-sdk'
|
|
248
|
+
*
|
|
249
|
+
* iii.registerFunction(
|
|
250
|
+
* 'my-api',
|
|
251
|
+
* http(async (req, res) => {
|
|
252
|
+
* res.status(200)
|
|
253
|
+
* res.headers({ 'content-type': 'application/json' })
|
|
254
|
+
* res.stream.end(JSON.stringify({ hello: 'world' }))
|
|
255
|
+
* res.close()
|
|
256
|
+
* }),
|
|
257
|
+
* )
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
const http = (callback) => {
|
|
261
|
+
return async (req) => {
|
|
262
|
+
const { response, ...request } = req;
|
|
263
|
+
return callback(request, {
|
|
264
|
+
status: (status_code) => response.sendMessage(JSON.stringify({
|
|
265
|
+
type: "set_status",
|
|
266
|
+
status_code
|
|
267
|
+
})),
|
|
268
|
+
headers: (headers) => response.sendMessage(JSON.stringify({
|
|
269
|
+
type: "set_headers",
|
|
270
|
+
headers
|
|
271
|
+
})),
|
|
272
|
+
stream: response.stream,
|
|
273
|
+
close: () => response.close()
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
/**
|
|
278
|
+
* Type guard that checks if a value is a {@link StreamChannelRef}.
|
|
279
|
+
*
|
|
280
|
+
* @param value - Value to check.
|
|
281
|
+
* @returns `true` if the value is a valid `StreamChannelRef`.
|
|
282
|
+
*/
|
|
283
|
+
const isChannelRef = (value) => {
|
|
284
|
+
if (typeof value !== "object" || value === null) return false;
|
|
285
|
+
const maybe = value;
|
|
286
|
+
return typeof maybe.channel_id === "string" && typeof maybe.access_key === "string" && (maybe.direction === "read" || maybe.direction === "write");
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* Recursively extract all {@link StreamChannelRef} values from a JSON-like
|
|
290
|
+
* input, returning each match paired with its dotted/bracketed path. Mirrors
|
|
291
|
+
* the Rust SDK's `extract_channel_refs`.
|
|
292
|
+
*
|
|
293
|
+
* @param data - Arbitrary JSON-like value.
|
|
294
|
+
* @returns Array of `[path, ref]` tuples. Empty when no refs are found.
|
|
295
|
+
*/
|
|
296
|
+
const extractChannelRefs = (data) => {
|
|
297
|
+
const refs = [];
|
|
298
|
+
extractRefsRecursive(data, "", refs);
|
|
299
|
+
return refs;
|
|
300
|
+
};
|
|
301
|
+
const extractRefsRecursive = (data, prefix, refs) => {
|
|
302
|
+
if (isChannelRef(data)) {
|
|
303
|
+
refs.push([prefix, data]);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (Array.isArray(data)) {
|
|
307
|
+
for (let i = 0; i < data.length; i++) {
|
|
308
|
+
const path = prefix === "" ? `[${i}]` : `${prefix}[${i}]`;
|
|
309
|
+
extractRefsRecursive(data[i], path, refs);
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (typeof data !== "object" || data === null) return;
|
|
314
|
+
for (const [key, value] of Object.entries(data)) extractRefsRecursive(value, prefix === "" ? key : `${prefix}.${key}`, refs);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
//#endregion
|
|
318
|
+
export { ChannelDirection as a, ChannelWriter as c, isChannelRef as i, extractChannelRefs as n, ChannelItem as o, http as r, ChannelReader as s, detectProjectName as t };
|
|
319
|
+
//# sourceMappingURL=utils-DXL7JI0q.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils-DXL7JI0q.mjs","names":[],"sources":["../src/channels.ts","../src/utils.ts"],"sourcesContent":["import { Readable, Writable } from 'node:stream'\nimport { WebSocket } from 'ws'\nimport type { StreamChannelRef } from './iii-types'\n\n/**\n * Direction of a streaming channel endpoint. Mirrors the Rust SDK's\n * `ChannelDirection` enum and matches the literal values used by\n * {@link StreamChannelRef.direction}.\n */\nexport const ChannelDirection = {\n Read: 'read',\n Write: 'write',\n} as const\nexport type ChannelDirection = (typeof ChannelDirection)[keyof typeof ChannelDirection]\n\n/**\n * Discriminated runtime tag for an item observed on a streaming channel.\n * Mirrors the Rust SDK's `ChannelItem` enum (`Text` / `Binary`). Carrier for\n * factory + type-guard helpers so callers can construct and discriminate\n * channel items without depending on Rust-specific shape.\n */\nexport type ChannelItem =\n | { type: 'text'; value: string }\n | { type: 'binary'; value: Uint8Array }\n\nexport const ChannelItem = {\n /** Construct a text channel item. */\n Text(value: string): ChannelItem {\n return { type: 'text', value }\n },\n /** Construct a binary channel item. */\n Binary(value: Uint8Array): ChannelItem {\n return { type: 'binary', value }\n },\n} as const\n\n/**\n * Write end of a streaming channel. Provides both a Node.js `Writable` stream\n * and a `sendMessage` method for sending structured text messages.\n *\n * @example\n * ```typescript\n * import { createChannel } from 'iii-sdk/helpers'\n * const channel = await createChannel(iii)\n *\n * // Stream binary data\n * channel.writer.stream.write(Buffer.from('hello'))\n * channel.writer.stream.end()\n *\n * // Or send text messages\n * channel.writer.sendMessage(JSON.stringify({ type: 'event', data: 'test' }))\n * channel.writer.close()\n * ```\n */\nexport class ChannelWriter {\n private static readonly FRAME_SIZE = 64 * 1024\n private ws: WebSocket | null = null\n private wsReady = false\n private readonly pendingMessages: {\n data: Buffer | string\n callback: (err?: Error | null) => void\n }[] = []\n /** Node.js Writable stream for binary data. */\n public readonly stream: Writable\n private readonly url: string\n\n constructor(engineWsBase: string, ref: StreamChannelRef) {\n this.url = buildChannelUrl(engineWsBase, ref.channel_id, ref.access_key, 'write')\n\n this.stream = new Writable({\n write: (chunk: Buffer, _encoding, callback) => {\n const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n this.sendChunked(buf, callback)\n },\n final: (callback) => {\n if (!this.ws) {\n callback()\n return\n }\n // Delay the close frame slightly to allow the TCP stack to flush\n // all buffered send() data. Without this, the close frame can arrive\n // at the engine before all data frames, causing data truncation.\n const doClose = () => {\n if (this.ws) {\n this.ws.close(1000, 'stream_complete')\n }\n callback()\n }\n if (this.wsReady) {\n setTimeout(doClose, 10)\n } else {\n this.ws.on('open', () => setTimeout(doClose, 10))\n }\n },\n destroy: (err, callback) => {\n if (this.ws) this.ws.terminate()\n callback(err)\n },\n })\n }\n\n private ensureConnected(): void {\n if (this.ws) return\n this.ws = new WebSocket(this.url)\n\n this.ws.on('open', () => {\n this.wsReady = true\n for (const { data, callback } of this.pendingMessages) {\n this.ws?.send(data, callback)\n }\n this.pendingMessages.length = 0\n })\n\n this.ws.on('error', (err) => {\n this.stream.destroy(err)\n })\n\n this.ws.on('close', () => {\n if (!this.stream.destroyed) {\n this.stream.destroy()\n }\n })\n }\n\n /** Send a text message through the channel. */\n sendMessage(msg: string): void {\n this.ensureConnected()\n this.sendRaw(msg, (err) => {\n if (err) this.stream.destroy(err)\n })\n }\n\n /** Close the channel writer. */\n close(): void {\n if (!this.ws) return\n const doClose = () => {\n if (this.ws) {\n this.ws.close(1000, 'channel_close')\n }\n }\n if (this.wsReady) {\n doClose()\n } else {\n this.ws.on('open', () => doClose())\n }\n }\n\n private sendChunked(data: Buffer, callback: (err?: Error | null) => void): void {\n let offset = 0\n const sendNext = (err?: Error | null): void => {\n if (err) {\n callback(err)\n return\n }\n\n if (offset >= data.length) {\n callback(null)\n return\n }\n\n const end = Math.min(offset + ChannelWriter.FRAME_SIZE, data.length)\n const part = data.subarray(offset, end)\n offset = end\n this.sendRaw(part, sendNext)\n }\n sendNext(null)\n }\n\n private sendRaw(data: Buffer | string, callback: (err?: Error | null) => void): void {\n this.ensureConnected()\n if (this.wsReady && this.ws) {\n this.ws.send(data, (err) => callback(err ?? null))\n } else {\n this.pendingMessages.push({ data, callback })\n }\n }\n}\n\n/**\n * Read end of a streaming channel. Provides both a Node.js `Readable` stream\n * for binary data and an `onMessage` callback for structured text messages.\n *\n * @example\n * ```typescript\n * import { createChannel } from 'iii-sdk/helpers'\n * const channel = await createChannel(iii)\n *\n * // Stream binary data\n * channel.reader.stream.on('data', (chunk) => console.log(chunk))\n *\n * // Or receive text messages\n * channel.reader.onMessage((msg) => console.log('Got:', msg))\n * ```\n */\nexport class ChannelReader {\n private ws: WebSocket | null = null\n private connected = false\n private readonly messageCallbacks: Array<(msg: string) => void> = []\n /** Node.js Readable stream for binary data. */\n public readonly stream: Readable\n private readonly url: string\n\n constructor(engineWsBase: string, ref: StreamChannelRef) {\n this.url = buildChannelUrl(engineWsBase, ref.channel_id, ref.access_key, 'read')\n\n const self = this\n this.stream = new Readable({\n read() {\n self.ensureConnected()\n if (self.ws) self.ws.resume()\n },\n destroy(err, callback) {\n if (self.ws && self.ws.readyState !== WebSocket.CLOSED) {\n self.ws.terminate()\n }\n self.ws = null\n callback(err)\n },\n })\n }\n\n private ensureConnected(): void {\n if (this.connected) return\n this.connected = true\n this.ws = new WebSocket(this.url)\n\n this.ws.on('open', () => {\n ;(this.ws as unknown as { binaryType: string }).binaryType = 'nodebuffer'\n })\n\n this.ws.on('message', (data: Buffer, isBinary: boolean) => {\n if (isBinary) {\n if (!this.stream.push(data)) {\n this.ws?.pause()\n }\n } else {\n const msg = data.toString('utf-8')\n for (const cb of this.messageCallbacks) {\n cb(msg)\n }\n }\n })\n\n this.ws.on('close', () => {\n this.ws = null\n if (!this.stream.destroyed) this.stream.push(null)\n })\n\n this.ws.on('error', (err) => {\n this.stream.destroy(err)\n })\n }\n\n /** Register a callback to receive text messages from the channel. */\n onMessage(callback: (msg: string) => void): void {\n this.messageCallbacks.push(callback)\n }\n\n async readAll(): Promise<Buffer> {\n this.ensureConnected()\n const chunks: Buffer[] = []\n\n for await (const chunk of this.stream) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))\n }\n\n return Buffer.concat(chunks)\n }\n\n close(): void {\n if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {\n this.ws.close(1000, 'channel_close')\n }\n }\n}\n\nfunction buildChannelUrl(\n engineWsBase: string,\n channelId: string,\n accessKey: string,\n direction: 'read' | 'write',\n): string {\n const base = engineWsBase.replace(/\\/$/, '')\n return `${base}/ws/channels/${channelId}?key=${encodeURIComponent(accessKey)}&dir=${direction}`\n}\n","import * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport type { StreamChannelRef } from './iii-types'\nimport type { ApiResponse, HttpRequest, HttpResponse, InternalHttpRequest } from './types'\n\n/**\n * Returns a project identifier for telemetry, derived from the current working\n * directory. Reads `package.json` `name` if present at `cwd`; otherwise falls\n * back to the basename of `cwd`. Returns `undefined` only when both signals\n * are unavailable (e.g. cwd is the filesystem root).\n *\n * No directory walking — only inspects `cwd` itself, so the SDK never reads\n * files outside the user's explicit working directory.\n */\nexport function detectProjectName(cwd: string = process.cwd()): string | undefined {\n try {\n const manifest = path.join(cwd, 'package.json')\n if (fs.existsSync(manifest)) {\n const parsed = JSON.parse(fs.readFileSync(manifest, 'utf8')) as { name?: unknown }\n if (typeof parsed.name === 'string') {\n const trimmed = parsed.name.trim()\n if (trimmed) return trimmed\n }\n }\n } catch {\n // fall through to directory-name fallback\n }\n\n const base = path.basename(cwd).trim()\n return base || undefined\n}\n\n/**\n * Helper that wraps an HTTP-style handler (with separate `req`/`res` arguments)\n * into the function handler format expected by the SDK.\n *\n * @param callback - Async handler receiving an {@link HttpRequest} and {@link HttpResponse}.\n * @returns A function handler compatible with {@link ISdk.registerFunction}.\n *\n * @example\n * ```typescript\n * import { http } from 'iii-sdk'\n *\n * iii.registerFunction(\n * 'my-api',\n * http(async (req, res) => {\n * res.status(200)\n * res.headers({ 'content-type': 'application/json' })\n * res.stream.end(JSON.stringify({ hello: 'world' }))\n * res.close()\n * }),\n * )\n * ```\n */\nexport const http = (\n // biome-ignore lint/suspicious/noConfusingVoidType: void is necessary here\n callback: (req: HttpRequest, res: HttpResponse) => Promise<void | ApiResponse>,\n) => {\n return async (req: InternalHttpRequest) => {\n const { response, ...request } = req\n\n const httpResponse: HttpResponse = {\n status: (status_code: number) =>\n response.sendMessage(JSON.stringify({ type: 'set_status', status_code })),\n headers: (headers: Record<string, string>) =>\n response.sendMessage(JSON.stringify({ type: 'set_headers', headers })),\n stream: response.stream,\n close: () => response.close(),\n }\n\n return callback(request, httpResponse)\n }\n}\n\n/**\n * Type guard that checks if a value is a {@link StreamChannelRef}.\n *\n * @param value - Value to check.\n * @returns `true` if the value is a valid `StreamChannelRef`.\n */\nexport const isChannelRef = (value: unknown): value is StreamChannelRef => {\n if (typeof value !== 'object' || value === null) return false\n const maybe = value as Partial<StreamChannelRef>\n return (\n typeof maybe.channel_id === 'string' &&\n typeof maybe.access_key === 'string' &&\n (maybe.direction === 'read' || maybe.direction === 'write')\n )\n}\n\n/**\n * Recursively extract all {@link StreamChannelRef} values from a JSON-like\n * input, returning each match paired with its dotted/bracketed path. Mirrors\n * the Rust SDK's `extract_channel_refs`.\n *\n * @param data - Arbitrary JSON-like value.\n * @returns Array of `[path, ref]` tuples. Empty when no refs are found.\n */\nexport const extractChannelRefs = (data: unknown): Array<[string, StreamChannelRef]> => {\n const refs: Array<[string, StreamChannelRef]> = []\n extractRefsRecursive(data, '', refs)\n return refs\n}\n\nconst extractRefsRecursive = (\n data: unknown,\n prefix: string,\n refs: Array<[string, StreamChannelRef]>,\n): void => {\n if (isChannelRef(data)) {\n refs.push([prefix, data])\n return\n }\n if (Array.isArray(data)) {\n for (let i = 0; i < data.length; i++) {\n const path = prefix === '' ? `[${i}]` : `${prefix}[${i}]`\n extractRefsRecursive(data[i], path, refs)\n }\n return\n }\n if (typeof data !== 'object' || data === null) return\n\n for (const [key, value] of Object.entries(data as Record<string, unknown>)) {\n const path = prefix === '' ? key : `${prefix}.${key}`\n extractRefsRecursive(value, path, refs)\n }\n}\n"],"mappings":"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"}
|