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