@lark-sh/client 0.1.3 → 0.1.5

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
@@ -5,17 +5,14 @@ var OnDisconnectAction = {
5
5
  DELETE: "d",
6
6
  CANCEL: "c"
7
7
  };
8
- var eventTypeFromShort = {
9
- v: "value",
10
- ca: "child_added",
11
- cc: "child_changed",
12
- cr: "child_removed"
13
- };
14
- var eventTypeToShort = {
15
- value: "v",
16
- child_added: "ca",
17
- child_changed: "cc",
18
- child_removed: "cr"
8
+ var ErrorCode = {
9
+ PERMISSION_DENIED: "permission_denied",
10
+ INVALID_DATA: "invalid_data",
11
+ NOT_FOUND: "not_found",
12
+ INVALID_PATH: "invalid_path",
13
+ INVALID_OPERATION: "invalid_operation",
14
+ INTERNAL_ERROR: "internal_error",
15
+ CONDITION_FAILED: "condition_failed"
19
16
  };
20
17
  var DEFAULT_COORDINATOR_URL = "https://db.lark.sh";
21
18
 
@@ -78,10 +75,10 @@ var LarkError = class _LarkError extends Error {
78
75
 
79
76
  // src/protocol/messages.ts
80
77
  function isAckMessage(msg) {
81
- return "a" in msg && !("uid" in msg);
78
+ return "a" in msg;
82
79
  }
83
- function isJoinAckMessage(msg) {
84
- return "a" in msg && "uid" in msg;
80
+ function isJoinCompleteMessage(msg) {
81
+ return "jc" in msg;
85
82
  }
86
83
  function isNackMessage(msg) {
87
84
  return "n" in msg;
@@ -137,16 +134,12 @@ var MessageQueue = class {
137
134
  * @returns true if the message was handled (was a response), false otherwise
138
135
  */
139
136
  handleMessage(message) {
140
- if (isJoinAckMessage(message)) {
141
- const pending = this.pending.get(message.a);
137
+ if (isJoinCompleteMessage(message)) {
138
+ const pending = this.pending.get(message.jc);
142
139
  if (pending) {
143
140
  clearTimeout(pending.timeout);
144
- this.pending.delete(message.a);
145
- pending.resolve({
146
- uid: message.uid,
147
- provider: message.provider,
148
- token: message.token
149
- });
141
+ this.pending.delete(message.jc);
142
+ pending.resolve(message.vp || []);
150
143
  return true;
151
144
  }
152
145
  }
@@ -206,6 +199,91 @@ var MessageQueue = class {
206
199
  }
207
200
  };
208
201
 
202
+ // src/connection/PendingWriteManager.ts
203
+ var PendingWriteManager = class {
204
+ constructor() {
205
+ this.pending = /* @__PURE__ */ new Map();
206
+ this.counter = 0;
207
+ }
208
+ /**
209
+ * Generate a unique operation ID.
210
+ * Format: op_{timestamp}_{counter}_{random}
211
+ */
212
+ generateOid() {
213
+ this.counter++;
214
+ const random = Math.random().toString(36).slice(2, 8);
215
+ return `op_${Date.now()}_${this.counter}_${random}`;
216
+ }
217
+ /**
218
+ * Track a new pending write operation.
219
+ */
220
+ trackWrite(oid, operation, path, value) {
221
+ this.pending.set(oid, {
222
+ oid,
223
+ operation,
224
+ path,
225
+ value,
226
+ timestamp: Date.now()
227
+ });
228
+ }
229
+ /**
230
+ * Called when a write is acknowledged by the server.
231
+ * Removes the write from pending tracking.
232
+ */
233
+ onAck(oid) {
234
+ return this.pending.delete(oid);
235
+ }
236
+ /**
237
+ * Called when a write is rejected by the server.
238
+ * Removes the write from pending tracking.
239
+ */
240
+ onNack(oid) {
241
+ return this.pending.delete(oid);
242
+ }
243
+ /**
244
+ * Get all pending writes for retry on reconnect.
245
+ * Returns writes in order of their timestamps (oldest first).
246
+ */
247
+ getPendingWrites() {
248
+ const writes = Array.from(this.pending.values());
249
+ writes.sort((a, b) => a.timestamp - b.timestamp);
250
+ return writes;
251
+ }
252
+ /**
253
+ * Check if a specific operation is still pending.
254
+ */
255
+ isPending(oid) {
256
+ return this.pending.has(oid);
257
+ }
258
+ /**
259
+ * Get the number of pending writes.
260
+ */
261
+ get pendingCount() {
262
+ return this.pending.size;
263
+ }
264
+ /**
265
+ * Clear all pending writes (e.g., on disconnect when not retrying).
266
+ */
267
+ clear() {
268
+ this.pending.clear();
269
+ }
270
+ /**
271
+ * Remove writes older than a specified age (in milliseconds).
272
+ * Useful for cleaning up stale pending writes that may never be acked.
273
+ */
274
+ removeStale(maxAgeMs) {
275
+ const cutoff = Date.now() - maxAgeMs;
276
+ let removed = 0;
277
+ for (const [oid, write] of this.pending) {
278
+ if (write.timestamp < cutoff) {
279
+ this.pending.delete(oid);
280
+ removed++;
281
+ }
282
+ }
283
+ return removed;
284
+ }
285
+ };
286
+
209
287
  // src/utils/path.ts
210
288
  function normalizePath(path) {
211
289
  if (!path || path === "/") return "/";
@@ -360,6 +438,8 @@ var DataCache = class {
360
438
  *
361
439
  * E.g., if /boxes is cached and /boxes/5 changes:
362
440
  * - Update the /boxes cache to reflect the new /boxes/5 value
441
+ *
442
+ * If an ancestor is null, it creates a new object to hold the child.
363
443
  */
364
444
  updateChild(path, value) {
365
445
  const normalized = normalizePath(path);
@@ -369,22 +449,17 @@ var DataCache = class {
369
449
  const ancestorPath = "/" + segments.slice(0, i).join("/");
370
450
  if (this.cache.has(ancestorPath)) {
371
451
  const ancestorValue = this.cache.get(ancestorPath);
372
- if (ancestorValue !== null && typeof ancestorValue === "object") {
373
- const childKey = segments[i];
374
- const remainingPath = "/" + segments.slice(i).join("/");
375
- const updatedAncestor = this.deepClone(ancestorValue);
376
- setValueAtPath(updatedAncestor, remainingPath, value);
377
- this.cache.set(ancestorPath, updatedAncestor);
378
- }
452
+ const baseValue = ancestorValue !== null && typeof ancestorValue === "object" ? this.deepClone(ancestorValue) : {};
453
+ const remainingPath = "/" + segments.slice(i).join("/");
454
+ setValueAtPath(baseValue, remainingPath, value);
455
+ this.cache.set(ancestorPath, baseValue);
379
456
  }
380
457
  }
381
458
  if (this.cache.has("/") && normalized !== "/") {
382
459
  const rootValue = this.cache.get("/");
383
- if (rootValue !== null && typeof rootValue === "object") {
384
- const updatedRoot = this.deepClone(rootValue);
385
- setValueAtPath(updatedRoot, normalized, value);
386
- this.cache.set("/", updatedRoot);
387
- }
460
+ const baseValue = rootValue !== null && typeof rootValue === "object" ? this.deepClone(rootValue) : {};
461
+ setValueAtPath(baseValue, normalized, value);
462
+ this.cache.set("/", baseValue);
388
463
  }
389
464
  }
390
465
  /**
@@ -437,10 +512,15 @@ var DataCache = class {
437
512
  // src/connection/SubscriptionManager.ts
438
513
  var SubscriptionManager = class {
439
514
  constructor() {
440
- // path -> eventType -> array of subscriptions
515
+ // subscriptionPath -> eventType -> array of subscriptions
441
516
  this.subscriptions = /* @__PURE__ */ new Map();
517
+ // Ordered child keys for each subscription path (for ordered queries)
518
+ // subscriptionPath -> ordered array of child keys
519
+ this.orderedChildren = /* @__PURE__ */ new Map();
442
520
  // Callback to send subscribe message to server
443
521
  this.sendSubscribe = null;
522
+ // Query params for each subscription path
523
+ this.queryParams = /* @__PURE__ */ new Map();
444
524
  // Callback to send unsubscribe message to server
445
525
  this.sendUnsubscribe = null;
446
526
  // Callback to create DataSnapshot from event data
@@ -459,7 +539,7 @@ var SubscriptionManager = class {
459
539
  * Subscribe to events at a path.
460
540
  * Returns an unsubscribe function.
461
541
  */
462
- subscribe(path, eventType, callback) {
542
+ subscribe(path, eventType, callback, queryParams) {
463
543
  const normalizedPath = path;
464
544
  const isFirstForPath = !this.subscriptions.has(normalizedPath);
465
545
  const existingEventTypes = this.getEventTypesForPath(normalizedPath);
@@ -472,14 +552,17 @@ var SubscriptionManager = class {
472
552
  pathSubs.set(eventType, []);
473
553
  }
474
554
  const eventSubs = pathSubs.get(eventType);
555
+ if (isFirstForPath && queryParams) {
556
+ this.queryParams.set(normalizedPath, queryParams);
557
+ }
475
558
  const unsubscribe = () => {
476
559
  this.unsubscribeCallback(normalizedPath, eventType, callback);
477
560
  };
478
561
  eventSubs.push({ callback, unsubscribe });
479
562
  if (isFirstForPath || isFirstForEventType) {
480
563
  const allEventTypes = this.getEventTypesForPath(normalizedPath);
481
- const shortEventTypes = allEventTypes.map((et) => eventTypeToShort[et]);
482
- this.sendSubscribe?.(normalizedPath, shortEventTypes).catch((err) => {
564
+ const storedQueryParams = this.queryParams.get(normalizedPath);
565
+ this.sendSubscribe?.(normalizedPath, allEventTypes, storedQueryParams).catch((err) => {
483
566
  console.error("Failed to subscribe:", err);
484
567
  });
485
568
  }
@@ -502,6 +585,9 @@ var SubscriptionManager = class {
502
585
  }
503
586
  if (pathSubs.size === 0) {
504
587
  this.subscriptions.delete(path);
588
+ this.orderedChildren.delete(path);
589
+ this.queryParams.delete(path);
590
+ this.cache.deleteTree(path);
505
591
  this.sendUnsubscribe?.(path).catch((err) => {
506
592
  console.error("Failed to unsubscribe:", err);
507
593
  });
@@ -517,13 +603,16 @@ var SubscriptionManager = class {
517
603
  pathSubs.delete(eventType);
518
604
  if (pathSubs.size === 0) {
519
605
  this.subscriptions.delete(normalizedPath);
606
+ this.orderedChildren.delete(normalizedPath);
607
+ this.queryParams.delete(normalizedPath);
608
+ this.cache.deleteTree(normalizedPath);
520
609
  this.sendUnsubscribe?.(normalizedPath).catch((err) => {
521
610
  console.error("Failed to unsubscribe:", err);
522
611
  });
523
612
  } else {
524
613
  const remainingEventTypes = this.getEventTypesForPath(normalizedPath);
525
- const shortEventTypes = remainingEventTypes.map((et) => eventTypeToShort[et]);
526
- this.sendSubscribe?.(normalizedPath, shortEventTypes).catch((err) => {
614
+ const storedQueryParams = this.queryParams.get(normalizedPath);
615
+ this.sendSubscribe?.(normalizedPath, remainingEventTypes, storedQueryParams).catch((err) => {
527
616
  console.error("Failed to update subscription:", err);
528
617
  });
529
618
  }
@@ -535,49 +624,347 @@ var SubscriptionManager = class {
535
624
  const normalizedPath = path;
536
625
  if (!this.subscriptions.has(normalizedPath)) return;
537
626
  this.subscriptions.delete(normalizedPath);
627
+ this.orderedChildren.delete(normalizedPath);
628
+ this.queryParams.delete(normalizedPath);
629
+ this.cache.deleteTree(normalizedPath);
538
630
  this.sendUnsubscribe?.(normalizedPath).catch((err) => {
539
631
  console.error("Failed to unsubscribe:", err);
540
632
  });
541
633
  }
542
634
  /**
543
635
  * Handle an incoming event message from the server.
636
+ * Server sends 'put' or 'patch' events; we generate child_* events client-side.
544
637
  */
545
638
  handleEvent(message) {
546
- const eventType = eventTypeFromShort[message.ev];
547
- if (!eventType) {
639
+ if (message.ev === "put") {
640
+ this.handlePutEvent(message);
641
+ } else if (message.ev === "patch") {
642
+ this.handlePatchEvent(message);
643
+ } else {
548
644
  console.warn("Unknown event type:", message.ev);
549
- return;
550
645
  }
551
- const path = message.p;
552
- const pathSubs = this.subscriptions.get(path);
646
+ }
647
+ /**
648
+ * Handle a 'put' event - single path change.
649
+ */
650
+ handlePutEvent(message) {
651
+ const subscriptionPath = message.sp;
652
+ const relativePath = message.p;
653
+ const value = message.v;
654
+ const isVolatile = message.x ?? false;
655
+ const serverTimestamp = message.ts;
656
+ const afterKey = message.ak;
657
+ const orderedKeys = message.k;
658
+ const pathSubs = this.subscriptions.get(subscriptionPath);
553
659
  if (!pathSubs) return;
554
- const eventSubs = pathSubs.get(eventType);
555
- if (!eventSubs || eventSubs.length === 0) return;
556
- let snapshotPath;
557
- let snapshotValue;
558
- if (eventType === "value") {
559
- snapshotPath = path;
560
- snapshotValue = message.v;
561
- this.cache.set(path, snapshotValue);
562
- } else if (eventType === "child_added" || eventType === "child_changed") {
563
- snapshotPath = path === "/" ? `/${message.k}` : `${path}/${message.k}`;
564
- snapshotValue = message.v;
565
- this.cache.updateChild(snapshotPath, snapshotValue);
566
- } else if (eventType === "child_removed") {
567
- snapshotPath = path === "/" ? `/${message.k}` : `${path}/${message.k}`;
568
- snapshotValue = message.v;
569
- this.cache.removeChild(snapshotPath);
660
+ const absolutePath = relativePath === "/" ? subscriptionPath : joinPath(subscriptionPath, relativePath);
661
+ const previousOrder = this.orderedChildren.get(subscriptionPath) ?? [];
662
+ const previousChildSet = new Set(previousOrder);
663
+ if (value !== void 0) {
664
+ if (relativePath === "/") {
665
+ this.cache.set(subscriptionPath, value);
666
+ } else if (value === null) {
667
+ this.cache.removeChild(absolutePath);
668
+ } else {
669
+ this.cache.updateChild(absolutePath, value);
670
+ }
671
+ }
672
+ if (relativePath === "/") {
673
+ if (orderedKeys) {
674
+ this.orderedChildren.set(subscriptionPath, [...orderedKeys]);
675
+ } else if (value && typeof value === "object" && value !== null) {
676
+ this.orderedChildren.set(subscriptionPath, Object.keys(value));
677
+ } else {
678
+ this.orderedChildren.set(subscriptionPath, []);
679
+ }
570
680
  } else {
571
- snapshotPath = path === "/" ? `/${message.k}` : `${path}/${message.k}`;
572
- snapshotValue = message.v;
681
+ const segments = relativePath.split("/").filter((s) => s.length > 0);
682
+ if (segments.length > 0) {
683
+ const childKey = segments[0];
684
+ const currentOrder2 = this.orderedChildren.get(subscriptionPath) ?? [];
685
+ if (value === null) {
686
+ const newOrder = currentOrder2.filter((k) => k !== childKey);
687
+ this.orderedChildren.set(subscriptionPath, newOrder);
688
+ } else if (!previousChildSet.has(childKey)) {
689
+ const newOrder = [...currentOrder2];
690
+ this.insertAfterKey(newOrder, childKey, afterKey);
691
+ this.orderedChildren.set(subscriptionPath, newOrder);
692
+ }
693
+ }
573
694
  }
574
- const snapshot = this.createSnapshot?.(snapshotPath, snapshotValue, message.x ?? false);
575
- if (!snapshot) return;
576
- for (const entry of eventSubs) {
577
- try {
578
- entry.callback(snapshot, void 0);
579
- } catch (err) {
580
- console.error("Error in subscription callback:", err);
695
+ const currentOrder = this.orderedChildren.get(subscriptionPath) ?? [];
696
+ const currentChildSet = new Set(currentOrder);
697
+ this.fireCallbacks(
698
+ subscriptionPath,
699
+ pathSubs,
700
+ relativePath,
701
+ value,
702
+ previousOrder,
703
+ currentOrder,
704
+ previousChildSet,
705
+ currentChildSet,
706
+ afterKey,
707
+ isVolatile,
708
+ serverTimestamp
709
+ );
710
+ }
711
+ /**
712
+ * Handle a 'patch' event - multi-path change and/or moves.
713
+ */
714
+ handlePatchEvent(message) {
715
+ const subscriptionPath = message.sp;
716
+ const basePath = message.p;
717
+ const patches = message.v;
718
+ const moves = message.mv;
719
+ const orderedKeys = message.k;
720
+ const isVolatile = message.x ?? false;
721
+ const serverTimestamp = message.ts;
722
+ const pathSubs = this.subscriptions.get(subscriptionPath);
723
+ if (!pathSubs) return;
724
+ const previousOrder = this.orderedChildren.get(subscriptionPath) ?? [];
725
+ const previousChildSet = new Set(previousOrder);
726
+ const affectedChildren = /* @__PURE__ */ new Set();
727
+ if (patches) {
728
+ for (const [relativePath, value] of Object.entries(patches)) {
729
+ const fullRelativePath = basePath === "/" ? "/" + relativePath : joinPath(basePath, relativePath);
730
+ const absolutePath = joinPath(subscriptionPath, fullRelativePath);
731
+ const segments = fullRelativePath.split("/").filter((s) => s.length > 0);
732
+ if (segments.length > 0) {
733
+ affectedChildren.add(segments[0]);
734
+ }
735
+ if (value === null) {
736
+ this.cache.removeChild(absolutePath);
737
+ } else {
738
+ this.cache.updateChild(absolutePath, value);
739
+ }
740
+ }
741
+ }
742
+ if (moves && moves.length > 0) {
743
+ this.handleMoves(subscriptionPath, pathSubs, moves, isVolatile, serverTimestamp);
744
+ }
745
+ if (orderedKeys) {
746
+ this.orderedChildren.set(subscriptionPath, [...orderedKeys]);
747
+ }
748
+ const currentOrder = this.orderedChildren.get(subscriptionPath) ?? [];
749
+ const currentChildSet = new Set(currentOrder);
750
+ const valueSubs = pathSubs.get("value");
751
+ if (valueSubs && valueSubs.length > 0) {
752
+ const fullValue = this.cache.get(subscriptionPath).value;
753
+ const snapshot = this.createSnapshot?.(subscriptionPath, fullValue, isVolatile, serverTimestamp);
754
+ if (snapshot) {
755
+ for (const entry of valueSubs) {
756
+ try {
757
+ entry.callback(snapshot, void 0);
758
+ } catch (err) {
759
+ console.error("Error in value subscription callback:", err);
760
+ }
761
+ }
762
+ }
763
+ }
764
+ if (patches && affectedChildren.size > 0) {
765
+ const childAddedSubs = pathSubs.get("child_added") ?? [];
766
+ const childChangedSubs = pathSubs.get("child_changed") ?? [];
767
+ const childRemovedSubs = pathSubs.get("child_removed") ?? [];
768
+ for (const childKey of affectedChildren) {
769
+ const wasPresent = previousChildSet.has(childKey);
770
+ const isPresent = currentChildSet.has(childKey);
771
+ if (!wasPresent && isPresent) {
772
+ const prevKey = this.getPreviousChildKey(currentOrder, childKey);
773
+ this.fireChildAdded(subscriptionPath, childKey, childAddedSubs, prevKey, isVolatile, serverTimestamp);
774
+ } else if (wasPresent && !isPresent) {
775
+ this.fireChildRemoved(subscriptionPath, childKey, childRemovedSubs, isVolatile, serverTimestamp);
776
+ } else if (wasPresent && isPresent) {
777
+ const prevKey = this.getPreviousChildKey(currentOrder, childKey);
778
+ this.fireChildChanged(subscriptionPath, childKey, childChangedSubs, prevKey, isVolatile, serverTimestamp);
779
+ }
780
+ }
781
+ }
782
+ }
783
+ /**
784
+ * Handle moves array - update order and fire child_moved events.
785
+ */
786
+ handleMoves(subscriptionPath, pathSubs, moves, isVolatile, serverTimestamp) {
787
+ const childMovedSubs = pathSubs.get("child_moved") ?? [];
788
+ const currentOrder = this.orderedChildren.get(subscriptionPath) ?? [];
789
+ for (const move of moves) {
790
+ const { k: childKey, ak: afterKey } = move;
791
+ const idx = currentOrder.indexOf(childKey);
792
+ if (idx !== -1) {
793
+ currentOrder.splice(idx, 1);
794
+ }
795
+ this.insertAfterKey(currentOrder, childKey, afterKey);
796
+ if (childMovedSubs.length > 0) {
797
+ const previousChildKey = afterKey === "" ? null : afterKey;
798
+ this.fireChildMoved(subscriptionPath, childKey, childMovedSubs, previousChildKey, isVolatile, serverTimestamp);
799
+ }
800
+ }
801
+ this.orderedChildren.set(subscriptionPath, currentOrder);
802
+ }
803
+ /**
804
+ * Insert a key into an ordered array after a specific key.
805
+ * If afterKey is empty string or undefined, insert at beginning.
806
+ * If afterKey is not found, append at end.
807
+ */
808
+ insertAfterKey(order, key, afterKey) {
809
+ if (afterKey === "" || afterKey === void 0) {
810
+ order.unshift(key);
811
+ } else {
812
+ const afterIdx = order.indexOf(afterKey);
813
+ if (afterIdx === -1) {
814
+ order.push(key);
815
+ } else {
816
+ order.splice(afterIdx + 1, 0, key);
817
+ }
818
+ }
819
+ }
820
+ /**
821
+ * Get the previous sibling key for a given key in the ordered array.
822
+ */
823
+ getPreviousChildKey(order, key) {
824
+ const idx = order.indexOf(key);
825
+ if (idx <= 0) return null;
826
+ return order[idx - 1];
827
+ }
828
+ /**
829
+ * Fire callbacks for subscribed event types.
830
+ */
831
+ fireCallbacks(subscriptionPath, pathSubs, relativePath, value, previousOrder, currentOrder, previousChildSet, currentChildSet, afterKey, isVolatile, serverTimestamp) {
832
+ const valueSubs = pathSubs.get("value");
833
+ if (valueSubs && valueSubs.length > 0) {
834
+ const fullValue = this.cache.get(subscriptionPath).value;
835
+ const snapshot = this.createSnapshot?.(subscriptionPath, fullValue, isVolatile, serverTimestamp);
836
+ if (snapshot) {
837
+ for (const entry of valueSubs) {
838
+ try {
839
+ entry.callback(snapshot, void 0);
840
+ } catch (err) {
841
+ console.error("Error in value subscription callback:", err);
842
+ }
843
+ }
844
+ }
845
+ }
846
+ this.fireChildEvents(
847
+ subscriptionPath,
848
+ pathSubs,
849
+ relativePath,
850
+ value,
851
+ previousOrder,
852
+ currentOrder,
853
+ previousChildSet,
854
+ currentChildSet,
855
+ afterKey,
856
+ isVolatile,
857
+ serverTimestamp
858
+ );
859
+ }
860
+ /**
861
+ * Generate and fire child_added, child_changed, child_removed events.
862
+ * child_moved is handled separately via handleMoves().
863
+ */
864
+ fireChildEvents(subscriptionPath, pathSubs, relativePath, value, previousOrder, currentOrder, previousChildSet, currentChildSet, afterKey, isVolatile, serverTimestamp) {
865
+ const childAddedSubs = pathSubs.get("child_added") ?? [];
866
+ const childChangedSubs = pathSubs.get("child_changed") ?? [];
867
+ const childRemovedSubs = pathSubs.get("child_removed") ?? [];
868
+ if (childAddedSubs.length === 0 && childChangedSubs.length === 0 && childRemovedSubs.length === 0) {
869
+ return;
870
+ }
871
+ if (relativePath === "/") {
872
+ for (const key of currentOrder) {
873
+ if (!previousChildSet.has(key)) {
874
+ const prevKey = this.getPreviousChildKey(currentOrder, key);
875
+ this.fireChildAdded(subscriptionPath, key, childAddedSubs, prevKey, isVolatile, serverTimestamp);
876
+ }
877
+ }
878
+ for (const key of previousOrder) {
879
+ if (!currentChildSet.has(key)) {
880
+ this.fireChildRemoved(subscriptionPath, key, childRemovedSubs, isVolatile, serverTimestamp);
881
+ }
882
+ }
883
+ } else {
884
+ const segments = relativePath.split("/").filter((s) => s.length > 0);
885
+ if (segments.length === 0) return;
886
+ const childKey = segments[0];
887
+ if (value === null) {
888
+ if (previousChildSet.has(childKey) && !currentChildSet.has(childKey)) {
889
+ this.fireChildRemoved(subscriptionPath, childKey, childRemovedSubs, isVolatile, serverTimestamp);
890
+ }
891
+ } else if (!previousChildSet.has(childKey)) {
892
+ const prevKey = afterKey !== void 0 ? afterKey === "" ? null : afterKey : this.getPreviousChildKey(currentOrder, childKey);
893
+ this.fireChildAdded(subscriptionPath, childKey, childAddedSubs, prevKey, isVolatile, serverTimestamp);
894
+ } else {
895
+ const prevKey = this.getPreviousChildKey(currentOrder, childKey);
896
+ this.fireChildChanged(subscriptionPath, childKey, childChangedSubs, prevKey, isVolatile, serverTimestamp);
897
+ }
898
+ }
899
+ }
900
+ /**
901
+ * Fire child_added callbacks for a child key.
902
+ */
903
+ fireChildAdded(subscriptionPath, childKey, subs, previousChildKey, isVolatile, serverTimestamp) {
904
+ if (subs.length === 0) return;
905
+ const childPath = joinPath(subscriptionPath, childKey);
906
+ const childValue = this.cache.get(childPath).value;
907
+ const snapshot = this.createSnapshot?.(childPath, childValue, isVolatile, serverTimestamp);
908
+ if (snapshot) {
909
+ for (const entry of subs) {
910
+ try {
911
+ entry.callback(snapshot, previousChildKey);
912
+ } catch (err) {
913
+ console.error("Error in child_added subscription callback:", err);
914
+ }
915
+ }
916
+ }
917
+ }
918
+ /**
919
+ * Fire child_changed callbacks for a child key.
920
+ */
921
+ fireChildChanged(subscriptionPath, childKey, subs, previousChildKey, isVolatile, serverTimestamp) {
922
+ if (subs.length === 0) return;
923
+ const childPath = joinPath(subscriptionPath, childKey);
924
+ const childValue = this.cache.get(childPath).value;
925
+ const snapshot = this.createSnapshot?.(childPath, childValue, isVolatile, serverTimestamp);
926
+ if (snapshot) {
927
+ for (const entry of subs) {
928
+ try {
929
+ entry.callback(snapshot, previousChildKey);
930
+ } catch (err) {
931
+ console.error("Error in child_changed subscription callback:", err);
932
+ }
933
+ }
934
+ }
935
+ }
936
+ /**
937
+ * Fire child_removed callbacks for a child key.
938
+ */
939
+ fireChildRemoved(subscriptionPath, childKey, subs, isVolatile, serverTimestamp) {
940
+ if (subs.length === 0) return;
941
+ const childPath = joinPath(subscriptionPath, childKey);
942
+ const snapshot = this.createSnapshot?.(childPath, null, isVolatile, serverTimestamp);
943
+ if (snapshot) {
944
+ for (const entry of subs) {
945
+ try {
946
+ entry.callback(snapshot, void 0);
947
+ } catch (err) {
948
+ console.error("Error in child_removed subscription callback:", err);
949
+ }
950
+ }
951
+ }
952
+ }
953
+ /**
954
+ * Fire child_moved callbacks for a child key.
955
+ */
956
+ fireChildMoved(subscriptionPath, childKey, subs, previousChildKey, isVolatile, serverTimestamp) {
957
+ if (subs.length === 0) return;
958
+ const childPath = joinPath(subscriptionPath, childKey);
959
+ const childValue = this.cache.get(childPath).value;
960
+ const snapshot = this.createSnapshot?.(childPath, childValue, isVolatile, serverTimestamp);
961
+ if (snapshot) {
962
+ for (const entry of subs) {
963
+ try {
964
+ entry.callback(snapshot, previousChildKey);
965
+ } catch (err) {
966
+ console.error("Error in child_moved subscription callback:", err);
967
+ }
581
968
  }
582
969
  }
583
970
  }
@@ -594,6 +981,8 @@ var SubscriptionManager = class {
594
981
  */
595
982
  clear() {
596
983
  this.subscriptions.clear();
984
+ this.orderedChildren.clear();
985
+ this.queryParams.clear();
597
986
  this.cache.clear();
598
987
  }
599
988
  /**
@@ -837,6 +1226,7 @@ function generatePushId() {
837
1226
  }
838
1227
 
839
1228
  // src/DatabaseReference.ts
1229
+ var DEFAULT_MAX_RETRIES = 25;
840
1230
  var DatabaseReference = class _DatabaseReference {
841
1231
  constructor(db, path, query = {}) {
842
1232
  this._db = db;
@@ -890,9 +1280,40 @@ var DatabaseReference = class _DatabaseReference {
890
1280
  }
891
1281
  /**
892
1282
  * Update specific children at this location without overwriting other children.
1283
+ *
1284
+ * Also supports Firebase-style multi-path updates when keys look like paths
1285
+ * (start with '/'). In this mode, each path is written atomically as a transaction.
1286
+ *
1287
+ * @example
1288
+ * ```javascript
1289
+ * // Normal update (merge at single path)
1290
+ * await ref.update({ score: 10, name: 'Riley' });
1291
+ *
1292
+ * // Multi-path update (atomic writes to multiple paths)
1293
+ * await db.ref().update({
1294
+ * '/users/alice/score': 100,
1295
+ * '/users/bob/score': 200,
1296
+ * '/leaderboard/alice': null // null = delete
1297
+ * });
1298
+ * ```
893
1299
  */
894
1300
  async update(values) {
895
- await this._db._sendUpdate(this._path, values);
1301
+ const hasPathKeys = Object.keys(values).some((key) => key.startsWith("/"));
1302
+ if (hasPathKeys) {
1303
+ const ops = [];
1304
+ for (const [key, value] of Object.entries(values)) {
1305
+ const fullPath = key.startsWith("/") ? joinPath(this._path, key) : joinPath(this._path, key);
1306
+ const normalizedPath = normalizePath(fullPath) || "/";
1307
+ if (value === null) {
1308
+ ops.push({ o: "d", p: normalizedPath });
1309
+ } else {
1310
+ ops.push({ o: "s", p: normalizedPath, v: value });
1311
+ }
1312
+ }
1313
+ await this._db._sendTransaction(ops);
1314
+ } else {
1315
+ await this._db._sendUpdate(this._path, values);
1316
+ }
896
1317
  }
897
1318
  /**
898
1319
  * Remove the data at this location.
@@ -932,6 +1353,64 @@ var DatabaseReference = class _DatabaseReference {
932
1353
  const snapshot = await this.once();
933
1354
  await this.setWithPriority(snapshot.val(), priority);
934
1355
  }
1356
+ /**
1357
+ * Atomically modify the data at this location using optimistic concurrency.
1358
+ *
1359
+ * The update function receives the current value and should return the new
1360
+ * value. If the data changed on the server before the write could be
1361
+ * committed, the function is called again with the new value, and the
1362
+ * process repeats until successful or the maximum retries are exceeded.
1363
+ *
1364
+ * @example
1365
+ * ```javascript
1366
+ * // Increment a counter atomically
1367
+ * const result = await ref.transaction(currentValue => {
1368
+ * return (currentValue || 0) + 1;
1369
+ * });
1370
+ * console.log('New value:', result.snapshot.val());
1371
+ * ```
1372
+ *
1373
+ * @param updateFunction - Function that receives current value and returns new value.
1374
+ * Return undefined to abort the transaction.
1375
+ * @param maxRetries - Maximum number of retries (default: 25)
1376
+ * @returns TransactionResult with committed status and final snapshot
1377
+ */
1378
+ async transaction(updateFunction, maxRetries = DEFAULT_MAX_RETRIES) {
1379
+ let retries = 0;
1380
+ while (retries < maxRetries) {
1381
+ const currentSnapshot = await this.once();
1382
+ const currentValue = currentSnapshot.val();
1383
+ const newValue = updateFunction(currentValue);
1384
+ if (newValue === void 0) {
1385
+ return {
1386
+ committed: false,
1387
+ snapshot: currentSnapshot
1388
+ };
1389
+ }
1390
+ const ops = [
1391
+ { o: "c", p: this._path, v: currentValue },
1392
+ { o: "s", p: this._path, v: newValue }
1393
+ ];
1394
+ try {
1395
+ await this._db._sendTransaction(ops);
1396
+ const finalSnapshot = await this.once();
1397
+ return {
1398
+ committed: true,
1399
+ snapshot: finalSnapshot
1400
+ };
1401
+ } catch (error) {
1402
+ if (error instanceof LarkError && error.code === ErrorCode.CONDITION_FAILED) {
1403
+ retries++;
1404
+ continue;
1405
+ }
1406
+ throw error;
1407
+ }
1408
+ }
1409
+ throw new LarkError(
1410
+ "max_retries_exceeded",
1411
+ `Transaction failed after ${maxRetries} retries`
1412
+ );
1413
+ }
935
1414
  // ============================================
936
1415
  // Read Operations
937
1416
  // ============================================
@@ -952,7 +1431,7 @@ var DatabaseReference = class _DatabaseReference {
952
1431
  * Returns an unsubscribe function.
953
1432
  */
954
1433
  on(eventType, callback) {
955
- return this._db._subscribe(this._path, eventType, callback);
1434
+ return this._db._subscribe(this._path, eventType, callback, this._buildQueryParams());
956
1435
  }
957
1436
  /**
958
1437
  * Unsubscribe from events.
@@ -998,10 +1477,8 @@ var DatabaseReference = class _DatabaseReference {
998
1477
  }
999
1478
  /**
1000
1479
  * Order results by a child key.
1001
- * NOTE: Phase 2 - not yet implemented on server.
1002
1480
  */
1003
1481
  orderByChild(path) {
1004
- console.warn("orderByChild() is not yet implemented");
1005
1482
  return new _DatabaseReference(this._db, this._path, {
1006
1483
  ...this._query,
1007
1484
  orderBy: "child",
@@ -1010,10 +1487,8 @@ var DatabaseReference = class _DatabaseReference {
1010
1487
  }
1011
1488
  /**
1012
1489
  * Order results by value.
1013
- * NOTE: Phase 2 - not yet implemented on server.
1014
1490
  */
1015
1491
  orderByValue() {
1016
- console.warn("orderByValue() is not yet implemented");
1017
1492
  return new _DatabaseReference(this._db, this._path, {
1018
1493
  ...this._query,
1019
1494
  orderBy: "value"
@@ -1039,10 +1514,8 @@ var DatabaseReference = class _DatabaseReference {
1039
1514
  }
1040
1515
  /**
1041
1516
  * Start at a specific value/key.
1042
- * NOTE: Phase 2 - not yet implemented on server.
1043
1517
  */
1044
1518
  startAt(value, key) {
1045
- console.warn("startAt() is not yet implemented");
1046
1519
  return new _DatabaseReference(this._db, this._path, {
1047
1520
  ...this._query,
1048
1521
  startAt: { value, key }
@@ -1050,10 +1523,8 @@ var DatabaseReference = class _DatabaseReference {
1050
1523
  }
1051
1524
  /**
1052
1525
  * End at a specific value/key.
1053
- * NOTE: Phase 2 - not yet implemented on server.
1054
1526
  */
1055
1527
  endAt(value, key) {
1056
- console.warn("endAt() is not yet implemented");
1057
1528
  return new _DatabaseReference(this._db, this._path, {
1058
1529
  ...this._query,
1059
1530
  endAt: { value, key }
@@ -1061,10 +1532,8 @@ var DatabaseReference = class _DatabaseReference {
1061
1532
  }
1062
1533
  /**
1063
1534
  * Filter to items equal to a specific value.
1064
- * NOTE: Phase 2 - not yet implemented on server.
1065
1535
  */
1066
1536
  equalTo(value, key) {
1067
- console.warn("equalTo() is not yet implemented");
1068
1537
  return new _DatabaseReference(this._db, this._path, {
1069
1538
  ...this._query,
1070
1539
  equalTo: { value, key }
@@ -1080,18 +1549,48 @@ var DatabaseReference = class _DatabaseReference {
1080
1549
  const params = {};
1081
1550
  let hasParams = false;
1082
1551
  if (this._query.orderBy === "key") {
1083
- params.ob = "k";
1552
+ params.orderBy = "key";
1084
1553
  hasParams = true;
1085
1554
  } else if (this._query.orderBy === "priority") {
1086
- params.ob = "p";
1555
+ params.orderBy = "priority";
1556
+ hasParams = true;
1557
+ } else if (this._query.orderBy === "child") {
1558
+ params.orderBy = "child";
1559
+ if (this._query.orderByChildPath) {
1560
+ params.orderByChild = this._query.orderByChildPath;
1561
+ }
1562
+ hasParams = true;
1563
+ } else if (this._query.orderBy === "value") {
1564
+ params.orderBy = "value";
1087
1565
  hasParams = true;
1088
1566
  }
1089
1567
  if (this._query.limitToFirst !== void 0) {
1090
- params.lf = this._query.limitToFirst;
1568
+ params.limitToFirst = this._query.limitToFirst;
1091
1569
  hasParams = true;
1092
1570
  }
1093
1571
  if (this._query.limitToLast !== void 0) {
1094
- params.ll = this._query.limitToLast;
1572
+ params.limitToLast = this._query.limitToLast;
1573
+ hasParams = true;
1574
+ }
1575
+ if (this._query.startAt !== void 0) {
1576
+ params.startAt = this._query.startAt.value;
1577
+ if (this._query.startAt.key !== void 0) {
1578
+ params.startAtKey = this._query.startAt.key;
1579
+ }
1580
+ hasParams = true;
1581
+ }
1582
+ if (this._query.endAt !== void 0) {
1583
+ params.endAt = this._query.endAt.value;
1584
+ if (this._query.endAt.key !== void 0) {
1585
+ params.endAtKey = this._query.endAt.key;
1586
+ }
1587
+ hasParams = true;
1588
+ }
1589
+ if (this._query.equalTo !== void 0) {
1590
+ params.equalTo = this._query.equalTo.value;
1591
+ if (this._query.equalTo.key !== void 0) {
1592
+ params.equalToKey = this._query.equalTo.key;
1593
+ }
1095
1594
  hasParams = true;
1096
1595
  }
1097
1596
  return hasParams ? params : void 0;
@@ -1117,6 +1616,7 @@ var DataSnapshot = class _DataSnapshot {
1117
1616
  this._db = db;
1118
1617
  this._volatile = options.volatile ?? false;
1119
1618
  this._priority = options.priority ?? null;
1619
+ this._serverTimestamp = options.serverTimestamp ?? null;
1120
1620
  }
1121
1621
  /**
1122
1622
  * Get a DatabaseReference for the location of this snapshot.
@@ -1149,7 +1649,8 @@ var DataSnapshot = class _DataSnapshot {
1149
1649
  const childPath = joinPath(this._path, path);
1150
1650
  const childData = getValueAtPath(this._data, path);
1151
1651
  return new _DataSnapshot(childData, childPath, this._db, {
1152
- volatile: this._volatile
1652
+ volatile: this._volatile,
1653
+ serverTimestamp: this._serverTimestamp
1153
1654
  });
1154
1655
  }
1155
1656
  /**
@@ -1205,6 +1706,15 @@ var DataSnapshot = class _DataSnapshot {
1205
1706
  isVolatile() {
1206
1707
  return this._volatile;
1207
1708
  }
1709
+ /**
1710
+ * Get the server timestamp for this snapshot (milliseconds since Unix epoch).
1711
+ * Only present on volatile value events. Use deltas between timestamps for
1712
+ * interpolation rather than absolute times to avoid clock sync issues.
1713
+ * This is a Lark extension not present in Firebase.
1714
+ */
1715
+ getServerTimestamp() {
1716
+ return this._serverTimestamp;
1717
+ }
1208
1718
  /**
1209
1719
  * Export the snapshot data as JSON (alias for val()).
1210
1720
  */
@@ -1238,6 +1748,7 @@ var LarkDatabase = class {
1238
1748
  this._auth = null;
1239
1749
  this._databaseId = null;
1240
1750
  this._coordinatorUrl = null;
1751
+ this._volatilePaths = [];
1241
1752
  this.ws = null;
1242
1753
  // Event callbacks
1243
1754
  this.connectCallbacks = /* @__PURE__ */ new Set();
@@ -1245,6 +1756,7 @@ var LarkDatabase = class {
1245
1756
  this.errorCallbacks = /* @__PURE__ */ new Set();
1246
1757
  this.messageQueue = new MessageQueue();
1247
1758
  this.subscriptionManager = new SubscriptionManager();
1759
+ this.pendingWrites = new PendingWriteManager();
1248
1760
  }
1249
1761
  // ============================================
1250
1762
  // Connection State
@@ -1270,6 +1782,34 @@ var LarkDatabase = class {
1270
1782
  }
1271
1783
  return "lark://";
1272
1784
  }
1785
+ /**
1786
+ * Get the volatile path patterns from the server.
1787
+ * These patterns indicate which paths should use unreliable transport.
1788
+ * WebSocket always uses reliable transport, but this is stored for future UDP support.
1789
+ */
1790
+ get volatilePaths() {
1791
+ return this._volatilePaths;
1792
+ }
1793
+ /**
1794
+ * Check if there are any pending writes waiting for acknowledgment.
1795
+ * Useful for showing "saving..." indicators in UI.
1796
+ */
1797
+ hasPendingWrites() {
1798
+ return this.pendingWrites.pendingCount > 0;
1799
+ }
1800
+ /**
1801
+ * Get the number of pending writes waiting for acknowledgment.
1802
+ */
1803
+ getPendingWriteCount() {
1804
+ return this.pendingWrites.pendingCount;
1805
+ }
1806
+ /**
1807
+ * Clear all pending writes.
1808
+ * Call this if you don't want to retry writes on reconnect.
1809
+ */
1810
+ clearPendingWrites() {
1811
+ this.pendingWrites.clear();
1812
+ }
1273
1813
  // ============================================
1274
1814
  // Connection Management
1275
1815
  // ============================================
@@ -1310,7 +1850,8 @@ var LarkDatabase = class {
1310
1850
  r: requestId
1311
1851
  };
1312
1852
  this.send(joinMessage);
1313
- await this.messageQueue.registerRequest(requestId);
1853
+ const volatilePaths = await this.messageQueue.registerRequest(requestId);
1854
+ this._volatilePaths = volatilePaths || [];
1314
1855
  const jwtPayload = decodeJwtPayload(connectResponse.token);
1315
1856
  this._auth = {
1316
1857
  uid: jwtPayload.sub,
@@ -1363,6 +1904,7 @@ var LarkDatabase = class {
1363
1904
  this._state = "disconnected";
1364
1905
  this._auth = null;
1365
1906
  this._databaseId = null;
1907
+ this._volatilePaths = [];
1366
1908
  this._coordinatorUrl = null;
1367
1909
  this.subscriptionManager.clear();
1368
1910
  this.messageQueue.rejectAll(new Error("Connection closed"));
@@ -1377,6 +1919,96 @@ var LarkDatabase = class {
1377
1919
  return new DatabaseReference(this, path);
1378
1920
  }
1379
1921
  // ============================================
1922
+ // Transactions
1923
+ // ============================================
1924
+ /**
1925
+ * Execute an atomic transaction with multiple operations.
1926
+ *
1927
+ * Supports two syntaxes:
1928
+ *
1929
+ * **Object syntax** (like Firebase multi-path update):
1930
+ * ```javascript
1931
+ * await db.transaction({
1932
+ * '/users/alice/name': 'Alice',
1933
+ * '/users/alice/score': 100,
1934
+ * '/temp/data': null // null = delete
1935
+ * });
1936
+ * ```
1937
+ *
1938
+ * **Array syntax** (explicit operations):
1939
+ * ```javascript
1940
+ * await db.transaction([
1941
+ * { op: 'set', path: '/users/alice/name', value: 'Alice' },
1942
+ * { op: 'update', path: '/metadata', value: { lastUpdated: '...' } },
1943
+ * { op: 'delete', path: '/temp/data' },
1944
+ * { op: 'condition', path: '/counter', value: 5 } // CAS check
1945
+ * ]);
1946
+ * ```
1947
+ *
1948
+ * All operations are atomic: either all succeed or all fail.
1949
+ * Conditions are checked first; if any fail, the transaction is rejected
1950
+ * with error code 'condition_failed'.
1951
+ */
1952
+ async transaction(operations) {
1953
+ let txOps;
1954
+ if (Array.isArray(operations)) {
1955
+ txOps = operations.map((op) => this.convertToTxOp(op));
1956
+ } else {
1957
+ txOps = this.convertObjectToTxOps(operations);
1958
+ }
1959
+ await this._sendTransaction(txOps);
1960
+ }
1961
+ /**
1962
+ * Convert a public TransactionOp to wire format TxOperation.
1963
+ */
1964
+ convertToTxOp(op) {
1965
+ const path = normalizePath(op.path) || "/";
1966
+ switch (op.op) {
1967
+ case "set":
1968
+ return { o: "s", p: path, v: op.value };
1969
+ case "update":
1970
+ return { o: "u", p: path, v: op.value };
1971
+ case "delete":
1972
+ return { o: "d", p: path };
1973
+ case "condition":
1974
+ return { o: "c", p: path, v: op.value };
1975
+ default:
1976
+ throw new Error(`Unknown transaction operation: ${op.op}`);
1977
+ }
1978
+ }
1979
+ /**
1980
+ * Convert object syntax to wire format TxOperations.
1981
+ * Each path becomes a set operation, null values become deletes.
1982
+ */
1983
+ convertObjectToTxOps(obj) {
1984
+ const ops = [];
1985
+ for (const [path, value] of Object.entries(obj)) {
1986
+ const normalizedPath = normalizePath(path) || "/";
1987
+ if (value === null) {
1988
+ ops.push({ o: "d", p: normalizedPath });
1989
+ } else {
1990
+ ops.push({ o: "s", p: normalizedPath, v: value });
1991
+ }
1992
+ }
1993
+ return ops;
1994
+ }
1995
+ /**
1996
+ * @internal Send a transaction to the server.
1997
+ */
1998
+ async _sendTransaction(ops) {
1999
+ const requestId = this.messageQueue.nextRequestId();
2000
+ const oid = this.pendingWrites.generateOid();
2001
+ this.pendingWrites.trackWrite(oid, "transaction", "/", ops);
2002
+ const message = {
2003
+ o: "tx",
2004
+ ops,
2005
+ r: requestId,
2006
+ oid
2007
+ };
2008
+ this.send(message);
2009
+ await this.messageQueue.registerRequest(requestId);
2010
+ }
2011
+ // ============================================
1380
2012
  // Connection Events
1381
2013
  // ============================================
1382
2014
  /**
@@ -1418,6 +2050,11 @@ var LarkDatabase = class {
1418
2050
  this.ws?.send(JSON.stringify({ o: "po" }));
1419
2051
  return;
1420
2052
  }
2053
+ if (isAckMessage(message) && message.oid) {
2054
+ this.pendingWrites.onAck(message.oid);
2055
+ } else if (isNackMessage(message) && message.oid) {
2056
+ this.pendingWrites.onNack(message.oid);
2057
+ }
1421
2058
  if (this.messageQueue.handleMessage(message)) {
1422
2059
  return;
1423
2060
  }
@@ -1450,11 +2087,15 @@ var LarkDatabase = class {
1450
2087
  */
1451
2088
  async _sendSet(path, value, priority) {
1452
2089
  const requestId = this.messageQueue.nextRequestId();
2090
+ const oid = this.pendingWrites.generateOid();
2091
+ const normalizedPath = normalizePath(path) || "/";
2092
+ this.pendingWrites.trackWrite(oid, "set", normalizedPath, value);
1453
2093
  const message = {
1454
2094
  o: "s",
1455
- p: normalizePath(path) || "/",
2095
+ p: normalizedPath,
1456
2096
  v: value,
1457
- r: requestId
2097
+ r: requestId,
2098
+ oid
1458
2099
  };
1459
2100
  if (priority !== void 0) {
1460
2101
  message.y = priority;
@@ -1467,11 +2108,15 @@ var LarkDatabase = class {
1467
2108
  */
1468
2109
  async _sendUpdate(path, values) {
1469
2110
  const requestId = this.messageQueue.nextRequestId();
2111
+ const oid = this.pendingWrites.generateOid();
2112
+ const normalizedPath = normalizePath(path) || "/";
2113
+ this.pendingWrites.trackWrite(oid, "update", normalizedPath, values);
1470
2114
  const message = {
1471
2115
  o: "u",
1472
- p: normalizePath(path) || "/",
2116
+ p: normalizedPath,
1473
2117
  v: values,
1474
- r: requestId
2118
+ r: requestId,
2119
+ oid
1475
2120
  };
1476
2121
  this.send(message);
1477
2122
  await this.messageQueue.registerRequest(requestId);
@@ -1481,10 +2126,14 @@ var LarkDatabase = class {
1481
2126
  */
1482
2127
  async _sendDelete(path) {
1483
2128
  const requestId = this.messageQueue.nextRequestId();
2129
+ const oid = this.pendingWrites.generateOid();
2130
+ const normalizedPath = normalizePath(path) || "/";
2131
+ this.pendingWrites.trackWrite(oid, "delete", normalizedPath);
1484
2132
  const message = {
1485
2133
  o: "d",
1486
- p: normalizePath(path) || "/",
1487
- r: requestId
2134
+ p: normalizedPath,
2135
+ r: requestId,
2136
+ oid
1488
2137
  };
1489
2138
  this.send(message);
1490
2139
  await this.messageQueue.registerRequest(requestId);
@@ -1494,11 +2143,15 @@ var LarkDatabase = class {
1494
2143
  */
1495
2144
  async _sendPush(path, value) {
1496
2145
  const requestId = this.messageQueue.nextRequestId();
2146
+ const oid = this.pendingWrites.generateOid();
2147
+ const normalizedPath = normalizePath(path) || "/";
2148
+ this.pendingWrites.trackWrite(oid, "push", normalizedPath, value);
1497
2149
  const message = {
1498
2150
  o: "p",
1499
- p: normalizePath(path) || "/",
2151
+ p: normalizedPath,
1500
2152
  v: value,
1501
- r: requestId
2153
+ r: requestId,
2154
+ oid
1502
2155
  };
1503
2156
  this.send(message);
1504
2157
  const key = await this.messageQueue.registerRequest(requestId);
@@ -1528,11 +2181,10 @@ var LarkDatabase = class {
1528
2181
  const message = {
1529
2182
  o: "o",
1530
2183
  p: normalizedPath,
1531
- r: requestId
2184
+ r: requestId,
2185
+ // Spread query params at top level (not nested in 'q')
2186
+ ...query
1532
2187
  };
1533
- if (query) {
1534
- message.q = query;
1535
- }
1536
2188
  this.send(message);
1537
2189
  const value = await this.messageQueue.registerRequest(requestId);
1538
2190
  return new DataSnapshot(value, path, this);
@@ -1560,13 +2212,14 @@ var LarkDatabase = class {
1560
2212
  /**
1561
2213
  * @internal Send a subscribe message to server.
1562
2214
  */
1563
- async sendSubscribeMessage(path, eventTypes) {
2215
+ async sendSubscribeMessage(path, eventTypes, queryParams) {
1564
2216
  const requestId = this.messageQueue.nextRequestId();
1565
2217
  const message = {
1566
2218
  o: "sb",
1567
2219
  p: normalizePath(path) || "/",
1568
2220
  e: eventTypes,
1569
- r: requestId
2221
+ r: requestId,
2222
+ ...queryParams
1570
2223
  };
1571
2224
  this.send(message);
1572
2225
  await this.messageQueue.registerRequest(requestId);
@@ -1587,8 +2240,11 @@ var LarkDatabase = class {
1587
2240
  /**
1588
2241
  * @internal Create a DataSnapshot from event data.
1589
2242
  */
1590
- createSnapshot(path, value, volatile) {
1591
- return new DataSnapshot(value, path, this, { volatile });
2243
+ createSnapshot(path, value, volatile, serverTimestamp) {
2244
+ return new DataSnapshot(value, path, this, {
2245
+ volatile,
2246
+ serverTimestamp: serverTimestamp ?? null
2247
+ });
1592
2248
  }
1593
2249
  // ============================================
1594
2250
  // Internal: Subscription Management
@@ -1596,8 +2252,8 @@ var LarkDatabase = class {
1596
2252
  /**
1597
2253
  * @internal Subscribe to events at a path.
1598
2254
  */
1599
- _subscribe(path, eventType, callback) {
1600
- return this.subscriptionManager.subscribe(path, eventType, callback);
2255
+ _subscribe(path, eventType, callback, queryParams) {
2256
+ return this.subscriptionManager.subscribe(path, eventType, callback, queryParams);
1601
2257
  }
1602
2258
  /**
1603
2259
  * @internal Unsubscribe from a specific event type at a path.
@@ -1612,12 +2268,29 @@ var LarkDatabase = class {
1612
2268
  this.subscriptionManager.unsubscribeAll(path);
1613
2269
  }
1614
2270
  };
2271
+
2272
+ // src/utils/volatile.ts
2273
+ function isVolatilePath(path, patterns) {
2274
+ if (!patterns || patterns.length === 0) {
2275
+ return false;
2276
+ }
2277
+ const segments = path.replace(/^\//, "").split("/");
2278
+ return patterns.some((pattern) => {
2279
+ const patternSegments = pattern.split("/");
2280
+ if (segments.length !== patternSegments.length) {
2281
+ return false;
2282
+ }
2283
+ return patternSegments.every((p, i) => p === "*" || p === segments[i]);
2284
+ });
2285
+ }
1615
2286
  export {
1616
2287
  DataSnapshot,
1617
2288
  DatabaseReference,
1618
2289
  LarkDatabase,
1619
2290
  LarkError,
1620
2291
  OnDisconnect,
1621
- generatePushId
2292
+ PendingWriteManager,
2293
+ generatePushId,
2294
+ isVolatilePath
1622
2295
  };
1623
2296
  //# sourceMappingURL=index.mjs.map