@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.mjs
CHANGED
|
@@ -13,7 +13,8 @@ var ErrorCode = {
|
|
|
13
13
|
INVALID_OPERATION: "invalid_operation",
|
|
14
14
|
INTERNAL_ERROR: "internal_error",
|
|
15
15
|
CONDITION_FAILED: "condition_failed",
|
|
16
|
-
INVALID_QUERY: "invalid_query"
|
|
16
|
+
INVALID_QUERY: "invalid_query",
|
|
17
|
+
AUTH_REQUIRED: "auth_required"
|
|
17
18
|
};
|
|
18
19
|
var DEFAULT_COORDINATOR_URL = "https://db.lark.sh";
|
|
19
20
|
|
|
@@ -419,6 +420,9 @@ function isAckMessage(msg) {
|
|
|
419
420
|
function isJoinCompleteMessage(msg) {
|
|
420
421
|
return "jc" in msg;
|
|
421
422
|
}
|
|
423
|
+
function isAuthCompleteMessage(msg) {
|
|
424
|
+
return "ac" in msg;
|
|
425
|
+
}
|
|
422
426
|
function isNackMessage(msg) {
|
|
423
427
|
return "n" in msg;
|
|
424
428
|
}
|
|
@@ -484,6 +488,18 @@ var MessageQueue = class {
|
|
|
484
488
|
return true;
|
|
485
489
|
}
|
|
486
490
|
}
|
|
491
|
+
if (isAuthCompleteMessage(message)) {
|
|
492
|
+
const pending = this.pending.get(message.ac);
|
|
493
|
+
if (pending) {
|
|
494
|
+
clearTimeout(pending.timeout);
|
|
495
|
+
this.pending.delete(message.ac);
|
|
496
|
+
const response = {
|
|
497
|
+
uid: message.au || null
|
|
498
|
+
};
|
|
499
|
+
pending.resolve(response);
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
487
503
|
if (isAckMessage(message)) {
|
|
488
504
|
const pending = this.pending.get(message.a);
|
|
489
505
|
if (pending) {
|
|
@@ -507,7 +523,7 @@ var MessageQueue = class {
|
|
|
507
523
|
if (pending) {
|
|
508
524
|
clearTimeout(pending.timeout);
|
|
509
525
|
this.pending.delete(message.oc);
|
|
510
|
-
pending.resolve(message.ov);
|
|
526
|
+
pending.resolve(message.ov ?? null);
|
|
511
527
|
return true;
|
|
512
528
|
}
|
|
513
529
|
}
|
|
@@ -805,7 +821,7 @@ function getNestedValue(obj, path) {
|
|
|
805
821
|
}
|
|
806
822
|
function getSortValue(value, queryParams) {
|
|
807
823
|
if (!queryParams) {
|
|
808
|
-
return
|
|
824
|
+
return getNestedValue(value, ".priority");
|
|
809
825
|
}
|
|
810
826
|
if (queryParams.orderBy === "priority") {
|
|
811
827
|
return getNestedValue(value, ".priority");
|
|
@@ -820,13 +836,14 @@ function getSortValue(value, queryParams) {
|
|
|
820
836
|
return null;
|
|
821
837
|
}
|
|
822
838
|
const hasRangeFilter = queryParams.startAt !== void 0 || queryParams.startAfter !== void 0 || queryParams.endAt !== void 0 || queryParams.endBefore !== void 0 || queryParams.equalTo !== void 0;
|
|
823
|
-
|
|
839
|
+
const hasLimit = queryParams.limitToFirst !== void 0 || queryParams.limitToLast !== void 0;
|
|
840
|
+
if (hasRangeFilter || hasLimit) {
|
|
824
841
|
return getNestedValue(value, ".priority");
|
|
825
842
|
}
|
|
826
843
|
return null;
|
|
827
844
|
}
|
|
828
845
|
function compareEntries(a, b, queryParams) {
|
|
829
|
-
if (
|
|
846
|
+
if (queryParams?.orderBy === "key") {
|
|
830
847
|
return compareKeys(a.key, b.key);
|
|
831
848
|
}
|
|
832
849
|
const cmp = compareValues(a.sortValue, b.sortValue);
|
|
@@ -1284,7 +1301,7 @@ var View = class {
|
|
|
1284
1301
|
return { value: displayCache, found: true };
|
|
1285
1302
|
}
|
|
1286
1303
|
if (this._hasReceivedInitialSnapshot || this._pendingWriteData.length > 0) {
|
|
1287
|
-
return { value: displayCache, found: true };
|
|
1304
|
+
return { value: displayCache ?? null, found: true };
|
|
1288
1305
|
}
|
|
1289
1306
|
return { value: void 0, found: false };
|
|
1290
1307
|
}
|
|
@@ -1292,7 +1309,7 @@ var View = class {
|
|
|
1292
1309
|
const relativePath = this.path === "/" ? normalized : normalized.slice(this.path.length);
|
|
1293
1310
|
if (this._hasReceivedInitialSnapshot || this._pendingWriteData.length > 0) {
|
|
1294
1311
|
const extractedValue = getValueAtPath(displayCache, relativePath);
|
|
1295
|
-
return { value: extractedValue, found: true };
|
|
1312
|
+
return { value: extractedValue ?? null, found: true };
|
|
1296
1313
|
}
|
|
1297
1314
|
}
|
|
1298
1315
|
return { value: void 0, found: false };
|
|
@@ -1385,6 +1402,17 @@ var View = class {
|
|
|
1385
1402
|
if (!this.queryParams) return false;
|
|
1386
1403
|
return !!(this.queryParams.limitToFirst || this.queryParams.limitToLast);
|
|
1387
1404
|
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Check if this View loads all data (no limits, no range filters).
|
|
1407
|
+
* A View that loads all data can serve as a "complete" cache for child paths.
|
|
1408
|
+
* This matches Firebase's loadsAllData() semantics.
|
|
1409
|
+
*/
|
|
1410
|
+
loadsAllData() {
|
|
1411
|
+
if (!this.queryParams) return true;
|
|
1412
|
+
const hasLimit = !!(this.queryParams.limitToFirst || this.queryParams.limitToLast);
|
|
1413
|
+
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;
|
|
1414
|
+
return !hasLimit && !hasRangeFilter;
|
|
1415
|
+
}
|
|
1388
1416
|
// ============================================
|
|
1389
1417
|
// Pending Writes (for local-first)
|
|
1390
1418
|
// ============================================
|
|
@@ -1557,10 +1585,13 @@ var SubscriptionManager = class {
|
|
|
1557
1585
|
this.unsubscribeCallback(normalizedPath, eventType, callback, queryId);
|
|
1558
1586
|
};
|
|
1559
1587
|
if (isNewView || isNewEventType || queryParamsChanged) {
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1588
|
+
const hasAncestorComplete = this.hasAncestorCompleteView(normalizedPath);
|
|
1589
|
+
if (!hasAncestorComplete) {
|
|
1590
|
+
const allEventTypes = view.getEventTypes();
|
|
1591
|
+
this.sendSubscribe?.(normalizedPath, allEventTypes, view.queryParams ?? void 0, tag).catch((err) => {
|
|
1592
|
+
console.error("Failed to subscribe:", err);
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1564
1595
|
}
|
|
1565
1596
|
if (!isNewView && view.hasReceivedInitialSnapshot) {
|
|
1566
1597
|
this.fireInitialEventsToCallback(view, eventType, callback);
|
|
@@ -1788,18 +1819,23 @@ var SubscriptionManager = class {
|
|
|
1788
1819
|
}
|
|
1789
1820
|
}
|
|
1790
1821
|
/**
|
|
1791
|
-
* Detect and fire child_moved events for children that changed position.
|
|
1822
|
+
* Detect and fire child_moved events for children that changed position OR sort value.
|
|
1823
|
+
*
|
|
1824
|
+
* Firebase fires child_moved for ANY priority/sort value change, regardless of whether
|
|
1825
|
+
* the position actually changes. This is Case 2003 behavior.
|
|
1792
1826
|
*
|
|
1793
|
-
* IMPORTANT: child_moved should only fire for children whose VALUE changed
|
|
1794
|
-
*
|
|
1795
|
-
* another child moving should NOT fire child_moved.
|
|
1827
|
+
* IMPORTANT: child_moved should only fire for children whose VALUE or PRIORITY changed.
|
|
1828
|
+
* Children that are merely "displaced" by another child moving should NOT fire child_moved.
|
|
1796
1829
|
*
|
|
1797
1830
|
* @param affectedChildren - Only check these children for moves. If not provided,
|
|
1798
1831
|
* checks all children (for full snapshots where we compare values).
|
|
1832
|
+
* @param previousDisplayCache - Previous display cache for comparing sort values
|
|
1833
|
+
* @param currentDisplayCache - Current display cache for comparing sort values
|
|
1799
1834
|
*/
|
|
1800
|
-
detectAndFireMoves(view, previousOrder, currentOrder, previousPositions, currentPositions, previousChildSet, currentChildSet, childMovedSubs, isVolatile, serverTimestamp, affectedChildren) {
|
|
1835
|
+
detectAndFireMoves(view, previousOrder, currentOrder, previousPositions, currentPositions, previousChildSet, currentChildSet, childMovedSubs, isVolatile, serverTimestamp, affectedChildren, previousDisplayCache, currentDisplayCache) {
|
|
1801
1836
|
if (childMovedSubs.length === 0) return;
|
|
1802
1837
|
const childrenToCheck = affectedChildren ?? new Set(currentOrder);
|
|
1838
|
+
const queryParams = view.queryParams;
|
|
1803
1839
|
for (const key of childrenToCheck) {
|
|
1804
1840
|
if (!previousChildSet.has(key) || !currentChildSet.has(key)) {
|
|
1805
1841
|
continue;
|
|
@@ -1811,7 +1847,24 @@ var SubscriptionManager = class {
|
|
|
1811
1847
|
}
|
|
1812
1848
|
const oldPrevKey = oldPos > 0 ? previousOrder[oldPos - 1] : null;
|
|
1813
1849
|
const newPrevKey = newPos > 0 ? currentOrder[newPos - 1] : null;
|
|
1814
|
-
|
|
1850
|
+
let positionChanged = oldPrevKey !== newPrevKey;
|
|
1851
|
+
let sortValueChanged = false;
|
|
1852
|
+
let isPriorityOrdering = false;
|
|
1853
|
+
if (previousDisplayCache && currentDisplayCache) {
|
|
1854
|
+
const prevValue = previousDisplayCache[key];
|
|
1855
|
+
const currValue = currentDisplayCache[key];
|
|
1856
|
+
const prevSortValue = getSortValue(prevValue, queryParams);
|
|
1857
|
+
const currSortValue = getSortValue(currValue, queryParams);
|
|
1858
|
+
sortValueChanged = JSON.stringify(prevSortValue) !== JSON.stringify(currSortValue);
|
|
1859
|
+
isPriorityOrdering = !queryParams?.orderBy || queryParams.orderBy === "priority";
|
|
1860
|
+
}
|
|
1861
|
+
let shouldFire;
|
|
1862
|
+
if (affectedChildren) {
|
|
1863
|
+
shouldFire = positionChanged || isPriorityOrdering && sortValueChanged;
|
|
1864
|
+
} else {
|
|
1865
|
+
shouldFire = isPriorityOrdering && sortValueChanged;
|
|
1866
|
+
}
|
|
1867
|
+
if (shouldFire) {
|
|
1815
1868
|
this.fireChildMoved(view, key, childMovedSubs, newPrevKey, isVolatile, serverTimestamp);
|
|
1816
1869
|
}
|
|
1817
1870
|
}
|
|
@@ -1855,10 +1908,10 @@ var SubscriptionManager = class {
|
|
|
1855
1908
|
/**
|
|
1856
1909
|
* Fire child_removed callbacks for a child key.
|
|
1857
1910
|
*/
|
|
1858
|
-
fireChildRemoved(view, childKey, subs, isVolatile, serverTimestamp) {
|
|
1911
|
+
fireChildRemoved(view, childKey, subs, isVolatile, serverTimestamp, previousValue) {
|
|
1859
1912
|
if (subs.length === 0) return;
|
|
1860
1913
|
const childPath = joinPath(view.path, childKey);
|
|
1861
|
-
const snapshot = this.createSnapshot?.(childPath, null, isVolatile, serverTimestamp);
|
|
1914
|
+
const snapshot = this.createSnapshot?.(childPath, previousValue ?? null, isVolatile, serverTimestamp);
|
|
1862
1915
|
if (snapshot) {
|
|
1863
1916
|
for (const entry of subs) {
|
|
1864
1917
|
try {
|
|
@@ -1887,6 +1940,30 @@ var SubscriptionManager = class {
|
|
|
1887
1940
|
}
|
|
1888
1941
|
}
|
|
1889
1942
|
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Handle subscription revocation due to auth change.
|
|
1945
|
+
* The server has already removed the subscription, so we just clean up locally.
|
|
1946
|
+
* This is different from unsubscribeAll which sends an unsubscribe message.
|
|
1947
|
+
*/
|
|
1948
|
+
handleSubscriptionRevoked(path) {
|
|
1949
|
+
const normalizedPath = path;
|
|
1950
|
+
const queryIds = this.pathToQueryIds.get(normalizedPath);
|
|
1951
|
+
if (!queryIds || queryIds.size === 0) return;
|
|
1952
|
+
for (const queryId of queryIds) {
|
|
1953
|
+
const viewKey = this.makeViewKey(normalizedPath, queryId);
|
|
1954
|
+
const view = this.views.get(viewKey);
|
|
1955
|
+
if (view) {
|
|
1956
|
+
const tag = this.viewKeyToTag.get(viewKey);
|
|
1957
|
+
if (tag !== void 0) {
|
|
1958
|
+
this.tagToViewKey.delete(tag);
|
|
1959
|
+
this.viewKeyToTag.delete(viewKey);
|
|
1960
|
+
}
|
|
1961
|
+
view.clear();
|
|
1962
|
+
this.views.delete(viewKey);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
this.pathToQueryIds.delete(normalizedPath);
|
|
1966
|
+
}
|
|
1890
1967
|
/**
|
|
1891
1968
|
* Clear all subscriptions (e.g., on disconnect).
|
|
1892
1969
|
*/
|
|
@@ -1908,35 +1985,71 @@ var SubscriptionManager = class {
|
|
|
1908
1985
|
return queryIds !== void 0 && queryIds.size > 0;
|
|
1909
1986
|
}
|
|
1910
1987
|
/**
|
|
1911
|
-
* Check if a path is "covered" by an active subscription.
|
|
1988
|
+
* Check if a path is "covered" by an active subscription that has received data.
|
|
1912
1989
|
*
|
|
1913
1990
|
* A path is covered if:
|
|
1914
|
-
* - There's an active
|
|
1915
|
-
* - There's an active
|
|
1991
|
+
* - There's an active subscription at that exact path that has data, OR
|
|
1992
|
+
* - There's an active subscription at an ancestor path that has data
|
|
1993
|
+
*
|
|
1994
|
+
* Note: Any subscription type (value, child_added, child_moved, etc.) receives
|
|
1995
|
+
* the initial snapshot from the server and thus has cached data.
|
|
1916
1996
|
*/
|
|
1917
1997
|
isPathCovered(path) {
|
|
1918
1998
|
const normalized = normalizePath(path);
|
|
1919
|
-
if (this.
|
|
1999
|
+
if (this.hasActiveSubscriptionWithData(normalized)) {
|
|
1920
2000
|
return true;
|
|
1921
2001
|
}
|
|
1922
2002
|
const segments = normalized.split("/").filter((s) => s.length > 0);
|
|
1923
2003
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
1924
2004
|
const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
1925
|
-
if (this.
|
|
2005
|
+
if (this.hasActiveSubscriptionWithData(ancestorPath)) {
|
|
1926
2006
|
return true;
|
|
1927
2007
|
}
|
|
1928
2008
|
}
|
|
1929
|
-
if (normalized !== "/" && this.
|
|
2009
|
+
if (normalized !== "/" && this.hasActiveSubscriptionWithData("/")) {
|
|
1930
2010
|
return true;
|
|
1931
2011
|
}
|
|
1932
2012
|
return false;
|
|
1933
2013
|
}
|
|
1934
2014
|
/**
|
|
1935
|
-
* Check if there's
|
|
2015
|
+
* Check if there's an active subscription at a path that has data.
|
|
2016
|
+
* A View has data if it has received the initial snapshot OR has pending writes.
|
|
2017
|
+
* Any subscription type (value, child_added, child_moved, etc.) counts.
|
|
1936
2018
|
*/
|
|
1937
|
-
|
|
2019
|
+
hasActiveSubscriptionWithData(path) {
|
|
1938
2020
|
const views = this.getViewsAtPath(path);
|
|
1939
|
-
return views.some((view) => view.
|
|
2021
|
+
return views.some((view) => view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites()));
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Check if any ancestor path has a "complete" View (one that loadsAllData).
|
|
2025
|
+
* A complete View has no limits and no range filters, so it contains all data
|
|
2026
|
+
* for its subtree. Child subscriptions can use the ancestor's data instead
|
|
2027
|
+
* of creating their own server subscription.
|
|
2028
|
+
*
|
|
2029
|
+
* This matches Firebase's behavior where child listeners don't need their own
|
|
2030
|
+
* server subscription if an ancestor has an unlimited listener.
|
|
2031
|
+
*/
|
|
2032
|
+
hasAncestorCompleteView(path) {
|
|
2033
|
+
const normalized = normalizePath(path);
|
|
2034
|
+
const segments = normalized.split("/").filter((s) => s.length > 0);
|
|
2035
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
2036
|
+
const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
2037
|
+
const views = this.getViewsAtPath(ancestorPath);
|
|
2038
|
+
for (const view of views) {
|
|
2039
|
+
if (view.hasCallbacks() && view.loadsAllData() && view.hasReceivedInitialSnapshot) {
|
|
2040
|
+
return true;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
if (normalized !== "/") {
|
|
2045
|
+
const rootViews = this.getViewsAtPath("/");
|
|
2046
|
+
for (const view of rootViews) {
|
|
2047
|
+
if (view.hasCallbacks() && view.loadsAllData() && view.hasReceivedInitialSnapshot) {
|
|
2048
|
+
return true;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
return false;
|
|
1940
2053
|
}
|
|
1941
2054
|
/**
|
|
1942
2055
|
* Get a cached value if the path is covered by an active subscription.
|
|
@@ -1959,7 +2072,7 @@ var SubscriptionManager = class {
|
|
|
1959
2072
|
const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
1960
2073
|
const ancestorViews = this.getViewsAtPath(ancestorPath);
|
|
1961
2074
|
for (const view of ancestorViews) {
|
|
1962
|
-
if (view.
|
|
2075
|
+
if (view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites())) {
|
|
1963
2076
|
const result = view.getCacheValue(normalized);
|
|
1964
2077
|
if (result.found) {
|
|
1965
2078
|
return result;
|
|
@@ -1970,7 +2083,7 @@ var SubscriptionManager = class {
|
|
|
1970
2083
|
if (normalized !== "/") {
|
|
1971
2084
|
const rootViews = this.getViewsAtPath("/");
|
|
1972
2085
|
for (const view of rootViews) {
|
|
1973
|
-
if (view.
|
|
2086
|
+
if (view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites())) {
|
|
1974
2087
|
const result = view.getCacheValue(normalized);
|
|
1975
2088
|
if (result.found) {
|
|
1976
2089
|
return result;
|
|
@@ -2079,8 +2192,11 @@ var SubscriptionManager = class {
|
|
|
2079
2192
|
const previousChildSet = new Set(previousOrder);
|
|
2080
2193
|
const isFirstSnapshot = !view.hasReceivedInitialSnapshot && !view.hasPendingWrites();
|
|
2081
2194
|
let previousCacheJson = null;
|
|
2195
|
+
let previousDisplayCache = null;
|
|
2082
2196
|
if (!isVolatile) {
|
|
2083
|
-
|
|
2197
|
+
const cache = view.getDisplayCache();
|
|
2198
|
+
previousDisplayCache = cache && typeof cache === "object" && !Array.isArray(cache) ? cache : null;
|
|
2199
|
+
previousCacheJson = this.serializeCacheForComparison(cache);
|
|
2084
2200
|
}
|
|
2085
2201
|
const affectedChildren = /* @__PURE__ */ new Set();
|
|
2086
2202
|
let isFullSnapshot = false;
|
|
@@ -2138,7 +2254,8 @@ var SubscriptionManager = class {
|
|
|
2138
2254
|
}
|
|
2139
2255
|
for (const key of previousOrder) {
|
|
2140
2256
|
if (!currentChildSet.has(key)) {
|
|
2141
|
-
|
|
2257
|
+
const prevValue = previousDisplayCache?.[key];
|
|
2258
|
+
this.fireChildRemoved(view, key, childRemovedSubs, isVolatile, serverTimestamp, prevValue);
|
|
2142
2259
|
}
|
|
2143
2260
|
}
|
|
2144
2261
|
} else {
|
|
@@ -2149,7 +2266,8 @@ var SubscriptionManager = class {
|
|
|
2149
2266
|
const prevKey = view.getPreviousChildKey(childKey);
|
|
2150
2267
|
this.fireChildAdded(view, childKey, childAddedSubs, prevKey, isVolatile, serverTimestamp);
|
|
2151
2268
|
} else if (wasPresent && !isPresent) {
|
|
2152
|
-
|
|
2269
|
+
const prevValue = previousDisplayCache?.[childKey];
|
|
2270
|
+
this.fireChildRemoved(view, childKey, childRemovedSubs, isVolatile, serverTimestamp, prevValue);
|
|
2153
2271
|
} else if (wasPresent && isPresent) {
|
|
2154
2272
|
const prevKey = view.getPreviousChildKey(childKey);
|
|
2155
2273
|
this.fireChildChanged(view, childKey, childChangedSubs, prevKey, isVolatile, serverTimestamp);
|
|
@@ -2160,6 +2278,8 @@ var SubscriptionManager = class {
|
|
|
2160
2278
|
previousOrder.forEach((key, idx) => previousPositions.set(key, idx));
|
|
2161
2279
|
const currentPositions = /* @__PURE__ */ new Map();
|
|
2162
2280
|
currentOrder.forEach((key, idx) => currentPositions.set(key, idx));
|
|
2281
|
+
const currentCache = view.getDisplayCache();
|
|
2282
|
+
const currentDisplayCache = currentCache && typeof currentCache === "object" && !Array.isArray(currentCache) ? currentCache : null;
|
|
2163
2283
|
this.detectAndFireMoves(
|
|
2164
2284
|
view,
|
|
2165
2285
|
previousOrder,
|
|
@@ -2171,7 +2291,9 @@ var SubscriptionManager = class {
|
|
|
2171
2291
|
childMovedSubs,
|
|
2172
2292
|
isVolatile,
|
|
2173
2293
|
serverTimestamp,
|
|
2174
|
-
isFullSnapshot ? void 0 : affectedChildren
|
|
2294
|
+
isFullSnapshot ? void 0 : affectedChildren,
|
|
2295
|
+
previousDisplayCache,
|
|
2296
|
+
currentDisplayCache
|
|
2175
2297
|
);
|
|
2176
2298
|
}
|
|
2177
2299
|
// ============================================
|
|
@@ -2190,8 +2312,12 @@ var SubscriptionManager = class {
|
|
|
2190
2312
|
views.push(view);
|
|
2191
2313
|
} else if (normalized.startsWith(viewPath + "/")) {
|
|
2192
2314
|
views.push(view);
|
|
2315
|
+
} else if (viewPath.startsWith(normalized + "/")) {
|
|
2316
|
+
views.push(view);
|
|
2193
2317
|
} else if (viewPath === "/") {
|
|
2194
2318
|
views.push(view);
|
|
2319
|
+
} else if (normalized === "/") {
|
|
2320
|
+
views.push(view);
|
|
2195
2321
|
}
|
|
2196
2322
|
}
|
|
2197
2323
|
return views;
|
|
@@ -2255,7 +2381,48 @@ var SubscriptionManager = class {
|
|
|
2255
2381
|
}
|
|
2256
2382
|
}
|
|
2257
2383
|
}
|
|
2258
|
-
this.
|
|
2384
|
+
this.fireChildEventsForAck(view, previousOrder, previousChildSet, currentOrder, currentChildSet, previousDisplayCache);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
/**
|
|
2388
|
+
* Fire child events for ACK handling, skipping child_moved.
|
|
2389
|
+
* This is a variant of fireChildEvents that doesn't fire moves because:
|
|
2390
|
+
* 1. Moves were already fired optimistically
|
|
2391
|
+
* 2. If server modifies data, PUT event will fire correct moves
|
|
2392
|
+
* 3. ACK can arrive before PUT, causing incorrect intermediate state
|
|
2393
|
+
*/
|
|
2394
|
+
fireChildEventsForAck(view, previousOrder, previousChildSet, currentOrder, currentChildSet, previousDisplayCache) {
|
|
2395
|
+
const childAddedSubs = view.getCallbacks("child_added");
|
|
2396
|
+
const childChangedSubs = view.getCallbacks("child_changed");
|
|
2397
|
+
const childRemovedSubs = view.getCallbacks("child_removed");
|
|
2398
|
+
if (childAddedSubs.length === 0 && childChangedSubs.length === 0 && childRemovedSubs.length === 0) {
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
const displayCache = view.getDisplayCache();
|
|
2402
|
+
for (const key of currentOrder) {
|
|
2403
|
+
if (!previousChildSet.has(key)) {
|
|
2404
|
+
if (childAddedSubs.length > 0 && displayCache) {
|
|
2405
|
+
const prevKey = view.getPreviousChildKey(key);
|
|
2406
|
+
this.fireChildAdded(view, key, childAddedSubs, prevKey, false, void 0);
|
|
2407
|
+
}
|
|
2408
|
+
} else if (previousDisplayCache && childChangedSubs.length > 0 && displayCache) {
|
|
2409
|
+
const prevValue = previousDisplayCache[key];
|
|
2410
|
+
const currentValue = displayCache[key];
|
|
2411
|
+
const prevJson = this.serializeCacheForComparison(prevValue);
|
|
2412
|
+
const currJson = this.serializeCacheForComparison(currentValue);
|
|
2413
|
+
if (prevJson !== currJson) {
|
|
2414
|
+
const prevKey = view.getPreviousChildKey(key);
|
|
2415
|
+
this.fireChildChanged(view, key, childChangedSubs, prevKey, false, void 0);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
for (const key of previousOrder) {
|
|
2420
|
+
if (!currentChildSet.has(key)) {
|
|
2421
|
+
if (childRemovedSubs.length > 0) {
|
|
2422
|
+
const prevValue = previousDisplayCache?.[key];
|
|
2423
|
+
this.fireChildRemoved(view, key, childRemovedSubs, false, void 0, prevValue);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2259
2426
|
}
|
|
2260
2427
|
}
|
|
2261
2428
|
/**
|
|
@@ -2324,18 +2491,28 @@ var SubscriptionManager = class {
|
|
|
2324
2491
|
const updatedViews = [];
|
|
2325
2492
|
for (const view of affectedViews) {
|
|
2326
2493
|
let relativePath;
|
|
2494
|
+
let effectiveValue = value;
|
|
2327
2495
|
if (normalized === view.path) {
|
|
2328
2496
|
relativePath = "/";
|
|
2329
2497
|
} else if (view.path === "/") {
|
|
2330
2498
|
relativePath = normalized;
|
|
2331
|
-
} else {
|
|
2499
|
+
} else if (normalized.startsWith(view.path + "/")) {
|
|
2332
2500
|
relativePath = normalized.slice(view.path.length);
|
|
2501
|
+
} else if (view.path.startsWith(normalized + "/")) {
|
|
2502
|
+
const pathDiff = view.path.slice(normalized.length);
|
|
2503
|
+
effectiveValue = getValueAtPath(value, pathDiff);
|
|
2504
|
+
if (operation === "update" && effectiveValue === void 0) {
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
relativePath = "/";
|
|
2508
|
+
} else {
|
|
2509
|
+
continue;
|
|
2333
2510
|
}
|
|
2334
2511
|
const previousDisplayCache = view.getDisplayCache();
|
|
2335
2512
|
const previousOrder = view.orderedChildren;
|
|
2336
2513
|
const previousChildSet = new Set(previousOrder);
|
|
2337
2514
|
const previousCacheJson = this.serializeCacheForComparison(previousDisplayCache);
|
|
2338
|
-
view.addPendingWriteData(requestId, relativePath,
|
|
2515
|
+
view.addPendingWriteData(requestId, relativePath, effectiveValue, operation);
|
|
2339
2516
|
const currentOrder = view.orderedChildren;
|
|
2340
2517
|
const currentChildSet = new Set(currentOrder);
|
|
2341
2518
|
const currentCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
|
|
@@ -2432,7 +2609,8 @@ var SubscriptionManager = class {
|
|
|
2432
2609
|
for (const key of previousOrder) {
|
|
2433
2610
|
if (!currentChildSet.has(key)) {
|
|
2434
2611
|
if (childRemovedSubs.length > 0) {
|
|
2435
|
-
const
|
|
2612
|
+
const prevValue = previousDisplayCache?.[key];
|
|
2613
|
+
const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue ?? null, isVolatile, serverTimestamp);
|
|
2436
2614
|
if (snapshot) {
|
|
2437
2615
|
for (const entry of childRemovedSubs) {
|
|
2438
2616
|
try {
|
|
@@ -2446,6 +2624,23 @@ var SubscriptionManager = class {
|
|
|
2446
2624
|
}
|
|
2447
2625
|
}
|
|
2448
2626
|
} else {
|
|
2627
|
+
if (childRemovedSubs.length > 0) {
|
|
2628
|
+
for (const key of previousOrder) {
|
|
2629
|
+
if (affectedChildren.has(key)) continue;
|
|
2630
|
+
if (currentChildSet.has(key)) continue;
|
|
2631
|
+
const prevValue = previousDisplayCache ? previousDisplayCache[key] : null;
|
|
2632
|
+
const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue, isVolatile, serverTimestamp);
|
|
2633
|
+
if (snapshot) {
|
|
2634
|
+
for (const entry of childRemovedSubs) {
|
|
2635
|
+
try {
|
|
2636
|
+
entry.callback(snapshot, void 0);
|
|
2637
|
+
} catch (err) {
|
|
2638
|
+
console.error("Error in child_removed callback:", err);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2449
2644
|
for (const key of affectedChildren) {
|
|
2450
2645
|
const wasPresent = previousChildSet.has(key);
|
|
2451
2646
|
const isPresent = currentChildSet.has(key);
|
|
@@ -2471,7 +2666,8 @@ var SubscriptionManager = class {
|
|
|
2471
2666
|
}
|
|
2472
2667
|
} else if (wasPresent && !isPresent) {
|
|
2473
2668
|
if (childRemovedSubs.length > 0) {
|
|
2474
|
-
const
|
|
2669
|
+
const prevValue = previousDisplayCache ? previousDisplayCache[key] : null;
|
|
2670
|
+
const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue, isVolatile, serverTimestamp);
|
|
2475
2671
|
if (snapshot) {
|
|
2476
2672
|
for (const entry of childRemovedSubs) {
|
|
2477
2673
|
try {
|
|
@@ -2504,18 +2700,24 @@ var SubscriptionManager = class {
|
|
|
2504
2700
|
}
|
|
2505
2701
|
}
|
|
2506
2702
|
}
|
|
2507
|
-
if (
|
|
2508
|
-
for (const key of
|
|
2703
|
+
if (childAddedSubs.length > 0 && displayCache) {
|
|
2704
|
+
for (const key of currentOrder) {
|
|
2705
|
+
if (previousChildSet.has(key)) continue;
|
|
2509
2706
|
if (affectedChildren.has(key)) continue;
|
|
2510
|
-
|
|
2511
|
-
const
|
|
2512
|
-
|
|
2707
|
+
const childValue = displayCache[key];
|
|
2708
|
+
const snapshot = this.createSnapshot?.(
|
|
2709
|
+
joinPath(view.path, key),
|
|
2710
|
+
childValue,
|
|
2711
|
+
isVolatile,
|
|
2712
|
+
serverTimestamp
|
|
2713
|
+
);
|
|
2513
2714
|
if (snapshot) {
|
|
2514
|
-
|
|
2715
|
+
const prevKey = view.getPreviousChildKey(key);
|
|
2716
|
+
for (const entry of childAddedSubs) {
|
|
2515
2717
|
try {
|
|
2516
|
-
entry.callback(snapshot,
|
|
2718
|
+
entry.callback(snapshot, prevKey);
|
|
2517
2719
|
} catch (err) {
|
|
2518
|
-
console.error("Error in
|
|
2720
|
+
console.error("Error in child_added callback:", err);
|
|
2519
2721
|
}
|
|
2520
2722
|
}
|
|
2521
2723
|
}
|
|
@@ -2538,7 +2740,9 @@ var SubscriptionManager = class {
|
|
|
2538
2740
|
childMovedSubs,
|
|
2539
2741
|
isVolatile,
|
|
2540
2742
|
serverTimestamp,
|
|
2541
|
-
isFullSnapshot ? void 0 : affectedChildren
|
|
2743
|
+
isFullSnapshot ? void 0 : affectedChildren,
|
|
2744
|
+
previousDisplayCache,
|
|
2745
|
+
displayCache
|
|
2542
2746
|
);
|
|
2543
2747
|
}
|
|
2544
2748
|
}
|
|
@@ -2761,26 +2965,26 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
2761
2965
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX] = this._query.orderByChildPath;
|
|
2762
2966
|
}
|
|
2763
2967
|
if (this._query.startAt !== void 0) {
|
|
2764
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAt.value;
|
|
2968
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAt.value ?? null;
|
|
2765
2969
|
if (this._query.startAt.key !== void 0) {
|
|
2766
2970
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.startAt.key;
|
|
2767
2971
|
}
|
|
2768
2972
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = true;
|
|
2769
2973
|
} else if (this._query.startAfter !== void 0) {
|
|
2770
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAfter.value;
|
|
2974
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAfter.value ?? null;
|
|
2771
2975
|
if (this._query.startAfter.key !== void 0) {
|
|
2772
2976
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.startAfter.key;
|
|
2773
2977
|
}
|
|
2774
2978
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = false;
|
|
2775
2979
|
}
|
|
2776
2980
|
if (this._query.endAt !== void 0) {
|
|
2777
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endAt.value;
|
|
2981
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endAt.value ?? null;
|
|
2778
2982
|
if (this._query.endAt.key !== void 0) {
|
|
2779
2983
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.endAt.key;
|
|
2780
2984
|
}
|
|
2781
2985
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_IS_INCLUSIVE] = true;
|
|
2782
2986
|
} else if (this._query.endBefore !== void 0) {
|
|
2783
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endBefore.value;
|
|
2987
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endBefore.value ?? null;
|
|
2784
2988
|
if (this._query.endBefore.key !== void 0) {
|
|
2785
2989
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.endBefore.key;
|
|
2786
2990
|
}
|
|
@@ -2939,17 +3143,39 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
2939
3143
|
}
|
|
2940
3144
|
/**
|
|
2941
3145
|
* Set the priority of the data at this location.
|
|
2942
|
-
*
|
|
3146
|
+
* Uses cached value for optimistic behavior (local effects are immediate).
|
|
3147
|
+
* The optimistic update happens synchronously, Promise resolves after server ack.
|
|
2943
3148
|
*/
|
|
2944
|
-
|
|
3149
|
+
setPriority(priority) {
|
|
2945
3150
|
validateNotInfoPath(this._path, "setPriority");
|
|
2946
|
-
const
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
3151
|
+
const { value: cachedValue, found } = this._db._getCachedValue(this._path);
|
|
3152
|
+
if (!found) {
|
|
3153
|
+
return this.once().then((snapshot) => {
|
|
3154
|
+
const actualValue2 = snapshot.val();
|
|
3155
|
+
if (actualValue2 === null || actualValue2 === void 0) {
|
|
3156
|
+
return this._db._sendSet(this._path, { ".priority": priority });
|
|
3157
|
+
}
|
|
3158
|
+
return this.setWithPriority(actualValue2, priority);
|
|
3159
|
+
});
|
|
2951
3160
|
}
|
|
2952
|
-
|
|
3161
|
+
let actualValue;
|
|
3162
|
+
if (cachedValue === null || cachedValue === void 0) {
|
|
3163
|
+
actualValue = null;
|
|
3164
|
+
} else if (typeof cachedValue === "object" && !Array.isArray(cachedValue)) {
|
|
3165
|
+
const obj = cachedValue;
|
|
3166
|
+
if (".value" in obj && Object.keys(obj).every((k) => k === ".value" || k === ".priority")) {
|
|
3167
|
+
actualValue = obj[".value"];
|
|
3168
|
+
} else {
|
|
3169
|
+
const { ".priority": _oldPriority, ...rest } = obj;
|
|
3170
|
+
actualValue = Object.keys(rest).length > 0 ? rest : null;
|
|
3171
|
+
}
|
|
3172
|
+
} else {
|
|
3173
|
+
actualValue = cachedValue;
|
|
3174
|
+
}
|
|
3175
|
+
if (actualValue === null || actualValue === void 0) {
|
|
3176
|
+
return this._db._sendSet(this._path, { ".priority": priority });
|
|
3177
|
+
}
|
|
3178
|
+
return this.setWithPriority(actualValue, priority);
|
|
2953
3179
|
}
|
|
2954
3180
|
/**
|
|
2955
3181
|
* Atomically modify the data at this location using optimistic concurrency.
|
|
@@ -3020,14 +3246,23 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3020
3246
|
/**
|
|
3021
3247
|
* Read the data at this location once.
|
|
3022
3248
|
*
|
|
3023
|
-
*
|
|
3249
|
+
* For 'value' events, this fetches data directly from the server.
|
|
3250
|
+
* For child events ('child_added', 'child_changed', 'child_removed', 'child_moved'),
|
|
3251
|
+
* this subscribes, waits for the first event, then unsubscribes.
|
|
3252
|
+
*
|
|
3253
|
+
* @param eventType - The event type
|
|
3024
3254
|
* @returns Promise that resolves to the DataSnapshot
|
|
3025
3255
|
*/
|
|
3026
3256
|
once(eventType = "value") {
|
|
3027
|
-
if (eventType
|
|
3028
|
-
|
|
3257
|
+
if (eventType === "value") {
|
|
3258
|
+
return this._db._sendOnce(this._path, this._buildQueryParams());
|
|
3029
3259
|
}
|
|
3030
|
-
return
|
|
3260
|
+
return new Promise((resolve) => {
|
|
3261
|
+
const unsubscribe = this.on(eventType, (snapshot) => {
|
|
3262
|
+
unsubscribe();
|
|
3263
|
+
resolve(snapshot);
|
|
3264
|
+
});
|
|
3265
|
+
});
|
|
3031
3266
|
}
|
|
3032
3267
|
// ============================================
|
|
3033
3268
|
// Subscriptions
|
|
@@ -3096,6 +3331,12 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3096
3331
|
*/
|
|
3097
3332
|
orderByChild(path) {
|
|
3098
3333
|
this._validateNoOrderBy("orderByChild");
|
|
3334
|
+
if (path.startsWith("$") || path.includes("/$")) {
|
|
3335
|
+
throw new LarkError(
|
|
3336
|
+
ErrorCode.INVALID_PATH,
|
|
3337
|
+
`orderByChild: Invalid path '${path}'. Paths cannot contain '$' prefix (reserved for internal use)`
|
|
3338
|
+
);
|
|
3339
|
+
}
|
|
3099
3340
|
return new _DatabaseReference(this._db, this._path, {
|
|
3100
3341
|
...this._query,
|
|
3101
3342
|
orderBy: "child",
|
|
@@ -3469,35 +3710,35 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3469
3710
|
hasParams = true;
|
|
3470
3711
|
}
|
|
3471
3712
|
if (this._query.startAt !== void 0) {
|
|
3472
|
-
params.startAt = this._query.startAt.value;
|
|
3713
|
+
params.startAt = this._query.startAt.value ?? null;
|
|
3473
3714
|
if (this._query.startAt.key !== void 0) {
|
|
3474
3715
|
params.startAtKey = this._query.startAt.key;
|
|
3475
3716
|
}
|
|
3476
3717
|
hasParams = true;
|
|
3477
3718
|
}
|
|
3478
3719
|
if (this._query.startAfter !== void 0) {
|
|
3479
|
-
params.startAfter = this._query.startAfter.value;
|
|
3720
|
+
params.startAfter = this._query.startAfter.value ?? null;
|
|
3480
3721
|
if (this._query.startAfter.key !== void 0) {
|
|
3481
3722
|
params.startAfterKey = this._query.startAfter.key;
|
|
3482
3723
|
}
|
|
3483
3724
|
hasParams = true;
|
|
3484
3725
|
}
|
|
3485
3726
|
if (this._query.endAt !== void 0) {
|
|
3486
|
-
params.endAt = this._query.endAt.value;
|
|
3727
|
+
params.endAt = this._query.endAt.value ?? null;
|
|
3487
3728
|
if (this._query.endAt.key !== void 0) {
|
|
3488
3729
|
params.endAtKey = this._query.endAt.key;
|
|
3489
3730
|
}
|
|
3490
3731
|
hasParams = true;
|
|
3491
3732
|
}
|
|
3492
3733
|
if (this._query.endBefore !== void 0) {
|
|
3493
|
-
params.endBefore = this._query.endBefore.value;
|
|
3734
|
+
params.endBefore = this._query.endBefore.value ?? null;
|
|
3494
3735
|
if (this._query.endBefore.key !== void 0) {
|
|
3495
3736
|
params.endBeforeKey = this._query.endBefore.key;
|
|
3496
3737
|
}
|
|
3497
3738
|
hasParams = true;
|
|
3498
3739
|
}
|
|
3499
3740
|
if (this._query.equalTo !== void 0) {
|
|
3500
|
-
params.equalTo = this._query.equalTo.value;
|
|
3741
|
+
params.equalTo = this._query.equalTo.value ?? null;
|
|
3501
3742
|
if (this._query.equalTo.key !== void 0) {
|
|
3502
3743
|
params.equalToKey = this._query.equalTo.key;
|
|
3503
3744
|
}
|
|
@@ -3516,6 +3757,13 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3516
3757
|
}
|
|
3517
3758
|
return `${baseUrl}${this._path}`;
|
|
3518
3759
|
}
|
|
3760
|
+
/**
|
|
3761
|
+
* Returns the URL for JSON serialization.
|
|
3762
|
+
* This allows refs to be serialized with JSON.stringify().
|
|
3763
|
+
*/
|
|
3764
|
+
toJSON() {
|
|
3765
|
+
return this.toString();
|
|
3766
|
+
}
|
|
3519
3767
|
};
|
|
3520
3768
|
var ThenableReference = class extends DatabaseReference {
|
|
3521
3769
|
constructor(db, path, promise) {
|
|
@@ -3536,11 +3784,17 @@ function isWrappedPrimitive(data) {
|
|
|
3536
3784
|
return false;
|
|
3537
3785
|
}
|
|
3538
3786
|
const keys = Object.keys(data);
|
|
3539
|
-
|
|
3787
|
+
if (keys.length === 2 && ".value" in data && ".priority" in data) {
|
|
3788
|
+
return true;
|
|
3789
|
+
}
|
|
3790
|
+
if (keys.length === 1 && ".value" in data) {
|
|
3791
|
+
return true;
|
|
3792
|
+
}
|
|
3793
|
+
return false;
|
|
3540
3794
|
}
|
|
3541
3795
|
function stripPriorityMetadata(data) {
|
|
3542
3796
|
if (data === null || data === void 0) {
|
|
3543
|
-
return
|
|
3797
|
+
return null;
|
|
3544
3798
|
}
|
|
3545
3799
|
if (typeof data !== "object") {
|
|
3546
3800
|
return data;
|
|
@@ -3603,9 +3857,19 @@ var DataSnapshot = class _DataSnapshot {
|
|
|
3603
3857
|
}
|
|
3604
3858
|
/**
|
|
3605
3859
|
* Check if data exists at this location (is not null/undefined).
|
|
3860
|
+
* Returns false for priority-only nodes (only .priority, no actual value).
|
|
3606
3861
|
*/
|
|
3607
3862
|
exists() {
|
|
3608
|
-
|
|
3863
|
+
if (this._data === null || this._data === void 0) {
|
|
3864
|
+
return false;
|
|
3865
|
+
}
|
|
3866
|
+
if (typeof this._data === "object" && !Array.isArray(this._data)) {
|
|
3867
|
+
const keys = Object.keys(this._data);
|
|
3868
|
+
if (keys.length === 1 && keys[0] === ".priority") {
|
|
3869
|
+
return false;
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
return true;
|
|
3609
3873
|
}
|
|
3610
3874
|
/**
|
|
3611
3875
|
* Get a child snapshot at the specified path.
|
|
@@ -3752,24 +4016,6 @@ var DataSnapshot = class _DataSnapshot {
|
|
|
3752
4016
|
}
|
|
3753
4017
|
};
|
|
3754
4018
|
|
|
3755
|
-
// src/utils/jwt.ts
|
|
3756
|
-
function decodeJwtPayload(token) {
|
|
3757
|
-
const parts = token.split(".");
|
|
3758
|
-
if (parts.length !== 3) {
|
|
3759
|
-
throw new Error("Invalid JWT format");
|
|
3760
|
-
}
|
|
3761
|
-
const payload = parts[1];
|
|
3762
|
-
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
3763
|
-
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
3764
|
-
let decoded;
|
|
3765
|
-
if (typeof atob === "function") {
|
|
3766
|
-
decoded = atob(padded);
|
|
3767
|
-
} else {
|
|
3768
|
-
decoded = Buffer.from(padded, "base64").toString("utf-8");
|
|
3769
|
-
}
|
|
3770
|
-
return JSON.parse(decoded);
|
|
3771
|
-
}
|
|
3772
|
-
|
|
3773
4019
|
// src/utils/volatile.ts
|
|
3774
4020
|
function isVolatilePath(path, patterns) {
|
|
3775
4021
|
if (!patterns || patterns.length === 0) {
|
|
@@ -3892,6 +4138,11 @@ var LarkDatabase = class {
|
|
|
3892
4138
|
this._coordinatorUrl = null;
|
|
3893
4139
|
this._volatilePaths = [];
|
|
3894
4140
|
this._transportType = null;
|
|
4141
|
+
// Auth state
|
|
4142
|
+
this._currentToken = null;
|
|
4143
|
+
// Token for auth (empty string = anonymous)
|
|
4144
|
+
this._isAnonymous = false;
|
|
4145
|
+
// True if connected anonymously
|
|
3895
4146
|
// Reconnection state
|
|
3896
4147
|
this._connectionId = null;
|
|
3897
4148
|
this._connectOptions = null;
|
|
@@ -3904,8 +4155,12 @@ var LarkDatabase = class {
|
|
|
3904
4155
|
this.disconnectCallbacks = /* @__PURE__ */ new Set();
|
|
3905
4156
|
this.errorCallbacks = /* @__PURE__ */ new Set();
|
|
3906
4157
|
this.reconnectingCallbacks = /* @__PURE__ */ new Set();
|
|
4158
|
+
this.authStateChangedCallbacks = /* @__PURE__ */ new Set();
|
|
3907
4159
|
// .info path subscriptions (handled locally, not sent to server)
|
|
3908
4160
|
this.infoSubscriptions = [];
|
|
4161
|
+
// Authentication promise - resolves when auth completes, allows operations to queue
|
|
4162
|
+
this.authenticationPromise = null;
|
|
4163
|
+
this.authenticationResolve = null;
|
|
3909
4164
|
this._serverTimeOffset = 0;
|
|
3910
4165
|
this.messageQueue = new MessageQueue();
|
|
3911
4166
|
this.subscriptionManager = new SubscriptionManager();
|
|
@@ -3915,10 +4170,11 @@ var LarkDatabase = class {
|
|
|
3915
4170
|
// Connection State
|
|
3916
4171
|
// ============================================
|
|
3917
4172
|
/**
|
|
3918
|
-
* Whether the database is
|
|
4173
|
+
* Whether the database is fully connected and authenticated.
|
|
4174
|
+
* Returns true when ready to perform database operations.
|
|
3919
4175
|
*/
|
|
3920
4176
|
get connected() {
|
|
3921
|
-
return this._state === "
|
|
4177
|
+
return this._state === "authenticated";
|
|
3922
4178
|
}
|
|
3923
4179
|
/**
|
|
3924
4180
|
* Whether the database is currently attempting to reconnect.
|
|
@@ -4003,16 +4259,27 @@ var LarkDatabase = class {
|
|
|
4003
4259
|
}
|
|
4004
4260
|
this._connectOptions = options;
|
|
4005
4261
|
this._intentionalDisconnect = false;
|
|
4262
|
+
this.authenticationPromise = new Promise((resolve) => {
|
|
4263
|
+
this.authenticationResolve = resolve;
|
|
4264
|
+
});
|
|
4006
4265
|
await this.performConnect(databaseId, options);
|
|
4007
4266
|
}
|
|
4008
4267
|
/**
|
|
4009
4268
|
* Internal connect implementation used by both initial connect and reconnect.
|
|
4269
|
+
* Implements the Join → Auth flow:
|
|
4270
|
+
* 1. Connect WebSocket
|
|
4271
|
+
* 2. Send join (identifies database)
|
|
4272
|
+
* 3. Send auth (authenticates user - required even for anonymous)
|
|
4010
4273
|
*/
|
|
4011
4274
|
async performConnect(databaseId, options, isReconnect = false) {
|
|
4012
4275
|
const previousState = this._state;
|
|
4013
4276
|
this._state = isReconnect ? "reconnecting" : "connecting";
|
|
4014
4277
|
this._databaseId = databaseId;
|
|
4015
4278
|
this._coordinatorUrl = options.coordinator || DEFAULT_COORDINATOR_URL;
|
|
4279
|
+
if (!isReconnect) {
|
|
4280
|
+
this._currentToken = options.token || "";
|
|
4281
|
+
this._isAnonymous = !options.token && options.anonymous !== false;
|
|
4282
|
+
}
|
|
4016
4283
|
try {
|
|
4017
4284
|
const coordinatorUrl = this._coordinatorUrl;
|
|
4018
4285
|
const coordinator = new Coordinator(coordinatorUrl);
|
|
@@ -4038,30 +4305,44 @@ var LarkDatabase = class {
|
|
|
4038
4305
|
);
|
|
4039
4306
|
this.transport = transportResult.transport;
|
|
4040
4307
|
this._transportType = transportResult.type;
|
|
4041
|
-
|
|
4308
|
+
this._state = "connected";
|
|
4309
|
+
const joinRequestId = this.messageQueue.nextRequestId();
|
|
4042
4310
|
const joinMessage = {
|
|
4043
4311
|
o: "j",
|
|
4044
|
-
|
|
4045
|
-
r:
|
|
4312
|
+
d: databaseId,
|
|
4313
|
+
r: joinRequestId
|
|
4046
4314
|
};
|
|
4047
4315
|
if (this._connectionId) {
|
|
4048
4316
|
joinMessage.pcid = this._connectionId;
|
|
4049
4317
|
}
|
|
4050
4318
|
this.send(joinMessage);
|
|
4051
|
-
const joinResponse = await this.messageQueue.registerRequest(
|
|
4319
|
+
const joinResponse = await this.messageQueue.registerRequest(joinRequestId);
|
|
4052
4320
|
this._volatilePaths = joinResponse.volatilePaths;
|
|
4053
4321
|
this._connectionId = joinResponse.connectionId;
|
|
4054
4322
|
if (joinResponse.serverTime != null) {
|
|
4055
4323
|
this._serverTimeOffset = joinResponse.serverTime - Date.now();
|
|
4056
4324
|
}
|
|
4057
|
-
|
|
4325
|
+
this._state = "joined";
|
|
4326
|
+
const authRequestId = this.messageQueue.nextRequestId();
|
|
4327
|
+
const authMessage = {
|
|
4328
|
+
o: "au",
|
|
4329
|
+
t: this._currentToken ?? "",
|
|
4330
|
+
r: authRequestId
|
|
4331
|
+
};
|
|
4332
|
+
this.send(authMessage);
|
|
4333
|
+
const authResponse = await this.messageQueue.registerRequest(authRequestId);
|
|
4058
4334
|
this._auth = {
|
|
4059
|
-
uid:
|
|
4060
|
-
provider:
|
|
4061
|
-
token:
|
|
4335
|
+
uid: authResponse.uid || "",
|
|
4336
|
+
provider: this._isAnonymous ? "anonymous" : "custom",
|
|
4337
|
+
token: {}
|
|
4338
|
+
// Token claims would need to be decoded from the token if needed
|
|
4062
4339
|
};
|
|
4063
|
-
this._state = "
|
|
4340
|
+
this._state = "authenticated";
|
|
4064
4341
|
this._reconnectAttempt = 0;
|
|
4342
|
+
if (this.authenticationResolve) {
|
|
4343
|
+
this.authenticationResolve();
|
|
4344
|
+
this.authenticationResolve = null;
|
|
4345
|
+
}
|
|
4065
4346
|
this.fireConnectionStateChange();
|
|
4066
4347
|
if (!isReconnect) {
|
|
4067
4348
|
this.subscriptionManager.initialize({
|
|
@@ -4074,6 +4355,7 @@ var LarkDatabase = class {
|
|
|
4074
4355
|
await this.restoreAfterReconnect();
|
|
4075
4356
|
}
|
|
4076
4357
|
this.connectCallbacks.forEach((cb) => cb());
|
|
4358
|
+
this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
|
|
4077
4359
|
} catch (error) {
|
|
4078
4360
|
if (isReconnect) {
|
|
4079
4361
|
this._state = "reconnecting";
|
|
@@ -4086,6 +4368,8 @@ var LarkDatabase = class {
|
|
|
4086
4368
|
this._connectOptions = null;
|
|
4087
4369
|
this._connectionId = null;
|
|
4088
4370
|
this._transportType = null;
|
|
4371
|
+
this._currentToken = null;
|
|
4372
|
+
this._isAnonymous = false;
|
|
4089
4373
|
this.transport?.close();
|
|
4090
4374
|
this.transport = null;
|
|
4091
4375
|
throw error;
|
|
@@ -4099,13 +4383,14 @@ var LarkDatabase = class {
|
|
|
4099
4383
|
if (this._state === "disconnected") {
|
|
4100
4384
|
return;
|
|
4101
4385
|
}
|
|
4102
|
-
const
|
|
4386
|
+
const wasAuthenticated = this._state === "authenticated";
|
|
4387
|
+
const wasPartiallyConnected = this._state === "connected" || this._state === "joined";
|
|
4103
4388
|
this._intentionalDisconnect = true;
|
|
4104
4389
|
if (this._reconnectTimer) {
|
|
4105
4390
|
clearTimeout(this._reconnectTimer);
|
|
4106
4391
|
this._reconnectTimer = null;
|
|
4107
4392
|
}
|
|
4108
|
-
if (
|
|
4393
|
+
if ((wasAuthenticated || wasPartiallyConnected) && this.transport) {
|
|
4109
4394
|
try {
|
|
4110
4395
|
const requestId = this.messageQueue.nextRequestId();
|
|
4111
4396
|
this.send({ o: "l", r: requestId });
|
|
@@ -4117,7 +4402,7 @@ var LarkDatabase = class {
|
|
|
4117
4402
|
}
|
|
4118
4403
|
}
|
|
4119
4404
|
this.cleanupFull();
|
|
4120
|
-
if (
|
|
4405
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4121
4406
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4122
4407
|
}
|
|
4123
4408
|
}
|
|
@@ -4126,7 +4411,7 @@ var LarkDatabase = class {
|
|
|
4126
4411
|
* Disconnects from the server but preserves subscriptions for later reconnection via goOnline().
|
|
4127
4412
|
*/
|
|
4128
4413
|
goOffline() {
|
|
4129
|
-
if (this._state === "connected" || this._state === "reconnecting") {
|
|
4414
|
+
if (this._state === "authenticated" || this._state === "joined" || this._state === "connected" || this._state === "reconnecting") {
|
|
4130
4415
|
this._intentionalDisconnect = true;
|
|
4131
4416
|
if (this._reconnectTimer) {
|
|
4132
4417
|
clearTimeout(this._reconnectTimer);
|
|
@@ -4157,7 +4442,7 @@ var LarkDatabase = class {
|
|
|
4157
4442
|
* Used for intentional disconnect.
|
|
4158
4443
|
*/
|
|
4159
4444
|
cleanupFull() {
|
|
4160
|
-
const
|
|
4445
|
+
const wasAuthenticated = this._state === "authenticated";
|
|
4161
4446
|
this.transport?.close();
|
|
4162
4447
|
this.transport = null;
|
|
4163
4448
|
this._state = "disconnected";
|
|
@@ -4168,11 +4453,15 @@ var LarkDatabase = class {
|
|
|
4168
4453
|
this._connectionId = null;
|
|
4169
4454
|
this._connectOptions = null;
|
|
4170
4455
|
this._transportType = null;
|
|
4456
|
+
this._currentToken = null;
|
|
4457
|
+
this._isAnonymous = false;
|
|
4171
4458
|
this._reconnectAttempt = 0;
|
|
4459
|
+
this.authenticationPromise = null;
|
|
4460
|
+
this.authenticationResolve = null;
|
|
4172
4461
|
this.subscriptionManager.clear();
|
|
4173
4462
|
this.messageQueue.rejectAll(new Error("Connection closed"));
|
|
4174
4463
|
this.pendingWrites.clear();
|
|
4175
|
-
if (
|
|
4464
|
+
if (wasAuthenticated) {
|
|
4176
4465
|
this.fireConnectionStateChange();
|
|
4177
4466
|
}
|
|
4178
4467
|
this.infoSubscriptions = [];
|
|
@@ -4205,7 +4494,7 @@ var LarkDatabase = class {
|
|
|
4205
4494
|
getInfoValue(path) {
|
|
4206
4495
|
const normalizedPath = normalizePath(path) || "/";
|
|
4207
4496
|
if (normalizedPath === "/.info/connected") {
|
|
4208
|
-
return this._state === "
|
|
4497
|
+
return this._state === "authenticated";
|
|
4209
4498
|
}
|
|
4210
4499
|
if (normalizedPath === "/.info/serverTimeOffset") {
|
|
4211
4500
|
return this._serverTimeOffset;
|
|
@@ -4272,6 +4561,9 @@ var LarkDatabase = class {
|
|
|
4272
4561
|
if (this._intentionalDisconnect || !this._databaseId || !this._connectOptions) {
|
|
4273
4562
|
return;
|
|
4274
4563
|
}
|
|
4564
|
+
this.authenticationPromise = new Promise((resolve) => {
|
|
4565
|
+
this.authenticationResolve = resolve;
|
|
4566
|
+
});
|
|
4275
4567
|
try {
|
|
4276
4568
|
await this.performConnect(this._databaseId, this._connectOptions, true);
|
|
4277
4569
|
} catch {
|
|
@@ -4430,6 +4722,9 @@ var LarkDatabase = class {
|
|
|
4430
4722
|
* @internal Send a transaction to the server.
|
|
4431
4723
|
*/
|
|
4432
4724
|
async _sendTransaction(ops) {
|
|
4725
|
+
if (!this.isAuthenticatedOrThrow()) {
|
|
4726
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4727
|
+
}
|
|
4433
4728
|
const requestId = this.messageQueue.nextRequestId();
|
|
4434
4729
|
this.pendingWrites.trackWrite(requestId, "transaction", "/", ops);
|
|
4435
4730
|
const message = {
|
|
@@ -4476,6 +4771,76 @@ var LarkDatabase = class {
|
|
|
4476
4771
|
this.reconnectingCallbacks.add(callback);
|
|
4477
4772
|
return () => this.reconnectingCallbacks.delete(callback);
|
|
4478
4773
|
}
|
|
4774
|
+
/**
|
|
4775
|
+
* Register a callback for auth state changes.
|
|
4776
|
+
* Fires when user signs in, signs out, or auth changes.
|
|
4777
|
+
* Returns an unsubscribe function.
|
|
4778
|
+
*/
|
|
4779
|
+
onAuthStateChanged(callback) {
|
|
4780
|
+
this.authStateChangedCallbacks.add(callback);
|
|
4781
|
+
return () => this.authStateChangedCallbacks.delete(callback);
|
|
4782
|
+
}
|
|
4783
|
+
// ============================================
|
|
4784
|
+
// Authentication Management
|
|
4785
|
+
// ============================================
|
|
4786
|
+
/**
|
|
4787
|
+
* Sign in with a new auth token while connected.
|
|
4788
|
+
* Changes the authenticated user without disconnecting.
|
|
4789
|
+
*
|
|
4790
|
+
* Note: Some subscriptions may be revoked if the new user doesn't have
|
|
4791
|
+
* permission. Listen for 'permission_denied' errors on your subscriptions.
|
|
4792
|
+
*
|
|
4793
|
+
* @param token - The auth token for the new user
|
|
4794
|
+
* @throws Error if not connected (must call connect() first)
|
|
4795
|
+
*/
|
|
4796
|
+
async signIn(token) {
|
|
4797
|
+
if (this._state !== "authenticated" && this._state !== "joined") {
|
|
4798
|
+
throw new LarkError("not_connected", "Must be connected first - call connect()");
|
|
4799
|
+
}
|
|
4800
|
+
const authRequestId = this.messageQueue.nextRequestId();
|
|
4801
|
+
const authMessage = {
|
|
4802
|
+
o: "au",
|
|
4803
|
+
t: token,
|
|
4804
|
+
r: authRequestId
|
|
4805
|
+
};
|
|
4806
|
+
this.send(authMessage);
|
|
4807
|
+
const authResponse = await this.messageQueue.registerRequest(authRequestId);
|
|
4808
|
+
this._currentToken = token;
|
|
4809
|
+
this._isAnonymous = false;
|
|
4810
|
+
this._auth = {
|
|
4811
|
+
uid: authResponse.uid || "",
|
|
4812
|
+
provider: "custom",
|
|
4813
|
+
token: {}
|
|
4814
|
+
};
|
|
4815
|
+
this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
|
|
4816
|
+
}
|
|
4817
|
+
/**
|
|
4818
|
+
* Sign out the current user.
|
|
4819
|
+
* Reverts to anonymous authentication.
|
|
4820
|
+
*
|
|
4821
|
+
* Note: Some subscriptions may be revoked if anonymous users don't have
|
|
4822
|
+
* permission. Listen for 'permission_denied' errors on your subscriptions.
|
|
4823
|
+
*/
|
|
4824
|
+
async signOut() {
|
|
4825
|
+
if (this._state !== "authenticated") {
|
|
4826
|
+
return;
|
|
4827
|
+
}
|
|
4828
|
+
const unauthRequestId = this.messageQueue.nextRequestId();
|
|
4829
|
+
const unauthMessage = {
|
|
4830
|
+
o: "ua",
|
|
4831
|
+
r: unauthRequestId
|
|
4832
|
+
};
|
|
4833
|
+
this.send(unauthMessage);
|
|
4834
|
+
const authResponse = await this.messageQueue.registerRequest(unauthRequestId);
|
|
4835
|
+
this._currentToken = "";
|
|
4836
|
+
this._isAnonymous = true;
|
|
4837
|
+
this._auth = {
|
|
4838
|
+
uid: authResponse.uid || "",
|
|
4839
|
+
provider: "anonymous",
|
|
4840
|
+
token: {}
|
|
4841
|
+
};
|
|
4842
|
+
this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
|
|
4843
|
+
}
|
|
4479
4844
|
// ============================================
|
|
4480
4845
|
// Internal: Message Handling
|
|
4481
4846
|
// ============================================
|
|
@@ -4487,6 +4852,9 @@ var LarkDatabase = class {
|
|
|
4487
4852
|
console.error("Failed to parse message:", data);
|
|
4488
4853
|
return;
|
|
4489
4854
|
}
|
|
4855
|
+
if (process.env.LARK_DEBUG) {
|
|
4856
|
+
console.log("[LARK] <<< SERVER:", JSON.stringify(message, null, 2));
|
|
4857
|
+
}
|
|
4490
4858
|
if (isPingMessage(message)) {
|
|
4491
4859
|
this.transport?.send(JSON.stringify({ o: "po" }));
|
|
4492
4860
|
return;
|
|
@@ -4496,6 +4864,12 @@ var LarkDatabase = class {
|
|
|
4496
4864
|
this.subscriptionManager.clearPendingWrite(message.a);
|
|
4497
4865
|
} else if (isNackMessage(message)) {
|
|
4498
4866
|
this.pendingWrites.onNack(message.n);
|
|
4867
|
+
if (message.e === "permission_denied" && message.sp) {
|
|
4868
|
+
const path = message.sp;
|
|
4869
|
+
console.warn(`Subscription revoked at ${path}: permission_denied`);
|
|
4870
|
+
this.subscriptionManager.handleSubscriptionRevoked(path);
|
|
4871
|
+
return;
|
|
4872
|
+
}
|
|
4499
4873
|
if (message.e !== "condition_failed") {
|
|
4500
4874
|
console.error(`Write failed (${message.e}): ${message.m || message.e}`);
|
|
4501
4875
|
}
|
|
@@ -4512,27 +4886,28 @@ var LarkDatabase = class {
|
|
|
4512
4886
|
if (this._state === "disconnected") {
|
|
4513
4887
|
return;
|
|
4514
4888
|
}
|
|
4515
|
-
const
|
|
4889
|
+
const wasAuthenticated = this._state === "authenticated";
|
|
4516
4890
|
const wasReconnecting = this._state === "reconnecting";
|
|
4891
|
+
const wasPartiallyConnected = this._state === "connected" || this._state === "joined";
|
|
4517
4892
|
if (this._intentionalDisconnect) {
|
|
4518
4893
|
this.cleanupFull();
|
|
4519
|
-
if (
|
|
4894
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4520
4895
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4521
4896
|
}
|
|
4522
4897
|
return;
|
|
4523
4898
|
}
|
|
4524
4899
|
const canReconnect = this._databaseId && this._connectOptions;
|
|
4525
|
-
if ((
|
|
4900
|
+
if ((wasAuthenticated || wasPartiallyConnected || wasReconnecting) && canReconnect) {
|
|
4526
4901
|
this._state = "reconnecting";
|
|
4527
4902
|
this.cleanupForReconnect();
|
|
4528
4903
|
this.reconnectingCallbacks.forEach((cb) => cb());
|
|
4529
|
-
if (
|
|
4904
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4530
4905
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4531
4906
|
}
|
|
4532
4907
|
this.scheduleReconnect();
|
|
4533
4908
|
} else {
|
|
4534
4909
|
this.cleanupFull();
|
|
4535
|
-
if (
|
|
4910
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4536
4911
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4537
4912
|
}
|
|
4538
4913
|
}
|
|
@@ -4543,10 +4918,48 @@ var LarkDatabase = class {
|
|
|
4543
4918
|
// ============================================
|
|
4544
4919
|
// Internal: Sending Messages
|
|
4545
4920
|
// ============================================
|
|
4921
|
+
/**
|
|
4922
|
+
* Check if authenticated synchronously.
|
|
4923
|
+
* Returns true if authenticated, false if connecting (should wait), throws if disconnected.
|
|
4924
|
+
*/
|
|
4925
|
+
isAuthenticatedOrThrow() {
|
|
4926
|
+
if (this._state === "authenticated") {
|
|
4927
|
+
return true;
|
|
4928
|
+
}
|
|
4929
|
+
if (this._state === "connecting" || this._state === "connected" || this._state === "joined" || this._state === "reconnecting") {
|
|
4930
|
+
return false;
|
|
4931
|
+
}
|
|
4932
|
+
throw new LarkError("not_connected", "Not connected - call connect() first");
|
|
4933
|
+
}
|
|
4934
|
+
/**
|
|
4935
|
+
* Wait for authentication to complete before performing an operation.
|
|
4936
|
+
* If already authenticated, returns immediately (synchronously).
|
|
4937
|
+
* If connecting/reconnecting, waits for auth to complete.
|
|
4938
|
+
* If disconnected and no connect in progress, throws.
|
|
4939
|
+
*
|
|
4940
|
+
* IMPORTANT: This returns a Promise only if waiting is needed.
|
|
4941
|
+
* Callers should use: `if (!this.isAuthenticatedOrThrow()) if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();`
|
|
4942
|
+
* to preserve synchronous execution when already authenticated.
|
|
4943
|
+
*/
|
|
4944
|
+
async waitForAuthenticated() {
|
|
4945
|
+
if (this._state === "authenticated") {
|
|
4946
|
+
return;
|
|
4947
|
+
}
|
|
4948
|
+
if (this._state === "connecting" || this._state === "connected" || this._state === "joined" || this._state === "reconnecting") {
|
|
4949
|
+
if (this.authenticationPromise) {
|
|
4950
|
+
await this.authenticationPromise;
|
|
4951
|
+
return;
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
throw new LarkError("not_connected", "Not connected - call connect() first");
|
|
4955
|
+
}
|
|
4546
4956
|
send(message) {
|
|
4547
4957
|
if (!this.transport || !this.transport.connected) {
|
|
4548
4958
|
throw new LarkError("not_connected", "Not connected to database");
|
|
4549
4959
|
}
|
|
4960
|
+
if (process.env.LARK_DEBUG) {
|
|
4961
|
+
console.log("[LARK] >>> CLIENT:", JSON.stringify(message, null, 2));
|
|
4962
|
+
}
|
|
4550
4963
|
this.transport.send(JSON.stringify(message));
|
|
4551
4964
|
}
|
|
4552
4965
|
/**
|
|
@@ -4554,6 +4967,7 @@ var LarkDatabase = class {
|
|
|
4554
4967
|
* Note: Priority is now part of the value (as .priority), not a separate field.
|
|
4555
4968
|
*/
|
|
4556
4969
|
async _sendSet(path, value) {
|
|
4970
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4557
4971
|
const normalizedPath = normalizePath(path) || "/";
|
|
4558
4972
|
validateWriteData(value, normalizedPath);
|
|
4559
4973
|
const requestId = this.messageQueue.nextRequestId();
|
|
@@ -4578,6 +4992,7 @@ var LarkDatabase = class {
|
|
|
4578
4992
|
* @internal Send an update operation.
|
|
4579
4993
|
*/
|
|
4580
4994
|
async _sendUpdate(path, values) {
|
|
4995
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4581
4996
|
const normalizedPath = normalizePath(path) || "/";
|
|
4582
4997
|
for (const [key, value] of Object.entries(values)) {
|
|
4583
4998
|
const fullPath = key.startsWith("/") ? key : `${normalizedPath}/${key}`;
|
|
@@ -4609,6 +5024,7 @@ var LarkDatabase = class {
|
|
|
4609
5024
|
* @internal Send a delete operation.
|
|
4610
5025
|
*/
|
|
4611
5026
|
async _sendDelete(path) {
|
|
5027
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4612
5028
|
const normalizedPath = normalizePath(path) || "/";
|
|
4613
5029
|
const requestId = this.messageQueue.nextRequestId();
|
|
4614
5030
|
const pendingWriteIds = this.subscriptionManager.getPendingWriteIdsForPath(normalizedPath);
|
|
@@ -4644,7 +5060,7 @@ var LarkDatabase = class {
|
|
|
4644
5060
|
_sendVolatileSet(path, value) {
|
|
4645
5061
|
const normalizedPath = normalizePath(path) || "/";
|
|
4646
5062
|
this.subscriptionManager.applyOptimisticWrite(normalizedPath, value, "", "set");
|
|
4647
|
-
if (!this.transport || !this.transport.connected) {
|
|
5063
|
+
if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
|
|
4648
5064
|
return;
|
|
4649
5065
|
}
|
|
4650
5066
|
const message = {
|
|
@@ -4660,7 +5076,7 @@ var LarkDatabase = class {
|
|
|
4660
5076
|
_sendVolatileUpdate(path, values) {
|
|
4661
5077
|
const normalizedPath = normalizePath(path) || "/";
|
|
4662
5078
|
this.subscriptionManager.applyOptimisticWrite(normalizedPath, values, "", "update");
|
|
4663
|
-
if (!this.transport || !this.transport.connected) {
|
|
5079
|
+
if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
|
|
4664
5080
|
return;
|
|
4665
5081
|
}
|
|
4666
5082
|
const message = {
|
|
@@ -4676,7 +5092,7 @@ var LarkDatabase = class {
|
|
|
4676
5092
|
_sendVolatileDelete(path) {
|
|
4677
5093
|
const normalizedPath = normalizePath(path) || "/";
|
|
4678
5094
|
this.subscriptionManager.applyOptimisticWrite(normalizedPath, null, "", "delete");
|
|
4679
|
-
if (!this.transport || !this.transport.connected) {
|
|
5095
|
+
if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
|
|
4680
5096
|
return;
|
|
4681
5097
|
}
|
|
4682
5098
|
const message = {
|
|
@@ -4715,6 +5131,7 @@ var LarkDatabase = class {
|
|
|
4715
5131
|
return new DataSnapshot(cached.value, path, this);
|
|
4716
5132
|
}
|
|
4717
5133
|
}
|
|
5134
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4718
5135
|
const requestId = this.messageQueue.nextRequestId();
|
|
4719
5136
|
const message = {
|
|
4720
5137
|
o: "o",
|
|
@@ -4731,6 +5148,7 @@ var LarkDatabase = class {
|
|
|
4731
5148
|
* @internal Send an onDisconnect operation.
|
|
4732
5149
|
*/
|
|
4733
5150
|
async _sendOnDisconnect(path, action, value) {
|
|
5151
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4734
5152
|
const requestId = this.messageQueue.nextRequestId();
|
|
4735
5153
|
const message = {
|
|
4736
5154
|
o: "od",
|
|
@@ -4744,11 +5162,20 @@ var LarkDatabase = class {
|
|
|
4744
5162
|
this.send(message);
|
|
4745
5163
|
await this.messageQueue.registerRequest(requestId);
|
|
4746
5164
|
}
|
|
5165
|
+
/**
|
|
5166
|
+
* @internal Get a cached value from the subscription manager.
|
|
5167
|
+
* Used for optimistic writes where we need the current value without a network fetch.
|
|
5168
|
+
*/
|
|
5169
|
+
_getCachedValue(path) {
|
|
5170
|
+
const normalizedPath = normalizePath(path) || "/";
|
|
5171
|
+
return this.subscriptionManager.getCachedValue(normalizedPath);
|
|
5172
|
+
}
|
|
4747
5173
|
/**
|
|
4748
5174
|
* @internal Send a subscribe message to server.
|
|
4749
5175
|
* Includes tag for non-default queries to enable proper event routing.
|
|
4750
5176
|
*/
|
|
4751
5177
|
async sendSubscribeMessage(path, eventTypes, queryParams, tag) {
|
|
5178
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4752
5179
|
const requestId = this.messageQueue.nextRequestId();
|
|
4753
5180
|
const message = {
|
|
4754
5181
|
o: "sb",
|
|
@@ -4766,6 +5193,7 @@ var LarkDatabase = class {
|
|
|
4766
5193
|
* Includes query params and tag so server can identify which specific subscription to remove.
|
|
4767
5194
|
*/
|
|
4768
5195
|
async sendUnsubscribeMessage(path, queryParams, tag) {
|
|
5196
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4769
5197
|
const requestId = this.messageQueue.nextRequestId();
|
|
4770
5198
|
const message = {
|
|
4771
5199
|
o: "us",
|