@lark-sh/client 0.1.4 → 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.d.mts +242 -13
- package/dist/index.d.ts +242 -13
- package/dist/index.js +745 -94
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +744 -94
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -5,17 +5,14 @@ var OnDisconnectAction = {
|
|
|
5
5
|
DELETE: "d",
|
|
6
6
|
CANCEL: "c"
|
|
7
7
|
};
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
|
@@ -202,6 +199,91 @@ var MessageQueue = class {
|
|
|
202
199
|
}
|
|
203
200
|
};
|
|
204
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
|
+
|
|
205
287
|
// src/utils/path.ts
|
|
206
288
|
function normalizePath(path) {
|
|
207
289
|
if (!path || path === "/") return "/";
|
|
@@ -356,6 +438,8 @@ var DataCache = class {
|
|
|
356
438
|
*
|
|
357
439
|
* E.g., if /boxes is cached and /boxes/5 changes:
|
|
358
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.
|
|
359
443
|
*/
|
|
360
444
|
updateChild(path, value) {
|
|
361
445
|
const normalized = normalizePath(path);
|
|
@@ -365,22 +449,17 @@ var DataCache = class {
|
|
|
365
449
|
const ancestorPath = "/" + segments.slice(0, i).join("/");
|
|
366
450
|
if (this.cache.has(ancestorPath)) {
|
|
367
451
|
const ancestorValue = this.cache.get(ancestorPath);
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
setValueAtPath(updatedAncestor, remainingPath, value);
|
|
373
|
-
this.cache.set(ancestorPath, updatedAncestor);
|
|
374
|
-
}
|
|
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);
|
|
375
456
|
}
|
|
376
457
|
}
|
|
377
458
|
if (this.cache.has("/") && normalized !== "/") {
|
|
378
459
|
const rootValue = this.cache.get("/");
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
this.cache.set("/", updatedRoot);
|
|
383
|
-
}
|
|
460
|
+
const baseValue = rootValue !== null && typeof rootValue === "object" ? this.deepClone(rootValue) : {};
|
|
461
|
+
setValueAtPath(baseValue, normalized, value);
|
|
462
|
+
this.cache.set("/", baseValue);
|
|
384
463
|
}
|
|
385
464
|
}
|
|
386
465
|
/**
|
|
@@ -433,10 +512,15 @@ var DataCache = class {
|
|
|
433
512
|
// src/connection/SubscriptionManager.ts
|
|
434
513
|
var SubscriptionManager = class {
|
|
435
514
|
constructor() {
|
|
436
|
-
//
|
|
515
|
+
// subscriptionPath -> eventType -> array of subscriptions
|
|
437
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();
|
|
438
520
|
// Callback to send subscribe message to server
|
|
439
521
|
this.sendSubscribe = null;
|
|
522
|
+
// Query params for each subscription path
|
|
523
|
+
this.queryParams = /* @__PURE__ */ new Map();
|
|
440
524
|
// Callback to send unsubscribe message to server
|
|
441
525
|
this.sendUnsubscribe = null;
|
|
442
526
|
// Callback to create DataSnapshot from event data
|
|
@@ -455,7 +539,7 @@ var SubscriptionManager = class {
|
|
|
455
539
|
* Subscribe to events at a path.
|
|
456
540
|
* Returns an unsubscribe function.
|
|
457
541
|
*/
|
|
458
|
-
subscribe(path, eventType, callback) {
|
|
542
|
+
subscribe(path, eventType, callback, queryParams) {
|
|
459
543
|
const normalizedPath = path;
|
|
460
544
|
const isFirstForPath = !this.subscriptions.has(normalizedPath);
|
|
461
545
|
const existingEventTypes = this.getEventTypesForPath(normalizedPath);
|
|
@@ -468,14 +552,17 @@ var SubscriptionManager = class {
|
|
|
468
552
|
pathSubs.set(eventType, []);
|
|
469
553
|
}
|
|
470
554
|
const eventSubs = pathSubs.get(eventType);
|
|
555
|
+
if (isFirstForPath && queryParams) {
|
|
556
|
+
this.queryParams.set(normalizedPath, queryParams);
|
|
557
|
+
}
|
|
471
558
|
const unsubscribe = () => {
|
|
472
559
|
this.unsubscribeCallback(normalizedPath, eventType, callback);
|
|
473
560
|
};
|
|
474
561
|
eventSubs.push({ callback, unsubscribe });
|
|
475
562
|
if (isFirstForPath || isFirstForEventType) {
|
|
476
563
|
const allEventTypes = this.getEventTypesForPath(normalizedPath);
|
|
477
|
-
const
|
|
478
|
-
this.sendSubscribe?.(normalizedPath,
|
|
564
|
+
const storedQueryParams = this.queryParams.get(normalizedPath);
|
|
565
|
+
this.sendSubscribe?.(normalizedPath, allEventTypes, storedQueryParams).catch((err) => {
|
|
479
566
|
console.error("Failed to subscribe:", err);
|
|
480
567
|
});
|
|
481
568
|
}
|
|
@@ -498,6 +585,9 @@ var SubscriptionManager = class {
|
|
|
498
585
|
}
|
|
499
586
|
if (pathSubs.size === 0) {
|
|
500
587
|
this.subscriptions.delete(path);
|
|
588
|
+
this.orderedChildren.delete(path);
|
|
589
|
+
this.queryParams.delete(path);
|
|
590
|
+
this.cache.deleteTree(path);
|
|
501
591
|
this.sendUnsubscribe?.(path).catch((err) => {
|
|
502
592
|
console.error("Failed to unsubscribe:", err);
|
|
503
593
|
});
|
|
@@ -513,13 +603,16 @@ var SubscriptionManager = class {
|
|
|
513
603
|
pathSubs.delete(eventType);
|
|
514
604
|
if (pathSubs.size === 0) {
|
|
515
605
|
this.subscriptions.delete(normalizedPath);
|
|
606
|
+
this.orderedChildren.delete(normalizedPath);
|
|
607
|
+
this.queryParams.delete(normalizedPath);
|
|
608
|
+
this.cache.deleteTree(normalizedPath);
|
|
516
609
|
this.sendUnsubscribe?.(normalizedPath).catch((err) => {
|
|
517
610
|
console.error("Failed to unsubscribe:", err);
|
|
518
611
|
});
|
|
519
612
|
} else {
|
|
520
613
|
const remainingEventTypes = this.getEventTypesForPath(normalizedPath);
|
|
521
|
-
const
|
|
522
|
-
this.sendSubscribe?.(normalizedPath,
|
|
614
|
+
const storedQueryParams = this.queryParams.get(normalizedPath);
|
|
615
|
+
this.sendSubscribe?.(normalizedPath, remainingEventTypes, storedQueryParams).catch((err) => {
|
|
523
616
|
console.error("Failed to update subscription:", err);
|
|
524
617
|
});
|
|
525
618
|
}
|
|
@@ -531,49 +624,347 @@ var SubscriptionManager = class {
|
|
|
531
624
|
const normalizedPath = path;
|
|
532
625
|
if (!this.subscriptions.has(normalizedPath)) return;
|
|
533
626
|
this.subscriptions.delete(normalizedPath);
|
|
627
|
+
this.orderedChildren.delete(normalizedPath);
|
|
628
|
+
this.queryParams.delete(normalizedPath);
|
|
629
|
+
this.cache.deleteTree(normalizedPath);
|
|
534
630
|
this.sendUnsubscribe?.(normalizedPath).catch((err) => {
|
|
535
631
|
console.error("Failed to unsubscribe:", err);
|
|
536
632
|
});
|
|
537
633
|
}
|
|
538
634
|
/**
|
|
539
635
|
* Handle an incoming event message from the server.
|
|
636
|
+
* Server sends 'put' or 'patch' events; we generate child_* events client-side.
|
|
540
637
|
*/
|
|
541
638
|
handleEvent(message) {
|
|
542
|
-
|
|
543
|
-
|
|
639
|
+
if (message.ev === "put") {
|
|
640
|
+
this.handlePutEvent(message);
|
|
641
|
+
} else if (message.ev === "patch") {
|
|
642
|
+
this.handlePatchEvent(message);
|
|
643
|
+
} else {
|
|
544
644
|
console.warn("Unknown event type:", message.ev);
|
|
545
|
-
return;
|
|
546
645
|
}
|
|
547
|
-
|
|
548
|
-
|
|
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);
|
|
549
659
|
if (!pathSubs) return;
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
+
}
|
|
566
680
|
} else {
|
|
567
|
-
|
|
568
|
-
|
|
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
|
+
}
|
|
569
694
|
}
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
+
}
|
|
577
968
|
}
|
|
578
969
|
}
|
|
579
970
|
}
|
|
@@ -590,6 +981,8 @@ var SubscriptionManager = class {
|
|
|
590
981
|
*/
|
|
591
982
|
clear() {
|
|
592
983
|
this.subscriptions.clear();
|
|
984
|
+
this.orderedChildren.clear();
|
|
985
|
+
this.queryParams.clear();
|
|
593
986
|
this.cache.clear();
|
|
594
987
|
}
|
|
595
988
|
/**
|
|
@@ -833,6 +1226,7 @@ function generatePushId() {
|
|
|
833
1226
|
}
|
|
834
1227
|
|
|
835
1228
|
// src/DatabaseReference.ts
|
|
1229
|
+
var DEFAULT_MAX_RETRIES = 25;
|
|
836
1230
|
var DatabaseReference = class _DatabaseReference {
|
|
837
1231
|
constructor(db, path, query = {}) {
|
|
838
1232
|
this._db = db;
|
|
@@ -886,9 +1280,40 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
886
1280
|
}
|
|
887
1281
|
/**
|
|
888
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
|
+
* ```
|
|
889
1299
|
*/
|
|
890
1300
|
async update(values) {
|
|
891
|
-
|
|
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
|
+
}
|
|
892
1317
|
}
|
|
893
1318
|
/**
|
|
894
1319
|
* Remove the data at this location.
|
|
@@ -928,6 +1353,64 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
928
1353
|
const snapshot = await this.once();
|
|
929
1354
|
await this.setWithPriority(snapshot.val(), priority);
|
|
930
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
|
+
}
|
|
931
1414
|
// ============================================
|
|
932
1415
|
// Read Operations
|
|
933
1416
|
// ============================================
|
|
@@ -948,7 +1431,7 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
948
1431
|
* Returns an unsubscribe function.
|
|
949
1432
|
*/
|
|
950
1433
|
on(eventType, callback) {
|
|
951
|
-
return this._db._subscribe(this._path, eventType, callback);
|
|
1434
|
+
return this._db._subscribe(this._path, eventType, callback, this._buildQueryParams());
|
|
952
1435
|
}
|
|
953
1436
|
/**
|
|
954
1437
|
* Unsubscribe from events.
|
|
@@ -994,10 +1477,8 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
994
1477
|
}
|
|
995
1478
|
/**
|
|
996
1479
|
* Order results by a child key.
|
|
997
|
-
* NOTE: Phase 2 - not yet implemented on server.
|
|
998
1480
|
*/
|
|
999
1481
|
orderByChild(path) {
|
|
1000
|
-
console.warn("orderByChild() is not yet implemented");
|
|
1001
1482
|
return new _DatabaseReference(this._db, this._path, {
|
|
1002
1483
|
...this._query,
|
|
1003
1484
|
orderBy: "child",
|
|
@@ -1006,10 +1487,8 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
1006
1487
|
}
|
|
1007
1488
|
/**
|
|
1008
1489
|
* Order results by value.
|
|
1009
|
-
* NOTE: Phase 2 - not yet implemented on server.
|
|
1010
1490
|
*/
|
|
1011
1491
|
orderByValue() {
|
|
1012
|
-
console.warn("orderByValue() is not yet implemented");
|
|
1013
1492
|
return new _DatabaseReference(this._db, this._path, {
|
|
1014
1493
|
...this._query,
|
|
1015
1494
|
orderBy: "value"
|
|
@@ -1035,10 +1514,8 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
1035
1514
|
}
|
|
1036
1515
|
/**
|
|
1037
1516
|
* Start at a specific value/key.
|
|
1038
|
-
* NOTE: Phase 2 - not yet implemented on server.
|
|
1039
1517
|
*/
|
|
1040
1518
|
startAt(value, key) {
|
|
1041
|
-
console.warn("startAt() is not yet implemented");
|
|
1042
1519
|
return new _DatabaseReference(this._db, this._path, {
|
|
1043
1520
|
...this._query,
|
|
1044
1521
|
startAt: { value, key }
|
|
@@ -1046,10 +1523,8 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
1046
1523
|
}
|
|
1047
1524
|
/**
|
|
1048
1525
|
* End at a specific value/key.
|
|
1049
|
-
* NOTE: Phase 2 - not yet implemented on server.
|
|
1050
1526
|
*/
|
|
1051
1527
|
endAt(value, key) {
|
|
1052
|
-
console.warn("endAt() is not yet implemented");
|
|
1053
1528
|
return new _DatabaseReference(this._db, this._path, {
|
|
1054
1529
|
...this._query,
|
|
1055
1530
|
endAt: { value, key }
|
|
@@ -1057,10 +1532,8 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
1057
1532
|
}
|
|
1058
1533
|
/**
|
|
1059
1534
|
* Filter to items equal to a specific value.
|
|
1060
|
-
* NOTE: Phase 2 - not yet implemented on server.
|
|
1061
1535
|
*/
|
|
1062
1536
|
equalTo(value, key) {
|
|
1063
|
-
console.warn("equalTo() is not yet implemented");
|
|
1064
1537
|
return new _DatabaseReference(this._db, this._path, {
|
|
1065
1538
|
...this._query,
|
|
1066
1539
|
equalTo: { value, key }
|
|
@@ -1076,18 +1549,48 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
1076
1549
|
const params = {};
|
|
1077
1550
|
let hasParams = false;
|
|
1078
1551
|
if (this._query.orderBy === "key") {
|
|
1079
|
-
params.
|
|
1552
|
+
params.orderBy = "key";
|
|
1080
1553
|
hasParams = true;
|
|
1081
1554
|
} else if (this._query.orderBy === "priority") {
|
|
1082
|
-
params.
|
|
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";
|
|
1083
1565
|
hasParams = true;
|
|
1084
1566
|
}
|
|
1085
1567
|
if (this._query.limitToFirst !== void 0) {
|
|
1086
|
-
params.
|
|
1568
|
+
params.limitToFirst = this._query.limitToFirst;
|
|
1087
1569
|
hasParams = true;
|
|
1088
1570
|
}
|
|
1089
1571
|
if (this._query.limitToLast !== void 0) {
|
|
1090
|
-
params.
|
|
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
|
+
}
|
|
1091
1594
|
hasParams = true;
|
|
1092
1595
|
}
|
|
1093
1596
|
return hasParams ? params : void 0;
|
|
@@ -1113,6 +1616,7 @@ var DataSnapshot = class _DataSnapshot {
|
|
|
1113
1616
|
this._db = db;
|
|
1114
1617
|
this._volatile = options.volatile ?? false;
|
|
1115
1618
|
this._priority = options.priority ?? null;
|
|
1619
|
+
this._serverTimestamp = options.serverTimestamp ?? null;
|
|
1116
1620
|
}
|
|
1117
1621
|
/**
|
|
1118
1622
|
* Get a DatabaseReference for the location of this snapshot.
|
|
@@ -1145,7 +1649,8 @@ var DataSnapshot = class _DataSnapshot {
|
|
|
1145
1649
|
const childPath = joinPath(this._path, path);
|
|
1146
1650
|
const childData = getValueAtPath(this._data, path);
|
|
1147
1651
|
return new _DataSnapshot(childData, childPath, this._db, {
|
|
1148
|
-
volatile: this._volatile
|
|
1652
|
+
volatile: this._volatile,
|
|
1653
|
+
serverTimestamp: this._serverTimestamp
|
|
1149
1654
|
});
|
|
1150
1655
|
}
|
|
1151
1656
|
/**
|
|
@@ -1201,6 +1706,15 @@ var DataSnapshot = class _DataSnapshot {
|
|
|
1201
1706
|
isVolatile() {
|
|
1202
1707
|
return this._volatile;
|
|
1203
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
|
+
}
|
|
1204
1718
|
/**
|
|
1205
1719
|
* Export the snapshot data as JSON (alias for val()).
|
|
1206
1720
|
*/
|
|
@@ -1242,6 +1756,7 @@ var LarkDatabase = class {
|
|
|
1242
1756
|
this.errorCallbacks = /* @__PURE__ */ new Set();
|
|
1243
1757
|
this.messageQueue = new MessageQueue();
|
|
1244
1758
|
this.subscriptionManager = new SubscriptionManager();
|
|
1759
|
+
this.pendingWrites = new PendingWriteManager();
|
|
1245
1760
|
}
|
|
1246
1761
|
// ============================================
|
|
1247
1762
|
// Connection State
|
|
@@ -1275,6 +1790,26 @@ var LarkDatabase = class {
|
|
|
1275
1790
|
get volatilePaths() {
|
|
1276
1791
|
return this._volatilePaths;
|
|
1277
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
|
+
}
|
|
1278
1813
|
// ============================================
|
|
1279
1814
|
// Connection Management
|
|
1280
1815
|
// ============================================
|
|
@@ -1384,6 +1919,96 @@ var LarkDatabase = class {
|
|
|
1384
1919
|
return new DatabaseReference(this, path);
|
|
1385
1920
|
}
|
|
1386
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
|
+
// ============================================
|
|
1387
2012
|
// Connection Events
|
|
1388
2013
|
// ============================================
|
|
1389
2014
|
/**
|
|
@@ -1425,6 +2050,11 @@ var LarkDatabase = class {
|
|
|
1425
2050
|
this.ws?.send(JSON.stringify({ o: "po" }));
|
|
1426
2051
|
return;
|
|
1427
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
|
+
}
|
|
1428
2058
|
if (this.messageQueue.handleMessage(message)) {
|
|
1429
2059
|
return;
|
|
1430
2060
|
}
|
|
@@ -1457,11 +2087,15 @@ var LarkDatabase = class {
|
|
|
1457
2087
|
*/
|
|
1458
2088
|
async _sendSet(path, value, priority) {
|
|
1459
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);
|
|
1460
2093
|
const message = {
|
|
1461
2094
|
o: "s",
|
|
1462
|
-
p:
|
|
2095
|
+
p: normalizedPath,
|
|
1463
2096
|
v: value,
|
|
1464
|
-
r: requestId
|
|
2097
|
+
r: requestId,
|
|
2098
|
+
oid
|
|
1465
2099
|
};
|
|
1466
2100
|
if (priority !== void 0) {
|
|
1467
2101
|
message.y = priority;
|
|
@@ -1474,11 +2108,15 @@ var LarkDatabase = class {
|
|
|
1474
2108
|
*/
|
|
1475
2109
|
async _sendUpdate(path, values) {
|
|
1476
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);
|
|
1477
2114
|
const message = {
|
|
1478
2115
|
o: "u",
|
|
1479
|
-
p:
|
|
2116
|
+
p: normalizedPath,
|
|
1480
2117
|
v: values,
|
|
1481
|
-
r: requestId
|
|
2118
|
+
r: requestId,
|
|
2119
|
+
oid
|
|
1482
2120
|
};
|
|
1483
2121
|
this.send(message);
|
|
1484
2122
|
await this.messageQueue.registerRequest(requestId);
|
|
@@ -1488,10 +2126,14 @@ var LarkDatabase = class {
|
|
|
1488
2126
|
*/
|
|
1489
2127
|
async _sendDelete(path) {
|
|
1490
2128
|
const requestId = this.messageQueue.nextRequestId();
|
|
2129
|
+
const oid = this.pendingWrites.generateOid();
|
|
2130
|
+
const normalizedPath = normalizePath(path) || "/";
|
|
2131
|
+
this.pendingWrites.trackWrite(oid, "delete", normalizedPath);
|
|
1491
2132
|
const message = {
|
|
1492
2133
|
o: "d",
|
|
1493
|
-
p:
|
|
1494
|
-
r: requestId
|
|
2134
|
+
p: normalizedPath,
|
|
2135
|
+
r: requestId,
|
|
2136
|
+
oid
|
|
1495
2137
|
};
|
|
1496
2138
|
this.send(message);
|
|
1497
2139
|
await this.messageQueue.registerRequest(requestId);
|
|
@@ -1501,11 +2143,15 @@ var LarkDatabase = class {
|
|
|
1501
2143
|
*/
|
|
1502
2144
|
async _sendPush(path, value) {
|
|
1503
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);
|
|
1504
2149
|
const message = {
|
|
1505
2150
|
o: "p",
|
|
1506
|
-
p:
|
|
2151
|
+
p: normalizedPath,
|
|
1507
2152
|
v: value,
|
|
1508
|
-
r: requestId
|
|
2153
|
+
r: requestId,
|
|
2154
|
+
oid
|
|
1509
2155
|
};
|
|
1510
2156
|
this.send(message);
|
|
1511
2157
|
const key = await this.messageQueue.registerRequest(requestId);
|
|
@@ -1535,11 +2181,10 @@ var LarkDatabase = class {
|
|
|
1535
2181
|
const message = {
|
|
1536
2182
|
o: "o",
|
|
1537
2183
|
p: normalizedPath,
|
|
1538
|
-
r: requestId
|
|
2184
|
+
r: requestId,
|
|
2185
|
+
// Spread query params at top level (not nested in 'q')
|
|
2186
|
+
...query
|
|
1539
2187
|
};
|
|
1540
|
-
if (query) {
|
|
1541
|
-
message.q = query;
|
|
1542
|
-
}
|
|
1543
2188
|
this.send(message);
|
|
1544
2189
|
const value = await this.messageQueue.registerRequest(requestId);
|
|
1545
2190
|
return new DataSnapshot(value, path, this);
|
|
@@ -1567,13 +2212,14 @@ var LarkDatabase = class {
|
|
|
1567
2212
|
/**
|
|
1568
2213
|
* @internal Send a subscribe message to server.
|
|
1569
2214
|
*/
|
|
1570
|
-
async sendSubscribeMessage(path, eventTypes) {
|
|
2215
|
+
async sendSubscribeMessage(path, eventTypes, queryParams) {
|
|
1571
2216
|
const requestId = this.messageQueue.nextRequestId();
|
|
1572
2217
|
const message = {
|
|
1573
2218
|
o: "sb",
|
|
1574
2219
|
p: normalizePath(path) || "/",
|
|
1575
2220
|
e: eventTypes,
|
|
1576
|
-
r: requestId
|
|
2221
|
+
r: requestId,
|
|
2222
|
+
...queryParams
|
|
1577
2223
|
};
|
|
1578
2224
|
this.send(message);
|
|
1579
2225
|
await this.messageQueue.registerRequest(requestId);
|
|
@@ -1594,8 +2240,11 @@ var LarkDatabase = class {
|
|
|
1594
2240
|
/**
|
|
1595
2241
|
* @internal Create a DataSnapshot from event data.
|
|
1596
2242
|
*/
|
|
1597
|
-
createSnapshot(path, value, volatile) {
|
|
1598
|
-
return new DataSnapshot(value, path, this, {
|
|
2243
|
+
createSnapshot(path, value, volatile, serverTimestamp) {
|
|
2244
|
+
return new DataSnapshot(value, path, this, {
|
|
2245
|
+
volatile,
|
|
2246
|
+
serverTimestamp: serverTimestamp ?? null
|
|
2247
|
+
});
|
|
1599
2248
|
}
|
|
1600
2249
|
// ============================================
|
|
1601
2250
|
// Internal: Subscription Management
|
|
@@ -1603,8 +2252,8 @@ var LarkDatabase = class {
|
|
|
1603
2252
|
/**
|
|
1604
2253
|
* @internal Subscribe to events at a path.
|
|
1605
2254
|
*/
|
|
1606
|
-
_subscribe(path, eventType, callback) {
|
|
1607
|
-
return this.subscriptionManager.subscribe(path, eventType, callback);
|
|
2255
|
+
_subscribe(path, eventType, callback, queryParams) {
|
|
2256
|
+
return this.subscriptionManager.subscribe(path, eventType, callback, queryParams);
|
|
1608
2257
|
}
|
|
1609
2258
|
/**
|
|
1610
2259
|
* @internal Unsubscribe from a specific event type at a path.
|
|
@@ -1640,6 +2289,7 @@ export {
|
|
|
1640
2289
|
LarkDatabase,
|
|
1641
2290
|
LarkError,
|
|
1642
2291
|
OnDisconnect,
|
|
2292
|
+
PendingWriteManager,
|
|
1643
2293
|
generatePushId,
|
|
1644
2294
|
isVolatilePath
|
|
1645
2295
|
};
|