@lark-sh/client 0.1.7 → 0.1.9

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 CHANGED
@@ -60,6 +60,331 @@ var Coordinator = class {
60
60
  }
61
61
  };
62
62
 
63
+ // src/connection/WebSocketTransport.ts
64
+ import WebSocketNode from "ws";
65
+ var WebSocketImpl = typeof WebSocket !== "undefined" ? WebSocket : WebSocketNode;
66
+ var WebSocketTransport = class {
67
+ constructor(options) {
68
+ this.ws = null;
69
+ this._state = "disconnected";
70
+ this.options = options;
71
+ }
72
+ get state() {
73
+ return this._state;
74
+ }
75
+ get connected() {
76
+ return this._state === "connected";
77
+ }
78
+ /**
79
+ * Connect to a WebSocket server.
80
+ */
81
+ connect(url) {
82
+ return new Promise((resolve, reject) => {
83
+ if (this._state !== "disconnected") {
84
+ reject(new Error("Already connected or connecting"));
85
+ return;
86
+ }
87
+ this._state = "connecting";
88
+ try {
89
+ this.ws = new WebSocketImpl(url);
90
+ } catch (err) {
91
+ this._state = "disconnected";
92
+ reject(err);
93
+ return;
94
+ }
95
+ const onOpen = () => {
96
+ cleanup();
97
+ this._state = "connected";
98
+ this.setupEventHandlers();
99
+ resolve();
100
+ this.options.onOpen();
101
+ };
102
+ const onError = (event) => {
103
+ cleanup();
104
+ this._state = "disconnected";
105
+ this.ws = null;
106
+ const error = new Error("WebSocket connection failed");
107
+ reject(error);
108
+ this.options.onError(error);
109
+ };
110
+ const onClose = (event) => {
111
+ cleanup();
112
+ this._state = "disconnected";
113
+ this.ws = null;
114
+ reject(new Error(`WebSocket closed: ${event.code} ${event.reason}`));
115
+ };
116
+ const cleanup = () => {
117
+ this.ws?.removeEventListener("open", onOpen);
118
+ this.ws?.removeEventListener("error", onError);
119
+ this.ws?.removeEventListener("close", onClose);
120
+ };
121
+ this.ws.addEventListener("open", onOpen);
122
+ this.ws.addEventListener("error", onError);
123
+ this.ws.addEventListener("close", onClose);
124
+ });
125
+ }
126
+ /**
127
+ * Set up persistent event handlers after connection is established.
128
+ */
129
+ setupEventHandlers() {
130
+ if (!this.ws) return;
131
+ this.ws.addEventListener("message", (event) => {
132
+ this.options.onMessage(event.data);
133
+ });
134
+ this.ws.addEventListener("close", (event) => {
135
+ this._state = "disconnected";
136
+ this.ws = null;
137
+ this.options.onClose(event.code, event.reason);
138
+ });
139
+ this.ws.addEventListener("error", () => {
140
+ this.options.onError(new Error("WebSocket error"));
141
+ });
142
+ }
143
+ /**
144
+ * Send a message over the WebSocket.
145
+ */
146
+ send(data) {
147
+ if (!this.ws || this._state !== "connected") {
148
+ throw new Error("WebSocket not connected");
149
+ }
150
+ this.ws.send(data);
151
+ }
152
+ /**
153
+ * Send a volatile message.
154
+ * WebSocket doesn't have a true unreliable channel, so this just calls send().
155
+ */
156
+ sendVolatile(data) {
157
+ this.send(data);
158
+ }
159
+ /**
160
+ * Close the WebSocket connection.
161
+ */
162
+ close(code = 1e3, reason = "Client disconnect") {
163
+ if (this.ws) {
164
+ this.ws.close(code, reason);
165
+ this.ws = null;
166
+ }
167
+ this._state = "disconnected";
168
+ }
169
+ };
170
+
171
+ // src/connection/WebTransportClient.ts
172
+ var WebTransportClient = class {
173
+ constructor(options) {
174
+ this.transport = null;
175
+ this.writer = null;
176
+ this.datagramWriter = null;
177
+ this._state = "disconnected";
178
+ this.readLoopActive = false;
179
+ this.datagramLoopActive = false;
180
+ this.options = options;
181
+ }
182
+ get state() {
183
+ return this._state;
184
+ }
185
+ get connected() {
186
+ return this._state === "connected";
187
+ }
188
+ /**
189
+ * Connect to a WebTransport server.
190
+ */
191
+ async connect(url) {
192
+ if (this._state !== "disconnected") {
193
+ throw new Error("Already connected or connecting");
194
+ }
195
+ this._state = "connecting";
196
+ try {
197
+ this.transport = new WebTransport(url);
198
+ await this.transport.ready;
199
+ const stream = await this.transport.createBidirectionalStream();
200
+ this.writer = stream.writable.getWriter();
201
+ this.datagramWriter = this.transport.datagrams.writable.getWriter();
202
+ this._state = "connected";
203
+ this.readLoop(stream.readable.getReader());
204
+ this.datagramLoop(this.transport.datagrams.readable.getReader());
205
+ this.transport.closed.then(({ closeCode, reason }) => {
206
+ this.handleClose(closeCode, reason);
207
+ }).catch(() => {
208
+ this.handleClose(0, "Connection failed");
209
+ });
210
+ this.options.onOpen();
211
+ } catch (err) {
212
+ this._state = "disconnected";
213
+ this.transport = null;
214
+ this.writer = null;
215
+ this.datagramWriter = null;
216
+ throw err;
217
+ }
218
+ }
219
+ /**
220
+ * Read messages from the reliable stream.
221
+ * Messages are framed with 4-byte big-endian length prefix.
222
+ */
223
+ async readLoop(reader) {
224
+ this.readLoopActive = true;
225
+ let buffer = new Uint8Array(0);
226
+ try {
227
+ while (this.readLoopActive) {
228
+ const { value, done } = await reader.read();
229
+ if (done) break;
230
+ const newBuffer = new Uint8Array(buffer.length + value.length);
231
+ newBuffer.set(buffer);
232
+ newBuffer.set(value, buffer.length);
233
+ buffer = newBuffer;
234
+ while (buffer.length >= 4) {
235
+ const view = new DataView(buffer.buffer, buffer.byteOffset);
236
+ const msgLength = view.getUint32(0, false);
237
+ if (buffer.length < 4 + msgLength) break;
238
+ const msgBytes = buffer.slice(4, 4 + msgLength);
239
+ const json = new TextDecoder().decode(msgBytes);
240
+ try {
241
+ this.options.onMessage(json);
242
+ } catch (err) {
243
+ console.error("Error handling message:", err);
244
+ }
245
+ buffer = buffer.slice(4 + msgLength);
246
+ }
247
+ }
248
+ } catch (err) {
249
+ if (this.readLoopActive) {
250
+ console.error("Read loop error:", err);
251
+ }
252
+ }
253
+ }
254
+ /**
255
+ * Read volatile events from datagrams.
256
+ * No framing needed - each datagram is a complete JSON message.
257
+ */
258
+ async datagramLoop(reader) {
259
+ this.datagramLoopActive = true;
260
+ try {
261
+ while (this.datagramLoopActive) {
262
+ const { value, done } = await reader.read();
263
+ if (done) break;
264
+ const json = new TextDecoder().decode(value);
265
+ try {
266
+ this.options.onMessage(json);
267
+ } catch (err) {
268
+ console.error("Error handling datagram:", err);
269
+ }
270
+ }
271
+ } catch (err) {
272
+ if (this.datagramLoopActive) {
273
+ console.error("Datagram loop error:", err);
274
+ }
275
+ }
276
+ }
277
+ /**
278
+ * Handle connection close.
279
+ */
280
+ handleClose(code, reason) {
281
+ if (this._state === "disconnected") return;
282
+ this.readLoopActive = false;
283
+ this.datagramLoopActive = false;
284
+ this._state = "disconnected";
285
+ this.transport = null;
286
+ this.writer = null;
287
+ this.datagramWriter = null;
288
+ this.options.onClose(code, reason);
289
+ }
290
+ /**
291
+ * Send a message over the reliable stream.
292
+ * Uses 4-byte big-endian length prefix.
293
+ */
294
+ send(data) {
295
+ if (!this.writer || this._state !== "connected") {
296
+ throw new Error("WebTransport not connected");
297
+ }
298
+ const encoder = new TextEncoder();
299
+ const msgBytes = encoder.encode(data);
300
+ const lengthBuffer = new ArrayBuffer(4);
301
+ new DataView(lengthBuffer).setUint32(0, msgBytes.length, false);
302
+ this.writer.write(new Uint8Array(lengthBuffer)).catch((err) => {
303
+ console.error("Failed to write length prefix:", err);
304
+ });
305
+ this.writer.write(msgBytes).catch((err) => {
306
+ console.error("Failed to write message:", err);
307
+ });
308
+ }
309
+ /**
310
+ * Send a volatile message via datagram (unreliable).
311
+ * No framing needed - each datagram is a complete message.
312
+ */
313
+ sendVolatile(data) {
314
+ if (!this.datagramWriter || this._state !== "connected") {
315
+ throw new Error("WebTransport not connected");
316
+ }
317
+ const encoder = new TextEncoder();
318
+ const msgBytes = encoder.encode(data);
319
+ this.datagramWriter.write(msgBytes).catch(() => {
320
+ });
321
+ }
322
+ /**
323
+ * Close the WebTransport connection.
324
+ */
325
+ close(code = 0, reason = "Client disconnect") {
326
+ this.readLoopActive = false;
327
+ this.datagramLoopActive = false;
328
+ if (this.writer) {
329
+ this.writer.releaseLock();
330
+ this.writer = null;
331
+ }
332
+ if (this.datagramWriter) {
333
+ this.datagramWriter.releaseLock();
334
+ this.datagramWriter = null;
335
+ }
336
+ if (this.transport) {
337
+ this.transport.close({ closeCode: code, reason });
338
+ this.transport = null;
339
+ }
340
+ this._state = "disconnected";
341
+ }
342
+ };
343
+ function isWebTransportAvailable() {
344
+ return typeof globalThis !== "undefined" && "WebTransport" in globalThis;
345
+ }
346
+
347
+ // src/connection/createTransport.ts
348
+ function wsUrlToWtUrl(wsUrl) {
349
+ const url = new URL(wsUrl);
350
+ url.protocol = url.protocol === "wss:" ? "https:" : "http:";
351
+ const port = 7778 + Math.floor(Math.random() * 32);
352
+ url.port = String(port);
353
+ url.pathname = "/wt";
354
+ return url.toString();
355
+ }
356
+ async function createTransport(wsUrl, transportOptions, options = {}) {
357
+ const preferredType = options.transport || "auto";
358
+ const shouldTryWebTransport = (preferredType === "auto" || preferredType === "webtransport") && isWebTransportAvailable();
359
+ if (preferredType === "webtransport" && !isWebTransportAvailable()) {
360
+ throw new Error("WebTransport is not available in this environment");
361
+ }
362
+ if (shouldTryWebTransport) {
363
+ const wtUrl = wsUrlToWtUrl(wsUrl);
364
+ const wtTransport = new WebTransportClient(transportOptions);
365
+ try {
366
+ await wtTransport.connect(wtUrl);
367
+ return {
368
+ transport: wtTransport,
369
+ type: "webtransport",
370
+ url: wtUrl
371
+ };
372
+ } catch (err) {
373
+ if (preferredType === "webtransport") {
374
+ throw err;
375
+ }
376
+ console.warn("WebTransport connection failed, falling back to WebSocket:", err);
377
+ }
378
+ }
379
+ const wsTransport = new WebSocketTransport(transportOptions);
380
+ await wsTransport.connect(wsUrl);
381
+ return {
382
+ transport: wsTransport,
383
+ type: "websocket",
384
+ url: wsUrl
385
+ };
386
+ }
387
+
63
388
  // src/LarkError.ts
64
389
  var LarkError = class _LarkError extends Error {
65
390
  constructor(code, message) {
@@ -1025,13 +1350,15 @@ var SubscriptionManager = class {
1025
1350
  }
1026
1351
  /**
1027
1352
  * Handle an incoming event message from the server.
1028
- * Server sends 'put' or 'patch' events; we generate child_* events client-side.
1353
+ * Server sends 'put', 'patch', or 'vb' (volatile batch) events; we generate child_* events client-side.
1029
1354
  */
1030
1355
  handleEvent(message) {
1031
1356
  if (message.ev === "put") {
1032
1357
  this.handlePutEvent(message);
1033
1358
  } else if (message.ev === "patch") {
1034
1359
  this.handlePatchEvent(message);
1360
+ } else if (message.ev === "vb") {
1361
+ this.handleVolatileBatchEvent(message);
1035
1362
  } else {
1036
1363
  console.warn("Unknown event type:", message.ev);
1037
1364
  }
@@ -1085,6 +1412,26 @@ var SubscriptionManager = class {
1085
1412
  }
1086
1413
  this.applyWriteToView(view, updates, isVolatile, serverTimestamp);
1087
1414
  }
1415
+ /**
1416
+ * Handle a 'vb' (volatile batch) event - batched volatile updates across subscriptions.
1417
+ * Server batches volatile events in 50ms intervals to reduce message overhead.
1418
+ * Format: { ev: 'vb', b: { subscriptionPath: { relativePath: value } }, ts: timestamp }
1419
+ */
1420
+ handleVolatileBatchEvent(message) {
1421
+ const batch = message.b;
1422
+ const serverTimestamp = message.ts;
1423
+ if (!batch) return;
1424
+ for (const [subscriptionPath, updates] of Object.entries(batch)) {
1425
+ const view = this.views.get(subscriptionPath);
1426
+ if (!view) continue;
1427
+ if (view.recovering) continue;
1428
+ const updatesList = [];
1429
+ for (const [relativePath, value] of Object.entries(updates)) {
1430
+ updatesList.push({ relativePath, value });
1431
+ }
1432
+ this.applyWriteToView(view, updatesList, true, serverTimestamp);
1433
+ }
1434
+ }
1088
1435
  /**
1089
1436
  * Detect and fire child_moved events for children that changed position.
1090
1437
  */
@@ -1301,6 +1648,34 @@ var SubscriptionManager = class {
1301
1648
  // ============================================
1302
1649
  // Shared Write Application (used by server events and optimistic writes)
1303
1650
  // ============================================
1651
+ /**
1652
+ * Recursively sort object keys for consistent comparison.
1653
+ * This ensures {a:1, b:2} and {b:2, a:1} compare as equal.
1654
+ * Uses simple alphabetical sorting (not Firebase key sorting) since we're
1655
+ * just checking data equality, not display order.
1656
+ */
1657
+ sortKeysForComparison(value) {
1658
+ if (value === null || typeof value !== "object") {
1659
+ return value;
1660
+ }
1661
+ if (Array.isArray(value)) {
1662
+ return value.map((item) => this.sortKeysForComparison(item));
1663
+ }
1664
+ const obj = value;
1665
+ const sortedKeys = Object.keys(obj).sort();
1666
+ const sorted = {};
1667
+ for (const key of sortedKeys) {
1668
+ sorted[key] = this.sortKeysForComparison(obj[key]);
1669
+ }
1670
+ return sorted;
1671
+ }
1672
+ /**
1673
+ * Serialize cache with sorted keys for consistent comparison.
1674
+ */
1675
+ serializeCacheForComparison(cache) {
1676
+ const sorted = this.sortKeysForComparison(cache);
1677
+ return JSON.stringify(sorted);
1678
+ }
1304
1679
  /**
1305
1680
  * Apply write(s) to a View's cache and fire appropriate events.
1306
1681
  * This is the core logic shared between server events and optimistic writes.
@@ -1313,7 +1688,11 @@ var SubscriptionManager = class {
1313
1688
  applyWriteToView(view, updates, isVolatile, serverTimestamp) {
1314
1689
  const previousOrder = view.orderedChildren;
1315
1690
  const previousChildSet = new Set(previousOrder);
1316
- const previousCacheJson = JSON.stringify(view.getCache());
1691
+ const isFirstSnapshot = !view.hasReceivedInitialSnapshot;
1692
+ let previousCacheJson = null;
1693
+ if (!isVolatile) {
1694
+ previousCacheJson = this.serializeCacheForComparison(view.getCache());
1695
+ }
1317
1696
  const affectedChildren = /* @__PURE__ */ new Set();
1318
1697
  let isFullSnapshot = false;
1319
1698
  for (const { relativePath, value } of updates) {
@@ -1334,9 +1713,11 @@ var SubscriptionManager = class {
1334
1713
  }
1335
1714
  const currentOrder = view.orderedChildren;
1336
1715
  const currentChildSet = new Set(currentOrder);
1337
- const currentCacheJson = JSON.stringify(view.getCache());
1338
- if (previousCacheJson === currentCacheJson) {
1339
- return;
1716
+ if (!isVolatile && !isFirstSnapshot && previousCacheJson !== null) {
1717
+ const currentCacheJson = this.serializeCacheForComparison(view.getCache());
1718
+ if (previousCacheJson === currentCacheJson) {
1719
+ return;
1720
+ }
1340
1721
  }
1341
1722
  const valueSubs = view.getCallbacks("value");
1342
1723
  if (valueSubs.length > 0) {
@@ -1546,106 +1927,6 @@ var SubscriptionManager = class {
1546
1927
  }
1547
1928
  };
1548
1929
 
1549
- // src/connection/WebSocketClient.ts
1550
- import WebSocketNode from "ws";
1551
- var WebSocketImpl = typeof WebSocket !== "undefined" ? WebSocket : WebSocketNode;
1552
- var WebSocketClient = class {
1553
- constructor(options) {
1554
- this.ws = null;
1555
- this._state = "disconnected";
1556
- this.options = options;
1557
- }
1558
- get state() {
1559
- return this._state;
1560
- }
1561
- get connected() {
1562
- return this._state === "connected";
1563
- }
1564
- /**
1565
- * Connect to a WebSocket server.
1566
- */
1567
- connect(url) {
1568
- return new Promise((resolve, reject) => {
1569
- if (this._state !== "disconnected") {
1570
- reject(new Error("Already connected or connecting"));
1571
- return;
1572
- }
1573
- this._state = "connecting";
1574
- try {
1575
- this.ws = new WebSocketImpl(url);
1576
- } catch (err) {
1577
- this._state = "disconnected";
1578
- reject(err);
1579
- return;
1580
- }
1581
- const onOpen = () => {
1582
- cleanup();
1583
- this._state = "connected";
1584
- this.setupEventHandlers();
1585
- resolve();
1586
- this.options.onOpen();
1587
- };
1588
- const onError = (event) => {
1589
- cleanup();
1590
- this._state = "disconnected";
1591
- this.ws = null;
1592
- reject(new Error("WebSocket connection failed"));
1593
- this.options.onError(event);
1594
- };
1595
- const onClose = (event) => {
1596
- cleanup();
1597
- this._state = "disconnected";
1598
- this.ws = null;
1599
- reject(new Error(`WebSocket closed: ${event.code} ${event.reason}`));
1600
- };
1601
- const cleanup = () => {
1602
- this.ws?.removeEventListener("open", onOpen);
1603
- this.ws?.removeEventListener("error", onError);
1604
- this.ws?.removeEventListener("close", onClose);
1605
- };
1606
- this.ws.addEventListener("open", onOpen);
1607
- this.ws.addEventListener("error", onError);
1608
- this.ws.addEventListener("close", onClose);
1609
- });
1610
- }
1611
- /**
1612
- * Set up persistent event handlers after connection is established.
1613
- */
1614
- setupEventHandlers() {
1615
- if (!this.ws) return;
1616
- this.ws.addEventListener("message", (event) => {
1617
- this.options.onMessage(event.data);
1618
- });
1619
- this.ws.addEventListener("close", (event) => {
1620
- this._state = "disconnected";
1621
- this.ws = null;
1622
- this.options.onClose(event.code, event.reason);
1623
- });
1624
- this.ws.addEventListener("error", (event) => {
1625
- this.options.onError(event);
1626
- });
1627
- }
1628
- /**
1629
- * Send a message over the WebSocket.
1630
- */
1631
- send(data) {
1632
- if (!this.ws || this._state !== "connected") {
1633
- throw new Error("WebSocket not connected");
1634
- }
1635
- this.ws.send(data);
1636
- }
1637
- /**
1638
- * Close the WebSocket connection.
1639
- */
1640
- close(code = 1e3, reason = "Client disconnect") {
1641
- if (this.ws) {
1642
- this.ws.close(code, reason);
1643
- this.ws = null;
1644
- }
1645
- this._state = "disconnected";
1646
- }
1647
- };
1648
-
1649
1930
  // src/OnDisconnect.ts
1650
1931
  var OnDisconnect = class {
1651
1932
  constructor(db, path) {
@@ -1693,6 +1974,27 @@ var OnDisconnect = class {
1693
1974
  }
1694
1975
  };
1695
1976
 
1977
+ // src/utils/hash.ts
1978
+ import canonicalize from "canonicalize";
1979
+ async function hashValue(value) {
1980
+ const canonical = canonicalize(value);
1981
+ if (canonical === void 0) {
1982
+ return hashString("");
1983
+ }
1984
+ return hashString(canonical);
1985
+ }
1986
+ async function hashString(str) {
1987
+ const encoder = new TextEncoder();
1988
+ const data = encoder.encode(str);
1989
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1990
+ const hashArray = new Uint8Array(hashBuffer);
1991
+ const hashHex = Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
1992
+ return hashHex;
1993
+ }
1994
+ function isPrimitive(value) {
1995
+ return value === null || typeof value !== "object";
1996
+ }
1997
+
1696
1998
  // src/utils/pushid.ts
1697
1999
  var PUSH_CHARS = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
1698
2000
  var lastPushTime = 0;
@@ -1777,8 +2079,15 @@ var DatabaseReference = class _DatabaseReference {
1777
2079
  // ============================================
1778
2080
  /**
1779
2081
  * Set the data at this location, overwriting any existing data.
2082
+ *
2083
+ * For volatile paths (high-frequency updates), this is fire-and-forget
2084
+ * and resolves immediately without waiting for server confirmation.
1780
2085
  */
1781
2086
  async set(value) {
2087
+ if (this._db.isVolatilePath(this._path)) {
2088
+ this._db._sendVolatileSet(this._path, value);
2089
+ return;
2090
+ }
1782
2091
  await this._db._sendSet(this._path, value);
1783
2092
  }
1784
2093
  /**
@@ -1815,13 +2124,23 @@ var DatabaseReference = class _DatabaseReference {
1815
2124
  }
1816
2125
  await this._db._sendTransaction(ops);
1817
2126
  } else {
2127
+ if (this._db.isVolatilePath(this._path)) {
2128
+ this._db._sendVolatileUpdate(this._path, values);
2129
+ return;
2130
+ }
1818
2131
  await this._db._sendUpdate(this._path, values);
1819
2132
  }
1820
2133
  }
1821
2134
  /**
1822
2135
  * Remove the data at this location.
2136
+ *
2137
+ * For volatile paths, this is fire-and-forget.
1823
2138
  */
1824
2139
  async remove() {
2140
+ if (this._db.isVolatilePath(this._path)) {
2141
+ this._db._sendVolatileDelete(this._path);
2142
+ return;
2143
+ }
1825
2144
  await this._db._sendDelete(this._path);
1826
2145
  }
1827
2146
  /**
@@ -1906,10 +2225,14 @@ var DatabaseReference = class _DatabaseReference {
1906
2225
  snapshot: currentSnapshot
1907
2226
  };
1908
2227
  }
1909
- const ops = [
1910
- { o: "c", p: this._path, v: currentValue },
1911
- { o: "s", p: this._path, v: newValue }
1912
- ];
2228
+ let condition;
2229
+ if (isPrimitive(currentValue)) {
2230
+ condition = { o: "c", p: this._path, v: currentValue };
2231
+ } else {
2232
+ const hash = await hashValue(currentValue);
2233
+ condition = { o: "c", p: this._path, h: hash };
2234
+ }
2235
+ const ops = [condition, { o: "s", p: this._path, v: newValue }];
1913
2236
  try {
1914
2237
  await this._db._sendTransaction(ops);
1915
2238
  const finalSnapshot = await this.once();
@@ -2274,6 +2597,28 @@ function decodeJwtPayload(token) {
2274
2597
  return JSON.parse(decoded);
2275
2598
  }
2276
2599
 
2600
+ // src/utils/volatile.ts
2601
+ function isVolatilePath(path, patterns) {
2602
+ if (!patterns || patterns.length === 0) {
2603
+ return false;
2604
+ }
2605
+ const segments = path.replace(/^\//, "").split("/").filter((s) => s.length > 0);
2606
+ return patterns.some((pattern) => {
2607
+ const patternSegments = pattern.replace(/^\//, "").split("/").filter((s) => s.length > 0);
2608
+ if (segments.length < patternSegments.length) {
2609
+ return false;
2610
+ }
2611
+ for (let i = 0; i < patternSegments.length; i++) {
2612
+ const p = patternSegments[i];
2613
+ const s = segments[i];
2614
+ if (p !== "*" && p !== s) {
2615
+ return false;
2616
+ }
2617
+ }
2618
+ return true;
2619
+ });
2620
+ }
2621
+
2277
2622
  // src/LarkDatabase.ts
2278
2623
  var RECONNECT_BASE_DELAY_MS = 1e3;
2279
2624
  var RECONNECT_MAX_DELAY_MS = 3e4;
@@ -2285,13 +2630,14 @@ var LarkDatabase = class {
2285
2630
  this._databaseId = null;
2286
2631
  this._coordinatorUrl = null;
2287
2632
  this._volatilePaths = [];
2633
+ this._transportType = null;
2288
2634
  // Reconnection state
2289
2635
  this._connectionId = null;
2290
2636
  this._connectOptions = null;
2291
2637
  this._intentionalDisconnect = false;
2292
2638
  this._reconnectAttempt = 0;
2293
2639
  this._reconnectTimer = null;
2294
- this.ws = null;
2640
+ this.transport = null;
2295
2641
  // Event callbacks
2296
2642
  this.connectCallbacks = /* @__PURE__ */ new Set();
2297
2643
  this.disconnectCallbacks = /* @__PURE__ */ new Set();
@@ -2340,11 +2686,17 @@ var LarkDatabase = class {
2340
2686
  /**
2341
2687
  * Get the volatile path patterns from the server.
2342
2688
  * These patterns indicate which paths should use unreliable transport.
2343
- * WebSocket always uses reliable transport, but this is stored for future UDP support.
2344
2689
  */
2345
2690
  get volatilePaths() {
2346
2691
  return this._volatilePaths;
2347
2692
  }
2693
+ /**
2694
+ * Get the current transport type.
2695
+ * Returns 'websocket' or 'webtransport', or null if not connected.
2696
+ */
2697
+ get transportType() {
2698
+ return this._transportType;
2699
+ }
2348
2700
  /**
2349
2701
  * Check if there are any pending writes waiting for acknowledgment.
2350
2702
  * Useful for showing "saving..." indicators in UI.
@@ -2398,15 +2750,20 @@ var LarkDatabase = class {
2398
2750
  anonymous: options.anonymous
2399
2751
  });
2400
2752
  const wsUrl = connectResponse.ws_url;
2401
- this.ws = new WebSocketClient({
2402
- onMessage: this.handleMessage.bind(this),
2403
- onOpen: () => {
2753
+ const transportResult = await createTransport(
2754
+ wsUrl,
2755
+ {
2756
+ onMessage: this.handleMessage.bind(this),
2757
+ onOpen: () => {
2758
+ },
2759
+ // Handled in connect flow
2760
+ onClose: this.handleClose.bind(this),
2761
+ onError: this.handleError.bind(this)
2404
2762
  },
2405
- // Handled in connect flow
2406
- onClose: this.handleClose.bind(this),
2407
- onError: this.handleError.bind(this)
2408
- });
2409
- await this.ws.connect(wsUrl);
2763
+ { transport: options.transport }
2764
+ );
2765
+ this.transport = transportResult.transport;
2766
+ this._transportType = transportResult.type;
2410
2767
  const requestId = this.messageQueue.nextRequestId();
2411
2768
  const joinMessage = {
2412
2769
  o: "j",
@@ -2450,8 +2807,9 @@ var LarkDatabase = class {
2450
2807
  this._databaseId = null;
2451
2808
  this._connectOptions = null;
2452
2809
  this._connectionId = null;
2453
- this.ws?.close();
2454
- this.ws = null;
2810
+ this._transportType = null;
2811
+ this.transport?.close();
2812
+ this.transport = null;
2455
2813
  throw error;
2456
2814
  }
2457
2815
  }
@@ -2469,7 +2827,7 @@ var LarkDatabase = class {
2469
2827
  clearTimeout(this._reconnectTimer);
2470
2828
  this._reconnectTimer = null;
2471
2829
  }
2472
- if (wasConnected && this.ws) {
2830
+ if (wasConnected && this.transport) {
2473
2831
  try {
2474
2832
  const requestId = this.messageQueue.nextRequestId();
2475
2833
  this.send({ o: "l", r: requestId });
@@ -2490,8 +2848,8 @@ var LarkDatabase = class {
2490
2848
  * Used for intentional disconnect.
2491
2849
  */
2492
2850
  cleanupFull() {
2493
- this.ws?.close();
2494
- this.ws = null;
2851
+ this.transport?.close();
2852
+ this.transport = null;
2495
2853
  this._state = "disconnected";
2496
2854
  this._auth = null;
2497
2855
  this._databaseId = null;
@@ -2499,6 +2857,7 @@ var LarkDatabase = class {
2499
2857
  this._coordinatorUrl = null;
2500
2858
  this._connectionId = null;
2501
2859
  this._connectOptions = null;
2860
+ this._transportType = null;
2502
2861
  this._reconnectAttempt = 0;
2503
2862
  this.subscriptionManager.clear();
2504
2863
  this.messageQueue.rejectAll(new Error("Connection closed"));
@@ -2509,8 +2868,8 @@ var LarkDatabase = class {
2509
2868
  * Used for unexpected disconnect.
2510
2869
  */
2511
2870
  cleanupForReconnect() {
2512
- this.ws?.close();
2513
- this.ws = null;
2871
+ this.transport?.close();
2872
+ this.transport = null;
2514
2873
  this._auth = null;
2515
2874
  this.subscriptionManager.clearCacheOnly();
2516
2875
  this.messageQueue.rejectAll(new Error("Connection closed"));
@@ -2646,7 +3005,7 @@ var LarkDatabase = class {
2646
3005
  async transaction(operations) {
2647
3006
  let txOps;
2648
3007
  if (Array.isArray(operations)) {
2649
- txOps = operations.map((op) => this.convertToTxOp(op));
3008
+ txOps = await Promise.all(operations.map((op) => this.convertToTxOp(op)));
2650
3009
  } else {
2651
3010
  txOps = this.convertObjectToTxOps(operations);
2652
3011
  }
@@ -2654,8 +3013,9 @@ var LarkDatabase = class {
2654
3013
  }
2655
3014
  /**
2656
3015
  * Convert a public TransactionOp to wire format TxOperation.
3016
+ * Async because condition operations may require hash computation.
2657
3017
  */
2658
- convertToTxOp(op) {
3018
+ async convertToTxOp(op) {
2659
3019
  const path = normalizePath(op.path) || "/";
2660
3020
  switch (op.op) {
2661
3021
  case "set":
@@ -2665,7 +3025,12 @@ var LarkDatabase = class {
2665
3025
  case "delete":
2666
3026
  return { o: "d", p: path };
2667
3027
  case "condition":
2668
- return { o: "c", p: path, v: op.value };
3028
+ if (isPrimitive(op.value)) {
3029
+ return { o: "c", p: path, v: op.value };
3030
+ } else {
3031
+ const hash = await hashValue(op.value);
3032
+ return { o: "c", p: path, h: hash };
3033
+ }
2669
3034
  default:
2670
3035
  throw new Error(`Unknown transaction operation: ${op.op}`);
2671
3036
  }
@@ -2748,7 +3113,7 @@ var LarkDatabase = class {
2748
3113
  return;
2749
3114
  }
2750
3115
  if (isPingMessage(message)) {
2751
- this.ws?.send(JSON.stringify({ o: "po" }));
3116
+ this.transport?.send(JSON.stringify({ o: "po" }));
2752
3117
  return;
2753
3118
  }
2754
3119
  if (isAckMessage(message)) {
@@ -2815,18 +3180,17 @@ var LarkDatabase = class {
2815
3180
  }
2816
3181
  }
2817
3182
  }
2818
- handleError(event) {
2819
- const error = new Error("WebSocket error");
3183
+ handleError(error) {
2820
3184
  this.errorCallbacks.forEach((cb) => cb(error));
2821
3185
  }
2822
3186
  // ============================================
2823
3187
  // Internal: Sending Messages
2824
3188
  // ============================================
2825
3189
  send(message) {
2826
- if (!this.ws || !this.ws.connected) {
3190
+ if (!this.transport || !this.transport.connected) {
2827
3191
  throw new LarkError("not_connected", "Not connected to database");
2828
3192
  }
2829
- this.ws.send(JSON.stringify(message));
3193
+ this.transport.send(JSON.stringify(message));
2830
3194
  }
2831
3195
  /**
2832
3196
  * @internal Send a set operation.
@@ -2897,6 +3261,73 @@ var LarkDatabase = class {
2897
3261
  this.send(message);
2898
3262
  await this.messageQueue.registerRequest(requestId);
2899
3263
  }
3264
+ // ============================================
3265
+ // Volatile Write Operations (Fire-and-Forget)
3266
+ // ============================================
3267
+ /**
3268
+ * @internal Send a volatile set operation (fire-and-forget).
3269
+ *
3270
+ * Volatile writes skip:
3271
+ * - Recovery checks (volatile paths don't participate in recovery)
3272
+ * - Request ID generation (no ack expected)
3273
+ * - Pending write tracking (no retry on reconnect)
3274
+ * - pw field (no dependency tracking)
3275
+ *
3276
+ * The write is applied optimistically to local cache for UI feedback,
3277
+ * but we don't await server confirmation.
3278
+ *
3279
+ * When using WebTransport, volatile writes are sent via datagrams (UDP).
3280
+ */
3281
+ _sendVolatileSet(path, value) {
3282
+ const normalizedPath = normalizePath(path) || "/";
3283
+ this.subscriptionManager.applyOptimisticWrite(normalizedPath, value, "", "set");
3284
+ if (!this.transport || !this.transport.connected) {
3285
+ return;
3286
+ }
3287
+ const message = {
3288
+ o: "s",
3289
+ p: normalizedPath,
3290
+ v: value
3291
+ };
3292
+ this.transport.sendVolatile(JSON.stringify(message));
3293
+ }
3294
+ /**
3295
+ * @internal Send a volatile update operation (fire-and-forget).
3296
+ */
3297
+ _sendVolatileUpdate(path, values) {
3298
+ const normalizedPath = normalizePath(path) || "/";
3299
+ this.subscriptionManager.applyOptimisticWrite(normalizedPath, values, "", "update");
3300
+ if (!this.transport || !this.transport.connected) {
3301
+ return;
3302
+ }
3303
+ const message = {
3304
+ o: "u",
3305
+ p: normalizedPath,
3306
+ v: values
3307
+ };
3308
+ this.transport.sendVolatile(JSON.stringify(message));
3309
+ }
3310
+ /**
3311
+ * @internal Send a volatile delete operation (fire-and-forget).
3312
+ */
3313
+ _sendVolatileDelete(path) {
3314
+ const normalizedPath = normalizePath(path) || "/";
3315
+ this.subscriptionManager.applyOptimisticWrite(normalizedPath, null, "", "delete");
3316
+ if (!this.transport || !this.transport.connected) {
3317
+ return;
3318
+ }
3319
+ const message = {
3320
+ o: "d",
3321
+ p: normalizedPath
3322
+ };
3323
+ this.transport.sendVolatile(JSON.stringify(message));
3324
+ }
3325
+ /**
3326
+ * Check if a path is a volatile path (high-frequency, fire-and-forget).
3327
+ */
3328
+ isVolatilePath(path) {
3329
+ return isVolatilePath(path, this._volatilePaths);
3330
+ }
2900
3331
  /**
2901
3332
  * @internal Send a once (read) operation.
2902
3333
  *
@@ -3005,21 +3436,6 @@ var LarkDatabase = class {
3005
3436
  this.subscriptionManager.unsubscribeAll(path);
3006
3437
  }
3007
3438
  };
3008
-
3009
- // src/utils/volatile.ts
3010
- function isVolatilePath(path, patterns) {
3011
- if (!patterns || patterns.length === 0) {
3012
- return false;
3013
- }
3014
- const segments = path.replace(/^\//, "").split("/");
3015
- return patterns.some((pattern) => {
3016
- const patternSegments = pattern.split("/");
3017
- if (segments.length !== patternSegments.length) {
3018
- return false;
3019
- }
3020
- return patternSegments.every((p, i) => p === "*" || p === segments[i]);
3021
- });
3022
- }
3023
3439
  export {
3024
3440
  DataSnapshot,
3025
3441
  DatabaseReference,