iii-browser-sdk 0.10.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/index.mjs ADDED
@@ -0,0 +1,800 @@
1
+ //#region src/channels.ts
2
+ /**
3
+ * Write end of a streaming channel. Uses native browser WebSocket.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * const channel = await iii.createChannel()
8
+ *
9
+ * channel.writer.sendMessage(JSON.stringify({ type: 'event', data: 'test' }))
10
+ * channel.writer.sendBinary(new Uint8Array([1, 2, 3]))
11
+ * channel.writer.close()
12
+ * ```
13
+ */
14
+ var ChannelWriter = class ChannelWriter {
15
+ static {
16
+ this.FRAME_SIZE = 64 * 1024;
17
+ }
18
+ constructor(engineWsBase, ref) {
19
+ this.ws = null;
20
+ this.wsReady = false;
21
+ this.pendingMessages = [];
22
+ this.url = buildChannelUrl(engineWsBase, ref.channel_id, ref.access_key, "write");
23
+ }
24
+ ensureConnected() {
25
+ if (this.ws) return;
26
+ this.ws = new WebSocket(this.url);
27
+ this.ws.binaryType = "arraybuffer";
28
+ this.ws.addEventListener("open", () => {
29
+ this.wsReady = true;
30
+ for (const { data, resolve, reject } of this.pendingMessages) try {
31
+ this.ws?.send(data);
32
+ resolve();
33
+ } catch (err) {
34
+ reject(err instanceof Error ? err : new Error(String(err)));
35
+ }
36
+ this.pendingMessages.length = 0;
37
+ });
38
+ this.ws.addEventListener("error", () => {
39
+ for (const { reject } of this.pendingMessages) reject(/* @__PURE__ */ new Error("WebSocket error"));
40
+ this.pendingMessages.length = 0;
41
+ });
42
+ }
43
+ /** Send a text message through the channel. */
44
+ sendMessage(msg) {
45
+ this.ensureConnected();
46
+ this.sendRaw(msg);
47
+ }
48
+ /** Send binary data through the channel. */
49
+ sendBinary(data) {
50
+ this.ensureConnected();
51
+ let offset = 0;
52
+ while (offset < data.length) {
53
+ const end = Math.min(offset + ChannelWriter.FRAME_SIZE, data.length);
54
+ const chunk = data.subarray(offset, end);
55
+ const buffer = chunk.buffer instanceof ArrayBuffer ? chunk.buffer : new ArrayBuffer(chunk.byteLength);
56
+ if (!(chunk.buffer instanceof ArrayBuffer)) new Uint8Array(buffer).set(chunk);
57
+ this.sendRaw(buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
58
+ offset = end;
59
+ }
60
+ }
61
+ /** Close the channel writer. */
62
+ close() {
63
+ if (!this.ws) return;
64
+ const doClose = () => {
65
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.close(1e3, "channel_close");
66
+ };
67
+ if (this.wsReady) doClose();
68
+ else this.ws.addEventListener("open", () => doClose());
69
+ }
70
+ sendRaw(data) {
71
+ if (this.wsReady && this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.send(data);
72
+ else {
73
+ this.ensureConnected();
74
+ this.pendingMessages.push({
75
+ data,
76
+ resolve: () => {},
77
+ reject: () => {}
78
+ });
79
+ }
80
+ }
81
+ };
82
+ /**
83
+ * Read end of a streaming channel. Uses native browser WebSocket.
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const channel = await iii.createChannel()
88
+ *
89
+ * channel.reader.onMessage((msg) => console.log('Got:', msg))
90
+ * channel.reader.onBinary((data) => console.log('Binary:', data.byteLength))
91
+ * ```
92
+ */
93
+ var ChannelReader = class {
94
+ constructor(engineWsBase, ref) {
95
+ this.ws = null;
96
+ this.connected = false;
97
+ this.messageCallbacks = [];
98
+ this.binaryCallbacks = [];
99
+ this.url = buildChannelUrl(engineWsBase, ref.channel_id, ref.access_key, "read");
100
+ }
101
+ ensureConnected() {
102
+ if (this.connected) return;
103
+ this.connected = true;
104
+ this.ws = new WebSocket(this.url);
105
+ this.ws.binaryType = "arraybuffer";
106
+ this.ws.addEventListener("message", (event) => {
107
+ if (event.data instanceof ArrayBuffer) {
108
+ const data = new Uint8Array(event.data);
109
+ for (const cb of this.binaryCallbacks) cb(data);
110
+ } else if (typeof event.data === "string") for (const cb of this.messageCallbacks) cb(event.data);
111
+ });
112
+ this.ws.addEventListener("close", () => {
113
+ this.ws = null;
114
+ });
115
+ this.ws.addEventListener("error", () => {
116
+ this.ws = null;
117
+ });
118
+ }
119
+ /** Register a callback to receive text messages from the channel. */
120
+ onMessage(callback) {
121
+ this.messageCallbacks.push(callback);
122
+ this.ensureConnected();
123
+ }
124
+ /** Register a callback to receive binary data from the channel. */
125
+ onBinary(callback) {
126
+ this.binaryCallbacks.push(callback);
127
+ this.ensureConnected();
128
+ }
129
+ /** Read all binary data from the channel until it closes. */
130
+ async readAll() {
131
+ this.ensureConnected();
132
+ const chunks = [];
133
+ return new Promise((resolve) => {
134
+ const onData = (data) => {
135
+ chunks.push(data);
136
+ };
137
+ this.binaryCallbacks.push(onData);
138
+ const originalWs = this.ws;
139
+ if (originalWs) originalWs.addEventListener("close", () => {
140
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
141
+ const result = new Uint8Array(totalLength);
142
+ let offset = 0;
143
+ for (const chunk of chunks) {
144
+ result.set(chunk, offset);
145
+ offset += chunk.length;
146
+ }
147
+ resolve(result);
148
+ });
149
+ });
150
+ }
151
+ /** Close the channel reader. */
152
+ close() {
153
+ if (this.ws && this.ws.readyState !== WebSocket.CLOSED) this.ws.close(1e3, "channel_close");
154
+ }
155
+ };
156
+ function buildChannelUrl(engineWsBase, channelId, accessKey, direction) {
157
+ return `${engineWsBase.replace(/\/$/, "")}/ws/channels/${channelId}?key=${encodeURIComponent(accessKey)}&dir=${direction}`;
158
+ }
159
+
160
+ //#endregion
161
+ //#region src/iii-constants.ts
162
+ /**
163
+ * Constants for the III module.
164
+ */
165
+ /** Engine function paths for internal operations */
166
+ const EngineFunctions = {
167
+ LIST_FUNCTIONS: "engine::functions::list",
168
+ LIST_WORKERS: "engine::workers::list",
169
+ LIST_TRIGGERS: "engine::triggers::list",
170
+ LIST_TRIGGER_TYPES: "engine::trigger-types::list",
171
+ REGISTER_WORKER: "engine::workers::register"
172
+ };
173
+ /** Engine trigger types */
174
+ const EngineTriggers = { FUNCTIONS_AVAILABLE: "engine::functions-available" };
175
+ /** Default reconnection configuration */
176
+ const DEFAULT_BRIDGE_RECONNECTION_CONFIG = {
177
+ initialDelayMs: 1e3,
178
+ maxDelayMs: 3e4,
179
+ backoffMultiplier: 2,
180
+ jitterFactor: .3,
181
+ maxRetries: -1
182
+ };
183
+ /** Default invocation timeout in milliseconds */
184
+ const DEFAULT_INVOCATION_TIMEOUT_MS = 3e4;
185
+
186
+ //#endregion
187
+ //#region src/iii-types.ts
188
+ let MessageType = /* @__PURE__ */ function(MessageType) {
189
+ MessageType["RegisterFunction"] = "registerfunction";
190
+ MessageType["UnregisterFunction"] = "unregisterfunction";
191
+ MessageType["RegisterService"] = "registerservice";
192
+ MessageType["InvokeFunction"] = "invokefunction";
193
+ MessageType["InvocationResult"] = "invocationresult";
194
+ MessageType["RegisterTriggerType"] = "registertriggertype";
195
+ MessageType["RegisterTrigger"] = "registertrigger";
196
+ MessageType["UnregisterTrigger"] = "unregistertrigger";
197
+ MessageType["UnregisterTriggerType"] = "unregistertriggertype";
198
+ MessageType["TriggerRegistrationResult"] = "triggerregistrationresult";
199
+ MessageType["WorkerRegistered"] = "workerregistered";
200
+ return MessageType;
201
+ }({});
202
+
203
+ //#endregion
204
+ //#region src/utils.ts
205
+ /**
206
+ * Type guard that checks if a value is a {@link StreamChannelRef}.
207
+ *
208
+ * @param value - Value to check.
209
+ * @returns `true` if the value is a valid `StreamChannelRef`.
210
+ */
211
+ const isChannelRef = (value) => {
212
+ if (typeof value !== "object" || value === null) return false;
213
+ const maybe = value;
214
+ return typeof maybe.channel_id === "string" && typeof maybe.access_key === "string" && (maybe.direction === "read" || maybe.direction === "write");
215
+ };
216
+
217
+ //#endregion
218
+ //#region src/iii.ts
219
+ const SDK_VERSION = "0.10.0";
220
+ function getBrowserInfo() {
221
+ if (typeof navigator !== "undefined" && navigator.userAgent) return navigator.userAgent;
222
+ return "browser (unknown)";
223
+ }
224
+ function getDefaultWorkerName() {
225
+ return `browser:${crypto.randomUUID().slice(0, 8)}`;
226
+ }
227
+ var Sdk = class {
228
+ constructor(address, options) {
229
+ this.address = address;
230
+ this.options = options;
231
+ this.functions = /* @__PURE__ */ new Map();
232
+ this.services = /* @__PURE__ */ new Map();
233
+ this.invocations = /* @__PURE__ */ new Map();
234
+ this.triggers = /* @__PURE__ */ new Map();
235
+ this.triggerTypes = /* @__PURE__ */ new Map();
236
+ this.functionsAvailableCallbacks = /* @__PURE__ */ new Set();
237
+ this.messagesToSend = [];
238
+ this.reconnectAttempt = 0;
239
+ this.connectionState = "disconnected";
240
+ this.isShuttingDown = false;
241
+ this.registerTriggerType = (triggerType, handler) => {
242
+ this.sendMessage(MessageType.RegisterTriggerType, triggerType, true);
243
+ this.triggerTypes.set(triggerType.id, {
244
+ message: {
245
+ ...triggerType,
246
+ message_type: MessageType.RegisterTriggerType
247
+ },
248
+ handler
249
+ });
250
+ return {
251
+ id: triggerType.id,
252
+ registerTrigger: (functionId, config) => {
253
+ return this.registerTrigger({
254
+ type: triggerType.id,
255
+ function_id: functionId,
256
+ config
257
+ });
258
+ },
259
+ registerFunction: (func, handler, config) => {
260
+ const ref = this.registerFunction(func, handler);
261
+ this.registerTrigger({
262
+ type: triggerType.id,
263
+ function_id: func.id,
264
+ config
265
+ });
266
+ return ref;
267
+ },
268
+ unregister: () => {
269
+ this.unregisterTriggerType(triggerType);
270
+ }
271
+ };
272
+ };
273
+ this.unregisterTriggerType = (triggerType) => {
274
+ this.sendMessage(MessageType.UnregisterTriggerType, triggerType, true);
275
+ this.triggerTypes.delete(triggerType.id);
276
+ };
277
+ this.registerTrigger = (trigger) => {
278
+ const id = crypto.randomUUID();
279
+ const fullTrigger = {
280
+ ...trigger,
281
+ id,
282
+ message_type: MessageType.RegisterTrigger
283
+ };
284
+ this.sendMessage(MessageType.RegisterTrigger, fullTrigger, true);
285
+ this.triggers.set(id, fullTrigger);
286
+ return { unregister: () => {
287
+ this.sendMessage(MessageType.UnregisterTrigger, {
288
+ id,
289
+ message_type: MessageType.UnregisterTrigger,
290
+ type: fullTrigger.type
291
+ });
292
+ this.triggers.delete(id);
293
+ } };
294
+ };
295
+ this.registerFunction = (message, handlerOrInvocation) => {
296
+ if (!message.id || message.id.trim() === "") throw new Error("id is required");
297
+ if (this.functions.has(message.id)) throw new Error(`function id already registered: ${message.id}`);
298
+ const isHandler = typeof handlerOrInvocation === "function";
299
+ const fullMessage = isHandler ? {
300
+ ...message,
301
+ message_type: MessageType.RegisterFunction
302
+ } : {
303
+ ...message,
304
+ message_type: MessageType.RegisterFunction,
305
+ invocation: {
306
+ url: handlerOrInvocation.url,
307
+ method: handlerOrInvocation.method ?? "POST",
308
+ timeout_ms: handlerOrInvocation.timeout_ms,
309
+ headers: handlerOrInvocation.headers,
310
+ auth: handlerOrInvocation.auth
311
+ }
312
+ };
313
+ this.sendMessage(MessageType.RegisterFunction, fullMessage, true);
314
+ if (isHandler) {
315
+ const handler = handlerOrInvocation;
316
+ this.functions.set(message.id, {
317
+ message: fullMessage,
318
+ handler: async (input, _traceparent, _baggage) => {
319
+ return await handler(input);
320
+ }
321
+ });
322
+ } else this.functions.set(message.id, { message: fullMessage });
323
+ return {
324
+ id: message.id,
325
+ unregister: () => {
326
+ this.sendMessage(MessageType.UnregisterFunction, { id: message.id }, true);
327
+ this.functions.delete(message.id);
328
+ }
329
+ };
330
+ };
331
+ this.registerService = (message) => {
332
+ const msg = {
333
+ ...message,
334
+ name: message.name ?? message.id
335
+ };
336
+ this.sendMessage(MessageType.RegisterService, msg, true);
337
+ this.services.set(message.id, {
338
+ ...msg,
339
+ message_type: MessageType.RegisterService
340
+ });
341
+ };
342
+ this.createChannel = async (bufferSize) => {
343
+ const result = await this.trigger({
344
+ function_id: "engine::channels::create",
345
+ payload: { buffer_size: bufferSize }
346
+ });
347
+ return {
348
+ writer: new ChannelWriter(this.address, result.writer),
349
+ reader: new ChannelReader(this.address, result.reader),
350
+ writerRef: result.writer,
351
+ readerRef: result.reader
352
+ };
353
+ };
354
+ this.trigger = async (request) => {
355
+ const { function_id, payload, action, timeoutMs } = request;
356
+ const effectiveTimeout = timeoutMs ?? this.invocationTimeoutMs;
357
+ if (action?.type === "void") {
358
+ this.sendMessage(MessageType.InvokeFunction, {
359
+ function_id,
360
+ data: payload,
361
+ action
362
+ });
363
+ return;
364
+ }
365
+ const invocation_id = crypto.randomUUID();
366
+ return new Promise((resolve, reject) => {
367
+ const timeout = setTimeout(() => {
368
+ if (this.invocations.get(invocation_id)) {
369
+ this.invocations.delete(invocation_id);
370
+ reject(/* @__PURE__ */ new Error(`Invocation timeout after ${effectiveTimeout}ms: ${function_id}`));
371
+ }
372
+ }, effectiveTimeout);
373
+ this.invocations.set(invocation_id, {
374
+ resolve: (result) => {
375
+ clearTimeout(timeout);
376
+ resolve(result);
377
+ },
378
+ reject: (error) => {
379
+ clearTimeout(timeout);
380
+ reject(error);
381
+ },
382
+ timeout
383
+ });
384
+ this.sendMessage(MessageType.InvokeFunction, {
385
+ invocation_id,
386
+ function_id,
387
+ data: payload,
388
+ action
389
+ });
390
+ });
391
+ };
392
+ this.listFunctions = async () => {
393
+ return (await this.trigger({
394
+ function_id: EngineFunctions.LIST_FUNCTIONS,
395
+ payload: {}
396
+ })).functions;
397
+ };
398
+ this.listWorkers = async () => {
399
+ return (await this.trigger({
400
+ function_id: EngineFunctions.LIST_WORKERS,
401
+ payload: {}
402
+ })).workers;
403
+ };
404
+ this.listTriggers = async (includeInternal = false) => {
405
+ return (await this.trigger({
406
+ function_id: EngineFunctions.LIST_TRIGGERS,
407
+ payload: { include_internal: includeInternal }
408
+ })).triggers;
409
+ };
410
+ this.listTriggerTypes = async (includeInternal = false) => {
411
+ return (await this.trigger({
412
+ function_id: EngineFunctions.LIST_TRIGGER_TYPES,
413
+ payload: { include_internal: includeInternal }
414
+ })).trigger_types;
415
+ };
416
+ this.createStream = (streamName, stream) => {
417
+ this.registerFunction({ id: `stream::get(${streamName})` }, stream.get.bind(stream));
418
+ this.registerFunction({ id: `stream::set(${streamName})` }, stream.set.bind(stream));
419
+ this.registerFunction({ id: `stream::delete(${streamName})` }, stream.delete.bind(stream));
420
+ this.registerFunction({ id: `stream::list(${streamName})` }, stream.list.bind(stream));
421
+ this.registerFunction({ id: `stream::list_groups(${streamName})` }, stream.listGroups.bind(stream));
422
+ };
423
+ this.onFunctionsAvailable = (callback) => {
424
+ this.functionsAvailableCallbacks.add(callback);
425
+ if (!this.functionsAvailableTrigger) {
426
+ if (!this.functionsAvailableFunctionPath) this.functionsAvailableFunctionPath = `engine.on_functions_available.${crypto.randomUUID()}`;
427
+ const function_id = this.functionsAvailableFunctionPath;
428
+ if (!this.functions.has(function_id)) this.registerFunction({ id: function_id }, async ({ functions }) => {
429
+ this.functionsAvailableCallbacks.forEach((handler) => {
430
+ handler(functions);
431
+ });
432
+ return null;
433
+ });
434
+ this.functionsAvailableTrigger = this.registerTrigger({
435
+ type: EngineTriggers.FUNCTIONS_AVAILABLE,
436
+ function_id,
437
+ config: {}
438
+ });
439
+ }
440
+ return () => {
441
+ this.functionsAvailableCallbacks.delete(callback);
442
+ if (this.functionsAvailableCallbacks.size === 0 && this.functionsAvailableTrigger) {
443
+ this.functionsAvailableTrigger.unregister();
444
+ this.functionsAvailableTrigger = void 0;
445
+ }
446
+ };
447
+ };
448
+ this.shutdown = async () => {
449
+ this.isShuttingDown = true;
450
+ this.clearReconnectTimeout();
451
+ for (const [_id, invocation] of this.invocations) {
452
+ if (invocation.timeout) clearTimeout(invocation.timeout);
453
+ invocation.reject(/* @__PURE__ */ new Error("iii is shutting down"));
454
+ }
455
+ this.invocations.clear();
456
+ if (this.ws) {
457
+ this.ws.onopen = null;
458
+ this.ws.onclose = null;
459
+ this.ws.onerror = null;
460
+ this.ws.onmessage = null;
461
+ this.ws.close();
462
+ this.ws = void 0;
463
+ }
464
+ this.setConnectionState("disconnected");
465
+ };
466
+ this.workerName = options?.workerName ?? getDefaultWorkerName();
467
+ this.invocationTimeoutMs = options?.invocationTimeoutMs ?? 3e4;
468
+ this.reconnectionConfig = {
469
+ ...DEFAULT_BRIDGE_RECONNECTION_CONFIG,
470
+ ...options?.reconnectionConfig
471
+ };
472
+ this.connect();
473
+ }
474
+ registerWorkerMetadata() {
475
+ const telemetryOpts = this.options?.telemetry;
476
+ const language = telemetryOpts?.language ?? (typeof navigator !== "undefined" ? navigator.language : void 0);
477
+ this.trigger({
478
+ function_id: EngineFunctions.REGISTER_WORKER,
479
+ payload: {
480
+ runtime: "browser",
481
+ version: SDK_VERSION,
482
+ name: this.workerName,
483
+ os: getBrowserInfo(),
484
+ pid: 0,
485
+ telemetry: {
486
+ language,
487
+ project_name: telemetryOpts?.project_name,
488
+ framework: telemetryOpts?.framework,
489
+ amplitude_api_key: telemetryOpts?.amplitude_api_key
490
+ }
491
+ },
492
+ action: { type: "void" }
493
+ });
494
+ }
495
+ setConnectionState(state) {
496
+ if (this.connectionState !== state) this.connectionState = state;
497
+ }
498
+ connect() {
499
+ if (this.isShuttingDown) return;
500
+ this.setConnectionState("connecting");
501
+ this.ws = new WebSocket(this.address);
502
+ this.ws.onopen = this.onSocketOpen.bind(this);
503
+ this.ws.onclose = this.onSocketClose.bind(this);
504
+ this.ws.onerror = this.onSocketError.bind(this);
505
+ }
506
+ clearReconnectTimeout() {
507
+ if (this.reconnectTimeout) {
508
+ clearTimeout(this.reconnectTimeout);
509
+ this.reconnectTimeout = void 0;
510
+ }
511
+ }
512
+ scheduleReconnect() {
513
+ if (this.isShuttingDown) return;
514
+ const { maxRetries, initialDelayMs, backoffMultiplier, maxDelayMs, jitterFactor } = this.reconnectionConfig;
515
+ if (maxRetries !== -1 && this.reconnectAttempt >= maxRetries) {
516
+ this.setConnectionState("failed");
517
+ console.error(`[iii] Max reconnection retries (${maxRetries}) reached, giving up`);
518
+ return;
519
+ }
520
+ if (this.reconnectTimeout) return;
521
+ const exponentialDelay = initialDelayMs * backoffMultiplier ** this.reconnectAttempt;
522
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
523
+ const jitter = cappedDelay * jitterFactor * (2 * Math.random() - 1);
524
+ const delay = Math.floor(cappedDelay + jitter);
525
+ this.setConnectionState("reconnecting");
526
+ console.debug(`[iii] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt + 1})...`);
527
+ this.reconnectTimeout = setTimeout(() => {
528
+ this.reconnectTimeout = void 0;
529
+ this.reconnectAttempt++;
530
+ this.connect();
531
+ }, delay);
532
+ }
533
+ onSocketError() {
534
+ console.error("[iii] WebSocket error");
535
+ }
536
+ onSocketClose() {
537
+ if (this.ws) {
538
+ this.ws.onopen = null;
539
+ this.ws.onclose = null;
540
+ this.ws.onerror = null;
541
+ this.ws.onmessage = null;
542
+ }
543
+ this.ws = void 0;
544
+ this.setConnectionState("disconnected");
545
+ this.scheduleReconnect();
546
+ }
547
+ onSocketOpen() {
548
+ this.clearReconnectTimeout();
549
+ this.reconnectAttempt = 0;
550
+ this.setConnectionState("connected");
551
+ if (this.ws) this.ws.onmessage = this.onMessage.bind(this);
552
+ this.triggerTypes.forEach(({ message }) => {
553
+ this.sendMessage(MessageType.RegisterTriggerType, message, true);
554
+ });
555
+ this.services.forEach((service) => {
556
+ this.sendMessage(MessageType.RegisterService, service, true);
557
+ });
558
+ this.functions.forEach(({ message }) => {
559
+ this.sendMessage(MessageType.RegisterFunction, message, true);
560
+ });
561
+ this.triggers.forEach((trigger) => {
562
+ this.sendMessage(MessageType.RegisterTrigger, trigger, true);
563
+ });
564
+ const pending = this.messagesToSend;
565
+ this.messagesToSend = [];
566
+ for (const message of pending) {
567
+ if (message.type === MessageType.InvokeFunction && typeof message.invocation_id === "string" && !this.invocations.has(message.invocation_id)) continue;
568
+ this.sendMessageRaw(JSON.stringify(message));
569
+ }
570
+ this.registerWorkerMetadata();
571
+ }
572
+ isOpen() {
573
+ return this.ws?.readyState === WebSocket.OPEN;
574
+ }
575
+ sendMessageRaw(data) {
576
+ if (this.ws && this.isOpen()) try {
577
+ this.ws.send(data);
578
+ } catch (error) {
579
+ console.error("[iii] Exception while sending message", error);
580
+ }
581
+ }
582
+ toWireFormat(messageType, message) {
583
+ const { message_type: _, ...rest } = message;
584
+ if (messageType === MessageType.RegisterTrigger && "type" in message) {
585
+ const { type: triggerType, ...triggerRest } = message;
586
+ return {
587
+ type: messageType,
588
+ ...triggerRest,
589
+ trigger_type: triggerType
590
+ };
591
+ }
592
+ if (messageType === MessageType.UnregisterTrigger && "type" in message) {
593
+ const { type: triggerType, ...triggerRest } = message;
594
+ return {
595
+ type: messageType,
596
+ ...triggerRest,
597
+ trigger_type: triggerType
598
+ };
599
+ }
600
+ if (messageType === MessageType.TriggerRegistrationResult && "type" in message) {
601
+ const { type: triggerType, ...resultRest } = message;
602
+ return {
603
+ type: messageType,
604
+ ...resultRest,
605
+ trigger_type: triggerType
606
+ };
607
+ }
608
+ return {
609
+ type: messageType,
610
+ ...rest
611
+ };
612
+ }
613
+ sendMessage(messageType, message, skipIfClosed = false) {
614
+ const wireMessage = this.toWireFormat(messageType, message);
615
+ if (this.isOpen()) this.sendMessageRaw(JSON.stringify(wireMessage));
616
+ else if (!skipIfClosed) this.messagesToSend.push(wireMessage);
617
+ }
618
+ onInvocationResult(invocation_id, result, error) {
619
+ const invocation = this.invocations.get(invocation_id);
620
+ if (invocation) {
621
+ if (invocation.timeout) clearTimeout(invocation.timeout);
622
+ error ? invocation.reject(error) : invocation.resolve(result);
623
+ }
624
+ this.invocations.delete(invocation_id);
625
+ }
626
+ resolveChannelValue(value) {
627
+ if (isChannelRef(value)) return value.direction === "read" ? new ChannelReader(this.address, value) : new ChannelWriter(this.address, value);
628
+ if (Array.isArray(value)) return value.map((item) => this.resolveChannelValue(item));
629
+ if (value !== null && typeof value === "object") {
630
+ const out = {};
631
+ for (const [k, v] of Object.entries(value)) out[k] = this.resolveChannelValue(v);
632
+ return out;
633
+ }
634
+ return value;
635
+ }
636
+ async onInvokeFunction(invocation_id, function_id, input, traceparent, baggage) {
637
+ const fn = this.functions.get(function_id);
638
+ const resolvedInput = this.resolveChannelValue(input);
639
+ if (fn?.handler) {
640
+ if (!invocation_id) {
641
+ try {
642
+ await fn.handler(resolvedInput, traceparent, baggage);
643
+ } catch (error) {
644
+ console.error(`[iii] Error invoking function ${function_id}`, error);
645
+ }
646
+ return;
647
+ }
648
+ try {
649
+ const result = await fn.handler(resolvedInput, traceparent, baggage);
650
+ this.sendMessage(MessageType.InvocationResult, {
651
+ invocation_id,
652
+ function_id,
653
+ result,
654
+ traceparent,
655
+ baggage
656
+ });
657
+ } catch (error) {
658
+ const isError = error instanceof Error;
659
+ this.sendMessage(MessageType.InvocationResult, {
660
+ invocation_id,
661
+ function_id,
662
+ error: {
663
+ code: "invocation_failed",
664
+ message: isError ? error.message : String(error),
665
+ stacktrace: isError ? error.stack : void 0
666
+ },
667
+ traceparent,
668
+ baggage
669
+ });
670
+ }
671
+ } else {
672
+ const errorCode = fn ? "function_not_invokable" : "function_not_found";
673
+ const errorMessage = fn ? "Function is HTTP-invoked and cannot be invoked locally" : "Function not found";
674
+ if (invocation_id) this.sendMessage(MessageType.InvocationResult, {
675
+ invocation_id,
676
+ function_id,
677
+ error: {
678
+ code: errorCode,
679
+ message: errorMessage
680
+ },
681
+ traceparent,
682
+ baggage
683
+ });
684
+ }
685
+ }
686
+ async onRegisterTrigger(message) {
687
+ const { trigger_type, id, function_id, config } = message;
688
+ const triggerTypeData = this.triggerTypes.get(trigger_type);
689
+ if (triggerTypeData) try {
690
+ await triggerTypeData.handler.registerTrigger({
691
+ id,
692
+ function_id,
693
+ config
694
+ });
695
+ this.sendMessage(MessageType.TriggerRegistrationResult, {
696
+ id,
697
+ message_type: MessageType.TriggerRegistrationResult,
698
+ type: trigger_type,
699
+ function_id
700
+ });
701
+ } catch (error) {
702
+ this.sendMessage(MessageType.TriggerRegistrationResult, {
703
+ id,
704
+ message_type: MessageType.TriggerRegistrationResult,
705
+ type: trigger_type,
706
+ function_id,
707
+ error: {
708
+ code: "trigger_registration_failed",
709
+ message: error.message
710
+ }
711
+ });
712
+ }
713
+ else this.sendMessage(MessageType.TriggerRegistrationResult, {
714
+ id,
715
+ message_type: MessageType.TriggerRegistrationResult,
716
+ type: trigger_type,
717
+ function_id,
718
+ error: {
719
+ code: "trigger_type_not_found",
720
+ message: "Trigger type not found"
721
+ }
722
+ });
723
+ }
724
+ onMessage(event) {
725
+ let msgType;
726
+ let message;
727
+ try {
728
+ const parsed = JSON.parse(typeof event.data === "string" ? event.data : "");
729
+ msgType = parsed.type;
730
+ const { type: _, ...rest } = parsed;
731
+ message = rest;
732
+ } catch (error) {
733
+ console.error("[iii] Failed to parse incoming message", error);
734
+ return;
735
+ }
736
+ if (msgType === MessageType.InvocationResult) {
737
+ const { invocation_id, result, error } = message;
738
+ this.onInvocationResult(invocation_id, result, error);
739
+ } else if (msgType === MessageType.InvokeFunction) {
740
+ const { invocation_id, function_id, data, traceparent, baggage } = message;
741
+ this.onInvokeFunction(invocation_id, function_id, data, traceparent, baggage);
742
+ } else if (msgType === MessageType.RegisterTrigger) this.onRegisterTrigger(message);
743
+ else if (msgType === MessageType.WorkerRegistered) {
744
+ const { worker_id } = message;
745
+ this.workerId = worker_id;
746
+ console.debug("[iii] Worker registered with ID:", worker_id);
747
+ }
748
+ }
749
+ };
750
+ /**
751
+ * Factory object that constructs routing actions for {@link ISdk.trigger}.
752
+ *
753
+ * @example
754
+ * ```typescript
755
+ * import { TriggerAction } from 'iii-browser-sdk'
756
+ *
757
+ * // Enqueue to a named queue
758
+ * iii.trigger({
759
+ * function_id: 'process',
760
+ * payload: { data: 'hello' },
761
+ * action: TriggerAction.Enqueue({ queue: 'jobs' }),
762
+ * })
763
+ *
764
+ * // Fire-and-forget
765
+ * iii.trigger({
766
+ * function_id: 'notify',
767
+ * payload: {},
768
+ * action: TriggerAction.Void(),
769
+ * })
770
+ * ```
771
+ */
772
+ const TriggerAction = {
773
+ Enqueue: (opts) => ({
774
+ type: "enqueue",
775
+ ...opts
776
+ }),
777
+ Void: () => ({ type: "void" })
778
+ };
779
+ /**
780
+ * Creates and returns a connected SDK instance. The WebSocket connection is
781
+ * established automatically -- there is no separate `connect()` call.
782
+ *
783
+ * @param address - WebSocket URL of the III engine (e.g. `ws://localhost:49135`).
784
+ * @param options - Optional {@link InitOptions} for worker name, timeouts, and reconnection.
785
+ * @returns A connected {@link ISdk} instance.
786
+ *
787
+ * @example
788
+ * ```typescript
789
+ * import { registerWorker } from 'iii-browser-sdk'
790
+ *
791
+ * const iii = registerWorker('ws://localhost:49135', {
792
+ * workerName: 'my-browser-worker',
793
+ * })
794
+ * ```
795
+ */
796
+ const registerWorker = (address, options) => new Sdk(address, options);
797
+
798
+ //#endregion
799
+ export { ChannelReader, ChannelWriter, TriggerAction, registerWorker };
800
+ //# sourceMappingURL=index.mjs.map