iii-browser-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 +38 -0
- package/dist/helpers.d.cts.map +1 -0
- package/dist/helpers.d.mts +38 -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 +8 -183
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -660
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1 -660
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -177
- 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 +239 -2
- package/dist/stream.d.cts.map +1 -0
- package/dist/stream.d.mts +239 -2
- package/dist/stream.d.mts.map +1 -0
- package/dist/types-Bw88g_Jj.d.mts +644 -0
- package/dist/types-Bw88g_Jj.d.mts.map +1 -0
- package/dist/types-CP-lPyex.d.cts +644 -0
- package/dist/types-CP-lPyex.d.cts.map +1 -0
- package/dist/utils-Cq61lImK.cjs +267 -0
- package/dist/utils-Cq61lImK.cjs.map +1 -0
- package/dist/utils-CwkqVmBx.mjs +231 -0
- package/dist/utils-CwkqVmBx.mjs.map +1 -0
- package/package.json +6 -1
- package/dist/stream-4S6io5EI.d.mts +0 -239
- package/dist/stream-4S6io5EI.d.mts.map +0 -1
- package/dist/stream-DUoMZNRq.d.cts +0 -239
- package/dist/stream-DUoMZNRq.d.cts.map +0 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/channels.ts
|
|
3
|
+
/**
|
|
4
|
+
* Direction of a streaming channel endpoint. Mirrors the Rust SDK's
|
|
5
|
+
* `ChannelDirection` enum and matches the literal values used by
|
|
6
|
+
* {@link StreamChannelRef.direction}.
|
|
7
|
+
*/
|
|
8
|
+
const ChannelDirection = {
|
|
9
|
+
Read: "read",
|
|
10
|
+
Write: "write"
|
|
11
|
+
};
|
|
12
|
+
const ChannelItem = {
|
|
13
|
+
Text(value) {
|
|
14
|
+
return {
|
|
15
|
+
type: "text",
|
|
16
|
+
value
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
Binary(value) {
|
|
20
|
+
return {
|
|
21
|
+
type: "binary",
|
|
22
|
+
value
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Write end of a streaming channel. Uses native browser WebSocket.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* import { createChannel } from 'iii-browser-sdk/helpers'
|
|
32
|
+
* const channel = await createChannel(iii)
|
|
33
|
+
*
|
|
34
|
+
* channel.writer.sendMessage(JSON.stringify({ type: 'event', data: 'test' }))
|
|
35
|
+
* channel.writer.sendBinary(new Uint8Array([1, 2, 3]))
|
|
36
|
+
* channel.writer.close()
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
var ChannelWriter = class ChannelWriter {
|
|
40
|
+
static {
|
|
41
|
+
this.FRAME_SIZE = 64 * 1024;
|
|
42
|
+
}
|
|
43
|
+
constructor(engineWsBase, ref) {
|
|
44
|
+
this.ws = null;
|
|
45
|
+
this.wsReady = false;
|
|
46
|
+
this.pendingMessages = [];
|
|
47
|
+
this.url = buildChannelUrl(engineWsBase, ref.channel_id, ref.access_key, "write");
|
|
48
|
+
}
|
|
49
|
+
ensureConnected() {
|
|
50
|
+
if (this.ws) return;
|
|
51
|
+
this.ws = new WebSocket(this.url);
|
|
52
|
+
this.ws.binaryType = "arraybuffer";
|
|
53
|
+
this.ws.addEventListener("open", () => {
|
|
54
|
+
this.wsReady = true;
|
|
55
|
+
for (const { data, resolve, reject } of this.pendingMessages) try {
|
|
56
|
+
this.ws?.send(data);
|
|
57
|
+
resolve();
|
|
58
|
+
} catch (err) {
|
|
59
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
60
|
+
}
|
|
61
|
+
this.pendingMessages.length = 0;
|
|
62
|
+
});
|
|
63
|
+
this.ws.addEventListener("error", () => {
|
|
64
|
+
for (const { reject } of this.pendingMessages) reject(/* @__PURE__ */ new Error("WebSocket error"));
|
|
65
|
+
this.pendingMessages.length = 0;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Send a text message through the channel. */
|
|
69
|
+
sendMessage(msg) {
|
|
70
|
+
this.ensureConnected();
|
|
71
|
+
this.sendRaw(msg);
|
|
72
|
+
}
|
|
73
|
+
/** Send binary data through the channel. */
|
|
74
|
+
sendBinary(data) {
|
|
75
|
+
this.ensureConnected();
|
|
76
|
+
let offset = 0;
|
|
77
|
+
while (offset < data.length) {
|
|
78
|
+
const end = Math.min(offset + ChannelWriter.FRAME_SIZE, data.length);
|
|
79
|
+
const chunk = data.subarray(offset, end);
|
|
80
|
+
const buffer = chunk.buffer instanceof ArrayBuffer ? chunk.buffer : new ArrayBuffer(chunk.byteLength);
|
|
81
|
+
if (!(chunk.buffer instanceof ArrayBuffer)) new Uint8Array(buffer).set(chunk);
|
|
82
|
+
this.sendRaw(buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
|
|
83
|
+
offset = end;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Close the channel writer. */
|
|
87
|
+
close() {
|
|
88
|
+
if (!this.ws) return;
|
|
89
|
+
const doClose = () => {
|
|
90
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.close(1e3, "channel_close");
|
|
91
|
+
};
|
|
92
|
+
if (this.wsReady) doClose();
|
|
93
|
+
else this.ws.addEventListener("open", () => doClose());
|
|
94
|
+
}
|
|
95
|
+
sendRaw(data) {
|
|
96
|
+
if (this.wsReady && this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.send(data);
|
|
97
|
+
else {
|
|
98
|
+
this.ensureConnected();
|
|
99
|
+
this.pendingMessages.push({
|
|
100
|
+
data,
|
|
101
|
+
resolve: () => {},
|
|
102
|
+
reject: () => {
|
|
103
|
+
console.error("Failed to send message");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Read end of a streaming channel. Uses native browser WebSocket.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* import { createChannel } from 'iii-browser-sdk/helpers'
|
|
115
|
+
* const channel = await createChannel(iii)
|
|
116
|
+
*
|
|
117
|
+
* channel.reader.onMessage((msg) => console.log('Got:', msg))
|
|
118
|
+
* channel.reader.onBinary((data) => console.log('Binary:', data.byteLength))
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
var ChannelReader = class {
|
|
122
|
+
constructor(engineWsBase, ref) {
|
|
123
|
+
this.ws = null;
|
|
124
|
+
this.connected = false;
|
|
125
|
+
this.messageCallbacks = [];
|
|
126
|
+
this.binaryCallbacks = [];
|
|
127
|
+
this.url = buildChannelUrl(engineWsBase, ref.channel_id, ref.access_key, "read");
|
|
128
|
+
}
|
|
129
|
+
ensureConnected() {
|
|
130
|
+
if (this.connected) return;
|
|
131
|
+
this.connected = true;
|
|
132
|
+
this.ws = new WebSocket(this.url);
|
|
133
|
+
this.ws.binaryType = "arraybuffer";
|
|
134
|
+
this.ws.addEventListener("message", (event) => {
|
|
135
|
+
if (event.data instanceof ArrayBuffer) {
|
|
136
|
+
const data = new Uint8Array(event.data);
|
|
137
|
+
for (const cb of this.binaryCallbacks) cb(data);
|
|
138
|
+
} else if (typeof event.data === "string") for (const cb of this.messageCallbacks) cb(event.data);
|
|
139
|
+
});
|
|
140
|
+
this.ws.addEventListener("close", () => {
|
|
141
|
+
this.ws = null;
|
|
142
|
+
});
|
|
143
|
+
this.ws.addEventListener("error", () => {
|
|
144
|
+
this.ws = null;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Register a callback to receive text messages from the channel. */
|
|
148
|
+
onMessage(callback) {
|
|
149
|
+
this.messageCallbacks.push(callback);
|
|
150
|
+
this.ensureConnected();
|
|
151
|
+
}
|
|
152
|
+
/** Register a callback to receive binary data from the channel. */
|
|
153
|
+
onBinary(callback) {
|
|
154
|
+
this.binaryCallbacks.push(callback);
|
|
155
|
+
this.ensureConnected();
|
|
156
|
+
}
|
|
157
|
+
/** Read all binary data from the channel until it closes. */
|
|
158
|
+
async readAll() {
|
|
159
|
+
this.ensureConnected();
|
|
160
|
+
const chunks = [];
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
const onData = (data) => {
|
|
163
|
+
chunks.push(data);
|
|
164
|
+
};
|
|
165
|
+
this.binaryCallbacks.push(onData);
|
|
166
|
+
const originalWs = this.ws;
|
|
167
|
+
if (originalWs) originalWs.addEventListener("close", () => {
|
|
168
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
169
|
+
const result = new Uint8Array(totalLength);
|
|
170
|
+
let offset = 0;
|
|
171
|
+
for (const chunk of chunks) {
|
|
172
|
+
result.set(chunk, offset);
|
|
173
|
+
offset += chunk.length;
|
|
174
|
+
}
|
|
175
|
+
resolve(result);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** Close the channel reader. */
|
|
180
|
+
close() {
|
|
181
|
+
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) this.ws.close(1e3, "channel_close");
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
function buildChannelUrl(engineWsBase, channelId, accessKey, direction) {
|
|
185
|
+
return `${engineWsBase.replace(/\/$/, "")}/ws/channels/${channelId}?key=${encodeURIComponent(accessKey)}&dir=${direction}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/utils.ts
|
|
190
|
+
/**
|
|
191
|
+
* Type guard that checks if a value is a {@link StreamChannelRef}.
|
|
192
|
+
*
|
|
193
|
+
* @param value - Value to check.
|
|
194
|
+
* @returns `true` if the value is a valid `StreamChannelRef`.
|
|
195
|
+
*/
|
|
196
|
+
const isChannelRef = (value) => {
|
|
197
|
+
if (typeof value !== "object" || value === null) return false;
|
|
198
|
+
const maybe = value;
|
|
199
|
+
return typeof maybe.channel_id === "string" && typeof maybe.access_key === "string" && (maybe.direction === "read" || maybe.direction === "write");
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Recursively extract all {@link StreamChannelRef} values from a JSON-like
|
|
203
|
+
* input, returning each match paired with its dotted/bracketed path. Mirrors
|
|
204
|
+
* the Rust SDK's `extract_channel_refs`.
|
|
205
|
+
*
|
|
206
|
+
* @param data - Arbitrary JSON-like value.
|
|
207
|
+
* @returns Array of `[path, ref]` tuples. Empty when no refs are found.
|
|
208
|
+
*/
|
|
209
|
+
const extractChannelRefs = (data) => {
|
|
210
|
+
const refs = [];
|
|
211
|
+
extractRefsRecursive(data, "", refs);
|
|
212
|
+
return refs;
|
|
213
|
+
};
|
|
214
|
+
const extractRefsRecursive = (data, prefix, refs) => {
|
|
215
|
+
if (isChannelRef(data)) {
|
|
216
|
+
refs.push([prefix, data]);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (Array.isArray(data)) {
|
|
220
|
+
for (let i = 0; i < data.length; i++) {
|
|
221
|
+
const path = prefix === "" ? `[${i}]` : `${prefix}[${i}]`;
|
|
222
|
+
extractRefsRecursive(data[i], path, refs);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (typeof data !== "object" || data === null) return;
|
|
227
|
+
for (const [key, value] of Object.entries(data)) extractRefsRecursive(value, prefix === "" ? key : `${prefix}.${key}`, refs);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
//#endregion
|
|
231
|
+
Object.defineProperty(exports, 'ChannelDirection', {
|
|
232
|
+
enumerable: true,
|
|
233
|
+
get: function () {
|
|
234
|
+
return ChannelDirection;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
Object.defineProperty(exports, 'ChannelItem', {
|
|
238
|
+
enumerable: true,
|
|
239
|
+
get: function () {
|
|
240
|
+
return ChannelItem;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
Object.defineProperty(exports, 'ChannelReader', {
|
|
244
|
+
enumerable: true,
|
|
245
|
+
get: function () {
|
|
246
|
+
return ChannelReader;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
Object.defineProperty(exports, 'ChannelWriter', {
|
|
250
|
+
enumerable: true,
|
|
251
|
+
get: function () {
|
|
252
|
+
return ChannelWriter;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
Object.defineProperty(exports, 'extractChannelRefs', {
|
|
256
|
+
enumerable: true,
|
|
257
|
+
get: function () {
|
|
258
|
+
return extractChannelRefs;
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
Object.defineProperty(exports, 'isChannelRef', {
|
|
262
|
+
enumerable: true,
|
|
263
|
+
get: function () {
|
|
264
|
+
return isChannelRef;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
//# sourceMappingURL=utils-Cq61lImK.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils-Cq61lImK.cjs","names":[],"sources":["../src/channels.ts","../src/utils.ts"],"sourcesContent":["import 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. Uses native browser WebSocket.\n *\n * @example\n * ```typescript\n * import { createChannel } from 'iii-browser-sdk/helpers'\n * const channel = await createChannel(iii)\n *\n * channel.writer.sendMessage(JSON.stringify({ type: 'event', data: 'test' }))\n * channel.writer.sendBinary(new Uint8Array([1, 2, 3]))\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: ArrayBuffer | string\n resolve: () => void\n reject: (err: Error) => void\n }[] = []\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\n private ensureConnected(): void {\n if (this.ws) return\n\n this.ws = new WebSocket(this.url)\n this.ws.binaryType = 'arraybuffer'\n\n this.ws.addEventListener('open', () => {\n this.wsReady = true\n for (const { data, resolve, reject } of this.pendingMessages) {\n try {\n this.ws?.send(data)\n resolve()\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n }\n }\n this.pendingMessages.length = 0\n })\n\n this.ws.addEventListener('error', () => {\n for (const { reject } of this.pendingMessages) {\n reject(new Error('WebSocket error'))\n }\n this.pendingMessages.length = 0\n })\n }\n\n /** Send a text message through the channel. */\n sendMessage(msg: string): void {\n this.ensureConnected()\n this.sendRaw(msg)\n }\n\n /** Send binary data through the channel. */\n sendBinary(data: Uint8Array): void {\n this.ensureConnected()\n\n let offset = 0\n while (offset < data.length) {\n const end = Math.min(offset + ChannelWriter.FRAME_SIZE, data.length)\n const chunk = data.subarray(offset, end)\n const buffer = chunk.buffer instanceof ArrayBuffer ? chunk.buffer : new ArrayBuffer(chunk.byteLength)\n if (!(chunk.buffer instanceof ArrayBuffer)) {\n new Uint8Array(buffer).set(chunk)\n }\n this.sendRaw(buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength))\n offset = end\n }\n }\n\n /** Close the channel writer. */\n close(): void {\n if (!this.ws) return\n\n const doClose = () => {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.close(1000, 'channel_close')\n }\n }\n\n if (this.wsReady) {\n doClose()\n } else {\n this.ws.addEventListener('open', () => doClose())\n }\n }\n\n private sendRaw(data: ArrayBuffer | string): void {\n if (this.wsReady && this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(data)\n } else {\n this.ensureConnected()\n this.pendingMessages.push({\n data,\n resolve: () => {\n //\n },\n reject: () => {\n console.error('Failed to send message')\n },\n })\n }\n }\n}\n\n/**\n * Read end of a streaming channel. Uses native browser WebSocket.\n *\n * @example\n * ```typescript\n * import { createChannel } from 'iii-browser-sdk/helpers'\n * const channel = await createChannel(iii)\n *\n * channel.reader.onMessage((msg) => console.log('Got:', msg))\n * channel.reader.onBinary((data) => console.log('Binary:', data.byteLength))\n * ```\n */\nexport class ChannelReader {\n private ws: WebSocket | null = null\n private connected = false\n private readonly messageCallbacks: Array<(msg: string) => void> = []\n private readonly binaryCallbacks: Array<(data: Uint8Array) => void> = []\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\n private ensureConnected(): void {\n if (this.connected) return\n this.connected = true\n\n this.ws = new WebSocket(this.url)\n this.ws.binaryType = 'arraybuffer'\n\n this.ws.addEventListener('message', (event: MessageEvent) => {\n if (event.data instanceof ArrayBuffer) {\n const data = new Uint8Array(event.data)\n for (const cb of this.binaryCallbacks) {\n cb(data)\n }\n } else if (typeof event.data === 'string') {\n for (const cb of this.messageCallbacks) {\n cb(event.data)\n }\n }\n })\n\n this.ws.addEventListener('close', () => {\n this.ws = null\n })\n\n this.ws.addEventListener('error', () => {\n this.ws = null\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 this.ensureConnected()\n }\n\n /** Register a callback to receive binary data from the channel. */\n onBinary(callback: (data: Uint8Array) => void): void {\n this.binaryCallbacks.push(callback)\n this.ensureConnected()\n }\n\n /** Read all binary data from the channel until it closes. */\n async readAll(): Promise<Uint8Array> {\n this.ensureConnected()\n const chunks: Uint8Array[] = []\n\n return new Promise<Uint8Array>((resolve) => {\n const onData = (data: Uint8Array) => {\n chunks.push(data)\n }\n this.binaryCallbacks.push(onData)\n\n const originalWs = this.ws\n if (originalWs) {\n originalWs.addEventListener('close', () => {\n const totalLength = chunks.reduce((sum, c) => sum + c.length, 0)\n const result = new Uint8Array(totalLength)\n let offset = 0\n for (const chunk of chunks) {\n result.set(chunk, offset)\n offset += chunk.length\n }\n resolve(result)\n })\n }\n })\n }\n\n /** Close the channel reader. */\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 type { StreamChannelRef } from './iii-types'\n\n/**\n * Safely stringify a value, handling circular references, BigInt, and other edge cases.\n * Returns \"[unserializable]\" if serialization fails for any reason.\n */\nexport function safeStringify(value: unknown): string {\n const seen = new WeakSet<object>()\n\n try {\n return JSON.stringify(value, (_key, val) => {\n if (typeof val === 'bigint') {\n return val.toString()\n }\n\n if (val !== null && typeof val === 'object') {\n if (seen.has(val)) {\n return '[Circular]'\n }\n seen.add(val)\n }\n\n return val\n })\n } catch {\n return '[unserializable]'\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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"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
//#region src/channels.ts
|
|
2
|
+
/**
|
|
3
|
+
* Direction of a streaming channel endpoint. Mirrors the Rust SDK's
|
|
4
|
+
* `ChannelDirection` enum and matches the literal values used by
|
|
5
|
+
* {@link StreamChannelRef.direction}.
|
|
6
|
+
*/
|
|
7
|
+
const ChannelDirection = {
|
|
8
|
+
Read: "read",
|
|
9
|
+
Write: "write"
|
|
10
|
+
};
|
|
11
|
+
const ChannelItem = {
|
|
12
|
+
Text(value) {
|
|
13
|
+
return {
|
|
14
|
+
type: "text",
|
|
15
|
+
value
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
Binary(value) {
|
|
19
|
+
return {
|
|
20
|
+
type: "binary",
|
|
21
|
+
value
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Write end of a streaming channel. Uses native browser WebSocket.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import { createChannel } from 'iii-browser-sdk/helpers'
|
|
31
|
+
* const channel = await createChannel(iii)
|
|
32
|
+
*
|
|
33
|
+
* channel.writer.sendMessage(JSON.stringify({ type: 'event', data: 'test' }))
|
|
34
|
+
* channel.writer.sendBinary(new Uint8Array([1, 2, 3]))
|
|
35
|
+
* channel.writer.close()
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
var ChannelWriter = class ChannelWriter {
|
|
39
|
+
static {
|
|
40
|
+
this.FRAME_SIZE = 64 * 1024;
|
|
41
|
+
}
|
|
42
|
+
constructor(engineWsBase, ref) {
|
|
43
|
+
this.ws = null;
|
|
44
|
+
this.wsReady = false;
|
|
45
|
+
this.pendingMessages = [];
|
|
46
|
+
this.url = buildChannelUrl(engineWsBase, ref.channel_id, ref.access_key, "write");
|
|
47
|
+
}
|
|
48
|
+
ensureConnected() {
|
|
49
|
+
if (this.ws) return;
|
|
50
|
+
this.ws = new WebSocket(this.url);
|
|
51
|
+
this.ws.binaryType = "arraybuffer";
|
|
52
|
+
this.ws.addEventListener("open", () => {
|
|
53
|
+
this.wsReady = true;
|
|
54
|
+
for (const { data, resolve, reject } of this.pendingMessages) try {
|
|
55
|
+
this.ws?.send(data);
|
|
56
|
+
resolve();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
59
|
+
}
|
|
60
|
+
this.pendingMessages.length = 0;
|
|
61
|
+
});
|
|
62
|
+
this.ws.addEventListener("error", () => {
|
|
63
|
+
for (const { reject } of this.pendingMessages) reject(/* @__PURE__ */ new Error("WebSocket error"));
|
|
64
|
+
this.pendingMessages.length = 0;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Send a text message through the channel. */
|
|
68
|
+
sendMessage(msg) {
|
|
69
|
+
this.ensureConnected();
|
|
70
|
+
this.sendRaw(msg);
|
|
71
|
+
}
|
|
72
|
+
/** Send binary data through the channel. */
|
|
73
|
+
sendBinary(data) {
|
|
74
|
+
this.ensureConnected();
|
|
75
|
+
let offset = 0;
|
|
76
|
+
while (offset < data.length) {
|
|
77
|
+
const end = Math.min(offset + ChannelWriter.FRAME_SIZE, data.length);
|
|
78
|
+
const chunk = data.subarray(offset, end);
|
|
79
|
+
const buffer = chunk.buffer instanceof ArrayBuffer ? chunk.buffer : new ArrayBuffer(chunk.byteLength);
|
|
80
|
+
if (!(chunk.buffer instanceof ArrayBuffer)) new Uint8Array(buffer).set(chunk);
|
|
81
|
+
this.sendRaw(buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
|
|
82
|
+
offset = end;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Close the channel writer. */
|
|
86
|
+
close() {
|
|
87
|
+
if (!this.ws) return;
|
|
88
|
+
const doClose = () => {
|
|
89
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.close(1e3, "channel_close");
|
|
90
|
+
};
|
|
91
|
+
if (this.wsReady) doClose();
|
|
92
|
+
else this.ws.addEventListener("open", () => doClose());
|
|
93
|
+
}
|
|
94
|
+
sendRaw(data) {
|
|
95
|
+
if (this.wsReady && this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.send(data);
|
|
96
|
+
else {
|
|
97
|
+
this.ensureConnected();
|
|
98
|
+
this.pendingMessages.push({
|
|
99
|
+
data,
|
|
100
|
+
resolve: () => {},
|
|
101
|
+
reject: () => {
|
|
102
|
+
console.error("Failed to send message");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Read end of a streaming channel. Uses native browser WebSocket.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* import { createChannel } from 'iii-browser-sdk/helpers'
|
|
114
|
+
* const channel = await createChannel(iii)
|
|
115
|
+
*
|
|
116
|
+
* channel.reader.onMessage((msg) => console.log('Got:', msg))
|
|
117
|
+
* channel.reader.onBinary((data) => console.log('Binary:', data.byteLength))
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
var ChannelReader = class {
|
|
121
|
+
constructor(engineWsBase, ref) {
|
|
122
|
+
this.ws = null;
|
|
123
|
+
this.connected = false;
|
|
124
|
+
this.messageCallbacks = [];
|
|
125
|
+
this.binaryCallbacks = [];
|
|
126
|
+
this.url = buildChannelUrl(engineWsBase, ref.channel_id, ref.access_key, "read");
|
|
127
|
+
}
|
|
128
|
+
ensureConnected() {
|
|
129
|
+
if (this.connected) return;
|
|
130
|
+
this.connected = true;
|
|
131
|
+
this.ws = new WebSocket(this.url);
|
|
132
|
+
this.ws.binaryType = "arraybuffer";
|
|
133
|
+
this.ws.addEventListener("message", (event) => {
|
|
134
|
+
if (event.data instanceof ArrayBuffer) {
|
|
135
|
+
const data = new Uint8Array(event.data);
|
|
136
|
+
for (const cb of this.binaryCallbacks) cb(data);
|
|
137
|
+
} else if (typeof event.data === "string") for (const cb of this.messageCallbacks) cb(event.data);
|
|
138
|
+
});
|
|
139
|
+
this.ws.addEventListener("close", () => {
|
|
140
|
+
this.ws = null;
|
|
141
|
+
});
|
|
142
|
+
this.ws.addEventListener("error", () => {
|
|
143
|
+
this.ws = null;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** Register a callback to receive text messages from the channel. */
|
|
147
|
+
onMessage(callback) {
|
|
148
|
+
this.messageCallbacks.push(callback);
|
|
149
|
+
this.ensureConnected();
|
|
150
|
+
}
|
|
151
|
+
/** Register a callback to receive binary data from the channel. */
|
|
152
|
+
onBinary(callback) {
|
|
153
|
+
this.binaryCallbacks.push(callback);
|
|
154
|
+
this.ensureConnected();
|
|
155
|
+
}
|
|
156
|
+
/** Read all binary data from the channel until it closes. */
|
|
157
|
+
async readAll() {
|
|
158
|
+
this.ensureConnected();
|
|
159
|
+
const chunks = [];
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
const onData = (data) => {
|
|
162
|
+
chunks.push(data);
|
|
163
|
+
};
|
|
164
|
+
this.binaryCallbacks.push(onData);
|
|
165
|
+
const originalWs = this.ws;
|
|
166
|
+
if (originalWs) originalWs.addEventListener("close", () => {
|
|
167
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
168
|
+
const result = new Uint8Array(totalLength);
|
|
169
|
+
let offset = 0;
|
|
170
|
+
for (const chunk of chunks) {
|
|
171
|
+
result.set(chunk, offset);
|
|
172
|
+
offset += chunk.length;
|
|
173
|
+
}
|
|
174
|
+
resolve(result);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** Close the channel reader. */
|
|
179
|
+
close() {
|
|
180
|
+
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) this.ws.close(1e3, "channel_close");
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
function buildChannelUrl(engineWsBase, channelId, accessKey, direction) {
|
|
184
|
+
return `${engineWsBase.replace(/\/$/, "")}/ws/channels/${channelId}?key=${encodeURIComponent(accessKey)}&dir=${direction}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/utils.ts
|
|
189
|
+
/**
|
|
190
|
+
* Type guard that checks if a value is a {@link StreamChannelRef}.
|
|
191
|
+
*
|
|
192
|
+
* @param value - Value to check.
|
|
193
|
+
* @returns `true` if the value is a valid `StreamChannelRef`.
|
|
194
|
+
*/
|
|
195
|
+
const isChannelRef = (value) => {
|
|
196
|
+
if (typeof value !== "object" || value === null) return false;
|
|
197
|
+
const maybe = value;
|
|
198
|
+
return typeof maybe.channel_id === "string" && typeof maybe.access_key === "string" && (maybe.direction === "read" || maybe.direction === "write");
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Recursively extract all {@link StreamChannelRef} values from a JSON-like
|
|
202
|
+
* input, returning each match paired with its dotted/bracketed path. Mirrors
|
|
203
|
+
* the Rust SDK's `extract_channel_refs`.
|
|
204
|
+
*
|
|
205
|
+
* @param data - Arbitrary JSON-like value.
|
|
206
|
+
* @returns Array of `[path, ref]` tuples. Empty when no refs are found.
|
|
207
|
+
*/
|
|
208
|
+
const extractChannelRefs = (data) => {
|
|
209
|
+
const refs = [];
|
|
210
|
+
extractRefsRecursive(data, "", refs);
|
|
211
|
+
return refs;
|
|
212
|
+
};
|
|
213
|
+
const extractRefsRecursive = (data, prefix, refs) => {
|
|
214
|
+
if (isChannelRef(data)) {
|
|
215
|
+
refs.push([prefix, data]);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (Array.isArray(data)) {
|
|
219
|
+
for (let i = 0; i < data.length; i++) {
|
|
220
|
+
const path = prefix === "" ? `[${i}]` : `${prefix}[${i}]`;
|
|
221
|
+
extractRefsRecursive(data[i], path, refs);
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (typeof data !== "object" || data === null) return;
|
|
226
|
+
for (const [key, value] of Object.entries(data)) extractRefsRecursive(value, prefix === "" ? key : `${prefix}.${key}`, refs);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
//#endregion
|
|
230
|
+
export { ChannelReader as a, ChannelItem as i, isChannelRef as n, ChannelWriter as o, ChannelDirection as r, extractChannelRefs as t };
|
|
231
|
+
//# sourceMappingURL=utils-CwkqVmBx.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils-CwkqVmBx.mjs","names":[],"sources":["../src/channels.ts","../src/utils.ts"],"sourcesContent":["import 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. Uses native browser WebSocket.\n *\n * @example\n * ```typescript\n * import { createChannel } from 'iii-browser-sdk/helpers'\n * const channel = await createChannel(iii)\n *\n * channel.writer.sendMessage(JSON.stringify({ type: 'event', data: 'test' }))\n * channel.writer.sendBinary(new Uint8Array([1, 2, 3]))\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: ArrayBuffer | string\n resolve: () => void\n reject: (err: Error) => void\n }[] = []\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\n private ensureConnected(): void {\n if (this.ws) return\n\n this.ws = new WebSocket(this.url)\n this.ws.binaryType = 'arraybuffer'\n\n this.ws.addEventListener('open', () => {\n this.wsReady = true\n for (const { data, resolve, reject } of this.pendingMessages) {\n try {\n this.ws?.send(data)\n resolve()\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n }\n }\n this.pendingMessages.length = 0\n })\n\n this.ws.addEventListener('error', () => {\n for (const { reject } of this.pendingMessages) {\n reject(new Error('WebSocket error'))\n }\n this.pendingMessages.length = 0\n })\n }\n\n /** Send a text message through the channel. */\n sendMessage(msg: string): void {\n this.ensureConnected()\n this.sendRaw(msg)\n }\n\n /** Send binary data through the channel. */\n sendBinary(data: Uint8Array): void {\n this.ensureConnected()\n\n let offset = 0\n while (offset < data.length) {\n const end = Math.min(offset + ChannelWriter.FRAME_SIZE, data.length)\n const chunk = data.subarray(offset, end)\n const buffer = chunk.buffer instanceof ArrayBuffer ? chunk.buffer : new ArrayBuffer(chunk.byteLength)\n if (!(chunk.buffer instanceof ArrayBuffer)) {\n new Uint8Array(buffer).set(chunk)\n }\n this.sendRaw(buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength))\n offset = end\n }\n }\n\n /** Close the channel writer. */\n close(): void {\n if (!this.ws) return\n\n const doClose = () => {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.close(1000, 'channel_close')\n }\n }\n\n if (this.wsReady) {\n doClose()\n } else {\n this.ws.addEventListener('open', () => doClose())\n }\n }\n\n private sendRaw(data: ArrayBuffer | string): void {\n if (this.wsReady && this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(data)\n } else {\n this.ensureConnected()\n this.pendingMessages.push({\n data,\n resolve: () => {\n //\n },\n reject: () => {\n console.error('Failed to send message')\n },\n })\n }\n }\n}\n\n/**\n * Read end of a streaming channel. Uses native browser WebSocket.\n *\n * @example\n * ```typescript\n * import { createChannel } from 'iii-browser-sdk/helpers'\n * const channel = await createChannel(iii)\n *\n * channel.reader.onMessage((msg) => console.log('Got:', msg))\n * channel.reader.onBinary((data) => console.log('Binary:', data.byteLength))\n * ```\n */\nexport class ChannelReader {\n private ws: WebSocket | null = null\n private connected = false\n private readonly messageCallbacks: Array<(msg: string) => void> = []\n private readonly binaryCallbacks: Array<(data: Uint8Array) => void> = []\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\n private ensureConnected(): void {\n if (this.connected) return\n this.connected = true\n\n this.ws = new WebSocket(this.url)\n this.ws.binaryType = 'arraybuffer'\n\n this.ws.addEventListener('message', (event: MessageEvent) => {\n if (event.data instanceof ArrayBuffer) {\n const data = new Uint8Array(event.data)\n for (const cb of this.binaryCallbacks) {\n cb(data)\n }\n } else if (typeof event.data === 'string') {\n for (const cb of this.messageCallbacks) {\n cb(event.data)\n }\n }\n })\n\n this.ws.addEventListener('close', () => {\n this.ws = null\n })\n\n this.ws.addEventListener('error', () => {\n this.ws = null\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 this.ensureConnected()\n }\n\n /** Register a callback to receive binary data from the channel. */\n onBinary(callback: (data: Uint8Array) => void): void {\n this.binaryCallbacks.push(callback)\n this.ensureConnected()\n }\n\n /** Read all binary data from the channel until it closes. */\n async readAll(): Promise<Uint8Array> {\n this.ensureConnected()\n const chunks: Uint8Array[] = []\n\n return new Promise<Uint8Array>((resolve) => {\n const onData = (data: Uint8Array) => {\n chunks.push(data)\n }\n this.binaryCallbacks.push(onData)\n\n const originalWs = this.ws\n if (originalWs) {\n originalWs.addEventListener('close', () => {\n const totalLength = chunks.reduce((sum, c) => sum + c.length, 0)\n const result = new Uint8Array(totalLength)\n let offset = 0\n for (const chunk of chunks) {\n result.set(chunk, offset)\n offset += chunk.length\n }\n resolve(result)\n })\n }\n })\n }\n\n /** Close the channel reader. */\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 type { StreamChannelRef } from './iii-types'\n\n/**\n * Safely stringify a value, handling circular references, BigInt, and other edge cases.\n * Returns \"[unserializable]\" if serialization fails for any reason.\n */\nexport function safeStringify(value: unknown): string {\n const seen = new WeakSet<object>()\n\n try {\n return JSON.stringify(value, (_key, val) => {\n if (typeof val === 'bigint') {\n return val.toString()\n }\n\n if (val !== null && typeof val === 'object') {\n if (seen.has(val)) {\n return '[Circular]'\n }\n seen.add(val)\n }\n\n return val\n })\n } catch {\n return '[unserializable]'\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iii-browser-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"types": "./dist/state.d.ts",
|
|
32
32
|
"import": "./dist/state.mjs",
|
|
33
33
|
"require": "./dist/state.cjs"
|
|
34
|
+
},
|
|
35
|
+
"./helpers": {
|
|
36
|
+
"types": "./dist/helpers.d.ts",
|
|
37
|
+
"import": "./dist/helpers.mjs",
|
|
38
|
+
"require": "./dist/helpers.cjs"
|
|
34
39
|
}
|
|
35
40
|
},
|
|
36
41
|
"devDependencies": {
|