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