@lark-sh/client 0.1.6 → 0.1.8

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
@@ -89,9 +89,6 @@ function isEventMessage(msg) {
89
89
  function isOnceResponseMessage(msg) {
90
90
  return "oc" in msg;
91
91
  }
92
- function isPushAckMessage(msg) {
93
- return "pa" in msg;
94
- }
95
92
  function isPingMessage(msg) {
96
93
  return "o" in msg && msg.o === "pi";
97
94
  }
@@ -139,7 +136,11 @@ var MessageQueue = class {
139
136
  if (pending) {
140
137
  clearTimeout(pending.timeout);
141
138
  this.pending.delete(message.jc);
142
- pending.resolve(message.vp || []);
139
+ const response = {
140
+ volatilePaths: message.vp || [],
141
+ connectionId: message.cid || null
142
+ };
143
+ pending.resolve(response);
143
144
  return true;
144
145
  }
145
146
  }
@@ -170,14 +171,22 @@ var MessageQueue = class {
170
171
  return true;
171
172
  }
172
173
  }
173
- if (isPushAckMessage(message)) {
174
- const pending = this.pending.get(message.pa);
175
- if (pending) {
176
- clearTimeout(pending.timeout);
177
- this.pending.delete(message.pa);
178
- pending.resolve(message.pk);
179
- return true;
180
- }
174
+ return false;
175
+ }
176
+ /**
177
+ * Reject a specific pending request locally (without server message).
178
+ * Used for client-side taint propagation when a write fails and
179
+ * dependent writes need to be rejected.
180
+ *
181
+ * @returns true if the request was pending and rejected, false otherwise
182
+ */
183
+ rejectLocally(requestId, error) {
184
+ const pending = this.pending.get(requestId);
185
+ if (pending) {
186
+ clearTimeout(pending.timeout);
187
+ this.pending.delete(requestId);
188
+ pending.reject(error);
189
+ return true;
181
190
  }
182
191
  return false;
183
192
  }
@@ -339,184 +348,560 @@ function setValueAtPath(obj, path, value) {
339
348
  current[lastSegment] = value;
340
349
  }
341
350
 
342
- // src/cache/DataCache.ts
343
- var DataCache = class {
344
- constructor() {
345
- // path -> cached value
346
- this.cache = /* @__PURE__ */ new Map();
351
+ // src/utils/sort.ts
352
+ function typeRank(value) {
353
+ if (value === null || value === void 0) {
354
+ return 0;
355
+ }
356
+ if (typeof value === "boolean") {
357
+ return value ? 2 : 1;
358
+ }
359
+ if (typeof value === "number") {
360
+ return 3;
361
+ }
362
+ if (typeof value === "string") {
363
+ return 4;
364
+ }
365
+ if (typeof value === "object") {
366
+ return 5;
367
+ }
368
+ return 6;
369
+ }
370
+ function compareValues(a, b) {
371
+ const rankA = typeRank(a);
372
+ const rankB = typeRank(b);
373
+ if (rankA !== rankB) {
374
+ return rankA < rankB ? -1 : 1;
375
+ }
376
+ switch (rankA) {
377
+ case 0:
378
+ return 0;
379
+ case 1:
380
+ // false
381
+ case 2:
382
+ return 0;
383
+ // Same boolean value (rank already differentiated)
384
+ case 3:
385
+ const numA = a;
386
+ const numB = b;
387
+ if (numA < numB) return -1;
388
+ if (numA > numB) return 1;
389
+ return 0;
390
+ case 4:
391
+ const strA = a;
392
+ const strB = b;
393
+ if (strA < strB) return -1;
394
+ if (strA > strB) return 1;
395
+ return 0;
396
+ case 5:
397
+ return 0;
398
+ // Objects are equal for value comparison, sort by key
399
+ default:
400
+ return 0;
401
+ }
402
+ }
403
+ function isInt32Key(key) {
404
+ if (key.length === 0) {
405
+ return false;
406
+ }
407
+ if (key.length > 1 && key[0] === "0") {
408
+ return false;
409
+ }
410
+ if (key[0] === "-") {
411
+ if (key.length === 1) {
412
+ return false;
413
+ }
414
+ if (key.length > 2 && key[1] === "0") {
415
+ return false;
416
+ }
417
+ if (key === "-0") {
418
+ return false;
419
+ }
420
+ }
421
+ const parsed = parseInt(key, 10);
422
+ if (isNaN(parsed)) {
423
+ return false;
424
+ }
425
+ if (String(parsed) !== key) {
426
+ return false;
427
+ }
428
+ const MIN_INT32 = -2147483648;
429
+ const MAX_INT32 = 2147483647;
430
+ return parsed >= MIN_INT32 && parsed <= MAX_INT32;
431
+ }
432
+ function compareKeys(a, b) {
433
+ const aIsInt = isInt32Key(a);
434
+ const bIsInt = isInt32Key(b);
435
+ if (aIsInt && !bIsInt) {
436
+ return -1;
437
+ }
438
+ if (!aIsInt && bIsInt) {
439
+ return 1;
440
+ }
441
+ if (aIsInt && bIsInt) {
442
+ const aInt = parseInt(a, 10);
443
+ const bInt = parseInt(b, 10);
444
+ if (aInt < bInt) return -1;
445
+ if (aInt > bInt) return 1;
446
+ return 0;
447
+ }
448
+ if (a < b) return -1;
449
+ if (a > b) return 1;
450
+ return 0;
451
+ }
452
+ function getNestedValue(obj, path) {
453
+ if (!obj || typeof obj !== "object") {
454
+ return void 0;
455
+ }
456
+ const segments = path.split("/").filter((s) => s.length > 0);
457
+ let current = obj;
458
+ for (const segment of segments) {
459
+ if (!current || typeof current !== "object") {
460
+ return void 0;
461
+ }
462
+ current = current[segment];
463
+ }
464
+ return current;
465
+ }
466
+ function getSortValue(value, queryParams) {
467
+ if (!queryParams) {
468
+ return null;
469
+ }
470
+ if (queryParams.orderBy === "priority") {
471
+ return getNestedValue(value, ".priority");
472
+ }
473
+ if (queryParams.orderByChild) {
474
+ return getNestedValue(value, queryParams.orderByChild);
475
+ }
476
+ if (queryParams.orderBy === "value") {
477
+ return value;
478
+ }
479
+ return null;
480
+ }
481
+ function compareEntries(a, b, queryParams) {
482
+ if (!queryParams || queryParams.orderBy === "key" || !queryParams.orderBy && !queryParams.orderByChild) {
483
+ return compareKeys(a.key, b.key);
484
+ }
485
+ const cmp = compareValues(a.sortValue, b.sortValue);
486
+ if (cmp !== 0) {
487
+ return cmp;
488
+ }
489
+ return compareKeys(a.key, b.key);
490
+ }
491
+ function sortEntries(entries, queryParams) {
492
+ entries.sort((a, b) => compareEntries(a, b, queryParams));
493
+ return entries;
494
+ }
495
+ function createSortEntries(data, queryParams) {
496
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
497
+ return [];
498
+ }
499
+ const obj = data;
500
+ const entries = [];
501
+ for (const key of Object.keys(obj)) {
502
+ if (key === ".priority") {
503
+ continue;
504
+ }
505
+ const value = obj[key];
506
+ entries.push({
507
+ key,
508
+ value,
509
+ sortValue: getSortValue(value, queryParams)
510
+ });
511
+ }
512
+ return sortEntries(entries, queryParams);
513
+ }
514
+ function getSortedKeys(data, queryParams) {
515
+ const entries = createSortEntries(data, queryParams);
516
+ return entries.map((e) => e.key);
517
+ }
518
+
519
+ // src/connection/View.ts
520
+ var View = class {
521
+ constructor(path, queryParams) {
522
+ /** Event callbacks organized by event type */
523
+ this.eventCallbacks = /* @__PURE__ */ new Map();
524
+ /** Child keys in sorted order */
525
+ this._orderedChildren = [];
526
+ /** Local cache: stores the value at the subscription path */
527
+ this._cache = void 0;
528
+ /** Whether we've received the initial snapshot from the server */
529
+ this._hasReceivedInitialSnapshot = false;
530
+ /** Pending write request IDs for this View (for local-first recovery) */
531
+ this._pendingWrites = /* @__PURE__ */ new Set();
532
+ /** Whether this View is in recovery mode after a nack */
533
+ this._recovering = false;
534
+ this.path = normalizePath(path);
535
+ this._queryParams = queryParams ?? null;
536
+ }
537
+ /** Get the current query parameters */
538
+ get queryParams() {
539
+ return this._queryParams;
540
+ }
541
+ /**
542
+ * Update query parameters and re-sort cached data.
543
+ * Returns true if queryParams actually changed.
544
+ */
545
+ updateQueryParams(newQueryParams) {
546
+ const newParams = newQueryParams ?? null;
547
+ if (this.queryParamsEqual(this._queryParams, newParams)) {
548
+ return false;
549
+ }
550
+ this._queryParams = newParams;
551
+ if (this._cache && typeof this._cache === "object" && this._cache !== null) {
552
+ this._orderedChildren = getSortedKeys(this._cache, this._queryParams);
553
+ }
554
+ return true;
347
555
  }
348
556
  /**
349
- * Store a value at a path.
350
- * Called when we receive data from a subscription event.
557
+ * Compare two QueryParams objects for equality.
351
558
  */
352
- set(path, value) {
353
- const normalized = normalizePath(path);
354
- this.cache.set(normalized, value);
559
+ queryParamsEqual(a, b) {
560
+ if (a === b) return true;
561
+ if (a === null || b === null) return false;
562
+ return a.orderBy === b.orderBy && a.orderByChild === b.orderByChild && a.limitToFirst === b.limitToFirst && a.limitToLast === b.limitToLast && a.startAt === b.startAt && a.startAtKey === b.startAtKey && a.endAt === b.endAt && a.endAtKey === b.endAtKey && a.equalTo === b.equalTo && a.equalToKey === b.equalToKey;
355
563
  }
564
+ // ============================================
565
+ // Subscription Management
566
+ // ============================================
356
567
  /**
357
- * Get the cached value at a path.
358
- * Returns undefined if not in cache.
359
- *
360
- * This method handles nested lookups:
361
- * - If /boxes is cached with {0: true, 1: false}
362
- * - get('/boxes/0') returns true (extracted from parent)
568
+ * Add a callback for an event type.
569
+ * Returns an unsubscribe function.
363
570
  */
364
- get(path) {
365
- const normalized = normalizePath(path);
366
- if (this.cache.has(normalized)) {
367
- return { value: this.cache.get(normalized), found: true };
571
+ addCallback(eventType, callback) {
572
+ if (!this.eventCallbacks.has(eventType)) {
573
+ this.eventCallbacks.set(eventType, []);
368
574
  }
369
- const ancestorResult = this.getFromAncestor(normalized);
370
- if (ancestorResult.found) {
371
- return ancestorResult;
575
+ const unsubscribe = () => {
576
+ this.removeCallback(eventType, callback);
577
+ };
578
+ this.eventCallbacks.get(eventType).push({ callback, unsubscribe });
579
+ return unsubscribe;
580
+ }
581
+ /**
582
+ * Remove a specific callback for an event type.
583
+ */
584
+ removeCallback(eventType, callback) {
585
+ const entries = this.eventCallbacks.get(eventType);
586
+ if (!entries) return;
587
+ const index = entries.findIndex((entry) => entry.callback === callback);
588
+ if (index !== -1) {
589
+ entries.splice(index, 1);
590
+ }
591
+ if (entries.length === 0) {
592
+ this.eventCallbacks.delete(eventType);
372
593
  }
373
- return { value: void 0, found: false };
374
594
  }
375
595
  /**
376
- * Check if we have cached data that covers the given path.
377
- * This includes exact matches and ancestor paths.
596
+ * Remove all callbacks for an event type.
378
597
  */
379
- has(path) {
380
- return this.get(path).found;
598
+ removeAllCallbacks(eventType) {
599
+ this.eventCallbacks.delete(eventType);
381
600
  }
382
601
  /**
383
- * Try to get data from an ancestor path.
384
- * E.g., if /boxes is cached and we want /boxes/5, extract it.
602
+ * Get all subscribed event types.
385
603
  */
386
- getFromAncestor(path) {
387
- const segments = path.split("/").filter((s) => s.length > 0);
388
- for (let i = segments.length - 1; i >= 0; i--) {
389
- const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
390
- if (this.cache.has(ancestorPath)) {
391
- const ancestorValue = this.cache.get(ancestorPath);
392
- const relativePath = "/" + segments.slice(i).join("/");
393
- const extractedValue = getValueAtPath(ancestorValue, relativePath);
394
- return { value: extractedValue, found: true };
395
- }
396
- }
397
- if (this.cache.has("/")) {
398
- const rootValue = this.cache.get("/");
399
- const extractedValue = getValueAtPath(rootValue, path);
400
- return { value: extractedValue, found: true };
401
- }
402
- return { value: void 0, found: false };
604
+ getEventTypes() {
605
+ return Array.from(this.eventCallbacks.keys());
403
606
  }
404
607
  /**
405
- * Remove cached data at a path.
406
- * Called when unsubscribing from a path that's no longer covered.
608
+ * Get callbacks for a specific event type.
407
609
  */
408
- delete(path) {
409
- const normalized = normalizePath(path);
410
- this.cache.delete(normalized);
610
+ getCallbacks(eventType) {
611
+ return this.eventCallbacks.get(eventType) ?? [];
411
612
  }
412
613
  /**
413
- * Remove cached data at a path and all descendant paths.
414
- * Called when a subscription is removed and no other subscription covers it.
614
+ * Check if there are any callbacks registered.
415
615
  */
416
- deleteTree(path) {
417
- const normalized = normalizePath(path);
418
- this.cache.delete(normalized);
419
- const prefix = normalized === "/" ? "/" : normalized + "/";
420
- for (const cachedPath of this.cache.keys()) {
421
- if (cachedPath.startsWith(prefix) || normalized === "/" && cachedPath !== "/") {
422
- this.cache.delete(cachedPath);
423
- }
616
+ hasCallbacks() {
617
+ return this.eventCallbacks.size > 0;
618
+ }
619
+ /**
620
+ * Check if there's a callback for a specific event type.
621
+ */
622
+ hasCallbacksForType(eventType) {
623
+ const entries = this.eventCallbacks.get(eventType);
624
+ return entries !== void 0 && entries.length > 0;
625
+ }
626
+ // ============================================
627
+ // Cache Management
628
+ // ============================================
629
+ /**
630
+ * Set the full cache value (used for initial snapshot).
631
+ * Children are sorted using client-side sorting rules.
632
+ */
633
+ setCache(value) {
634
+ this._cache = value;
635
+ this._hasReceivedInitialSnapshot = true;
636
+ if (value && typeof value === "object" && value !== null && !Array.isArray(value)) {
637
+ this._orderedChildren = getSortedKeys(value, this.queryParams);
638
+ } else {
639
+ this._orderedChildren = [];
424
640
  }
425
641
  }
426
642
  /**
427
- * Update cache when a child value changes.
428
- * This updates both the child path and any cached ancestor that contains it.
429
- *
430
- * E.g., if /boxes is cached and /boxes/5 changes:
431
- * - Update the /boxes cache to reflect the new /boxes/5 value
432
- *
433
- * If an ancestor is null, it creates a new object to hold the child.
643
+ * Get the full cached value at the subscription path.
644
+ */
645
+ getCache() {
646
+ return this._cache;
647
+ }
648
+ /**
649
+ * Get a value from the cache at a relative or absolute path.
650
+ * If the path is outside this View's scope, returns undefined.
434
651
  */
435
- updateChild(path, value) {
652
+ getCacheValue(path) {
436
653
  const normalized = normalizePath(path);
437
- this.cache.set(normalized, value);
438
- const segments = normalized.split("/").filter((s) => s.length > 0);
439
- for (let i = segments.length - 1; i >= 1; i--) {
440
- const ancestorPath = "/" + segments.slice(0, i).join("/");
441
- if (this.cache.has(ancestorPath)) {
442
- const ancestorValue = this.cache.get(ancestorPath);
443
- const baseValue = ancestorValue !== null && typeof ancestorValue === "object" ? this.deepClone(ancestorValue) : {};
444
- const remainingPath = "/" + segments.slice(i).join("/");
445
- setValueAtPath(baseValue, remainingPath, value);
446
- this.cache.set(ancestorPath, baseValue);
654
+ if (normalized === this.path) {
655
+ if (this._cache !== void 0) {
656
+ return { value: this._cache, found: true };
447
657
  }
658
+ return { value: void 0, found: false };
448
659
  }
449
- if (this.cache.has("/") && normalized !== "/") {
450
- const rootValue = this.cache.get("/");
451
- const baseValue = rootValue !== null && typeof rootValue === "object" ? this.deepClone(rootValue) : {};
452
- setValueAtPath(baseValue, normalized, value);
453
- this.cache.set("/", baseValue);
660
+ if (normalized.startsWith(this.path + "/") || this.path === "/") {
661
+ const relativePath = this.path === "/" ? normalized : normalized.slice(this.path.length);
662
+ if (this._cache !== void 0) {
663
+ const extractedValue = getValueAtPath(this._cache, relativePath);
664
+ return { value: extractedValue, found: true };
665
+ }
454
666
  }
667
+ return { value: void 0, found: false };
455
668
  }
456
669
  /**
457
- * Remove a child from the cache (e.g., on child_removed event).
670
+ * Update a child value in the cache.
671
+ * relativePath should be relative to this View's path.
672
+ * Maintains sorted order of children using client-side sorting.
458
673
  */
459
- removeChild(path) {
460
- const normalized = normalizePath(path);
461
- this.deleteTree(normalized);
462
- const segments = normalized.split("/").filter((s) => s.length > 0);
463
- for (let i = segments.length - 1; i >= 1; i--) {
464
- const ancestorPath = "/" + segments.slice(0, i).join("/");
465
- if (this.cache.has(ancestorPath)) {
466
- const ancestorValue = this.cache.get(ancestorPath);
467
- if (ancestorValue !== null && typeof ancestorValue === "object") {
468
- const updatedAncestor = this.deepClone(ancestorValue);
469
- const parentPath = "/" + segments.slice(i, -1).join("/");
470
- const childKey = segments[segments.length - 1];
471
- const parent = parentPath === "/" ? updatedAncestor : getValueAtPath(updatedAncestor, parentPath);
472
- if (parent && typeof parent === "object") {
473
- delete parent[childKey];
474
- }
475
- this.cache.set(ancestorPath, updatedAncestor);
674
+ updateCacheChild(relativePath, value) {
675
+ if (relativePath === "/") {
676
+ this.setCache(value);
677
+ return;
678
+ }
679
+ const segments = relativePath.split("/").filter((s) => s.length > 0);
680
+ if (segments.length === 0) return;
681
+ const childKey = segments[0];
682
+ if (this._cache === null || this._cache === void 0 || typeof this._cache !== "object") {
683
+ this._cache = {};
684
+ }
685
+ const cache = this._cache;
686
+ if (segments.length === 1) {
687
+ if (value === null) {
688
+ delete cache[childKey];
689
+ const idx = this._orderedChildren.indexOf(childKey);
690
+ if (idx !== -1) {
691
+ this._orderedChildren.splice(idx, 1);
692
+ }
693
+ } else {
694
+ const wasPresent = childKey in cache;
695
+ cache[childKey] = value;
696
+ if (!wasPresent) {
697
+ this.insertChildSorted(childKey, value);
698
+ } else {
699
+ this.resortChild(childKey);
700
+ }
701
+ }
702
+ } else {
703
+ if (value === null) {
704
+ setValueAtPath(cache, relativePath, void 0);
705
+ } else {
706
+ const wasPresent = childKey in cache;
707
+ if (!wasPresent) {
708
+ cache[childKey] = {};
709
+ }
710
+ setValueAtPath(cache, relativePath, value);
711
+ if (!wasPresent) {
712
+ this.insertChildSorted(childKey, cache[childKey]);
713
+ } else {
714
+ this.resortChild(childKey);
476
715
  }
477
716
  }
478
717
  }
479
718
  }
480
719
  /**
481
- * Clear all cached data.
720
+ * Insert a child key at the correct sorted position.
482
721
  */
483
- clear() {
484
- this.cache.clear();
722
+ insertChildSorted(key, value) {
723
+ if (this._orderedChildren.includes(key)) {
724
+ this.resortChild(key);
725
+ return;
726
+ }
727
+ const cache = this._cache;
728
+ const newSortValue = getSortValue(value, this.queryParams);
729
+ const newEntry = { key, value, sortValue: newSortValue };
730
+ let insertIdx = this._orderedChildren.length;
731
+ for (let i = 0; i < this._orderedChildren.length; i++) {
732
+ const existingKey = this._orderedChildren[i];
733
+ const existingValue = cache[existingKey];
734
+ const existingSortValue = getSortValue(existingValue, this.queryParams);
735
+ const existingEntry = { key: existingKey, value: existingValue, sortValue: existingSortValue };
736
+ if (compareEntries(newEntry, existingEntry, this.queryParams) < 0) {
737
+ insertIdx = i;
738
+ break;
739
+ }
740
+ }
741
+ this._orderedChildren.splice(insertIdx, 0, key);
485
742
  }
486
743
  /**
487
- * Get the number of cached paths (for testing/debugging).
744
+ * Re-sort a child that already exists (its sort value may have changed).
488
745
  */
489
- get size() {
490
- return this.cache.size;
746
+ resortChild(key) {
747
+ const idx = this._orderedChildren.indexOf(key);
748
+ if (idx === -1) return;
749
+ this._orderedChildren.splice(idx, 1);
750
+ const cache = this._cache;
751
+ const value = cache[key];
752
+ this.insertChildSorted(key, value);
491
753
  }
492
754
  /**
493
- * Deep clone a value to avoid mutation issues.
755
+ * Remove a child from the cache.
494
756
  */
495
- deepClone(value) {
496
- if (value === null || typeof value !== "object") {
497
- return value;
498
- }
499
- return JSON.parse(JSON.stringify(value));
757
+ removeCacheChild(relativePath) {
758
+ this.updateCacheChild(relativePath, null);
759
+ }
760
+ /**
761
+ * Check if we've received the initial snapshot.
762
+ */
763
+ get hasReceivedInitialSnapshot() {
764
+ return this._hasReceivedInitialSnapshot;
765
+ }
766
+ // ============================================
767
+ // Ordered Children
768
+ // ============================================
769
+ /**
770
+ * Get the ordered children keys.
771
+ */
772
+ get orderedChildren() {
773
+ return [...this._orderedChildren];
774
+ }
775
+ /**
776
+ * Get the previous sibling key for a given key.
777
+ * Returns null if the key is first or not found.
778
+ */
779
+ getPreviousChildKey(key) {
780
+ const idx = this._orderedChildren.indexOf(key);
781
+ if (idx <= 0) return null;
782
+ return this._orderedChildren[idx - 1];
783
+ }
784
+ /**
785
+ * Check if a key exists in the ordered children.
786
+ */
787
+ hasChild(key) {
788
+ return this._orderedChildren.includes(key);
789
+ }
790
+ // ============================================
791
+ // Query Helpers
792
+ // ============================================
793
+ /**
794
+ * Check if this View has query parameters.
795
+ */
796
+ hasQuery() {
797
+ if (!this.queryParams) return false;
798
+ return !!(this.queryParams.limitToFirst || this.queryParams.limitToLast || this.queryParams.startAt !== void 0 || this.queryParams.endAt !== void 0 || this.queryParams.equalTo !== void 0 || this.queryParams.orderBy || this.queryParams.orderByChild);
799
+ }
800
+ /**
801
+ * Check if this View has limit constraints.
802
+ */
803
+ hasLimit() {
804
+ if (!this.queryParams) return false;
805
+ return !!(this.queryParams.limitToFirst || this.queryParams.limitToLast);
806
+ }
807
+ // ============================================
808
+ // Pending Writes (for local-first)
809
+ // ============================================
810
+ /**
811
+ * Get current pending write IDs (to include in pw field).
812
+ * Returns a copy of the set as an array.
813
+ */
814
+ getPendingWriteIds() {
815
+ return Array.from(this._pendingWrites);
816
+ }
817
+ /**
818
+ * Add a pending write request ID.
819
+ */
820
+ addPendingWrite(requestId) {
821
+ this._pendingWrites.add(requestId);
822
+ }
823
+ /**
824
+ * Remove a pending write (on ack or nack).
825
+ */
826
+ removePendingWrite(requestId) {
827
+ return this._pendingWrites.delete(requestId);
828
+ }
829
+ /**
830
+ * Clear all pending writes (on nack recovery).
831
+ */
832
+ clearPendingWrites() {
833
+ this._pendingWrites.clear();
834
+ }
835
+ /**
836
+ * Check if this View has pending writes.
837
+ */
838
+ hasPendingWrites() {
839
+ return this._pendingWrites.size > 0;
840
+ }
841
+ /**
842
+ * Check if a specific write is pending.
843
+ */
844
+ isWritePending(requestId) {
845
+ return this._pendingWrites.has(requestId);
846
+ }
847
+ // ============================================
848
+ // Recovery State (for local-first nack handling)
849
+ // ============================================
850
+ /**
851
+ * Check if this View is in recovery mode.
852
+ */
853
+ get recovering() {
854
+ return this._recovering;
855
+ }
856
+ /**
857
+ * Enter recovery mode (after a nack).
858
+ */
859
+ enterRecovery() {
860
+ this._recovering = true;
861
+ this.clearPendingWrites();
862
+ }
863
+ /**
864
+ * Exit recovery mode (after fresh snapshot received).
865
+ */
866
+ exitRecovery() {
867
+ this._recovering = false;
868
+ }
869
+ // ============================================
870
+ // Cleanup
871
+ // ============================================
872
+ /**
873
+ * Clear the cache but preserve callbacks and pending writes.
874
+ * Used during reconnection.
875
+ */
876
+ clearCache() {
877
+ this._cache = void 0;
878
+ this._orderedChildren = [];
879
+ this._hasReceivedInitialSnapshot = false;
880
+ }
881
+ /**
882
+ * Clear everything - used when unsubscribing completely.
883
+ */
884
+ clear() {
885
+ this.eventCallbacks.clear();
886
+ this._cache = void 0;
887
+ this._orderedChildren = [];
888
+ this._hasReceivedInitialSnapshot = false;
889
+ this._pendingWrites.clear();
890
+ this._recovering = false;
500
891
  }
501
892
  };
502
893
 
503
894
  // src/connection/SubscriptionManager.ts
504
895
  var SubscriptionManager = class {
505
896
  constructor() {
506
- // subscriptionPath -> eventType -> array of subscriptions
507
- this.subscriptions = /* @__PURE__ */ new Map();
508
- // Ordered child keys for each subscription path (for ordered queries)
509
- // subscriptionPath -> ordered array of child keys
510
- this.orderedChildren = /* @__PURE__ */ new Map();
897
+ // path -> View (one View per subscribed path)
898
+ this.views = /* @__PURE__ */ new Map();
511
899
  // Callback to send subscribe message to server
512
900
  this.sendSubscribe = null;
513
- // Query params for each subscription path
514
- this.queryParams = /* @__PURE__ */ new Map();
515
901
  // Callback to send unsubscribe message to server
516
902
  this.sendUnsubscribe = null;
517
903
  // Callback to create DataSnapshot from event data
518
904
  this.createSnapshot = null;
519
- this.cache = new DataCache();
520
905
  }
521
906
  /**
522
907
  * Initialize the manager with server communication callbacks.
@@ -532,53 +917,74 @@ var SubscriptionManager = class {
532
917
  */
533
918
  subscribe(path, eventType, callback, queryParams) {
534
919
  const normalizedPath = path;
535
- const isFirstForPath = !this.subscriptions.has(normalizedPath);
536
- const existingEventTypes = this.getEventTypesForPath(normalizedPath);
537
- const isFirstForEventType = !existingEventTypes.includes(eventType);
538
- if (!this.subscriptions.has(normalizedPath)) {
539
- this.subscriptions.set(normalizedPath, /* @__PURE__ */ new Map());
540
- }
541
- const pathSubs = this.subscriptions.get(normalizedPath);
542
- if (!pathSubs.has(eventType)) {
543
- pathSubs.set(eventType, []);
544
- }
545
- const eventSubs = pathSubs.get(eventType);
546
- if (isFirstForPath && queryParams) {
547
- this.queryParams.set(normalizedPath, queryParams);
920
+ let view = this.views.get(normalizedPath);
921
+ const isNewView = !view;
922
+ let queryParamsChanged = false;
923
+ if (!view) {
924
+ view = new View(normalizedPath, queryParams);
925
+ this.views.set(normalizedPath, view);
926
+ } else {
927
+ queryParamsChanged = view.updateQueryParams(queryParams);
548
928
  }
549
- const unsubscribe = () => {
929
+ const existingEventTypes = view.getEventTypes();
930
+ const isNewEventType = !existingEventTypes.includes(eventType);
931
+ const unsubscribe = view.addCallback(eventType, callback);
932
+ const wrappedUnsubscribe = () => {
550
933
  this.unsubscribeCallback(normalizedPath, eventType, callback);
551
934
  };
552
- eventSubs.push({ callback, unsubscribe });
553
- if (isFirstForPath || isFirstForEventType) {
554
- const allEventTypes = this.getEventTypesForPath(normalizedPath);
555
- const storedQueryParams = this.queryParams.get(normalizedPath);
556
- this.sendSubscribe?.(normalizedPath, allEventTypes, storedQueryParams).catch((err) => {
935
+ if (isNewView || isNewEventType || queryParamsChanged) {
936
+ const allEventTypes = view.getEventTypes();
937
+ this.sendSubscribe?.(normalizedPath, allEventTypes, view.queryParams ?? void 0).catch((err) => {
557
938
  console.error("Failed to subscribe:", err);
558
939
  });
559
940
  }
560
- return unsubscribe;
941
+ if (!isNewView && view.hasReceivedInitialSnapshot) {
942
+ this.fireInitialEventsToCallback(view, eventType, callback);
943
+ }
944
+ return wrappedUnsubscribe;
945
+ }
946
+ /**
947
+ * Fire initial events to a newly added callback from cached data.
948
+ * This ensures new subscribers get current state even if View already existed.
949
+ */
950
+ fireInitialEventsToCallback(view, eventType, callback) {
951
+ const cache = view.getCache();
952
+ if (eventType === "value") {
953
+ const snapshot = this.createSnapshot?.(view.path, cache, false);
954
+ if (snapshot) {
955
+ try {
956
+ callback(snapshot, void 0);
957
+ } catch (err) {
958
+ console.error("Error in value subscription callback:", err);
959
+ }
960
+ }
961
+ } else if (eventType === "child_added") {
962
+ const orderedChildren = view.orderedChildren;
963
+ for (let i = 0; i < orderedChildren.length; i++) {
964
+ const childKey = orderedChildren[i];
965
+ const previousChildKey = i > 0 ? orderedChildren[i - 1] : null;
966
+ const childPath = joinPath(view.path, childKey);
967
+ const { value: childValue } = view.getCacheValue(childPath);
968
+ const snapshot = this.createSnapshot?.(childPath, childValue, false);
969
+ if (snapshot) {
970
+ try {
971
+ callback(snapshot, previousChildKey);
972
+ } catch (err) {
973
+ console.error("Error in child_added subscription callback:", err);
974
+ }
975
+ }
976
+ }
977
+ }
561
978
  }
562
979
  /**
563
980
  * Remove a specific callback from a subscription.
564
981
  */
565
982
  unsubscribeCallback(path, eventType, callback) {
566
- const pathSubs = this.subscriptions.get(path);
567
- if (!pathSubs) return;
568
- const eventSubs = pathSubs.get(eventType);
569
- if (!eventSubs) return;
570
- const index = eventSubs.findIndex((entry) => entry.callback === callback);
571
- if (index !== -1) {
572
- eventSubs.splice(index, 1);
573
- }
574
- if (eventSubs.length === 0) {
575
- pathSubs.delete(eventType);
576
- }
577
- if (pathSubs.size === 0) {
578
- this.subscriptions.delete(path);
579
- this.orderedChildren.delete(path);
580
- this.queryParams.delete(path);
581
- this.cache.deleteTree(path);
983
+ const view = this.views.get(path);
984
+ if (!view) return;
985
+ view.removeCallback(eventType, callback);
986
+ if (!view.hasCallbacks()) {
987
+ this.views.delete(path);
582
988
  this.sendUnsubscribe?.(path).catch((err) => {
583
989
  console.error("Failed to unsubscribe:", err);
584
990
  });
@@ -589,21 +995,17 @@ var SubscriptionManager = class {
589
995
  */
590
996
  unsubscribeEventType(path, eventType) {
591
997
  const normalizedPath = path;
592
- const pathSubs = this.subscriptions.get(normalizedPath);
593
- if (!pathSubs) return;
594
- pathSubs.delete(eventType);
595
- if (pathSubs.size === 0) {
596
- this.subscriptions.delete(normalizedPath);
597
- this.orderedChildren.delete(normalizedPath);
598
- this.queryParams.delete(normalizedPath);
599
- this.cache.deleteTree(normalizedPath);
998
+ const view = this.views.get(normalizedPath);
999
+ if (!view) return;
1000
+ view.removeAllCallbacks(eventType);
1001
+ if (!view.hasCallbacks()) {
1002
+ this.views.delete(normalizedPath);
600
1003
  this.sendUnsubscribe?.(normalizedPath).catch((err) => {
601
1004
  console.error("Failed to unsubscribe:", err);
602
1005
  });
603
1006
  } else {
604
- const remainingEventTypes = this.getEventTypesForPath(normalizedPath);
605
- const storedQueryParams = this.queryParams.get(normalizedPath);
606
- this.sendSubscribe?.(normalizedPath, remainingEventTypes, storedQueryParams).catch((err) => {
1007
+ const remainingEventTypes = view.getEventTypes();
1008
+ this.sendSubscribe?.(normalizedPath, remainingEventTypes, view.queryParams ?? void 0).catch((err) => {
607
1009
  console.error("Failed to update subscription:", err);
608
1010
  });
609
1011
  }
@@ -613,11 +1015,10 @@ var SubscriptionManager = class {
613
1015
  */
614
1016
  unsubscribeAll(path) {
615
1017
  const normalizedPath = path;
616
- if (!this.subscriptions.has(normalizedPath)) return;
617
- this.subscriptions.delete(normalizedPath);
618
- this.orderedChildren.delete(normalizedPath);
619
- this.queryParams.delete(normalizedPath);
620
- this.cache.deleteTree(normalizedPath);
1018
+ const view = this.views.get(normalizedPath);
1019
+ if (!view) return;
1020
+ view.clear();
1021
+ this.views.delete(normalizedPath);
621
1022
  this.sendUnsubscribe?.(normalizedPath).catch((err) => {
622
1023
  console.error("Failed to unsubscribe:", err);
623
1024
  });
@@ -644,186 +1045,303 @@ var SubscriptionManager = class {
644
1045
  const value = message.v;
645
1046
  const isVolatile = message.x ?? false;
646
1047
  const serverTimestamp = message.ts;
647
- const afterKey = message.ak;
648
- const orderedKeys = message.k;
649
- const pathSubs = this.subscriptions.get(subscriptionPath);
650
- if (!pathSubs) return;
651
- const absolutePath = relativePath === "/" ? subscriptionPath : joinPath(subscriptionPath, relativePath);
652
- const previousOrder = this.orderedChildren.get(subscriptionPath) ?? [];
653
- const previousChildSet = new Set(previousOrder);
654
- if (value !== void 0) {
655
- if (relativePath === "/") {
656
- this.cache.set(subscriptionPath, value);
657
- } else if (value === null) {
658
- this.cache.removeChild(absolutePath);
659
- } else {
660
- this.cache.updateChild(absolutePath, value);
661
- }
662
- }
663
- if (relativePath === "/") {
664
- if (orderedKeys) {
665
- this.orderedChildren.set(subscriptionPath, [...orderedKeys]);
666
- } else if (value && typeof value === "object" && value !== null) {
667
- this.orderedChildren.set(subscriptionPath, Object.keys(value));
668
- } else {
669
- this.orderedChildren.set(subscriptionPath, []);
670
- }
671
- } else {
672
- const segments = relativePath.split("/").filter((s) => s.length > 0);
673
- if (segments.length > 0) {
674
- const childKey = segments[0];
675
- const currentOrder2 = this.orderedChildren.get(subscriptionPath) ?? [];
676
- if (value === null) {
677
- const newOrder = currentOrder2.filter((k) => k !== childKey);
678
- this.orderedChildren.set(subscriptionPath, newOrder);
679
- } else if (!previousChildSet.has(childKey)) {
680
- const newOrder = [...currentOrder2];
681
- this.insertAfterKey(newOrder, childKey, afterKey);
682
- this.orderedChildren.set(subscriptionPath, newOrder);
683
- }
1048
+ const view = this.views.get(subscriptionPath);
1049
+ if (!view) return;
1050
+ if (value === void 0) return;
1051
+ if (view.recovering) {
1052
+ if (relativePath !== "/") {
1053
+ return;
684
1054
  }
1055
+ this.applyWriteToView(view, [{ relativePath, value }], isVolatile, serverTimestamp);
1056
+ view.exitRecovery();
1057
+ return;
685
1058
  }
686
- const currentOrder = this.orderedChildren.get(subscriptionPath) ?? [];
687
- const currentChildSet = new Set(currentOrder);
688
- this.fireCallbacks(
689
- subscriptionPath,
690
- pathSubs,
691
- relativePath,
692
- value,
693
- previousOrder,
694
- currentOrder,
695
- previousChildSet,
696
- currentChildSet,
697
- afterKey,
698
- isVolatile,
699
- serverTimestamp
700
- );
1059
+ this.applyWriteToView(view, [{ relativePath, value }], isVolatile, serverTimestamp);
701
1060
  }
702
1061
  /**
703
- * Handle a 'patch' event - multi-path change and/or moves.
1062
+ * Handle a 'patch' event - multi-path change.
704
1063
  */
705
1064
  handlePatchEvent(message) {
706
1065
  const subscriptionPath = message.sp;
707
1066
  const basePath = message.p;
708
1067
  const patches = message.v;
709
- const moves = message.mv;
710
- const orderedKeys = message.k;
711
1068
  const isVolatile = message.x ?? false;
712
1069
  const serverTimestamp = message.ts;
713
- const pathSubs = this.subscriptions.get(subscriptionPath);
714
- if (!pathSubs) return;
715
- const previousOrder = this.orderedChildren.get(subscriptionPath) ?? [];
716
- const previousChildSet = new Set(previousOrder);
717
- const affectedChildren = /* @__PURE__ */ new Set();
718
- if (patches) {
719
- for (const [relativePath, value] of Object.entries(patches)) {
720
- const fullRelativePath = basePath === "/" ? "/" + relativePath : joinPath(basePath, relativePath);
721
- const absolutePath = joinPath(subscriptionPath, fullRelativePath);
722
- const segments = fullRelativePath.split("/").filter((s) => s.length > 0);
723
- if (segments.length > 0) {
724
- affectedChildren.add(segments[0]);
725
- }
726
- if (value === null) {
727
- this.cache.removeChild(absolutePath);
728
- } else {
729
- this.cache.updateChild(absolutePath, value);
730
- }
1070
+ const view = this.views.get(subscriptionPath);
1071
+ if (!view) return;
1072
+ if (view.recovering) {
1073
+ return;
1074
+ }
1075
+ if (!patches) return;
1076
+ const updates = [];
1077
+ for (const [relativePath, patchValue] of Object.entries(patches)) {
1078
+ let fullRelativePath;
1079
+ if (basePath === "/") {
1080
+ fullRelativePath = relativePath.startsWith("/") ? relativePath : "/" + relativePath;
1081
+ } else {
1082
+ fullRelativePath = joinPath(basePath, relativePath);
731
1083
  }
1084
+ updates.push({ relativePath: fullRelativePath, value: patchValue });
732
1085
  }
733
- if (moves && moves.length > 0) {
734
- this.handleMoves(subscriptionPath, pathSubs, moves, isVolatile, serverTimestamp);
1086
+ this.applyWriteToView(view, updates, isVolatile, serverTimestamp);
1087
+ }
1088
+ /**
1089
+ * Detect and fire child_moved events for children that changed position.
1090
+ */
1091
+ detectAndFireMoves(view, previousOrder, currentOrder, previousPositions, currentPositions, previousChildSet, currentChildSet, childMovedSubs, isVolatile, serverTimestamp) {
1092
+ if (childMovedSubs.length === 0) return;
1093
+ for (const key of currentOrder) {
1094
+ if (!previousChildSet.has(key) || !currentChildSet.has(key)) {
1095
+ continue;
1096
+ }
1097
+ const oldPos = previousPositions.get(key);
1098
+ const newPos = currentPositions.get(key);
1099
+ const oldPrevKey = oldPos > 0 ? previousOrder[oldPos - 1] : null;
1100
+ const newPrevKey = newPos > 0 ? currentOrder[newPos - 1] : null;
1101
+ if (oldPrevKey !== newPrevKey) {
1102
+ this.fireChildMoved(view, key, childMovedSubs, newPrevKey, isVolatile, serverTimestamp);
1103
+ }
735
1104
  }
736
- if (orderedKeys) {
737
- this.orderedChildren.set(subscriptionPath, [...orderedKeys]);
1105
+ }
1106
+ /**
1107
+ * Fire child_added callbacks for a child key.
1108
+ */
1109
+ fireChildAdded(view, childKey, subs, previousChildKey, isVolatile, serverTimestamp) {
1110
+ if (subs.length === 0) return;
1111
+ const childPath = joinPath(view.path, childKey);
1112
+ const { value: childValue } = view.getCacheValue(childPath);
1113
+ const snapshot = this.createSnapshot?.(childPath, childValue, isVolatile, serverTimestamp);
1114
+ if (snapshot) {
1115
+ for (const entry of subs) {
1116
+ try {
1117
+ entry.callback(snapshot, previousChildKey);
1118
+ } catch (err) {
1119
+ console.error("Error in child_added subscription callback:", err);
1120
+ }
1121
+ }
738
1122
  }
739
- const currentOrder = this.orderedChildren.get(subscriptionPath) ?? [];
740
- const currentChildSet = new Set(currentOrder);
741
- const valueSubs = pathSubs.get("value");
742
- if (valueSubs && valueSubs.length > 0) {
743
- const fullValue = this.cache.get(subscriptionPath).value;
744
- const snapshot = this.createSnapshot?.(subscriptionPath, fullValue, isVolatile, serverTimestamp);
745
- if (snapshot) {
746
- for (const entry of valueSubs) {
747
- try {
748
- entry.callback(snapshot, void 0);
749
- } catch (err) {
750
- console.error("Error in value subscription callback:", err);
751
- }
1123
+ }
1124
+ /**
1125
+ * Fire child_changed callbacks for a child key.
1126
+ */
1127
+ fireChildChanged(view, childKey, subs, previousChildKey, isVolatile, serverTimestamp) {
1128
+ if (subs.length === 0) return;
1129
+ const childPath = joinPath(view.path, childKey);
1130
+ const { value: childValue } = view.getCacheValue(childPath);
1131
+ const snapshot = this.createSnapshot?.(childPath, childValue, isVolatile, serverTimestamp);
1132
+ if (snapshot) {
1133
+ for (const entry of subs) {
1134
+ try {
1135
+ entry.callback(snapshot, previousChildKey);
1136
+ } catch (err) {
1137
+ console.error("Error in child_changed subscription callback:", err);
752
1138
  }
753
1139
  }
754
1140
  }
755
- if (patches && affectedChildren.size > 0) {
756
- const childAddedSubs = pathSubs.get("child_added") ?? [];
757
- const childChangedSubs = pathSubs.get("child_changed") ?? [];
758
- const childRemovedSubs = pathSubs.get("child_removed") ?? [];
759
- for (const childKey of affectedChildren) {
760
- const wasPresent = previousChildSet.has(childKey);
761
- const isPresent = currentChildSet.has(childKey);
762
- if (!wasPresent && isPresent) {
763
- const prevKey = this.getPreviousChildKey(currentOrder, childKey);
764
- this.fireChildAdded(subscriptionPath, childKey, childAddedSubs, prevKey, isVolatile, serverTimestamp);
765
- } else if (wasPresent && !isPresent) {
766
- this.fireChildRemoved(subscriptionPath, childKey, childRemovedSubs, isVolatile, serverTimestamp);
767
- } else if (wasPresent && isPresent) {
768
- const prevKey = this.getPreviousChildKey(currentOrder, childKey);
769
- this.fireChildChanged(subscriptionPath, childKey, childChangedSubs, prevKey, isVolatile, serverTimestamp);
1141
+ }
1142
+ /**
1143
+ * Fire child_removed callbacks for a child key.
1144
+ */
1145
+ fireChildRemoved(view, childKey, subs, isVolatile, serverTimestamp) {
1146
+ if (subs.length === 0) return;
1147
+ const childPath = joinPath(view.path, childKey);
1148
+ const snapshot = this.createSnapshot?.(childPath, null, isVolatile, serverTimestamp);
1149
+ if (snapshot) {
1150
+ for (const entry of subs) {
1151
+ try {
1152
+ entry.callback(snapshot, void 0);
1153
+ } catch (err) {
1154
+ console.error("Error in child_removed subscription callback:", err);
770
1155
  }
771
1156
  }
772
1157
  }
773
1158
  }
774
1159
  /**
775
- * Handle moves array - update order and fire child_moved events.
1160
+ * Fire child_moved callbacks for a child key.
776
1161
  */
777
- handleMoves(subscriptionPath, pathSubs, moves, isVolatile, serverTimestamp) {
778
- const childMovedSubs = pathSubs.get("child_moved") ?? [];
779
- const currentOrder = this.orderedChildren.get(subscriptionPath) ?? [];
780
- for (const move of moves) {
781
- const { k: childKey, ak: afterKey } = move;
782
- const idx = currentOrder.indexOf(childKey);
783
- if (idx !== -1) {
784
- currentOrder.splice(idx, 1);
1162
+ fireChildMoved(view, childKey, subs, previousChildKey, isVolatile, serverTimestamp) {
1163
+ if (subs.length === 0) return;
1164
+ const childPath = joinPath(view.path, childKey);
1165
+ const { value: childValue } = view.getCacheValue(childPath);
1166
+ const snapshot = this.createSnapshot?.(childPath, childValue, isVolatile, serverTimestamp);
1167
+ if (snapshot) {
1168
+ for (const entry of subs) {
1169
+ try {
1170
+ entry.callback(snapshot, previousChildKey);
1171
+ } catch (err) {
1172
+ console.error("Error in child_moved subscription callback:", err);
1173
+ }
785
1174
  }
786
- this.insertAfterKey(currentOrder, childKey, afterKey);
787
- if (childMovedSubs.length > 0) {
788
- const previousChildKey = afterKey === "" ? null : afterKey;
789
- this.fireChildMoved(subscriptionPath, childKey, childMovedSubs, previousChildKey, isVolatile, serverTimestamp);
1175
+ }
1176
+ }
1177
+ /**
1178
+ * Clear all subscriptions (e.g., on disconnect).
1179
+ */
1180
+ clear() {
1181
+ for (const view of this.views.values()) {
1182
+ view.clear();
1183
+ }
1184
+ this.views.clear();
1185
+ }
1186
+ /**
1187
+ * Check if there are any subscriptions at a path.
1188
+ */
1189
+ hasSubscriptions(path) {
1190
+ return this.views.has(path);
1191
+ }
1192
+ /**
1193
+ * Check if a path is "covered" by an active subscription.
1194
+ *
1195
+ * A path is covered if:
1196
+ * - There's an active 'value' subscription at that exact path, OR
1197
+ * - There's an active 'value' subscription at an ancestor path
1198
+ */
1199
+ isPathCovered(path) {
1200
+ const normalized = normalizePath(path);
1201
+ if (this.hasValueSubscription(normalized)) {
1202
+ return true;
1203
+ }
1204
+ const segments = normalized.split("/").filter((s) => s.length > 0);
1205
+ for (let i = segments.length - 1; i >= 0; i--) {
1206
+ const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
1207
+ if (this.hasValueSubscription(ancestorPath)) {
1208
+ return true;
790
1209
  }
791
1210
  }
792
- this.orderedChildren.set(subscriptionPath, currentOrder);
1211
+ if (normalized !== "/" && this.hasValueSubscription("/")) {
1212
+ return true;
1213
+ }
1214
+ return false;
793
1215
  }
794
1216
  /**
795
- * Insert a key into an ordered array after a specific key.
796
- * If afterKey is empty string or undefined, insert at beginning.
797
- * If afterKey is not found, append at end.
1217
+ * Check if there's a 'value' subscription at a path.
798
1218
  */
799
- insertAfterKey(order, key, afterKey) {
800
- if (afterKey === "" || afterKey === void 0) {
801
- order.unshift(key);
802
- } else {
803
- const afterIdx = order.indexOf(afterKey);
804
- if (afterIdx === -1) {
805
- order.push(key);
806
- } else {
807
- order.splice(afterIdx + 1, 0, key);
1219
+ hasValueSubscription(path) {
1220
+ const view = this.views.get(path);
1221
+ return view !== void 0 && view.hasCallbacksForType("value");
1222
+ }
1223
+ /**
1224
+ * Get a cached value if the path is covered by an active subscription.
1225
+ */
1226
+ getCachedValue(path) {
1227
+ const normalized = normalizePath(path);
1228
+ if (!this.isPathCovered(normalized)) {
1229
+ return { value: void 0, found: false };
1230
+ }
1231
+ const exactView = this.views.get(normalized);
1232
+ if (exactView) {
1233
+ return exactView.getCacheValue(normalized);
1234
+ }
1235
+ const segments = normalized.split("/").filter((s) => s.length > 0);
1236
+ for (let i = segments.length - 1; i >= 0; i--) {
1237
+ const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
1238
+ const ancestorView = this.views.get(ancestorPath);
1239
+ if (ancestorView && ancestorView.hasCallbacksForType("value")) {
1240
+ return ancestorView.getCacheValue(normalized);
808
1241
  }
809
1242
  }
1243
+ if (normalized !== "/") {
1244
+ const rootView = this.views.get("/");
1245
+ if (rootView && rootView.hasCallbacksForType("value")) {
1246
+ return rootView.getCacheValue(normalized);
1247
+ }
1248
+ }
1249
+ return { value: void 0, found: false };
810
1250
  }
811
1251
  /**
812
- * Get the previous sibling key for a given key in the ordered array.
1252
+ * Get the cache size (for testing/debugging).
1253
+ * Returns the number of Views.
813
1254
  */
814
- getPreviousChildKey(order, key) {
815
- const idx = order.indexOf(key);
816
- if (idx <= 0) return null;
817
- return order[idx - 1];
1255
+ get cacheSize() {
1256
+ return this.views.size;
818
1257
  }
819
1258
  /**
820
- * Fire callbacks for subscribed event types.
1259
+ * Clear only the cache, preserving subscription registrations.
1260
+ * Used when preparing for reconnect - we keep subscriptions but will get fresh data.
821
1261
  */
822
- fireCallbacks(subscriptionPath, pathSubs, relativePath, value, previousOrder, currentOrder, previousChildSet, currentChildSet, afterKey, isVolatile, serverTimestamp) {
823
- const valueSubs = pathSubs.get("value");
824
- if (valueSubs && valueSubs.length > 0) {
825
- const fullValue = this.cache.get(subscriptionPath).value;
826
- const snapshot = this.createSnapshot?.(subscriptionPath, fullValue, isVolatile, serverTimestamp);
1262
+ clearCacheOnly() {
1263
+ for (const view of this.views.values()) {
1264
+ view.clearCache();
1265
+ }
1266
+ }
1267
+ /**
1268
+ * Re-subscribe to all active subscriptions.
1269
+ * Used after reconnecting to restore subscriptions on the server.
1270
+ */
1271
+ async resubscribeAll() {
1272
+ for (const [path, view] of this.views) {
1273
+ const eventTypes = view.getEventTypes();
1274
+ if (eventTypes.length > 0) {
1275
+ try {
1276
+ await this.sendSubscribe?.(path, eventTypes, view.queryParams ?? void 0);
1277
+ } catch (err) {
1278
+ console.error(`Failed to resubscribe to ${path}:`, err);
1279
+ }
1280
+ }
1281
+ }
1282
+ }
1283
+ /**
1284
+ * Get information about active subscriptions (for debugging).
1285
+ */
1286
+ getActiveSubscriptions() {
1287
+ const result = [];
1288
+ for (const [path, view] of this.views) {
1289
+ const eventTypes = view.getEventTypes();
1290
+ const queryParams = view.queryParams ?? void 0;
1291
+ result.push({ path, eventTypes, queryParams });
1292
+ }
1293
+ return result;
1294
+ }
1295
+ /**
1296
+ * Get a View by path (for testing/debugging).
1297
+ */
1298
+ getView(path) {
1299
+ return this.views.get(path);
1300
+ }
1301
+ // ============================================
1302
+ // Shared Write Application (used by server events and optimistic writes)
1303
+ // ============================================
1304
+ /**
1305
+ * Apply write(s) to a View's cache and fire appropriate events.
1306
+ * This is the core logic shared between server events and optimistic writes.
1307
+ *
1308
+ * @param view - The View to update
1309
+ * @param updates - Array of {relativePath, value} pairs to apply
1310
+ * @param isVolatile - Whether this is a volatile update
1311
+ * @param serverTimestamp - Optional server timestamp
1312
+ */
1313
+ applyWriteToView(view, updates, isVolatile, serverTimestamp) {
1314
+ const previousOrder = view.orderedChildren;
1315
+ const previousChildSet = new Set(previousOrder);
1316
+ const previousCacheJson = JSON.stringify(view.getCache());
1317
+ const affectedChildren = /* @__PURE__ */ new Set();
1318
+ let isFullSnapshot = false;
1319
+ for (const { relativePath, value } of updates) {
1320
+ if (relativePath === "/") {
1321
+ view.setCache(value);
1322
+ isFullSnapshot = true;
1323
+ } else {
1324
+ const segments = relativePath.split("/").filter((s) => s.length > 0);
1325
+ if (segments.length > 0) {
1326
+ affectedChildren.add(segments[0]);
1327
+ }
1328
+ if (value === null) {
1329
+ view.removeCacheChild(relativePath);
1330
+ } else {
1331
+ view.updateCacheChild(relativePath, value);
1332
+ }
1333
+ }
1334
+ }
1335
+ const currentOrder = view.orderedChildren;
1336
+ const currentChildSet = new Set(currentOrder);
1337
+ const currentCacheJson = JSON.stringify(view.getCache());
1338
+ if (previousCacheJson === currentCacheJson) {
1339
+ return;
1340
+ }
1341
+ const valueSubs = view.getCallbacks("value");
1342
+ if (valueSubs.length > 0) {
1343
+ const fullValue = view.getCache();
1344
+ const snapshot = this.createSnapshot?.(view.path, fullValue, isVolatile, serverTimestamp);
827
1345
  if (snapshot) {
828
1346
  for (const entry of valueSubs) {
829
1347
  try {
@@ -834,212 +1352,197 @@ var SubscriptionManager = class {
834
1352
  }
835
1353
  }
836
1354
  }
837
- this.fireChildEvents(
838
- subscriptionPath,
839
- pathSubs,
840
- relativePath,
841
- value,
842
- previousOrder,
843
- currentOrder,
844
- previousChildSet,
845
- currentChildSet,
846
- afterKey,
847
- isVolatile,
848
- serverTimestamp
849
- );
850
- }
851
- /**
852
- * Generate and fire child_added, child_changed, child_removed events.
853
- * child_moved is handled separately via handleMoves().
854
- */
855
- fireChildEvents(subscriptionPath, pathSubs, relativePath, value, previousOrder, currentOrder, previousChildSet, currentChildSet, afterKey, isVolatile, serverTimestamp) {
856
- const childAddedSubs = pathSubs.get("child_added") ?? [];
857
- const childChangedSubs = pathSubs.get("child_changed") ?? [];
858
- const childRemovedSubs = pathSubs.get("child_removed") ?? [];
859
- if (childAddedSubs.length === 0 && childChangedSubs.length === 0 && childRemovedSubs.length === 0) {
1355
+ const childAddedSubs = view.getCallbacks("child_added");
1356
+ const childChangedSubs = view.getCallbacks("child_changed");
1357
+ const childRemovedSubs = view.getCallbacks("child_removed");
1358
+ const childMovedSubs = view.getCallbacks("child_moved");
1359
+ if (childAddedSubs.length === 0 && childChangedSubs.length === 0 && childRemovedSubs.length === 0 && childMovedSubs.length === 0) {
860
1360
  return;
861
1361
  }
862
- if (relativePath === "/") {
1362
+ if (isFullSnapshot) {
863
1363
  for (const key of currentOrder) {
864
1364
  if (!previousChildSet.has(key)) {
865
- const prevKey = this.getPreviousChildKey(currentOrder, key);
866
- this.fireChildAdded(subscriptionPath, key, childAddedSubs, prevKey, isVolatile, serverTimestamp);
1365
+ const prevKey = view.getPreviousChildKey(key);
1366
+ this.fireChildAdded(view, key, childAddedSubs, prevKey, isVolatile, serverTimestamp);
867
1367
  }
868
1368
  }
869
1369
  for (const key of previousOrder) {
870
1370
  if (!currentChildSet.has(key)) {
871
- this.fireChildRemoved(subscriptionPath, key, childRemovedSubs, isVolatile, serverTimestamp);
872
- }
873
- }
874
- } else {
875
- const segments = relativePath.split("/").filter((s) => s.length > 0);
876
- if (segments.length === 0) return;
877
- const childKey = segments[0];
878
- if (value === null) {
879
- if (previousChildSet.has(childKey) && !currentChildSet.has(childKey)) {
880
- this.fireChildRemoved(subscriptionPath, childKey, childRemovedSubs, isVolatile, serverTimestamp);
1371
+ this.fireChildRemoved(view, key, childRemovedSubs, isVolatile, serverTimestamp);
881
1372
  }
882
- } else if (!previousChildSet.has(childKey)) {
883
- const prevKey = afterKey !== void 0 ? afterKey === "" ? null : afterKey : this.getPreviousChildKey(currentOrder, childKey);
884
- this.fireChildAdded(subscriptionPath, childKey, childAddedSubs, prevKey, isVolatile, serverTimestamp);
885
- } else {
886
- const prevKey = this.getPreviousChildKey(currentOrder, childKey);
887
- this.fireChildChanged(subscriptionPath, childKey, childChangedSubs, prevKey, isVolatile, serverTimestamp);
888
1373
  }
889
- }
890
- }
891
- /**
892
- * Fire child_added callbacks for a child key.
893
- */
894
- fireChildAdded(subscriptionPath, childKey, subs, previousChildKey, isVolatile, serverTimestamp) {
895
- if (subs.length === 0) return;
896
- const childPath = joinPath(subscriptionPath, childKey);
897
- const childValue = this.cache.get(childPath).value;
898
- const snapshot = this.createSnapshot?.(childPath, childValue, isVolatile, serverTimestamp);
899
- if (snapshot) {
900
- for (const entry of subs) {
901
- try {
902
- entry.callback(snapshot, previousChildKey);
903
- } catch (err) {
904
- console.error("Error in child_added subscription callback:", err);
905
- }
906
- }
907
- }
908
- }
909
- /**
910
- * Fire child_changed callbacks for a child key.
911
- */
912
- fireChildChanged(subscriptionPath, childKey, subs, previousChildKey, isVolatile, serverTimestamp) {
913
- if (subs.length === 0) return;
914
- const childPath = joinPath(subscriptionPath, childKey);
915
- const childValue = this.cache.get(childPath).value;
916
- const snapshot = this.createSnapshot?.(childPath, childValue, isVolatile, serverTimestamp);
917
- if (snapshot) {
918
- for (const entry of subs) {
919
- try {
920
- entry.callback(snapshot, previousChildKey);
921
- } catch (err) {
922
- console.error("Error in child_changed subscription callback:", err);
1374
+ } else {
1375
+ for (const childKey of affectedChildren) {
1376
+ const wasPresent = previousChildSet.has(childKey);
1377
+ const isPresent = currentChildSet.has(childKey);
1378
+ if (!wasPresent && isPresent) {
1379
+ const prevKey = view.getPreviousChildKey(childKey);
1380
+ this.fireChildAdded(view, childKey, childAddedSubs, prevKey, isVolatile, serverTimestamp);
1381
+ } else if (wasPresent && !isPresent) {
1382
+ this.fireChildRemoved(view, childKey, childRemovedSubs, isVolatile, serverTimestamp);
1383
+ } else if (wasPresent && isPresent) {
1384
+ const prevKey = view.getPreviousChildKey(childKey);
1385
+ this.fireChildChanged(view, childKey, childChangedSubs, prevKey, isVolatile, serverTimestamp);
923
1386
  }
924
1387
  }
925
1388
  }
1389
+ const previousPositions = /* @__PURE__ */ new Map();
1390
+ previousOrder.forEach((key, idx) => previousPositions.set(key, idx));
1391
+ const currentPositions = /* @__PURE__ */ new Map();
1392
+ currentOrder.forEach((key, idx) => currentPositions.set(key, idx));
1393
+ this.detectAndFireMoves(
1394
+ view,
1395
+ previousOrder,
1396
+ currentOrder,
1397
+ previousPositions,
1398
+ currentPositions,
1399
+ previousChildSet,
1400
+ currentChildSet,
1401
+ childMovedSubs,
1402
+ isVolatile,
1403
+ serverTimestamp
1404
+ );
926
1405
  }
1406
+ // ============================================
1407
+ // Pending Writes (for local-first)
1408
+ // ============================================
927
1409
  /**
928
- * Fire child_removed callbacks for a child key.
929
- */
930
- fireChildRemoved(subscriptionPath, childKey, subs, isVolatile, serverTimestamp) {
931
- if (subs.length === 0) return;
932
- const childPath = joinPath(subscriptionPath, childKey);
933
- const snapshot = this.createSnapshot?.(childPath, null, isVolatile, serverTimestamp);
934
- if (snapshot) {
935
- for (const entry of subs) {
936
- try {
937
- entry.callback(snapshot, void 0);
938
- } catch (err) {
939
- console.error("Error in child_removed subscription callback:", err);
940
- }
1410
+ * Find all Views that cover a given write path.
1411
+ * A View covers a path if the View's path is an ancestor of or equal to the write path.
1412
+ */
1413
+ findViewsForWritePath(writePath) {
1414
+ const normalized = normalizePath(writePath);
1415
+ const views = [];
1416
+ for (const [viewPath, view] of this.views) {
1417
+ if (normalized === viewPath) {
1418
+ views.push(view);
1419
+ } else if (normalized.startsWith(viewPath + "/")) {
1420
+ views.push(view);
1421
+ } else if (viewPath === "/") {
1422
+ views.push(view);
941
1423
  }
942
1424
  }
1425
+ return views;
943
1426
  }
944
1427
  /**
945
- * Fire child_moved callbacks for a child key.
1428
+ * Get pending write IDs for a write path.
1429
+ * Returns the union of pending writes from all Views that cover this path.
946
1430
  */
947
- fireChildMoved(subscriptionPath, childKey, subs, previousChildKey, isVolatile, serverTimestamp) {
948
- if (subs.length === 0) return;
949
- const childPath = joinPath(subscriptionPath, childKey);
950
- const childValue = this.cache.get(childPath).value;
951
- const snapshot = this.createSnapshot?.(childPath, childValue, isVolatile, serverTimestamp);
952
- if (snapshot) {
953
- for (const entry of subs) {
954
- try {
955
- entry.callback(snapshot, previousChildKey);
956
- } catch (err) {
957
- console.error("Error in child_moved subscription callback:", err);
958
- }
1431
+ getPendingWriteIdsForPath(writePath) {
1432
+ const views = this.findViewsForWritePath(writePath);
1433
+ const allPendingIds = /* @__PURE__ */ new Set();
1434
+ for (const view of views) {
1435
+ for (const id of view.getPendingWriteIds()) {
1436
+ allPendingIds.add(id);
959
1437
  }
960
1438
  }
1439
+ return Array.from(allPendingIds);
961
1440
  }
962
1441
  /**
963
- * Get all event types currently subscribed at a path.
964
- */
965
- getEventTypesForPath(path) {
966
- const pathSubs = this.subscriptions.get(path);
967
- if (!pathSubs) return [];
968
- return Array.from(pathSubs.keys());
969
- }
970
- /**
971
- * Clear all subscriptions (e.g., on disconnect).
1442
+ * Track a pending write for all Views that cover the write path.
972
1443
  */
973
- clear() {
974
- this.subscriptions.clear();
975
- this.orderedChildren.clear();
976
- this.queryParams.clear();
977
- this.cache.clear();
1444
+ trackPendingWrite(writePath, requestId) {
1445
+ const views = this.findViewsForWritePath(writePath);
1446
+ for (const view of views) {
1447
+ view.addPendingWrite(requestId);
1448
+ }
978
1449
  }
979
1450
  /**
980
- * Check if there are any subscriptions at a path.
1451
+ * Clear a pending write from all Views (on ack).
981
1452
  */
982
- hasSubscriptions(path) {
983
- return this.subscriptions.has(path);
1453
+ clearPendingWrite(requestId) {
1454
+ for (const view of this.views.values()) {
1455
+ view.removePendingWrite(requestId);
1456
+ }
984
1457
  }
985
1458
  /**
986
- * Check if a path is "covered" by an active subscription.
987
- *
988
- * A path is covered if:
989
- * - There's an active 'value' subscription at that exact path, OR
990
- * - There's an active 'value' subscription at an ancestor path
991
- *
992
- * Child event subscriptions (child_added, etc.) don't provide full coverage
993
- * because they only notify of changes, not the complete value.
1459
+ * Handle a nack for a pending write.
1460
+ * Collects all tainted request IDs and puts affected Views into recovery mode.
1461
+ * Returns the affected Views and all tainted request IDs for local rejection.
994
1462
  */
995
- isPathCovered(path) {
996
- const normalized = normalizePath(path);
997
- if (this.hasValueSubscription(normalized)) {
998
- return true;
999
- }
1000
- const segments = normalized.split("/").filter((s) => s.length > 0);
1001
- for (let i = segments.length - 1; i >= 0; i--) {
1002
- const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
1003
- if (this.hasValueSubscription(ancestorPath)) {
1004
- return true;
1463
+ handleWriteNack(requestId) {
1464
+ const affectedViews = [];
1465
+ const taintedIds = /* @__PURE__ */ new Set();
1466
+ for (const view of this.views.values()) {
1467
+ if (view.isWritePending(requestId)) {
1468
+ affectedViews.push(view);
1469
+ for (const id of view.getPendingWriteIds()) {
1470
+ taintedIds.add(id);
1471
+ }
1005
1472
  }
1006
1473
  }
1007
- if (normalized !== "/" && this.hasValueSubscription("/")) {
1008
- return true;
1474
+ for (const view of affectedViews) {
1475
+ view.enterRecovery();
1009
1476
  }
1010
- return false;
1477
+ return { affectedViews, taintedIds: Array.from(taintedIds) };
1011
1478
  }
1012
1479
  /**
1013
- * Check if there's a 'value' subscription at a path.
1480
+ * Check if any View covering a path is in recovery mode.
1014
1481
  */
1015
- hasValueSubscription(path) {
1016
- const pathSubs = this.subscriptions.get(path);
1017
- if (!pathSubs) return false;
1018
- const valueSubs = pathSubs.get("value");
1019
- return valueSubs !== void 0 && valueSubs.length > 0;
1482
+ isPathInRecovery(writePath) {
1483
+ const views = this.findViewsForWritePath(writePath);
1484
+ return views.some((view) => view.recovering);
1020
1485
  }
1021
1486
  /**
1022
- * Get a cached value if the path is covered by an active subscription.
1023
- *
1024
- * Returns { value, found: true } if we have cached data for this path
1025
- * (either exact match or extractable from a cached ancestor).
1026
- *
1027
- * Returns { value: undefined, found: false } if:
1028
- * - The path is not covered by any subscription, OR
1029
- * - We don't have cached data yet
1487
+ * Re-subscribe a specific View to get a fresh snapshot.
1488
+ * Used during recovery after a nack.
1030
1489
  */
1031
- getCachedValue(path) {
1032
- const normalized = normalizePath(path);
1033
- if (!this.isPathCovered(normalized)) {
1034
- return { value: void 0, found: false };
1490
+ async resubscribeView(path) {
1491
+ const view = this.views.get(path);
1492
+ if (!view) return;
1493
+ const eventTypes = view.getEventTypes();
1494
+ if (eventTypes.length > 0 && this.sendSubscribe) {
1495
+ await this.sendSubscribe(path, eventTypes, view.queryParams ?? void 0);
1035
1496
  }
1036
- return this.cache.get(normalized);
1037
1497
  }
1498
+ // ============================================
1499
+ // Optimistic Writes (for local-first)
1500
+ // ============================================
1038
1501
  /**
1039
- * Get the cache size (for testing/debugging).
1040
- */
1041
- get cacheSize() {
1042
- return this.cache.size;
1502
+ * Apply an optimistic write to local cache and fire events.
1503
+ * Called before sending the write to the server.
1504
+ *
1505
+ * @param writePath - The absolute path being written to
1506
+ * @param value - The value to write (null for delete)
1507
+ * @param requestId - The request ID for pending write tracking
1508
+ * @param operation - The type of operation ('set', 'update', 'delete')
1509
+ * @returns The Views that were updated
1510
+ */
1511
+ applyOptimisticWrite(writePath, value, requestId, operation) {
1512
+ const normalized = normalizePath(writePath);
1513
+ const affectedViews = this.findViewsForWritePath(normalized);
1514
+ if (affectedViews.length === 0) {
1515
+ return [];
1516
+ }
1517
+ const updatedViews = [];
1518
+ for (const view of affectedViews) {
1519
+ if (!view.hasReceivedInitialSnapshot) {
1520
+ continue;
1521
+ }
1522
+ let relativePath;
1523
+ if (normalized === view.path) {
1524
+ relativePath = "/";
1525
+ } else if (view.path === "/") {
1526
+ relativePath = normalized;
1527
+ } else {
1528
+ relativePath = normalized.slice(view.path.length);
1529
+ }
1530
+ const updates = [];
1531
+ if (operation === "delete") {
1532
+ updates.push({ relativePath, value: null });
1533
+ } else if (operation === "update") {
1534
+ const updateObj = value;
1535
+ for (const [key, val] of Object.entries(updateObj)) {
1536
+ const updatePath = relativePath === "/" ? "/" + key : relativePath + "/" + key;
1537
+ updates.push({ relativePath: updatePath, value: val });
1538
+ }
1539
+ } else {
1540
+ updates.push({ relativePath, value });
1541
+ }
1542
+ this.applyWriteToView(view, updates, false, void 0);
1543
+ updatedViews.push(view);
1544
+ }
1545
+ return updatedViews;
1043
1546
  }
1044
1547
  };
1045
1548
 
@@ -1175,9 +1678,18 @@ var OnDisconnect = class {
1175
1678
  }
1176
1679
  /**
1177
1680
  * Set a value with priority when disconnected.
1681
+ * Priority is injected as `.priority` into the value object.
1178
1682
  */
1179
1683
  async setWithPriority(value, priority) {
1180
- await this._db._sendOnDisconnect(this._path, OnDisconnectAction.SET, value, priority);
1684
+ if (value === null || value === void 0) {
1685
+ await this._db._sendOnDisconnect(this._path, OnDisconnectAction.SET, value);
1686
+ return;
1687
+ }
1688
+ if (typeof value !== "object" || Array.isArray(value)) {
1689
+ throw new Error("Priority can only be set on object values");
1690
+ }
1691
+ const valueWithPriority = { ...value, ".priority": priority };
1692
+ await this._db._sendOnDisconnect(this._path, OnDisconnectAction.SET, valueWithPriority);
1181
1693
  }
1182
1694
  };
1183
1695
 
@@ -1265,8 +1777,15 @@ var DatabaseReference = class _DatabaseReference {
1265
1777
  // ============================================
1266
1778
  /**
1267
1779
  * Set the data at this location, overwriting any existing data.
1780
+ *
1781
+ * For volatile paths (high-frequency updates), this is fire-and-forget
1782
+ * and resolves immediately without waiting for server confirmation.
1268
1783
  */
1269
1784
  async set(value) {
1785
+ if (this._db.isVolatilePath(this._path)) {
1786
+ this._db._sendVolatileSet(this._path, value);
1787
+ return;
1788
+ }
1270
1789
  await this._db._sendSet(this._path, value);
1271
1790
  }
1272
1791
  /**
@@ -1303,13 +1822,23 @@ var DatabaseReference = class _DatabaseReference {
1303
1822
  }
1304
1823
  await this._db._sendTransaction(ops);
1305
1824
  } else {
1825
+ if (this._db.isVolatilePath(this._path)) {
1826
+ this._db._sendVolatileUpdate(this._path, values);
1827
+ return;
1828
+ }
1306
1829
  await this._db._sendUpdate(this._path, values);
1307
1830
  }
1308
1831
  }
1309
1832
  /**
1310
1833
  * Remove the data at this location.
1834
+ *
1835
+ * For volatile paths, this is fire-and-forget.
1311
1836
  */
1312
1837
  async remove() {
1838
+ if (this._db.isVolatilePath(this._path)) {
1839
+ this._db._sendVolatileDelete(this._path);
1840
+ return;
1841
+ }
1313
1842
  await this._db._sendDelete(this._path);
1314
1843
  }
1315
1844
  /**
@@ -1322,27 +1851,43 @@ var DatabaseReference = class _DatabaseReference {
1322
1851
  * push key (you can then call set() on it).
1323
1852
  */
1324
1853
  push(value) {
1854
+ const key = generatePushId();
1855
+ const childRef = this.child(key);
1325
1856
  if (value === void 0) {
1326
- const key = generatePushId();
1327
- return this.child(key);
1857
+ return childRef;
1328
1858
  }
1329
- return this._db._sendPush(this._path, value).then((key) => {
1330
- return this.child(key);
1331
- });
1859
+ return childRef.set(value).then(() => childRef);
1332
1860
  }
1333
1861
  /**
1334
1862
  * Set the data with a priority value for ordering.
1863
+ * Priority is injected as `.priority` into the value object.
1335
1864
  */
1336
1865
  async setWithPriority(value, priority) {
1337
- await this._db._sendSet(this._path, value, priority);
1866
+ if (value === null || value === void 0) {
1867
+ await this._db._sendSet(this._path, value);
1868
+ return;
1869
+ }
1870
+ if (typeof value !== "object" || Array.isArray(value)) {
1871
+ throw new Error("Priority can only be set on object values");
1872
+ }
1873
+ const valueWithPriority = { ...value, ".priority": priority };
1874
+ await this._db._sendSet(this._path, valueWithPriority);
1338
1875
  }
1339
1876
  /**
1340
1877
  * Set the priority of the data at this location.
1878
+ * Fetches current value and sets it with the new priority.
1341
1879
  */
1342
1880
  async setPriority(priority) {
1343
- console.warn("setPriority: fetching current value to preserve it");
1344
1881
  const snapshot = await this.once();
1345
- await this.setWithPriority(snapshot.val(), priority);
1882
+ const currentVal = snapshot.val();
1883
+ if (currentVal === null || currentVal === void 0) {
1884
+ await this._db._sendSet(this._path, { ".priority": priority });
1885
+ return;
1886
+ }
1887
+ if (typeof currentVal !== "object" || Array.isArray(currentVal)) {
1888
+ throw new Error("Priority can only be set on object values");
1889
+ }
1890
+ await this.setWithPriority(currentVal, priority);
1346
1891
  }
1347
1892
  /**
1348
1893
  * Atomically modify the data at this location using optimistic concurrency.
@@ -1606,7 +2151,6 @@ var DataSnapshot = class _DataSnapshot {
1606
2151
  this._path = normalizePath(path);
1607
2152
  this._db = db;
1608
2153
  this._volatile = options.volatile ?? false;
1609
- this._priority = options.priority ?? null;
1610
2154
  this._serverTimestamp = options.serverTimestamp ?? null;
1611
2155
  }
1612
2156
  /**
@@ -1622,9 +2166,17 @@ var DataSnapshot = class _DataSnapshot {
1622
2166
  return getKey(this._path);
1623
2167
  }
1624
2168
  /**
1625
- * Get the raw data value.
2169
+ * Get the data value, with `.priority` stripped if present.
2170
+ * Priority is internal metadata and should not be visible to app code.
1626
2171
  */
1627
2172
  val() {
2173
+ if (this._data && typeof this._data === "object" && !Array.isArray(this._data)) {
2174
+ const data = this._data;
2175
+ if (".priority" in data) {
2176
+ const { ".priority": _, ...rest } = data;
2177
+ return Object.keys(rest).length > 0 ? rest : Object.keys(data).length === 1 ? null : rest;
2178
+ }
2179
+ }
1628
2180
  return this._data;
1629
2181
  }
1630
2182
  /**
@@ -1686,9 +2238,16 @@ var DataSnapshot = class _DataSnapshot {
1686
2238
  }
1687
2239
  /**
1688
2240
  * Get the priority of the data at this location.
2241
+ * Priority is stored as `.priority` in the data object.
1689
2242
  */
1690
2243
  getPriority() {
1691
- return this._priority;
2244
+ if (this._data && typeof this._data === "object" && !Array.isArray(this._data)) {
2245
+ const priority = this._data[".priority"];
2246
+ if (typeof priority === "number" || typeof priority === "string") {
2247
+ return priority;
2248
+ }
2249
+ }
2250
+ return null;
1692
2251
  }
1693
2252
  /**
1694
2253
  * Check if this snapshot was from a volatile (high-frequency) update.
@@ -1732,7 +2291,32 @@ function decodeJwtPayload(token) {
1732
2291
  return JSON.parse(decoded);
1733
2292
  }
1734
2293
 
2294
+ // src/utils/volatile.ts
2295
+ function isVolatilePath(path, patterns) {
2296
+ if (!patterns || patterns.length === 0) {
2297
+ return false;
2298
+ }
2299
+ const segments = path.replace(/^\//, "").split("/").filter((s) => s.length > 0);
2300
+ return patterns.some((pattern) => {
2301
+ const patternSegments = pattern.replace(/^\//, "").split("/").filter((s) => s.length > 0);
2302
+ if (segments.length < patternSegments.length) {
2303
+ return false;
2304
+ }
2305
+ for (let i = 0; i < patternSegments.length; i++) {
2306
+ const p = patternSegments[i];
2307
+ const s = segments[i];
2308
+ if (p !== "*" && p !== s) {
2309
+ return false;
2310
+ }
2311
+ }
2312
+ return true;
2313
+ });
2314
+ }
2315
+
1735
2316
  // src/LarkDatabase.ts
2317
+ var RECONNECT_BASE_DELAY_MS = 1e3;
2318
+ var RECONNECT_MAX_DELAY_MS = 3e4;
2319
+ var RECONNECT_JITTER_FACTOR = 0.5;
1736
2320
  var LarkDatabase = class {
1737
2321
  constructor() {
1738
2322
  this._state = "disconnected";
@@ -1740,11 +2324,18 @@ var LarkDatabase = class {
1740
2324
  this._databaseId = null;
1741
2325
  this._coordinatorUrl = null;
1742
2326
  this._volatilePaths = [];
2327
+ // Reconnection state
2328
+ this._connectionId = null;
2329
+ this._connectOptions = null;
2330
+ this._intentionalDisconnect = false;
2331
+ this._reconnectAttempt = 0;
2332
+ this._reconnectTimer = null;
1743
2333
  this.ws = null;
1744
2334
  // Event callbacks
1745
2335
  this.connectCallbacks = /* @__PURE__ */ new Set();
1746
2336
  this.disconnectCallbacks = /* @__PURE__ */ new Set();
1747
2337
  this.errorCallbacks = /* @__PURE__ */ new Set();
2338
+ this.reconnectingCallbacks = /* @__PURE__ */ new Set();
1748
2339
  this.messageQueue = new MessageQueue();
1749
2340
  this.subscriptionManager = new SubscriptionManager();
1750
2341
  this.pendingWrites = new PendingWriteManager();
@@ -1758,6 +2349,18 @@ var LarkDatabase = class {
1758
2349
  get connected() {
1759
2350
  return this._state === "connected";
1760
2351
  }
2352
+ /**
2353
+ * Whether the database is currently attempting to reconnect.
2354
+ */
2355
+ get reconnecting() {
2356
+ return this._state === "reconnecting";
2357
+ }
2358
+ /**
2359
+ * Current connection state.
2360
+ */
2361
+ get state() {
2362
+ return this._state;
2363
+ }
1761
2364
  /**
1762
2365
  * Current auth info, or null if not connected.
1763
2366
  */
@@ -1814,7 +2417,16 @@ var LarkDatabase = class {
1814
2417
  if (this._state !== "disconnected") {
1815
2418
  throw new Error("Already connected or connecting");
1816
2419
  }
1817
- this._state = "connecting";
2420
+ this._connectOptions = options;
2421
+ this._intentionalDisconnect = false;
2422
+ await this.performConnect(databaseId, options);
2423
+ }
2424
+ /**
2425
+ * Internal connect implementation used by both initial connect and reconnect.
2426
+ */
2427
+ async performConnect(databaseId, options, isReconnect = false) {
2428
+ const previousState = this._state;
2429
+ this._state = isReconnect ? "reconnecting" : "connecting";
1818
2430
  this._databaseId = databaseId;
1819
2431
  this._coordinatorUrl = options.coordinator || DEFAULT_COORDINATOR_URL;
1820
2432
  try {
@@ -1840,9 +2452,13 @@ var LarkDatabase = class {
1840
2452
  t: connectResponse.token,
1841
2453
  r: requestId
1842
2454
  };
2455
+ if (this._connectionId) {
2456
+ joinMessage.pcid = this._connectionId;
2457
+ }
1843
2458
  this.send(joinMessage);
1844
- const volatilePaths = await this.messageQueue.registerRequest(requestId);
1845
- this._volatilePaths = volatilePaths || [];
2459
+ const joinResponse = await this.messageQueue.registerRequest(requestId);
2460
+ this._volatilePaths = joinResponse.volatilePaths;
2461
+ this._connectionId = joinResponse.connectionId;
1846
2462
  const jwtPayload = decodeJwtPayload(connectResponse.token);
1847
2463
  this._auth = {
1848
2464
  uid: jwtPayload.sub,
@@ -1850,16 +2466,29 @@ var LarkDatabase = class {
1850
2466
  token: jwtPayload.claims || {}
1851
2467
  };
1852
2468
  this._state = "connected";
1853
- this.subscriptionManager.initialize({
1854
- sendSubscribe: this.sendSubscribeMessage.bind(this),
1855
- sendUnsubscribe: this.sendUnsubscribeMessage.bind(this),
1856
- createSnapshot: this.createSnapshot.bind(this)
1857
- });
2469
+ this._reconnectAttempt = 0;
2470
+ if (!isReconnect) {
2471
+ this.subscriptionManager.initialize({
2472
+ sendSubscribe: this.sendSubscribeMessage.bind(this),
2473
+ sendUnsubscribe: this.sendUnsubscribeMessage.bind(this),
2474
+ createSnapshot: this.createSnapshot.bind(this)
2475
+ });
2476
+ }
2477
+ if (isReconnect) {
2478
+ await this.restoreAfterReconnect();
2479
+ }
1858
2480
  this.connectCallbacks.forEach((cb) => cb());
1859
2481
  } catch (error) {
2482
+ if (isReconnect) {
2483
+ this._state = "reconnecting";
2484
+ this.scheduleReconnect();
2485
+ return;
2486
+ }
1860
2487
  this._state = "disconnected";
1861
2488
  this._auth = null;
1862
2489
  this._databaseId = null;
2490
+ this._connectOptions = null;
2491
+ this._connectionId = null;
1863
2492
  this.ws?.close();
1864
2493
  this.ws = null;
1865
2494
  throw error;
@@ -1873,7 +2502,13 @@ var LarkDatabase = class {
1873
2502
  if (this._state === "disconnected") {
1874
2503
  return;
1875
2504
  }
1876
- if (this._state === "connected" && this.ws) {
2505
+ const wasConnected = this._state === "connected";
2506
+ this._intentionalDisconnect = true;
2507
+ if (this._reconnectTimer) {
2508
+ clearTimeout(this._reconnectTimer);
2509
+ this._reconnectTimer = null;
2510
+ }
2511
+ if (wasConnected && this.ws) {
1877
2512
  try {
1878
2513
  const requestId = this.messageQueue.nextRequestId();
1879
2514
  this.send({ o: "l", r: requestId });
@@ -1884,12 +2519,16 @@ var LarkDatabase = class {
1884
2519
  } catch {
1885
2520
  }
1886
2521
  }
1887
- this.cleanup();
2522
+ this.cleanupFull();
2523
+ if (wasConnected) {
2524
+ this.disconnectCallbacks.forEach((cb) => cb());
2525
+ }
1888
2526
  }
1889
2527
  /**
1890
- * Clean up connection state.
2528
+ * Full cleanup - clears all state including subscriptions.
2529
+ * Used for intentional disconnect.
1891
2530
  */
1892
- cleanup() {
2531
+ cleanupFull() {
1893
2532
  this.ws?.close();
1894
2533
  this.ws = null;
1895
2534
  this._state = "disconnected";
@@ -1897,8 +2536,111 @@ var LarkDatabase = class {
1897
2536
  this._databaseId = null;
1898
2537
  this._volatilePaths = [];
1899
2538
  this._coordinatorUrl = null;
2539
+ this._connectionId = null;
2540
+ this._connectOptions = null;
2541
+ this._reconnectAttempt = 0;
1900
2542
  this.subscriptionManager.clear();
1901
2543
  this.messageQueue.rejectAll(new Error("Connection closed"));
2544
+ this.pendingWrites.clear();
2545
+ }
2546
+ /**
2547
+ * Partial cleanup - preserves state needed for reconnect.
2548
+ * Used for unexpected disconnect.
2549
+ */
2550
+ cleanupForReconnect() {
2551
+ this.ws?.close();
2552
+ this.ws = null;
2553
+ this._auth = null;
2554
+ this.subscriptionManager.clearCacheOnly();
2555
+ this.messageQueue.rejectAll(new Error("Connection closed"));
2556
+ }
2557
+ /**
2558
+ * Schedule a reconnection attempt with exponential backoff.
2559
+ */
2560
+ scheduleReconnect() {
2561
+ this._reconnectAttempt++;
2562
+ const baseDelay = Math.min(
2563
+ RECONNECT_BASE_DELAY_MS * Math.pow(2, this._reconnectAttempt - 1),
2564
+ RECONNECT_MAX_DELAY_MS
2565
+ );
2566
+ const jitter = baseDelay * RECONNECT_JITTER_FACTOR * Math.random();
2567
+ const delay = baseDelay + jitter;
2568
+ this._reconnectTimer = setTimeout(() => {
2569
+ this._reconnectTimer = null;
2570
+ this.attemptReconnect();
2571
+ }, delay);
2572
+ }
2573
+ /**
2574
+ * Attempt to reconnect to the database.
2575
+ */
2576
+ async attemptReconnect() {
2577
+ if (this._intentionalDisconnect || !this._databaseId || !this._connectOptions) {
2578
+ return;
2579
+ }
2580
+ try {
2581
+ await this.performConnect(this._databaseId, this._connectOptions, true);
2582
+ } catch {
2583
+ }
2584
+ }
2585
+ /**
2586
+ * Restore subscriptions and retry pending writes after reconnect.
2587
+ */
2588
+ async restoreAfterReconnect() {
2589
+ await this.subscriptionManager.resubscribeAll();
2590
+ await this.retryPendingWrites();
2591
+ }
2592
+ /**
2593
+ * Retry all pending writes after reconnect.
2594
+ */
2595
+ async retryPendingWrites() {
2596
+ const pendingWrites = this.pendingWrites.getPendingWrites();
2597
+ for (const write of pendingWrites) {
2598
+ try {
2599
+ switch (write.operation) {
2600
+ case "set": {
2601
+ const message = {
2602
+ o: "s",
2603
+ p: write.path,
2604
+ v: write.value,
2605
+ r: write.oid
2606
+ // Use original request ID
2607
+ };
2608
+ this.send(message);
2609
+ break;
2610
+ }
2611
+ case "update": {
2612
+ const message = {
2613
+ o: "u",
2614
+ p: write.path,
2615
+ v: write.value,
2616
+ r: write.oid
2617
+ };
2618
+ this.send(message);
2619
+ break;
2620
+ }
2621
+ case "delete": {
2622
+ const message = {
2623
+ o: "d",
2624
+ p: write.path,
2625
+ r: write.oid
2626
+ };
2627
+ this.send(message);
2628
+ break;
2629
+ }
2630
+ case "transaction": {
2631
+ const message = {
2632
+ o: "tx",
2633
+ ops: write.value,
2634
+ r: write.oid
2635
+ };
2636
+ this.send(message);
2637
+ break;
2638
+ }
2639
+ }
2640
+ } catch (error) {
2641
+ console.error("Failed to retry pending write:", error);
2642
+ }
2643
+ }
1902
2644
  }
1903
2645
  // ============================================
1904
2646
  // Reference Access
@@ -2024,6 +2766,15 @@ var LarkDatabase = class {
2024
2766
  this.errorCallbacks.add(callback);
2025
2767
  return () => this.errorCallbacks.delete(callback);
2026
2768
  }
2769
+ /**
2770
+ * Register a callback for when reconnection attempts begin.
2771
+ * This fires when an unexpected disconnect occurs and auto-reconnect starts.
2772
+ * Returns an unsubscribe function.
2773
+ */
2774
+ onReconnecting(callback) {
2775
+ this.reconnectingCallbacks.add(callback);
2776
+ return () => this.reconnectingCallbacks.delete(callback);
2777
+ }
2027
2778
  // ============================================
2028
2779
  // Internal: Message Handling
2029
2780
  // ============================================
@@ -2041,8 +2792,31 @@ var LarkDatabase = class {
2041
2792
  }
2042
2793
  if (isAckMessage(message)) {
2043
2794
  this.pendingWrites.onAck(message.a);
2795
+ this.subscriptionManager.clearPendingWrite(message.a);
2044
2796
  } else if (isNackMessage(message)) {
2045
2797
  this.pendingWrites.onNack(message.n);
2798
+ if (message.e !== "condition_failed") {
2799
+ console.error(`Write failed (${message.e}): ${message.m || message.e}`);
2800
+ const { affectedViews, taintedIds } = this.subscriptionManager.handleWriteNack(message.n);
2801
+ if (affectedViews.length > 0) {
2802
+ for (const id of taintedIds) {
2803
+ if (id !== message.n) {
2804
+ this.messageQueue.rejectLocally(
2805
+ id,
2806
+ new LarkError("write_tainted", "Write cancelled: depends on failed write")
2807
+ );
2808
+ this.pendingWrites.onNack(id);
2809
+ }
2810
+ }
2811
+ for (const view of affectedViews) {
2812
+ this.subscriptionManager.resubscribeView(view.path).catch((err) => {
2813
+ console.error(`Failed to re-subscribe ${view.path} during recovery:`, err);
2814
+ });
2815
+ }
2816
+ }
2817
+ } else {
2818
+ this.subscriptionManager.clearPendingWrite(message.n);
2819
+ }
2046
2820
  }
2047
2821
  if (this.messageQueue.handleMessage(message)) {
2048
2822
  return;
@@ -2052,10 +2826,32 @@ var LarkDatabase = class {
2052
2826
  }
2053
2827
  }
2054
2828
  handleClose(code, reason) {
2829
+ if (this._state === "disconnected") {
2830
+ return;
2831
+ }
2055
2832
  const wasConnected = this._state === "connected";
2056
- this.cleanup();
2057
- if (wasConnected) {
2058
- this.disconnectCallbacks.forEach((cb) => cb());
2833
+ const wasReconnecting = this._state === "reconnecting";
2834
+ if (this._intentionalDisconnect) {
2835
+ this.cleanupFull();
2836
+ if (wasConnected) {
2837
+ this.disconnectCallbacks.forEach((cb) => cb());
2838
+ }
2839
+ return;
2840
+ }
2841
+ const canReconnect = this._databaseId && this._connectOptions;
2842
+ if ((wasConnected || wasReconnecting) && canReconnect) {
2843
+ this._state = "reconnecting";
2844
+ this.cleanupForReconnect();
2845
+ this.reconnectingCallbacks.forEach((cb) => cb());
2846
+ if (wasConnected) {
2847
+ this.disconnectCallbacks.forEach((cb) => cb());
2848
+ }
2849
+ this.scheduleReconnect();
2850
+ } else {
2851
+ this.cleanupFull();
2852
+ if (wasConnected) {
2853
+ this.disconnectCallbacks.forEach((cb) => cb());
2854
+ }
2059
2855
  }
2060
2856
  }
2061
2857
  handleError(event) {
@@ -2073,20 +2869,25 @@ var LarkDatabase = class {
2073
2869
  }
2074
2870
  /**
2075
2871
  * @internal Send a set operation.
2872
+ * Note: Priority is now part of the value (as .priority), not a separate field.
2076
2873
  */
2077
- async _sendSet(path, value, priority) {
2078
- const requestId = this.messageQueue.nextRequestId();
2874
+ async _sendSet(path, value) {
2079
2875
  const normalizedPath = normalizePath(path) || "/";
2876
+ if (this.subscriptionManager.isPathInRecovery(normalizedPath)) {
2877
+ throw new LarkError("view_recovering", "Cannot write: View is recovering from failed write");
2878
+ }
2879
+ const requestId = this.messageQueue.nextRequestId();
2880
+ const pendingWriteIds = this.subscriptionManager.getPendingWriteIdsForPath(normalizedPath);
2080
2881
  this.pendingWrites.trackWrite(requestId, "set", normalizedPath, value);
2882
+ this.subscriptionManager.trackPendingWrite(normalizedPath, requestId);
2883
+ this.subscriptionManager.applyOptimisticWrite(normalizedPath, value, requestId, "set");
2081
2884
  const message = {
2082
2885
  o: "s",
2083
2886
  p: normalizedPath,
2084
2887
  v: value,
2085
- r: requestId
2888
+ r: requestId,
2889
+ pw: pendingWriteIds.length > 0 ? pendingWriteIds : void 0
2086
2890
  };
2087
- if (priority !== void 0) {
2088
- message.y = priority;
2089
- }
2090
2891
  this.send(message);
2091
2892
  await this.messageQueue.registerRequest(requestId);
2092
2893
  }
@@ -2094,14 +2895,21 @@ var LarkDatabase = class {
2094
2895
  * @internal Send an update operation.
2095
2896
  */
2096
2897
  async _sendUpdate(path, values) {
2097
- const requestId = this.messageQueue.nextRequestId();
2098
2898
  const normalizedPath = normalizePath(path) || "/";
2899
+ if (this.subscriptionManager.isPathInRecovery(normalizedPath)) {
2900
+ throw new LarkError("view_recovering", "Cannot write: View is recovering from failed write");
2901
+ }
2902
+ const requestId = this.messageQueue.nextRequestId();
2903
+ const pendingWriteIds = this.subscriptionManager.getPendingWriteIdsForPath(normalizedPath);
2099
2904
  this.pendingWrites.trackWrite(requestId, "update", normalizedPath, values);
2905
+ this.subscriptionManager.trackPendingWrite(normalizedPath, requestId);
2906
+ this.subscriptionManager.applyOptimisticWrite(normalizedPath, values, requestId, "update");
2100
2907
  const message = {
2101
2908
  o: "u",
2102
2909
  p: normalizedPath,
2103
2910
  v: values,
2104
- r: requestId
2911
+ r: requestId,
2912
+ pw: pendingWriteIds.length > 0 ? pendingWriteIds : void 0
2105
2913
  };
2106
2914
  this.send(message);
2107
2915
  await this.messageQueue.registerRequest(requestId);
@@ -2110,33 +2918,79 @@ var LarkDatabase = class {
2110
2918
  * @internal Send a delete operation.
2111
2919
  */
2112
2920
  async _sendDelete(path) {
2113
- const requestId = this.messageQueue.nextRequestId();
2114
2921
  const normalizedPath = normalizePath(path) || "/";
2922
+ if (this.subscriptionManager.isPathInRecovery(normalizedPath)) {
2923
+ throw new LarkError("view_recovering", "Cannot write: View is recovering from failed write");
2924
+ }
2925
+ const requestId = this.messageQueue.nextRequestId();
2926
+ const pendingWriteIds = this.subscriptionManager.getPendingWriteIdsForPath(normalizedPath);
2115
2927
  this.pendingWrites.trackWrite(requestId, "delete", normalizedPath);
2928
+ this.subscriptionManager.trackPendingWrite(normalizedPath, requestId);
2929
+ this.subscriptionManager.applyOptimisticWrite(normalizedPath, null, requestId, "delete");
2116
2930
  const message = {
2117
2931
  o: "d",
2118
2932
  p: normalizedPath,
2119
- r: requestId
2933
+ r: requestId,
2934
+ pw: pendingWriteIds.length > 0 ? pendingWriteIds : void 0
2120
2935
  };
2121
2936
  this.send(message);
2122
2937
  await this.messageQueue.registerRequest(requestId);
2123
2938
  }
2939
+ // ============================================
2940
+ // Volatile Write Operations (Fire-and-Forget)
2941
+ // ============================================
2124
2942
  /**
2125
- * @internal Send a push operation. Returns the generated key.
2943
+ * @internal Send a volatile set operation (fire-and-forget).
2944
+ *
2945
+ * Volatile writes skip:
2946
+ * - Recovery checks (volatile paths don't participate in recovery)
2947
+ * - Request ID generation (no ack expected)
2948
+ * - Pending write tracking (no retry on reconnect)
2949
+ * - pw field (no dependency tracking)
2950
+ *
2951
+ * The write is applied optimistically to local cache for UI feedback,
2952
+ * but we don't await server confirmation.
2126
2953
  */
2127
- async _sendPush(path, value) {
2128
- const requestId = this.messageQueue.nextRequestId();
2954
+ _sendVolatileSet(path, value) {
2129
2955
  const normalizedPath = normalizePath(path) || "/";
2130
- this.pendingWrites.trackWrite(requestId, "push", normalizedPath, value);
2956
+ this.subscriptionManager.applyOptimisticWrite(normalizedPath, value, "", "set");
2131
2957
  const message = {
2132
- o: "p",
2958
+ o: "s",
2133
2959
  p: normalizedPath,
2134
- v: value,
2135
- r: requestId
2960
+ v: value
2136
2961
  };
2137
2962
  this.send(message);
2138
- const key = await this.messageQueue.registerRequest(requestId);
2139
- return key;
2963
+ }
2964
+ /**
2965
+ * @internal Send a volatile update operation (fire-and-forget).
2966
+ */
2967
+ _sendVolatileUpdate(path, values) {
2968
+ const normalizedPath = normalizePath(path) || "/";
2969
+ this.subscriptionManager.applyOptimisticWrite(normalizedPath, values, "", "update");
2970
+ const message = {
2971
+ o: "u",
2972
+ p: normalizedPath,
2973
+ v: values
2974
+ };
2975
+ this.send(message);
2976
+ }
2977
+ /**
2978
+ * @internal Send a volatile delete operation (fire-and-forget).
2979
+ */
2980
+ _sendVolatileDelete(path) {
2981
+ const normalizedPath = normalizePath(path) || "/";
2982
+ this.subscriptionManager.applyOptimisticWrite(normalizedPath, null, "", "delete");
2983
+ const message = {
2984
+ o: "d",
2985
+ p: normalizedPath
2986
+ };
2987
+ this.send(message);
2988
+ }
2989
+ /**
2990
+ * Check if a path is a volatile path (high-frequency, fire-and-forget).
2991
+ */
2992
+ isVolatilePath(path) {
2993
+ return isVolatilePath(path, this._volatilePaths);
2140
2994
  }
2141
2995
  /**
2142
2996
  * @internal Send a once (read) operation.
@@ -2173,7 +3027,7 @@ var LarkDatabase = class {
2173
3027
  /**
2174
3028
  * @internal Send an onDisconnect operation.
2175
3029
  */
2176
- async _sendOnDisconnect(path, action, value, priority) {
3030
+ async _sendOnDisconnect(path, action, value) {
2177
3031
  const requestId = this.messageQueue.nextRequestId();
2178
3032
  const message = {
2179
3033
  o: "od",
@@ -2184,9 +3038,6 @@ var LarkDatabase = class {
2184
3038
  if (value !== void 0) {
2185
3039
  message.v = value;
2186
3040
  }
2187
- if (priority !== void 0) {
2188
- message.y = priority;
2189
- }
2190
3041
  this.send(message);
2191
3042
  await this.messageQueue.registerRequest(requestId);
2192
3043
  }
@@ -2249,21 +3100,6 @@ var LarkDatabase = class {
2249
3100
  this.subscriptionManager.unsubscribeAll(path);
2250
3101
  }
2251
3102
  };
2252
-
2253
- // src/utils/volatile.ts
2254
- function isVolatilePath(path, patterns) {
2255
- if (!patterns || patterns.length === 0) {
2256
- return false;
2257
- }
2258
- const segments = path.replace(/^\//, "").split("/");
2259
- return patterns.some((pattern) => {
2260
- const patternSegments = pattern.split("/");
2261
- if (segments.length !== patternSegments.length) {
2262
- return false;
2263
- }
2264
- return patternSegments.every((p, i) => p === "*" || p === segments[i]);
2265
- });
2266
- }
2267
3103
  export {
2268
3104
  DataSnapshot,
2269
3105
  DatabaseReference,