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