@lark-sh/client 0.1.11 → 0.1.12
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 +85 -6
- package/dist/index.d.ts +85 -6
- package/dist/index.js +544 -116
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +544 -116
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -58,7 +58,8 @@ var ErrorCode = {
|
|
|
58
58
|
INVALID_OPERATION: "invalid_operation",
|
|
59
59
|
INTERNAL_ERROR: "internal_error",
|
|
60
60
|
CONDITION_FAILED: "condition_failed",
|
|
61
|
-
INVALID_QUERY: "invalid_query"
|
|
61
|
+
INVALID_QUERY: "invalid_query",
|
|
62
|
+
AUTH_REQUIRED: "auth_required"
|
|
62
63
|
};
|
|
63
64
|
var DEFAULT_COORDINATOR_URL = "https://db.lark.sh";
|
|
64
65
|
|
|
@@ -464,6 +465,9 @@ function isAckMessage(msg) {
|
|
|
464
465
|
function isJoinCompleteMessage(msg) {
|
|
465
466
|
return "jc" in msg;
|
|
466
467
|
}
|
|
468
|
+
function isAuthCompleteMessage(msg) {
|
|
469
|
+
return "ac" in msg;
|
|
470
|
+
}
|
|
467
471
|
function isNackMessage(msg) {
|
|
468
472
|
return "n" in msg;
|
|
469
473
|
}
|
|
@@ -529,6 +533,18 @@ var MessageQueue = class {
|
|
|
529
533
|
return true;
|
|
530
534
|
}
|
|
531
535
|
}
|
|
536
|
+
if (isAuthCompleteMessage(message)) {
|
|
537
|
+
const pending = this.pending.get(message.ac);
|
|
538
|
+
if (pending) {
|
|
539
|
+
clearTimeout(pending.timeout);
|
|
540
|
+
this.pending.delete(message.ac);
|
|
541
|
+
const response = {
|
|
542
|
+
uid: message.au || null
|
|
543
|
+
};
|
|
544
|
+
pending.resolve(response);
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
532
548
|
if (isAckMessage(message)) {
|
|
533
549
|
const pending = this.pending.get(message.a);
|
|
534
550
|
if (pending) {
|
|
@@ -552,7 +568,7 @@ var MessageQueue = class {
|
|
|
552
568
|
if (pending) {
|
|
553
569
|
clearTimeout(pending.timeout);
|
|
554
570
|
this.pending.delete(message.oc);
|
|
555
|
-
pending.resolve(message.ov);
|
|
571
|
+
pending.resolve(message.ov ?? null);
|
|
556
572
|
return true;
|
|
557
573
|
}
|
|
558
574
|
}
|
|
@@ -850,7 +866,7 @@ function getNestedValue(obj, path) {
|
|
|
850
866
|
}
|
|
851
867
|
function getSortValue(value, queryParams) {
|
|
852
868
|
if (!queryParams) {
|
|
853
|
-
return
|
|
869
|
+
return getNestedValue(value, ".priority");
|
|
854
870
|
}
|
|
855
871
|
if (queryParams.orderBy === "priority") {
|
|
856
872
|
return getNestedValue(value, ".priority");
|
|
@@ -865,13 +881,14 @@ function getSortValue(value, queryParams) {
|
|
|
865
881
|
return null;
|
|
866
882
|
}
|
|
867
883
|
const hasRangeFilter = queryParams.startAt !== void 0 || queryParams.startAfter !== void 0 || queryParams.endAt !== void 0 || queryParams.endBefore !== void 0 || queryParams.equalTo !== void 0;
|
|
868
|
-
|
|
884
|
+
const hasLimit = queryParams.limitToFirst !== void 0 || queryParams.limitToLast !== void 0;
|
|
885
|
+
if (hasRangeFilter || hasLimit) {
|
|
869
886
|
return getNestedValue(value, ".priority");
|
|
870
887
|
}
|
|
871
888
|
return null;
|
|
872
889
|
}
|
|
873
890
|
function compareEntries(a, b, queryParams) {
|
|
874
|
-
if (
|
|
891
|
+
if (queryParams?.orderBy === "key") {
|
|
875
892
|
return compareKeys(a.key, b.key);
|
|
876
893
|
}
|
|
877
894
|
const cmp = compareValues(a.sortValue, b.sortValue);
|
|
@@ -1329,7 +1346,7 @@ var View = class {
|
|
|
1329
1346
|
return { value: displayCache, found: true };
|
|
1330
1347
|
}
|
|
1331
1348
|
if (this._hasReceivedInitialSnapshot || this._pendingWriteData.length > 0) {
|
|
1332
|
-
return { value: displayCache, found: true };
|
|
1349
|
+
return { value: displayCache ?? null, found: true };
|
|
1333
1350
|
}
|
|
1334
1351
|
return { value: void 0, found: false };
|
|
1335
1352
|
}
|
|
@@ -1337,7 +1354,7 @@ var View = class {
|
|
|
1337
1354
|
const relativePath = this.path === "/" ? normalized : normalized.slice(this.path.length);
|
|
1338
1355
|
if (this._hasReceivedInitialSnapshot || this._pendingWriteData.length > 0) {
|
|
1339
1356
|
const extractedValue = getValueAtPath(displayCache, relativePath);
|
|
1340
|
-
return { value: extractedValue, found: true };
|
|
1357
|
+
return { value: extractedValue ?? null, found: true };
|
|
1341
1358
|
}
|
|
1342
1359
|
}
|
|
1343
1360
|
return { value: void 0, found: false };
|
|
@@ -1430,6 +1447,17 @@ var View = class {
|
|
|
1430
1447
|
if (!this.queryParams) return false;
|
|
1431
1448
|
return !!(this.queryParams.limitToFirst || this.queryParams.limitToLast);
|
|
1432
1449
|
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Check if this View loads all data (no limits, no range filters).
|
|
1452
|
+
* A View that loads all data can serve as a "complete" cache for child paths.
|
|
1453
|
+
* This matches Firebase's loadsAllData() semantics.
|
|
1454
|
+
*/
|
|
1455
|
+
loadsAllData() {
|
|
1456
|
+
if (!this.queryParams) return true;
|
|
1457
|
+
const hasLimit = !!(this.queryParams.limitToFirst || this.queryParams.limitToLast);
|
|
1458
|
+
const hasRangeFilter = this.queryParams.startAt !== void 0 || this.queryParams.endAt !== void 0 || this.queryParams.equalTo !== void 0 || this.queryParams.startAfter !== void 0 || this.queryParams.endBefore !== void 0;
|
|
1459
|
+
return !hasLimit && !hasRangeFilter;
|
|
1460
|
+
}
|
|
1433
1461
|
// ============================================
|
|
1434
1462
|
// Pending Writes (for local-first)
|
|
1435
1463
|
// ============================================
|
|
@@ -1602,10 +1630,13 @@ var SubscriptionManager = class {
|
|
|
1602
1630
|
this.unsubscribeCallback(normalizedPath, eventType, callback, queryId);
|
|
1603
1631
|
};
|
|
1604
1632
|
if (isNewView || isNewEventType || queryParamsChanged) {
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1633
|
+
const hasAncestorComplete = this.hasAncestorCompleteView(normalizedPath);
|
|
1634
|
+
if (!hasAncestorComplete) {
|
|
1635
|
+
const allEventTypes = view.getEventTypes();
|
|
1636
|
+
this.sendSubscribe?.(normalizedPath, allEventTypes, view.queryParams ?? void 0, tag).catch((err) => {
|
|
1637
|
+
console.error("Failed to subscribe:", err);
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1609
1640
|
}
|
|
1610
1641
|
if (!isNewView && view.hasReceivedInitialSnapshot) {
|
|
1611
1642
|
this.fireInitialEventsToCallback(view, eventType, callback);
|
|
@@ -1833,18 +1864,23 @@ var SubscriptionManager = class {
|
|
|
1833
1864
|
}
|
|
1834
1865
|
}
|
|
1835
1866
|
/**
|
|
1836
|
-
* Detect and fire child_moved events for children that changed position.
|
|
1867
|
+
* Detect and fire child_moved events for children that changed position OR sort value.
|
|
1868
|
+
*
|
|
1869
|
+
* Firebase fires child_moved for ANY priority/sort value change, regardless of whether
|
|
1870
|
+
* the position actually changes. This is Case 2003 behavior.
|
|
1837
1871
|
*
|
|
1838
|
-
* IMPORTANT: child_moved should only fire for children whose VALUE changed
|
|
1839
|
-
*
|
|
1840
|
-
* another child moving should NOT fire child_moved.
|
|
1872
|
+
* IMPORTANT: child_moved should only fire for children whose VALUE or PRIORITY changed.
|
|
1873
|
+
* Children that are merely "displaced" by another child moving should NOT fire child_moved.
|
|
1841
1874
|
*
|
|
1842
1875
|
* @param affectedChildren - Only check these children for moves. If not provided,
|
|
1843
1876
|
* checks all children (for full snapshots where we compare values).
|
|
1877
|
+
* @param previousDisplayCache - Previous display cache for comparing sort values
|
|
1878
|
+
* @param currentDisplayCache - Current display cache for comparing sort values
|
|
1844
1879
|
*/
|
|
1845
|
-
detectAndFireMoves(view, previousOrder, currentOrder, previousPositions, currentPositions, previousChildSet, currentChildSet, childMovedSubs, isVolatile, serverTimestamp, affectedChildren) {
|
|
1880
|
+
detectAndFireMoves(view, previousOrder, currentOrder, previousPositions, currentPositions, previousChildSet, currentChildSet, childMovedSubs, isVolatile, serverTimestamp, affectedChildren, previousDisplayCache, currentDisplayCache) {
|
|
1846
1881
|
if (childMovedSubs.length === 0) return;
|
|
1847
1882
|
const childrenToCheck = affectedChildren ?? new Set(currentOrder);
|
|
1883
|
+
const queryParams = view.queryParams;
|
|
1848
1884
|
for (const key of childrenToCheck) {
|
|
1849
1885
|
if (!previousChildSet.has(key) || !currentChildSet.has(key)) {
|
|
1850
1886
|
continue;
|
|
@@ -1856,7 +1892,24 @@ var SubscriptionManager = class {
|
|
|
1856
1892
|
}
|
|
1857
1893
|
const oldPrevKey = oldPos > 0 ? previousOrder[oldPos - 1] : null;
|
|
1858
1894
|
const newPrevKey = newPos > 0 ? currentOrder[newPos - 1] : null;
|
|
1859
|
-
|
|
1895
|
+
let positionChanged = oldPrevKey !== newPrevKey;
|
|
1896
|
+
let sortValueChanged = false;
|
|
1897
|
+
let isPriorityOrdering = false;
|
|
1898
|
+
if (previousDisplayCache && currentDisplayCache) {
|
|
1899
|
+
const prevValue = previousDisplayCache[key];
|
|
1900
|
+
const currValue = currentDisplayCache[key];
|
|
1901
|
+
const prevSortValue = getSortValue(prevValue, queryParams);
|
|
1902
|
+
const currSortValue = getSortValue(currValue, queryParams);
|
|
1903
|
+
sortValueChanged = JSON.stringify(prevSortValue) !== JSON.stringify(currSortValue);
|
|
1904
|
+
isPriorityOrdering = !queryParams?.orderBy || queryParams.orderBy === "priority";
|
|
1905
|
+
}
|
|
1906
|
+
let shouldFire;
|
|
1907
|
+
if (affectedChildren) {
|
|
1908
|
+
shouldFire = positionChanged || isPriorityOrdering && sortValueChanged;
|
|
1909
|
+
} else {
|
|
1910
|
+
shouldFire = isPriorityOrdering && sortValueChanged;
|
|
1911
|
+
}
|
|
1912
|
+
if (shouldFire) {
|
|
1860
1913
|
this.fireChildMoved(view, key, childMovedSubs, newPrevKey, isVolatile, serverTimestamp);
|
|
1861
1914
|
}
|
|
1862
1915
|
}
|
|
@@ -1900,10 +1953,10 @@ var SubscriptionManager = class {
|
|
|
1900
1953
|
/**
|
|
1901
1954
|
* Fire child_removed callbacks for a child key.
|
|
1902
1955
|
*/
|
|
1903
|
-
fireChildRemoved(view, childKey, subs, isVolatile, serverTimestamp) {
|
|
1956
|
+
fireChildRemoved(view, childKey, subs, isVolatile, serverTimestamp, previousValue) {
|
|
1904
1957
|
if (subs.length === 0) return;
|
|
1905
1958
|
const childPath = joinPath(view.path, childKey);
|
|
1906
|
-
const snapshot = this.createSnapshot?.(childPath, null, isVolatile, serverTimestamp);
|
|
1959
|
+
const snapshot = this.createSnapshot?.(childPath, previousValue ?? null, isVolatile, serverTimestamp);
|
|
1907
1960
|
if (snapshot) {
|
|
1908
1961
|
for (const entry of subs) {
|
|
1909
1962
|
try {
|
|
@@ -1932,6 +1985,30 @@ var SubscriptionManager = class {
|
|
|
1932
1985
|
}
|
|
1933
1986
|
}
|
|
1934
1987
|
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Handle subscription revocation due to auth change.
|
|
1990
|
+
* The server has already removed the subscription, so we just clean up locally.
|
|
1991
|
+
* This is different from unsubscribeAll which sends an unsubscribe message.
|
|
1992
|
+
*/
|
|
1993
|
+
handleSubscriptionRevoked(path) {
|
|
1994
|
+
const normalizedPath = path;
|
|
1995
|
+
const queryIds = this.pathToQueryIds.get(normalizedPath);
|
|
1996
|
+
if (!queryIds || queryIds.size === 0) return;
|
|
1997
|
+
for (const queryId of queryIds) {
|
|
1998
|
+
const viewKey = this.makeViewKey(normalizedPath, queryId);
|
|
1999
|
+
const view = this.views.get(viewKey);
|
|
2000
|
+
if (view) {
|
|
2001
|
+
const tag = this.viewKeyToTag.get(viewKey);
|
|
2002
|
+
if (tag !== void 0) {
|
|
2003
|
+
this.tagToViewKey.delete(tag);
|
|
2004
|
+
this.viewKeyToTag.delete(viewKey);
|
|
2005
|
+
}
|
|
2006
|
+
view.clear();
|
|
2007
|
+
this.views.delete(viewKey);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
this.pathToQueryIds.delete(normalizedPath);
|
|
2011
|
+
}
|
|
1935
2012
|
/**
|
|
1936
2013
|
* Clear all subscriptions (e.g., on disconnect).
|
|
1937
2014
|
*/
|
|
@@ -1953,35 +2030,71 @@ var SubscriptionManager = class {
|
|
|
1953
2030
|
return queryIds !== void 0 && queryIds.size > 0;
|
|
1954
2031
|
}
|
|
1955
2032
|
/**
|
|
1956
|
-
* Check if a path is "covered" by an active subscription.
|
|
2033
|
+
* Check if a path is "covered" by an active subscription that has received data.
|
|
1957
2034
|
*
|
|
1958
2035
|
* A path is covered if:
|
|
1959
|
-
* - There's an active
|
|
1960
|
-
* - There's an active
|
|
2036
|
+
* - There's an active subscription at that exact path that has data, OR
|
|
2037
|
+
* - There's an active subscription at an ancestor path that has data
|
|
2038
|
+
*
|
|
2039
|
+
* Note: Any subscription type (value, child_added, child_moved, etc.) receives
|
|
2040
|
+
* the initial snapshot from the server and thus has cached data.
|
|
1961
2041
|
*/
|
|
1962
2042
|
isPathCovered(path) {
|
|
1963
2043
|
const normalized = normalizePath(path);
|
|
1964
|
-
if (this.
|
|
2044
|
+
if (this.hasActiveSubscriptionWithData(normalized)) {
|
|
1965
2045
|
return true;
|
|
1966
2046
|
}
|
|
1967
2047
|
const segments = normalized.split("/").filter((s) => s.length > 0);
|
|
1968
2048
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
1969
2049
|
const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
1970
|
-
if (this.
|
|
2050
|
+
if (this.hasActiveSubscriptionWithData(ancestorPath)) {
|
|
1971
2051
|
return true;
|
|
1972
2052
|
}
|
|
1973
2053
|
}
|
|
1974
|
-
if (normalized !== "/" && this.
|
|
2054
|
+
if (normalized !== "/" && this.hasActiveSubscriptionWithData("/")) {
|
|
1975
2055
|
return true;
|
|
1976
2056
|
}
|
|
1977
2057
|
return false;
|
|
1978
2058
|
}
|
|
1979
2059
|
/**
|
|
1980
|
-
* Check if there's
|
|
2060
|
+
* Check if there's an active subscription at a path that has data.
|
|
2061
|
+
* A View has data if it has received the initial snapshot OR has pending writes.
|
|
2062
|
+
* Any subscription type (value, child_added, child_moved, etc.) counts.
|
|
1981
2063
|
*/
|
|
1982
|
-
|
|
2064
|
+
hasActiveSubscriptionWithData(path) {
|
|
1983
2065
|
const views = this.getViewsAtPath(path);
|
|
1984
|
-
return views.some((view) => view.
|
|
2066
|
+
return views.some((view) => view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites()));
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Check if any ancestor path has a "complete" View (one that loadsAllData).
|
|
2070
|
+
* A complete View has no limits and no range filters, so it contains all data
|
|
2071
|
+
* for its subtree. Child subscriptions can use the ancestor's data instead
|
|
2072
|
+
* of creating their own server subscription.
|
|
2073
|
+
*
|
|
2074
|
+
* This matches Firebase's behavior where child listeners don't need their own
|
|
2075
|
+
* server subscription if an ancestor has an unlimited listener.
|
|
2076
|
+
*/
|
|
2077
|
+
hasAncestorCompleteView(path) {
|
|
2078
|
+
const normalized = normalizePath(path);
|
|
2079
|
+
const segments = normalized.split("/").filter((s) => s.length > 0);
|
|
2080
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
2081
|
+
const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
2082
|
+
const views = this.getViewsAtPath(ancestorPath);
|
|
2083
|
+
for (const view of views) {
|
|
2084
|
+
if (view.hasCallbacks() && view.loadsAllData() && view.hasReceivedInitialSnapshot) {
|
|
2085
|
+
return true;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
if (normalized !== "/") {
|
|
2090
|
+
const rootViews = this.getViewsAtPath("/");
|
|
2091
|
+
for (const view of rootViews) {
|
|
2092
|
+
if (view.hasCallbacks() && view.loadsAllData() && view.hasReceivedInitialSnapshot) {
|
|
2093
|
+
return true;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return false;
|
|
1985
2098
|
}
|
|
1986
2099
|
/**
|
|
1987
2100
|
* Get a cached value if the path is covered by an active subscription.
|
|
@@ -2004,7 +2117,7 @@ var SubscriptionManager = class {
|
|
|
2004
2117
|
const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
2005
2118
|
const ancestorViews = this.getViewsAtPath(ancestorPath);
|
|
2006
2119
|
for (const view of ancestorViews) {
|
|
2007
|
-
if (view.
|
|
2120
|
+
if (view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites())) {
|
|
2008
2121
|
const result = view.getCacheValue(normalized);
|
|
2009
2122
|
if (result.found) {
|
|
2010
2123
|
return result;
|
|
@@ -2015,7 +2128,7 @@ var SubscriptionManager = class {
|
|
|
2015
2128
|
if (normalized !== "/") {
|
|
2016
2129
|
const rootViews = this.getViewsAtPath("/");
|
|
2017
2130
|
for (const view of rootViews) {
|
|
2018
|
-
if (view.
|
|
2131
|
+
if (view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites())) {
|
|
2019
2132
|
const result = view.getCacheValue(normalized);
|
|
2020
2133
|
if (result.found) {
|
|
2021
2134
|
return result;
|
|
@@ -2124,8 +2237,11 @@ var SubscriptionManager = class {
|
|
|
2124
2237
|
const previousChildSet = new Set(previousOrder);
|
|
2125
2238
|
const isFirstSnapshot = !view.hasReceivedInitialSnapshot && !view.hasPendingWrites();
|
|
2126
2239
|
let previousCacheJson = null;
|
|
2240
|
+
let previousDisplayCache = null;
|
|
2127
2241
|
if (!isVolatile) {
|
|
2128
|
-
|
|
2242
|
+
const cache = view.getDisplayCache();
|
|
2243
|
+
previousDisplayCache = cache && typeof cache === "object" && !Array.isArray(cache) ? cache : null;
|
|
2244
|
+
previousCacheJson = this.serializeCacheForComparison(cache);
|
|
2129
2245
|
}
|
|
2130
2246
|
const affectedChildren = /* @__PURE__ */ new Set();
|
|
2131
2247
|
let isFullSnapshot = false;
|
|
@@ -2183,7 +2299,8 @@ var SubscriptionManager = class {
|
|
|
2183
2299
|
}
|
|
2184
2300
|
for (const key of previousOrder) {
|
|
2185
2301
|
if (!currentChildSet.has(key)) {
|
|
2186
|
-
|
|
2302
|
+
const prevValue = previousDisplayCache?.[key];
|
|
2303
|
+
this.fireChildRemoved(view, key, childRemovedSubs, isVolatile, serverTimestamp, prevValue);
|
|
2187
2304
|
}
|
|
2188
2305
|
}
|
|
2189
2306
|
} else {
|
|
@@ -2194,7 +2311,8 @@ var SubscriptionManager = class {
|
|
|
2194
2311
|
const prevKey = view.getPreviousChildKey(childKey);
|
|
2195
2312
|
this.fireChildAdded(view, childKey, childAddedSubs, prevKey, isVolatile, serverTimestamp);
|
|
2196
2313
|
} else if (wasPresent && !isPresent) {
|
|
2197
|
-
|
|
2314
|
+
const prevValue = previousDisplayCache?.[childKey];
|
|
2315
|
+
this.fireChildRemoved(view, childKey, childRemovedSubs, isVolatile, serverTimestamp, prevValue);
|
|
2198
2316
|
} else if (wasPresent && isPresent) {
|
|
2199
2317
|
const prevKey = view.getPreviousChildKey(childKey);
|
|
2200
2318
|
this.fireChildChanged(view, childKey, childChangedSubs, prevKey, isVolatile, serverTimestamp);
|
|
@@ -2205,6 +2323,8 @@ var SubscriptionManager = class {
|
|
|
2205
2323
|
previousOrder.forEach((key, idx) => previousPositions.set(key, idx));
|
|
2206
2324
|
const currentPositions = /* @__PURE__ */ new Map();
|
|
2207
2325
|
currentOrder.forEach((key, idx) => currentPositions.set(key, idx));
|
|
2326
|
+
const currentCache = view.getDisplayCache();
|
|
2327
|
+
const currentDisplayCache = currentCache && typeof currentCache === "object" && !Array.isArray(currentCache) ? currentCache : null;
|
|
2208
2328
|
this.detectAndFireMoves(
|
|
2209
2329
|
view,
|
|
2210
2330
|
previousOrder,
|
|
@@ -2216,7 +2336,9 @@ var SubscriptionManager = class {
|
|
|
2216
2336
|
childMovedSubs,
|
|
2217
2337
|
isVolatile,
|
|
2218
2338
|
serverTimestamp,
|
|
2219
|
-
isFullSnapshot ? void 0 : affectedChildren
|
|
2339
|
+
isFullSnapshot ? void 0 : affectedChildren,
|
|
2340
|
+
previousDisplayCache,
|
|
2341
|
+
currentDisplayCache
|
|
2220
2342
|
);
|
|
2221
2343
|
}
|
|
2222
2344
|
// ============================================
|
|
@@ -2235,8 +2357,12 @@ var SubscriptionManager = class {
|
|
|
2235
2357
|
views.push(view);
|
|
2236
2358
|
} else if (normalized.startsWith(viewPath + "/")) {
|
|
2237
2359
|
views.push(view);
|
|
2360
|
+
} else if (viewPath.startsWith(normalized + "/")) {
|
|
2361
|
+
views.push(view);
|
|
2238
2362
|
} else if (viewPath === "/") {
|
|
2239
2363
|
views.push(view);
|
|
2364
|
+
} else if (normalized === "/") {
|
|
2365
|
+
views.push(view);
|
|
2240
2366
|
}
|
|
2241
2367
|
}
|
|
2242
2368
|
return views;
|
|
@@ -2300,7 +2426,48 @@ var SubscriptionManager = class {
|
|
|
2300
2426
|
}
|
|
2301
2427
|
}
|
|
2302
2428
|
}
|
|
2303
|
-
this.
|
|
2429
|
+
this.fireChildEventsForAck(view, previousOrder, previousChildSet, currentOrder, currentChildSet, previousDisplayCache);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Fire child events for ACK handling, skipping child_moved.
|
|
2434
|
+
* This is a variant of fireChildEvents that doesn't fire moves because:
|
|
2435
|
+
* 1. Moves were already fired optimistically
|
|
2436
|
+
* 2. If server modifies data, PUT event will fire correct moves
|
|
2437
|
+
* 3. ACK can arrive before PUT, causing incorrect intermediate state
|
|
2438
|
+
*/
|
|
2439
|
+
fireChildEventsForAck(view, previousOrder, previousChildSet, currentOrder, currentChildSet, previousDisplayCache) {
|
|
2440
|
+
const childAddedSubs = view.getCallbacks("child_added");
|
|
2441
|
+
const childChangedSubs = view.getCallbacks("child_changed");
|
|
2442
|
+
const childRemovedSubs = view.getCallbacks("child_removed");
|
|
2443
|
+
if (childAddedSubs.length === 0 && childChangedSubs.length === 0 && childRemovedSubs.length === 0) {
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
const displayCache = view.getDisplayCache();
|
|
2447
|
+
for (const key of currentOrder) {
|
|
2448
|
+
if (!previousChildSet.has(key)) {
|
|
2449
|
+
if (childAddedSubs.length > 0 && displayCache) {
|
|
2450
|
+
const prevKey = view.getPreviousChildKey(key);
|
|
2451
|
+
this.fireChildAdded(view, key, childAddedSubs, prevKey, false, void 0);
|
|
2452
|
+
}
|
|
2453
|
+
} else if (previousDisplayCache && childChangedSubs.length > 0 && displayCache) {
|
|
2454
|
+
const prevValue = previousDisplayCache[key];
|
|
2455
|
+
const currentValue = displayCache[key];
|
|
2456
|
+
const prevJson = this.serializeCacheForComparison(prevValue);
|
|
2457
|
+
const currJson = this.serializeCacheForComparison(currentValue);
|
|
2458
|
+
if (prevJson !== currJson) {
|
|
2459
|
+
const prevKey = view.getPreviousChildKey(key);
|
|
2460
|
+
this.fireChildChanged(view, key, childChangedSubs, prevKey, false, void 0);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
for (const key of previousOrder) {
|
|
2465
|
+
if (!currentChildSet.has(key)) {
|
|
2466
|
+
if (childRemovedSubs.length > 0) {
|
|
2467
|
+
const prevValue = previousDisplayCache?.[key];
|
|
2468
|
+
this.fireChildRemoved(view, key, childRemovedSubs, false, void 0, prevValue);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2304
2471
|
}
|
|
2305
2472
|
}
|
|
2306
2473
|
/**
|
|
@@ -2369,18 +2536,28 @@ var SubscriptionManager = class {
|
|
|
2369
2536
|
const updatedViews = [];
|
|
2370
2537
|
for (const view of affectedViews) {
|
|
2371
2538
|
let relativePath;
|
|
2539
|
+
let effectiveValue = value;
|
|
2372
2540
|
if (normalized === view.path) {
|
|
2373
2541
|
relativePath = "/";
|
|
2374
2542
|
} else if (view.path === "/") {
|
|
2375
2543
|
relativePath = normalized;
|
|
2376
|
-
} else {
|
|
2544
|
+
} else if (normalized.startsWith(view.path + "/")) {
|
|
2377
2545
|
relativePath = normalized.slice(view.path.length);
|
|
2546
|
+
} else if (view.path.startsWith(normalized + "/")) {
|
|
2547
|
+
const pathDiff = view.path.slice(normalized.length);
|
|
2548
|
+
effectiveValue = getValueAtPath(value, pathDiff);
|
|
2549
|
+
if (operation === "update" && effectiveValue === void 0) {
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
relativePath = "/";
|
|
2553
|
+
} else {
|
|
2554
|
+
continue;
|
|
2378
2555
|
}
|
|
2379
2556
|
const previousDisplayCache = view.getDisplayCache();
|
|
2380
2557
|
const previousOrder = view.orderedChildren;
|
|
2381
2558
|
const previousChildSet = new Set(previousOrder);
|
|
2382
2559
|
const previousCacheJson = this.serializeCacheForComparison(previousDisplayCache);
|
|
2383
|
-
view.addPendingWriteData(requestId, relativePath,
|
|
2560
|
+
view.addPendingWriteData(requestId, relativePath, effectiveValue, operation);
|
|
2384
2561
|
const currentOrder = view.orderedChildren;
|
|
2385
2562
|
const currentChildSet = new Set(currentOrder);
|
|
2386
2563
|
const currentCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
|
|
@@ -2477,7 +2654,8 @@ var SubscriptionManager = class {
|
|
|
2477
2654
|
for (const key of previousOrder) {
|
|
2478
2655
|
if (!currentChildSet.has(key)) {
|
|
2479
2656
|
if (childRemovedSubs.length > 0) {
|
|
2480
|
-
const
|
|
2657
|
+
const prevValue = previousDisplayCache?.[key];
|
|
2658
|
+
const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue ?? null, isVolatile, serverTimestamp);
|
|
2481
2659
|
if (snapshot) {
|
|
2482
2660
|
for (const entry of childRemovedSubs) {
|
|
2483
2661
|
try {
|
|
@@ -2491,6 +2669,23 @@ var SubscriptionManager = class {
|
|
|
2491
2669
|
}
|
|
2492
2670
|
}
|
|
2493
2671
|
} else {
|
|
2672
|
+
if (childRemovedSubs.length > 0) {
|
|
2673
|
+
for (const key of previousOrder) {
|
|
2674
|
+
if (affectedChildren.has(key)) continue;
|
|
2675
|
+
if (currentChildSet.has(key)) continue;
|
|
2676
|
+
const prevValue = previousDisplayCache ? previousDisplayCache[key] : null;
|
|
2677
|
+
const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue, isVolatile, serverTimestamp);
|
|
2678
|
+
if (snapshot) {
|
|
2679
|
+
for (const entry of childRemovedSubs) {
|
|
2680
|
+
try {
|
|
2681
|
+
entry.callback(snapshot, void 0);
|
|
2682
|
+
} catch (err) {
|
|
2683
|
+
console.error("Error in child_removed callback:", err);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2494
2689
|
for (const key of affectedChildren) {
|
|
2495
2690
|
const wasPresent = previousChildSet.has(key);
|
|
2496
2691
|
const isPresent = currentChildSet.has(key);
|
|
@@ -2516,7 +2711,8 @@ var SubscriptionManager = class {
|
|
|
2516
2711
|
}
|
|
2517
2712
|
} else if (wasPresent && !isPresent) {
|
|
2518
2713
|
if (childRemovedSubs.length > 0) {
|
|
2519
|
-
const
|
|
2714
|
+
const prevValue = previousDisplayCache ? previousDisplayCache[key] : null;
|
|
2715
|
+
const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue, isVolatile, serverTimestamp);
|
|
2520
2716
|
if (snapshot) {
|
|
2521
2717
|
for (const entry of childRemovedSubs) {
|
|
2522
2718
|
try {
|
|
@@ -2549,18 +2745,24 @@ var SubscriptionManager = class {
|
|
|
2549
2745
|
}
|
|
2550
2746
|
}
|
|
2551
2747
|
}
|
|
2552
|
-
if (
|
|
2553
|
-
for (const key of
|
|
2748
|
+
if (childAddedSubs.length > 0 && displayCache) {
|
|
2749
|
+
for (const key of currentOrder) {
|
|
2750
|
+
if (previousChildSet.has(key)) continue;
|
|
2554
2751
|
if (affectedChildren.has(key)) continue;
|
|
2555
|
-
|
|
2556
|
-
const
|
|
2557
|
-
|
|
2752
|
+
const childValue = displayCache[key];
|
|
2753
|
+
const snapshot = this.createSnapshot?.(
|
|
2754
|
+
joinPath(view.path, key),
|
|
2755
|
+
childValue,
|
|
2756
|
+
isVolatile,
|
|
2757
|
+
serverTimestamp
|
|
2758
|
+
);
|
|
2558
2759
|
if (snapshot) {
|
|
2559
|
-
|
|
2760
|
+
const prevKey = view.getPreviousChildKey(key);
|
|
2761
|
+
for (const entry of childAddedSubs) {
|
|
2560
2762
|
try {
|
|
2561
|
-
entry.callback(snapshot,
|
|
2763
|
+
entry.callback(snapshot, prevKey);
|
|
2562
2764
|
} catch (err) {
|
|
2563
|
-
console.error("Error in
|
|
2765
|
+
console.error("Error in child_added callback:", err);
|
|
2564
2766
|
}
|
|
2565
2767
|
}
|
|
2566
2768
|
}
|
|
@@ -2583,7 +2785,9 @@ var SubscriptionManager = class {
|
|
|
2583
2785
|
childMovedSubs,
|
|
2584
2786
|
isVolatile,
|
|
2585
2787
|
serverTimestamp,
|
|
2586
|
-
isFullSnapshot ? void 0 : affectedChildren
|
|
2788
|
+
isFullSnapshot ? void 0 : affectedChildren,
|
|
2789
|
+
previousDisplayCache,
|
|
2790
|
+
displayCache
|
|
2587
2791
|
);
|
|
2588
2792
|
}
|
|
2589
2793
|
}
|
|
@@ -2806,26 +3010,26 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
2806
3010
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX] = this._query.orderByChildPath;
|
|
2807
3011
|
}
|
|
2808
3012
|
if (this._query.startAt !== void 0) {
|
|
2809
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAt.value;
|
|
3013
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAt.value ?? null;
|
|
2810
3014
|
if (this._query.startAt.key !== void 0) {
|
|
2811
3015
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.startAt.key;
|
|
2812
3016
|
}
|
|
2813
3017
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = true;
|
|
2814
3018
|
} else if (this._query.startAfter !== void 0) {
|
|
2815
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAfter.value;
|
|
3019
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAfter.value ?? null;
|
|
2816
3020
|
if (this._query.startAfter.key !== void 0) {
|
|
2817
3021
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.startAfter.key;
|
|
2818
3022
|
}
|
|
2819
3023
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = false;
|
|
2820
3024
|
}
|
|
2821
3025
|
if (this._query.endAt !== void 0) {
|
|
2822
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endAt.value;
|
|
3026
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endAt.value ?? null;
|
|
2823
3027
|
if (this._query.endAt.key !== void 0) {
|
|
2824
3028
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.endAt.key;
|
|
2825
3029
|
}
|
|
2826
3030
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_IS_INCLUSIVE] = true;
|
|
2827
3031
|
} else if (this._query.endBefore !== void 0) {
|
|
2828
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endBefore.value;
|
|
3032
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endBefore.value ?? null;
|
|
2829
3033
|
if (this._query.endBefore.key !== void 0) {
|
|
2830
3034
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.endBefore.key;
|
|
2831
3035
|
}
|
|
@@ -2984,17 +3188,39 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
2984
3188
|
}
|
|
2985
3189
|
/**
|
|
2986
3190
|
* Set the priority of the data at this location.
|
|
2987
|
-
*
|
|
3191
|
+
* Uses cached value for optimistic behavior (local effects are immediate).
|
|
3192
|
+
* The optimistic update happens synchronously, Promise resolves after server ack.
|
|
2988
3193
|
*/
|
|
2989
|
-
|
|
3194
|
+
setPriority(priority) {
|
|
2990
3195
|
validateNotInfoPath(this._path, "setPriority");
|
|
2991
|
-
const
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
3196
|
+
const { value: cachedValue, found } = this._db._getCachedValue(this._path);
|
|
3197
|
+
if (!found) {
|
|
3198
|
+
return this.once().then((snapshot) => {
|
|
3199
|
+
const actualValue2 = snapshot.val();
|
|
3200
|
+
if (actualValue2 === null || actualValue2 === void 0) {
|
|
3201
|
+
return this._db._sendSet(this._path, { ".priority": priority });
|
|
3202
|
+
}
|
|
3203
|
+
return this.setWithPriority(actualValue2, priority);
|
|
3204
|
+
});
|
|
2996
3205
|
}
|
|
2997
|
-
|
|
3206
|
+
let actualValue;
|
|
3207
|
+
if (cachedValue === null || cachedValue === void 0) {
|
|
3208
|
+
actualValue = null;
|
|
3209
|
+
} else if (typeof cachedValue === "object" && !Array.isArray(cachedValue)) {
|
|
3210
|
+
const obj = cachedValue;
|
|
3211
|
+
if (".value" in obj && Object.keys(obj).every((k) => k === ".value" || k === ".priority")) {
|
|
3212
|
+
actualValue = obj[".value"];
|
|
3213
|
+
} else {
|
|
3214
|
+
const { ".priority": _oldPriority, ...rest } = obj;
|
|
3215
|
+
actualValue = Object.keys(rest).length > 0 ? rest : null;
|
|
3216
|
+
}
|
|
3217
|
+
} else {
|
|
3218
|
+
actualValue = cachedValue;
|
|
3219
|
+
}
|
|
3220
|
+
if (actualValue === null || actualValue === void 0) {
|
|
3221
|
+
return this._db._sendSet(this._path, { ".priority": priority });
|
|
3222
|
+
}
|
|
3223
|
+
return this.setWithPriority(actualValue, priority);
|
|
2998
3224
|
}
|
|
2999
3225
|
/**
|
|
3000
3226
|
* Atomically modify the data at this location using optimistic concurrency.
|
|
@@ -3065,14 +3291,23 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3065
3291
|
/**
|
|
3066
3292
|
* Read the data at this location once.
|
|
3067
3293
|
*
|
|
3068
|
-
*
|
|
3294
|
+
* For 'value' events, this fetches data directly from the server.
|
|
3295
|
+
* For child events ('child_added', 'child_changed', 'child_removed', 'child_moved'),
|
|
3296
|
+
* this subscribes, waits for the first event, then unsubscribes.
|
|
3297
|
+
*
|
|
3298
|
+
* @param eventType - The event type
|
|
3069
3299
|
* @returns Promise that resolves to the DataSnapshot
|
|
3070
3300
|
*/
|
|
3071
3301
|
once(eventType = "value") {
|
|
3072
|
-
if (eventType
|
|
3073
|
-
|
|
3302
|
+
if (eventType === "value") {
|
|
3303
|
+
return this._db._sendOnce(this._path, this._buildQueryParams());
|
|
3074
3304
|
}
|
|
3075
|
-
return
|
|
3305
|
+
return new Promise((resolve) => {
|
|
3306
|
+
const unsubscribe = this.on(eventType, (snapshot) => {
|
|
3307
|
+
unsubscribe();
|
|
3308
|
+
resolve(snapshot);
|
|
3309
|
+
});
|
|
3310
|
+
});
|
|
3076
3311
|
}
|
|
3077
3312
|
// ============================================
|
|
3078
3313
|
// Subscriptions
|
|
@@ -3141,6 +3376,12 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3141
3376
|
*/
|
|
3142
3377
|
orderByChild(path) {
|
|
3143
3378
|
this._validateNoOrderBy("orderByChild");
|
|
3379
|
+
if (path.startsWith("$") || path.includes("/$")) {
|
|
3380
|
+
throw new LarkError(
|
|
3381
|
+
ErrorCode.INVALID_PATH,
|
|
3382
|
+
`orderByChild: Invalid path '${path}'. Paths cannot contain '$' prefix (reserved for internal use)`
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3144
3385
|
return new _DatabaseReference(this._db, this._path, {
|
|
3145
3386
|
...this._query,
|
|
3146
3387
|
orderBy: "child",
|
|
@@ -3514,35 +3755,35 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3514
3755
|
hasParams = true;
|
|
3515
3756
|
}
|
|
3516
3757
|
if (this._query.startAt !== void 0) {
|
|
3517
|
-
params.startAt = this._query.startAt.value;
|
|
3758
|
+
params.startAt = this._query.startAt.value ?? null;
|
|
3518
3759
|
if (this._query.startAt.key !== void 0) {
|
|
3519
3760
|
params.startAtKey = this._query.startAt.key;
|
|
3520
3761
|
}
|
|
3521
3762
|
hasParams = true;
|
|
3522
3763
|
}
|
|
3523
3764
|
if (this._query.startAfter !== void 0) {
|
|
3524
|
-
params.startAfter = this._query.startAfter.value;
|
|
3765
|
+
params.startAfter = this._query.startAfter.value ?? null;
|
|
3525
3766
|
if (this._query.startAfter.key !== void 0) {
|
|
3526
3767
|
params.startAfterKey = this._query.startAfter.key;
|
|
3527
3768
|
}
|
|
3528
3769
|
hasParams = true;
|
|
3529
3770
|
}
|
|
3530
3771
|
if (this._query.endAt !== void 0) {
|
|
3531
|
-
params.endAt = this._query.endAt.value;
|
|
3772
|
+
params.endAt = this._query.endAt.value ?? null;
|
|
3532
3773
|
if (this._query.endAt.key !== void 0) {
|
|
3533
3774
|
params.endAtKey = this._query.endAt.key;
|
|
3534
3775
|
}
|
|
3535
3776
|
hasParams = true;
|
|
3536
3777
|
}
|
|
3537
3778
|
if (this._query.endBefore !== void 0) {
|
|
3538
|
-
params.endBefore = this._query.endBefore.value;
|
|
3779
|
+
params.endBefore = this._query.endBefore.value ?? null;
|
|
3539
3780
|
if (this._query.endBefore.key !== void 0) {
|
|
3540
3781
|
params.endBeforeKey = this._query.endBefore.key;
|
|
3541
3782
|
}
|
|
3542
3783
|
hasParams = true;
|
|
3543
3784
|
}
|
|
3544
3785
|
if (this._query.equalTo !== void 0) {
|
|
3545
|
-
params.equalTo = this._query.equalTo.value;
|
|
3786
|
+
params.equalTo = this._query.equalTo.value ?? null;
|
|
3546
3787
|
if (this._query.equalTo.key !== void 0) {
|
|
3547
3788
|
params.equalToKey = this._query.equalTo.key;
|
|
3548
3789
|
}
|
|
@@ -3561,6 +3802,13 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3561
3802
|
}
|
|
3562
3803
|
return `${baseUrl}${this._path}`;
|
|
3563
3804
|
}
|
|
3805
|
+
/**
|
|
3806
|
+
* Returns the URL for JSON serialization.
|
|
3807
|
+
* This allows refs to be serialized with JSON.stringify().
|
|
3808
|
+
*/
|
|
3809
|
+
toJSON() {
|
|
3810
|
+
return this.toString();
|
|
3811
|
+
}
|
|
3564
3812
|
};
|
|
3565
3813
|
var ThenableReference = class extends DatabaseReference {
|
|
3566
3814
|
constructor(db, path, promise) {
|
|
@@ -3581,11 +3829,17 @@ function isWrappedPrimitive(data) {
|
|
|
3581
3829
|
return false;
|
|
3582
3830
|
}
|
|
3583
3831
|
const keys = Object.keys(data);
|
|
3584
|
-
|
|
3832
|
+
if (keys.length === 2 && ".value" in data && ".priority" in data) {
|
|
3833
|
+
return true;
|
|
3834
|
+
}
|
|
3835
|
+
if (keys.length === 1 && ".value" in data) {
|
|
3836
|
+
return true;
|
|
3837
|
+
}
|
|
3838
|
+
return false;
|
|
3585
3839
|
}
|
|
3586
3840
|
function stripPriorityMetadata(data) {
|
|
3587
3841
|
if (data === null || data === void 0) {
|
|
3588
|
-
return
|
|
3842
|
+
return null;
|
|
3589
3843
|
}
|
|
3590
3844
|
if (typeof data !== "object") {
|
|
3591
3845
|
return data;
|
|
@@ -3648,9 +3902,19 @@ var DataSnapshot = class _DataSnapshot {
|
|
|
3648
3902
|
}
|
|
3649
3903
|
/**
|
|
3650
3904
|
* Check if data exists at this location (is not null/undefined).
|
|
3905
|
+
* Returns false for priority-only nodes (only .priority, no actual value).
|
|
3651
3906
|
*/
|
|
3652
3907
|
exists() {
|
|
3653
|
-
|
|
3908
|
+
if (this._data === null || this._data === void 0) {
|
|
3909
|
+
return false;
|
|
3910
|
+
}
|
|
3911
|
+
if (typeof this._data === "object" && !Array.isArray(this._data)) {
|
|
3912
|
+
const keys = Object.keys(this._data);
|
|
3913
|
+
if (keys.length === 1 && keys[0] === ".priority") {
|
|
3914
|
+
return false;
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
return true;
|
|
3654
3918
|
}
|
|
3655
3919
|
/**
|
|
3656
3920
|
* Get a child snapshot at the specified path.
|
|
@@ -3797,24 +4061,6 @@ var DataSnapshot = class _DataSnapshot {
|
|
|
3797
4061
|
}
|
|
3798
4062
|
};
|
|
3799
4063
|
|
|
3800
|
-
// src/utils/jwt.ts
|
|
3801
|
-
function decodeJwtPayload(token) {
|
|
3802
|
-
const parts = token.split(".");
|
|
3803
|
-
if (parts.length !== 3) {
|
|
3804
|
-
throw new Error("Invalid JWT format");
|
|
3805
|
-
}
|
|
3806
|
-
const payload = parts[1];
|
|
3807
|
-
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
3808
|
-
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
3809
|
-
let decoded;
|
|
3810
|
-
if (typeof atob === "function") {
|
|
3811
|
-
decoded = atob(padded);
|
|
3812
|
-
} else {
|
|
3813
|
-
decoded = Buffer.from(padded, "base64").toString("utf-8");
|
|
3814
|
-
}
|
|
3815
|
-
return JSON.parse(decoded);
|
|
3816
|
-
}
|
|
3817
|
-
|
|
3818
4064
|
// src/utils/volatile.ts
|
|
3819
4065
|
function isVolatilePath(path, patterns) {
|
|
3820
4066
|
if (!patterns || patterns.length === 0) {
|
|
@@ -3937,6 +4183,11 @@ var LarkDatabase = class {
|
|
|
3937
4183
|
this._coordinatorUrl = null;
|
|
3938
4184
|
this._volatilePaths = [];
|
|
3939
4185
|
this._transportType = null;
|
|
4186
|
+
// Auth state
|
|
4187
|
+
this._currentToken = null;
|
|
4188
|
+
// Token for auth (empty string = anonymous)
|
|
4189
|
+
this._isAnonymous = false;
|
|
4190
|
+
// True if connected anonymously
|
|
3940
4191
|
// Reconnection state
|
|
3941
4192
|
this._connectionId = null;
|
|
3942
4193
|
this._connectOptions = null;
|
|
@@ -3949,8 +4200,12 @@ var LarkDatabase = class {
|
|
|
3949
4200
|
this.disconnectCallbacks = /* @__PURE__ */ new Set();
|
|
3950
4201
|
this.errorCallbacks = /* @__PURE__ */ new Set();
|
|
3951
4202
|
this.reconnectingCallbacks = /* @__PURE__ */ new Set();
|
|
4203
|
+
this.authStateChangedCallbacks = /* @__PURE__ */ new Set();
|
|
3952
4204
|
// .info path subscriptions (handled locally, not sent to server)
|
|
3953
4205
|
this.infoSubscriptions = [];
|
|
4206
|
+
// Authentication promise - resolves when auth completes, allows operations to queue
|
|
4207
|
+
this.authenticationPromise = null;
|
|
4208
|
+
this.authenticationResolve = null;
|
|
3954
4209
|
this._serverTimeOffset = 0;
|
|
3955
4210
|
this.messageQueue = new MessageQueue();
|
|
3956
4211
|
this.subscriptionManager = new SubscriptionManager();
|
|
@@ -3960,10 +4215,11 @@ var LarkDatabase = class {
|
|
|
3960
4215
|
// Connection State
|
|
3961
4216
|
// ============================================
|
|
3962
4217
|
/**
|
|
3963
|
-
* Whether the database is
|
|
4218
|
+
* Whether the database is fully connected and authenticated.
|
|
4219
|
+
* Returns true when ready to perform database operations.
|
|
3964
4220
|
*/
|
|
3965
4221
|
get connected() {
|
|
3966
|
-
return this._state === "
|
|
4222
|
+
return this._state === "authenticated";
|
|
3967
4223
|
}
|
|
3968
4224
|
/**
|
|
3969
4225
|
* Whether the database is currently attempting to reconnect.
|
|
@@ -4048,16 +4304,27 @@ var LarkDatabase = class {
|
|
|
4048
4304
|
}
|
|
4049
4305
|
this._connectOptions = options;
|
|
4050
4306
|
this._intentionalDisconnect = false;
|
|
4307
|
+
this.authenticationPromise = new Promise((resolve) => {
|
|
4308
|
+
this.authenticationResolve = resolve;
|
|
4309
|
+
});
|
|
4051
4310
|
await this.performConnect(databaseId, options);
|
|
4052
4311
|
}
|
|
4053
4312
|
/**
|
|
4054
4313
|
* Internal connect implementation used by both initial connect and reconnect.
|
|
4314
|
+
* Implements the Join → Auth flow:
|
|
4315
|
+
* 1. Connect WebSocket
|
|
4316
|
+
* 2. Send join (identifies database)
|
|
4317
|
+
* 3. Send auth (authenticates user - required even for anonymous)
|
|
4055
4318
|
*/
|
|
4056
4319
|
async performConnect(databaseId, options, isReconnect = false) {
|
|
4057
4320
|
const previousState = this._state;
|
|
4058
4321
|
this._state = isReconnect ? "reconnecting" : "connecting";
|
|
4059
4322
|
this._databaseId = databaseId;
|
|
4060
4323
|
this._coordinatorUrl = options.coordinator || DEFAULT_COORDINATOR_URL;
|
|
4324
|
+
if (!isReconnect) {
|
|
4325
|
+
this._currentToken = options.token || "";
|
|
4326
|
+
this._isAnonymous = !options.token && options.anonymous !== false;
|
|
4327
|
+
}
|
|
4061
4328
|
try {
|
|
4062
4329
|
const coordinatorUrl = this._coordinatorUrl;
|
|
4063
4330
|
const coordinator = new Coordinator(coordinatorUrl);
|
|
@@ -4083,30 +4350,44 @@ var LarkDatabase = class {
|
|
|
4083
4350
|
);
|
|
4084
4351
|
this.transport = transportResult.transport;
|
|
4085
4352
|
this._transportType = transportResult.type;
|
|
4086
|
-
|
|
4353
|
+
this._state = "connected";
|
|
4354
|
+
const joinRequestId = this.messageQueue.nextRequestId();
|
|
4087
4355
|
const joinMessage = {
|
|
4088
4356
|
o: "j",
|
|
4089
|
-
|
|
4090
|
-
r:
|
|
4357
|
+
d: databaseId,
|
|
4358
|
+
r: joinRequestId
|
|
4091
4359
|
};
|
|
4092
4360
|
if (this._connectionId) {
|
|
4093
4361
|
joinMessage.pcid = this._connectionId;
|
|
4094
4362
|
}
|
|
4095
4363
|
this.send(joinMessage);
|
|
4096
|
-
const joinResponse = await this.messageQueue.registerRequest(
|
|
4364
|
+
const joinResponse = await this.messageQueue.registerRequest(joinRequestId);
|
|
4097
4365
|
this._volatilePaths = joinResponse.volatilePaths;
|
|
4098
4366
|
this._connectionId = joinResponse.connectionId;
|
|
4099
4367
|
if (joinResponse.serverTime != null) {
|
|
4100
4368
|
this._serverTimeOffset = joinResponse.serverTime - Date.now();
|
|
4101
4369
|
}
|
|
4102
|
-
|
|
4370
|
+
this._state = "joined";
|
|
4371
|
+
const authRequestId = this.messageQueue.nextRequestId();
|
|
4372
|
+
const authMessage = {
|
|
4373
|
+
o: "au",
|
|
4374
|
+
t: this._currentToken ?? "",
|
|
4375
|
+
r: authRequestId
|
|
4376
|
+
};
|
|
4377
|
+
this.send(authMessage);
|
|
4378
|
+
const authResponse = await this.messageQueue.registerRequest(authRequestId);
|
|
4103
4379
|
this._auth = {
|
|
4104
|
-
uid:
|
|
4105
|
-
provider:
|
|
4106
|
-
token:
|
|
4380
|
+
uid: authResponse.uid || "",
|
|
4381
|
+
provider: this._isAnonymous ? "anonymous" : "custom",
|
|
4382
|
+
token: {}
|
|
4383
|
+
// Token claims would need to be decoded from the token if needed
|
|
4107
4384
|
};
|
|
4108
|
-
this._state = "
|
|
4385
|
+
this._state = "authenticated";
|
|
4109
4386
|
this._reconnectAttempt = 0;
|
|
4387
|
+
if (this.authenticationResolve) {
|
|
4388
|
+
this.authenticationResolve();
|
|
4389
|
+
this.authenticationResolve = null;
|
|
4390
|
+
}
|
|
4110
4391
|
this.fireConnectionStateChange();
|
|
4111
4392
|
if (!isReconnect) {
|
|
4112
4393
|
this.subscriptionManager.initialize({
|
|
@@ -4119,6 +4400,7 @@ var LarkDatabase = class {
|
|
|
4119
4400
|
await this.restoreAfterReconnect();
|
|
4120
4401
|
}
|
|
4121
4402
|
this.connectCallbacks.forEach((cb) => cb());
|
|
4403
|
+
this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
|
|
4122
4404
|
} catch (error) {
|
|
4123
4405
|
if (isReconnect) {
|
|
4124
4406
|
this._state = "reconnecting";
|
|
@@ -4131,6 +4413,8 @@ var LarkDatabase = class {
|
|
|
4131
4413
|
this._connectOptions = null;
|
|
4132
4414
|
this._connectionId = null;
|
|
4133
4415
|
this._transportType = null;
|
|
4416
|
+
this._currentToken = null;
|
|
4417
|
+
this._isAnonymous = false;
|
|
4134
4418
|
this.transport?.close();
|
|
4135
4419
|
this.transport = null;
|
|
4136
4420
|
throw error;
|
|
@@ -4144,13 +4428,14 @@ var LarkDatabase = class {
|
|
|
4144
4428
|
if (this._state === "disconnected") {
|
|
4145
4429
|
return;
|
|
4146
4430
|
}
|
|
4147
|
-
const
|
|
4431
|
+
const wasAuthenticated = this._state === "authenticated";
|
|
4432
|
+
const wasPartiallyConnected = this._state === "connected" || this._state === "joined";
|
|
4148
4433
|
this._intentionalDisconnect = true;
|
|
4149
4434
|
if (this._reconnectTimer) {
|
|
4150
4435
|
clearTimeout(this._reconnectTimer);
|
|
4151
4436
|
this._reconnectTimer = null;
|
|
4152
4437
|
}
|
|
4153
|
-
if (
|
|
4438
|
+
if ((wasAuthenticated || wasPartiallyConnected) && this.transport) {
|
|
4154
4439
|
try {
|
|
4155
4440
|
const requestId = this.messageQueue.nextRequestId();
|
|
4156
4441
|
this.send({ o: "l", r: requestId });
|
|
@@ -4162,7 +4447,7 @@ var LarkDatabase = class {
|
|
|
4162
4447
|
}
|
|
4163
4448
|
}
|
|
4164
4449
|
this.cleanupFull();
|
|
4165
|
-
if (
|
|
4450
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4166
4451
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4167
4452
|
}
|
|
4168
4453
|
}
|
|
@@ -4171,7 +4456,7 @@ var LarkDatabase = class {
|
|
|
4171
4456
|
* Disconnects from the server but preserves subscriptions for later reconnection via goOnline().
|
|
4172
4457
|
*/
|
|
4173
4458
|
goOffline() {
|
|
4174
|
-
if (this._state === "connected" || this._state === "reconnecting") {
|
|
4459
|
+
if (this._state === "authenticated" || this._state === "joined" || this._state === "connected" || this._state === "reconnecting") {
|
|
4175
4460
|
this._intentionalDisconnect = true;
|
|
4176
4461
|
if (this._reconnectTimer) {
|
|
4177
4462
|
clearTimeout(this._reconnectTimer);
|
|
@@ -4202,7 +4487,7 @@ var LarkDatabase = class {
|
|
|
4202
4487
|
* Used for intentional disconnect.
|
|
4203
4488
|
*/
|
|
4204
4489
|
cleanupFull() {
|
|
4205
|
-
const
|
|
4490
|
+
const wasAuthenticated = this._state === "authenticated";
|
|
4206
4491
|
this.transport?.close();
|
|
4207
4492
|
this.transport = null;
|
|
4208
4493
|
this._state = "disconnected";
|
|
@@ -4213,11 +4498,15 @@ var LarkDatabase = class {
|
|
|
4213
4498
|
this._connectionId = null;
|
|
4214
4499
|
this._connectOptions = null;
|
|
4215
4500
|
this._transportType = null;
|
|
4501
|
+
this._currentToken = null;
|
|
4502
|
+
this._isAnonymous = false;
|
|
4216
4503
|
this._reconnectAttempt = 0;
|
|
4504
|
+
this.authenticationPromise = null;
|
|
4505
|
+
this.authenticationResolve = null;
|
|
4217
4506
|
this.subscriptionManager.clear();
|
|
4218
4507
|
this.messageQueue.rejectAll(new Error("Connection closed"));
|
|
4219
4508
|
this.pendingWrites.clear();
|
|
4220
|
-
if (
|
|
4509
|
+
if (wasAuthenticated) {
|
|
4221
4510
|
this.fireConnectionStateChange();
|
|
4222
4511
|
}
|
|
4223
4512
|
this.infoSubscriptions = [];
|
|
@@ -4250,7 +4539,7 @@ var LarkDatabase = class {
|
|
|
4250
4539
|
getInfoValue(path) {
|
|
4251
4540
|
const normalizedPath = normalizePath(path) || "/";
|
|
4252
4541
|
if (normalizedPath === "/.info/connected") {
|
|
4253
|
-
return this._state === "
|
|
4542
|
+
return this._state === "authenticated";
|
|
4254
4543
|
}
|
|
4255
4544
|
if (normalizedPath === "/.info/serverTimeOffset") {
|
|
4256
4545
|
return this._serverTimeOffset;
|
|
@@ -4317,6 +4606,9 @@ var LarkDatabase = class {
|
|
|
4317
4606
|
if (this._intentionalDisconnect || !this._databaseId || !this._connectOptions) {
|
|
4318
4607
|
return;
|
|
4319
4608
|
}
|
|
4609
|
+
this.authenticationPromise = new Promise((resolve) => {
|
|
4610
|
+
this.authenticationResolve = resolve;
|
|
4611
|
+
});
|
|
4320
4612
|
try {
|
|
4321
4613
|
await this.performConnect(this._databaseId, this._connectOptions, true);
|
|
4322
4614
|
} catch {
|
|
@@ -4475,6 +4767,9 @@ var LarkDatabase = class {
|
|
|
4475
4767
|
* @internal Send a transaction to the server.
|
|
4476
4768
|
*/
|
|
4477
4769
|
async _sendTransaction(ops) {
|
|
4770
|
+
if (!this.isAuthenticatedOrThrow()) {
|
|
4771
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4772
|
+
}
|
|
4478
4773
|
const requestId = this.messageQueue.nextRequestId();
|
|
4479
4774
|
this.pendingWrites.trackWrite(requestId, "transaction", "/", ops);
|
|
4480
4775
|
const message = {
|
|
@@ -4521,6 +4816,76 @@ var LarkDatabase = class {
|
|
|
4521
4816
|
this.reconnectingCallbacks.add(callback);
|
|
4522
4817
|
return () => this.reconnectingCallbacks.delete(callback);
|
|
4523
4818
|
}
|
|
4819
|
+
/**
|
|
4820
|
+
* Register a callback for auth state changes.
|
|
4821
|
+
* Fires when user signs in, signs out, or auth changes.
|
|
4822
|
+
* Returns an unsubscribe function.
|
|
4823
|
+
*/
|
|
4824
|
+
onAuthStateChanged(callback) {
|
|
4825
|
+
this.authStateChangedCallbacks.add(callback);
|
|
4826
|
+
return () => this.authStateChangedCallbacks.delete(callback);
|
|
4827
|
+
}
|
|
4828
|
+
// ============================================
|
|
4829
|
+
// Authentication Management
|
|
4830
|
+
// ============================================
|
|
4831
|
+
/**
|
|
4832
|
+
* Sign in with a new auth token while connected.
|
|
4833
|
+
* Changes the authenticated user without disconnecting.
|
|
4834
|
+
*
|
|
4835
|
+
* Note: Some subscriptions may be revoked if the new user doesn't have
|
|
4836
|
+
* permission. Listen for 'permission_denied' errors on your subscriptions.
|
|
4837
|
+
*
|
|
4838
|
+
* @param token - The auth token for the new user
|
|
4839
|
+
* @throws Error if not connected (must call connect() first)
|
|
4840
|
+
*/
|
|
4841
|
+
async signIn(token) {
|
|
4842
|
+
if (this._state !== "authenticated" && this._state !== "joined") {
|
|
4843
|
+
throw new LarkError("not_connected", "Must be connected first - call connect()");
|
|
4844
|
+
}
|
|
4845
|
+
const authRequestId = this.messageQueue.nextRequestId();
|
|
4846
|
+
const authMessage = {
|
|
4847
|
+
o: "au",
|
|
4848
|
+
t: token,
|
|
4849
|
+
r: authRequestId
|
|
4850
|
+
};
|
|
4851
|
+
this.send(authMessage);
|
|
4852
|
+
const authResponse = await this.messageQueue.registerRequest(authRequestId);
|
|
4853
|
+
this._currentToken = token;
|
|
4854
|
+
this._isAnonymous = false;
|
|
4855
|
+
this._auth = {
|
|
4856
|
+
uid: authResponse.uid || "",
|
|
4857
|
+
provider: "custom",
|
|
4858
|
+
token: {}
|
|
4859
|
+
};
|
|
4860
|
+
this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
|
|
4861
|
+
}
|
|
4862
|
+
/**
|
|
4863
|
+
* Sign out the current user.
|
|
4864
|
+
* Reverts to anonymous authentication.
|
|
4865
|
+
*
|
|
4866
|
+
* Note: Some subscriptions may be revoked if anonymous users don't have
|
|
4867
|
+
* permission. Listen for 'permission_denied' errors on your subscriptions.
|
|
4868
|
+
*/
|
|
4869
|
+
async signOut() {
|
|
4870
|
+
if (this._state !== "authenticated") {
|
|
4871
|
+
return;
|
|
4872
|
+
}
|
|
4873
|
+
const unauthRequestId = this.messageQueue.nextRequestId();
|
|
4874
|
+
const unauthMessage = {
|
|
4875
|
+
o: "ua",
|
|
4876
|
+
r: unauthRequestId
|
|
4877
|
+
};
|
|
4878
|
+
this.send(unauthMessage);
|
|
4879
|
+
const authResponse = await this.messageQueue.registerRequest(unauthRequestId);
|
|
4880
|
+
this._currentToken = "";
|
|
4881
|
+
this._isAnonymous = true;
|
|
4882
|
+
this._auth = {
|
|
4883
|
+
uid: authResponse.uid || "",
|
|
4884
|
+
provider: "anonymous",
|
|
4885
|
+
token: {}
|
|
4886
|
+
};
|
|
4887
|
+
this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
|
|
4888
|
+
}
|
|
4524
4889
|
// ============================================
|
|
4525
4890
|
// Internal: Message Handling
|
|
4526
4891
|
// ============================================
|
|
@@ -4532,6 +4897,9 @@ var LarkDatabase = class {
|
|
|
4532
4897
|
console.error("Failed to parse message:", data);
|
|
4533
4898
|
return;
|
|
4534
4899
|
}
|
|
4900
|
+
if (process.env.LARK_DEBUG) {
|
|
4901
|
+
console.log("[LARK] <<< SERVER:", JSON.stringify(message, null, 2));
|
|
4902
|
+
}
|
|
4535
4903
|
if (isPingMessage(message)) {
|
|
4536
4904
|
this.transport?.send(JSON.stringify({ o: "po" }));
|
|
4537
4905
|
return;
|
|
@@ -4541,6 +4909,12 @@ var LarkDatabase = class {
|
|
|
4541
4909
|
this.subscriptionManager.clearPendingWrite(message.a);
|
|
4542
4910
|
} else if (isNackMessage(message)) {
|
|
4543
4911
|
this.pendingWrites.onNack(message.n);
|
|
4912
|
+
if (message.e === "permission_denied" && message.sp) {
|
|
4913
|
+
const path = message.sp;
|
|
4914
|
+
console.warn(`Subscription revoked at ${path}: permission_denied`);
|
|
4915
|
+
this.subscriptionManager.handleSubscriptionRevoked(path);
|
|
4916
|
+
return;
|
|
4917
|
+
}
|
|
4544
4918
|
if (message.e !== "condition_failed") {
|
|
4545
4919
|
console.error(`Write failed (${message.e}): ${message.m || message.e}`);
|
|
4546
4920
|
}
|
|
@@ -4557,27 +4931,28 @@ var LarkDatabase = class {
|
|
|
4557
4931
|
if (this._state === "disconnected") {
|
|
4558
4932
|
return;
|
|
4559
4933
|
}
|
|
4560
|
-
const
|
|
4934
|
+
const wasAuthenticated = this._state === "authenticated";
|
|
4561
4935
|
const wasReconnecting = this._state === "reconnecting";
|
|
4936
|
+
const wasPartiallyConnected = this._state === "connected" || this._state === "joined";
|
|
4562
4937
|
if (this._intentionalDisconnect) {
|
|
4563
4938
|
this.cleanupFull();
|
|
4564
|
-
if (
|
|
4939
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4565
4940
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4566
4941
|
}
|
|
4567
4942
|
return;
|
|
4568
4943
|
}
|
|
4569
4944
|
const canReconnect = this._databaseId && this._connectOptions;
|
|
4570
|
-
if ((
|
|
4945
|
+
if ((wasAuthenticated || wasPartiallyConnected || wasReconnecting) && canReconnect) {
|
|
4571
4946
|
this._state = "reconnecting";
|
|
4572
4947
|
this.cleanupForReconnect();
|
|
4573
4948
|
this.reconnectingCallbacks.forEach((cb) => cb());
|
|
4574
|
-
if (
|
|
4949
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4575
4950
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4576
4951
|
}
|
|
4577
4952
|
this.scheduleReconnect();
|
|
4578
4953
|
} else {
|
|
4579
4954
|
this.cleanupFull();
|
|
4580
|
-
if (
|
|
4955
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4581
4956
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4582
4957
|
}
|
|
4583
4958
|
}
|
|
@@ -4588,10 +4963,48 @@ var LarkDatabase = class {
|
|
|
4588
4963
|
// ============================================
|
|
4589
4964
|
// Internal: Sending Messages
|
|
4590
4965
|
// ============================================
|
|
4966
|
+
/**
|
|
4967
|
+
* Check if authenticated synchronously.
|
|
4968
|
+
* Returns true if authenticated, false if connecting (should wait), throws if disconnected.
|
|
4969
|
+
*/
|
|
4970
|
+
isAuthenticatedOrThrow() {
|
|
4971
|
+
if (this._state === "authenticated") {
|
|
4972
|
+
return true;
|
|
4973
|
+
}
|
|
4974
|
+
if (this._state === "connecting" || this._state === "connected" || this._state === "joined" || this._state === "reconnecting") {
|
|
4975
|
+
return false;
|
|
4976
|
+
}
|
|
4977
|
+
throw new LarkError("not_connected", "Not connected - call connect() first");
|
|
4978
|
+
}
|
|
4979
|
+
/**
|
|
4980
|
+
* Wait for authentication to complete before performing an operation.
|
|
4981
|
+
* If already authenticated, returns immediately (synchronously).
|
|
4982
|
+
* If connecting/reconnecting, waits for auth to complete.
|
|
4983
|
+
* If disconnected and no connect in progress, throws.
|
|
4984
|
+
*
|
|
4985
|
+
* IMPORTANT: This returns a Promise only if waiting is needed.
|
|
4986
|
+
* Callers should use: `if (!this.isAuthenticatedOrThrow()) if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();`
|
|
4987
|
+
* to preserve synchronous execution when already authenticated.
|
|
4988
|
+
*/
|
|
4989
|
+
async waitForAuthenticated() {
|
|
4990
|
+
if (this._state === "authenticated") {
|
|
4991
|
+
return;
|
|
4992
|
+
}
|
|
4993
|
+
if (this._state === "connecting" || this._state === "connected" || this._state === "joined" || this._state === "reconnecting") {
|
|
4994
|
+
if (this.authenticationPromise) {
|
|
4995
|
+
await this.authenticationPromise;
|
|
4996
|
+
return;
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
throw new LarkError("not_connected", "Not connected - call connect() first");
|
|
5000
|
+
}
|
|
4591
5001
|
send(message) {
|
|
4592
5002
|
if (!this.transport || !this.transport.connected) {
|
|
4593
5003
|
throw new LarkError("not_connected", "Not connected to database");
|
|
4594
5004
|
}
|
|
5005
|
+
if (process.env.LARK_DEBUG) {
|
|
5006
|
+
console.log("[LARK] >>> CLIENT:", JSON.stringify(message, null, 2));
|
|
5007
|
+
}
|
|
4595
5008
|
this.transport.send(JSON.stringify(message));
|
|
4596
5009
|
}
|
|
4597
5010
|
/**
|
|
@@ -4599,6 +5012,7 @@ var LarkDatabase = class {
|
|
|
4599
5012
|
* Note: Priority is now part of the value (as .priority), not a separate field.
|
|
4600
5013
|
*/
|
|
4601
5014
|
async _sendSet(path, value) {
|
|
5015
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4602
5016
|
const normalizedPath = normalizePath(path) || "/";
|
|
4603
5017
|
validateWriteData(value, normalizedPath);
|
|
4604
5018
|
const requestId = this.messageQueue.nextRequestId();
|
|
@@ -4623,6 +5037,7 @@ var LarkDatabase = class {
|
|
|
4623
5037
|
* @internal Send an update operation.
|
|
4624
5038
|
*/
|
|
4625
5039
|
async _sendUpdate(path, values) {
|
|
5040
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4626
5041
|
const normalizedPath = normalizePath(path) || "/";
|
|
4627
5042
|
for (const [key, value] of Object.entries(values)) {
|
|
4628
5043
|
const fullPath = key.startsWith("/") ? key : `${normalizedPath}/${key}`;
|
|
@@ -4654,6 +5069,7 @@ var LarkDatabase = class {
|
|
|
4654
5069
|
* @internal Send a delete operation.
|
|
4655
5070
|
*/
|
|
4656
5071
|
async _sendDelete(path) {
|
|
5072
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4657
5073
|
const normalizedPath = normalizePath(path) || "/";
|
|
4658
5074
|
const requestId = this.messageQueue.nextRequestId();
|
|
4659
5075
|
const pendingWriteIds = this.subscriptionManager.getPendingWriteIdsForPath(normalizedPath);
|
|
@@ -4689,7 +5105,7 @@ var LarkDatabase = class {
|
|
|
4689
5105
|
_sendVolatileSet(path, value) {
|
|
4690
5106
|
const normalizedPath = normalizePath(path) || "/";
|
|
4691
5107
|
this.subscriptionManager.applyOptimisticWrite(normalizedPath, value, "", "set");
|
|
4692
|
-
if (!this.transport || !this.transport.connected) {
|
|
5108
|
+
if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
|
|
4693
5109
|
return;
|
|
4694
5110
|
}
|
|
4695
5111
|
const message = {
|
|
@@ -4705,7 +5121,7 @@ var LarkDatabase = class {
|
|
|
4705
5121
|
_sendVolatileUpdate(path, values) {
|
|
4706
5122
|
const normalizedPath = normalizePath(path) || "/";
|
|
4707
5123
|
this.subscriptionManager.applyOptimisticWrite(normalizedPath, values, "", "update");
|
|
4708
|
-
if (!this.transport || !this.transport.connected) {
|
|
5124
|
+
if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
|
|
4709
5125
|
return;
|
|
4710
5126
|
}
|
|
4711
5127
|
const message = {
|
|
@@ -4721,7 +5137,7 @@ var LarkDatabase = class {
|
|
|
4721
5137
|
_sendVolatileDelete(path) {
|
|
4722
5138
|
const normalizedPath = normalizePath(path) || "/";
|
|
4723
5139
|
this.subscriptionManager.applyOptimisticWrite(normalizedPath, null, "", "delete");
|
|
4724
|
-
if (!this.transport || !this.transport.connected) {
|
|
5140
|
+
if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
|
|
4725
5141
|
return;
|
|
4726
5142
|
}
|
|
4727
5143
|
const message = {
|
|
@@ -4760,6 +5176,7 @@ var LarkDatabase = class {
|
|
|
4760
5176
|
return new DataSnapshot(cached.value, path, this);
|
|
4761
5177
|
}
|
|
4762
5178
|
}
|
|
5179
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4763
5180
|
const requestId = this.messageQueue.nextRequestId();
|
|
4764
5181
|
const message = {
|
|
4765
5182
|
o: "o",
|
|
@@ -4776,6 +5193,7 @@ var LarkDatabase = class {
|
|
|
4776
5193
|
* @internal Send an onDisconnect operation.
|
|
4777
5194
|
*/
|
|
4778
5195
|
async _sendOnDisconnect(path, action, value) {
|
|
5196
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4779
5197
|
const requestId = this.messageQueue.nextRequestId();
|
|
4780
5198
|
const message = {
|
|
4781
5199
|
o: "od",
|
|
@@ -4789,11 +5207,20 @@ var LarkDatabase = class {
|
|
|
4789
5207
|
this.send(message);
|
|
4790
5208
|
await this.messageQueue.registerRequest(requestId);
|
|
4791
5209
|
}
|
|
5210
|
+
/**
|
|
5211
|
+
* @internal Get a cached value from the subscription manager.
|
|
5212
|
+
* Used for optimistic writes where we need the current value without a network fetch.
|
|
5213
|
+
*/
|
|
5214
|
+
_getCachedValue(path) {
|
|
5215
|
+
const normalizedPath = normalizePath(path) || "/";
|
|
5216
|
+
return this.subscriptionManager.getCachedValue(normalizedPath);
|
|
5217
|
+
}
|
|
4792
5218
|
/**
|
|
4793
5219
|
* @internal Send a subscribe message to server.
|
|
4794
5220
|
* Includes tag for non-default queries to enable proper event routing.
|
|
4795
5221
|
*/
|
|
4796
5222
|
async sendSubscribeMessage(path, eventTypes, queryParams, tag) {
|
|
5223
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4797
5224
|
const requestId = this.messageQueue.nextRequestId();
|
|
4798
5225
|
const message = {
|
|
4799
5226
|
o: "sb",
|
|
@@ -4811,6 +5238,7 @@ var LarkDatabase = class {
|
|
|
4811
5238
|
* Includes query params and tag so server can identify which specific subscription to remove.
|
|
4812
5239
|
*/
|
|
4813
5240
|
async sendUnsubscribeMessage(path, queryParams, tag) {
|
|
5241
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4814
5242
|
const requestId = this.messageQueue.nextRequestId();
|
|
4815
5243
|
const message = {
|
|
4816
5244
|
o: "us",
|