@lark-sh/client 0.1.11 → 0.1.13
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 +596 -136
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +596 -136
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
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);
|
|
@@ -868,6 +885,39 @@ function getSortedKeys(data, queryParams) {
|
|
|
868
885
|
}
|
|
869
886
|
|
|
870
887
|
// src/connection/View.ts
|
|
888
|
+
function expandPathKeys(obj) {
|
|
889
|
+
const result = {};
|
|
890
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
891
|
+
if (key.includes("/")) {
|
|
892
|
+
const segments = key.split("/").filter((s) => s.length > 0);
|
|
893
|
+
let current = result;
|
|
894
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
895
|
+
const segment = segments[i];
|
|
896
|
+
if (!(segment in current) || typeof current[segment] !== "object" || current[segment] === null) {
|
|
897
|
+
current[segment] = {};
|
|
898
|
+
}
|
|
899
|
+
current = current[segment];
|
|
900
|
+
}
|
|
901
|
+
current[segments[segments.length - 1]] = value;
|
|
902
|
+
} else {
|
|
903
|
+
result[key] = value;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return result;
|
|
907
|
+
}
|
|
908
|
+
function deepMerge(target, source) {
|
|
909
|
+
const result = { ...target };
|
|
910
|
+
for (const [key, value] of Object.entries(source)) {
|
|
911
|
+
if (value === null) {
|
|
912
|
+
delete result[key];
|
|
913
|
+
} else if (value !== null && typeof value === "object" && !Array.isArray(value) && result[key] !== null && typeof result[key] === "object" && !Array.isArray(result[key])) {
|
|
914
|
+
result[key] = deepMerge(result[key], value);
|
|
915
|
+
} else {
|
|
916
|
+
result[key] = value;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return result;
|
|
920
|
+
}
|
|
871
921
|
var View = class {
|
|
872
922
|
constructor(path, queryParams) {
|
|
873
923
|
/** Event callbacks organized by event type */
|
|
@@ -1172,29 +1222,19 @@ var View = class {
|
|
|
1172
1222
|
return this.setAtPath(base, relativePath, value);
|
|
1173
1223
|
}
|
|
1174
1224
|
if (operation === "update") {
|
|
1225
|
+
const expanded = expandPathKeys(value);
|
|
1175
1226
|
if (relativePath === "/") {
|
|
1176
1227
|
if (base === null || base === void 0 || typeof base !== "object") {
|
|
1177
1228
|
base = {};
|
|
1178
1229
|
}
|
|
1179
|
-
|
|
1180
|
-
for (const key of Object.keys(merged2)) {
|
|
1181
|
-
if (merged2[key] === null) {
|
|
1182
|
-
delete merged2[key];
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
return merged2;
|
|
1230
|
+
return deepMerge(base, expanded);
|
|
1186
1231
|
}
|
|
1187
1232
|
const current = getValueAtPath(base, relativePath);
|
|
1188
1233
|
let merged;
|
|
1189
|
-
if (current && typeof current === "object") {
|
|
1190
|
-
merged =
|
|
1191
|
-
for (const key of Object.keys(merged)) {
|
|
1192
|
-
if (merged[key] === null) {
|
|
1193
|
-
delete merged[key];
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1234
|
+
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
1235
|
+
merged = deepMerge(current, expanded);
|
|
1196
1236
|
} else {
|
|
1197
|
-
merged =
|
|
1237
|
+
merged = expanded;
|
|
1198
1238
|
}
|
|
1199
1239
|
return this.setAtPath(base, relativePath, merged);
|
|
1200
1240
|
}
|
|
@@ -1284,7 +1324,7 @@ var View = class {
|
|
|
1284
1324
|
return { value: displayCache, found: true };
|
|
1285
1325
|
}
|
|
1286
1326
|
if (this._hasReceivedInitialSnapshot || this._pendingWriteData.length > 0) {
|
|
1287
|
-
return { value: displayCache, found: true };
|
|
1327
|
+
return { value: displayCache ?? null, found: true };
|
|
1288
1328
|
}
|
|
1289
1329
|
return { value: void 0, found: false };
|
|
1290
1330
|
}
|
|
@@ -1292,7 +1332,7 @@ var View = class {
|
|
|
1292
1332
|
const relativePath = this.path === "/" ? normalized : normalized.slice(this.path.length);
|
|
1293
1333
|
if (this._hasReceivedInitialSnapshot || this._pendingWriteData.length > 0) {
|
|
1294
1334
|
const extractedValue = getValueAtPath(displayCache, relativePath);
|
|
1295
|
-
return { value: extractedValue, found: true };
|
|
1335
|
+
return { value: extractedValue ?? null, found: true };
|
|
1296
1336
|
}
|
|
1297
1337
|
}
|
|
1298
1338
|
return { value: void 0, found: false };
|
|
@@ -1385,6 +1425,17 @@ var View = class {
|
|
|
1385
1425
|
if (!this.queryParams) return false;
|
|
1386
1426
|
return !!(this.queryParams.limitToFirst || this.queryParams.limitToLast);
|
|
1387
1427
|
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Check if this View loads all data (no limits, no range filters).
|
|
1430
|
+
* A View that loads all data can serve as a "complete" cache for child paths.
|
|
1431
|
+
* This matches Firebase's loadsAllData() semantics.
|
|
1432
|
+
*/
|
|
1433
|
+
loadsAllData() {
|
|
1434
|
+
if (!this.queryParams) return true;
|
|
1435
|
+
const hasLimit = !!(this.queryParams.limitToFirst || this.queryParams.limitToLast);
|
|
1436
|
+
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;
|
|
1437
|
+
return !hasLimit && !hasRangeFilter;
|
|
1438
|
+
}
|
|
1388
1439
|
// ============================================
|
|
1389
1440
|
// Pending Writes (for local-first)
|
|
1390
1441
|
// ============================================
|
|
@@ -1557,10 +1608,13 @@ var SubscriptionManager = class {
|
|
|
1557
1608
|
this.unsubscribeCallback(normalizedPath, eventType, callback, queryId);
|
|
1558
1609
|
};
|
|
1559
1610
|
if (isNewView || isNewEventType || queryParamsChanged) {
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1611
|
+
const hasAncestorComplete = this.hasAncestorCompleteView(normalizedPath);
|
|
1612
|
+
if (!hasAncestorComplete) {
|
|
1613
|
+
const allEventTypes = view.getEventTypes();
|
|
1614
|
+
this.sendSubscribe?.(normalizedPath, allEventTypes, view.queryParams ?? void 0, tag).catch((err) => {
|
|
1615
|
+
console.error("Failed to subscribe:", err);
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1564
1618
|
}
|
|
1565
1619
|
if (!isNewView && view.hasReceivedInitialSnapshot) {
|
|
1566
1620
|
this.fireInitialEventsToCallback(view, eventType, callback);
|
|
@@ -1788,18 +1842,23 @@ var SubscriptionManager = class {
|
|
|
1788
1842
|
}
|
|
1789
1843
|
}
|
|
1790
1844
|
/**
|
|
1791
|
-
* Detect and fire child_moved events for children that changed position.
|
|
1845
|
+
* Detect and fire child_moved events for children that changed position OR sort value.
|
|
1846
|
+
*
|
|
1847
|
+
* Firebase fires child_moved for ANY priority/sort value change, regardless of whether
|
|
1848
|
+
* the position actually changes. This is Case 2003 behavior.
|
|
1792
1849
|
*
|
|
1793
|
-
* IMPORTANT: child_moved should only fire for children whose VALUE changed
|
|
1794
|
-
*
|
|
1795
|
-
* another child moving should NOT fire child_moved.
|
|
1850
|
+
* IMPORTANT: child_moved should only fire for children whose VALUE or PRIORITY changed.
|
|
1851
|
+
* Children that are merely "displaced" by another child moving should NOT fire child_moved.
|
|
1796
1852
|
*
|
|
1797
1853
|
* @param affectedChildren - Only check these children for moves. If not provided,
|
|
1798
1854
|
* checks all children (for full snapshots where we compare values).
|
|
1855
|
+
* @param previousDisplayCache - Previous display cache for comparing sort values
|
|
1856
|
+
* @param currentDisplayCache - Current display cache for comparing sort values
|
|
1799
1857
|
*/
|
|
1800
|
-
detectAndFireMoves(view, previousOrder, currentOrder, previousPositions, currentPositions, previousChildSet, currentChildSet, childMovedSubs, isVolatile, serverTimestamp, affectedChildren) {
|
|
1858
|
+
detectAndFireMoves(view, previousOrder, currentOrder, previousPositions, currentPositions, previousChildSet, currentChildSet, childMovedSubs, isVolatile, serverTimestamp, affectedChildren, previousDisplayCache, currentDisplayCache) {
|
|
1801
1859
|
if (childMovedSubs.length === 0) return;
|
|
1802
1860
|
const childrenToCheck = affectedChildren ?? new Set(currentOrder);
|
|
1861
|
+
const queryParams = view.queryParams;
|
|
1803
1862
|
for (const key of childrenToCheck) {
|
|
1804
1863
|
if (!previousChildSet.has(key) || !currentChildSet.has(key)) {
|
|
1805
1864
|
continue;
|
|
@@ -1811,7 +1870,24 @@ var SubscriptionManager = class {
|
|
|
1811
1870
|
}
|
|
1812
1871
|
const oldPrevKey = oldPos > 0 ? previousOrder[oldPos - 1] : null;
|
|
1813
1872
|
const newPrevKey = newPos > 0 ? currentOrder[newPos - 1] : null;
|
|
1814
|
-
|
|
1873
|
+
let positionChanged = oldPrevKey !== newPrevKey;
|
|
1874
|
+
let sortValueChanged = false;
|
|
1875
|
+
let isPriorityOrdering = false;
|
|
1876
|
+
if (previousDisplayCache && currentDisplayCache) {
|
|
1877
|
+
const prevValue = previousDisplayCache[key];
|
|
1878
|
+
const currValue = currentDisplayCache[key];
|
|
1879
|
+
const prevSortValue = getSortValue(prevValue, queryParams);
|
|
1880
|
+
const currSortValue = getSortValue(currValue, queryParams);
|
|
1881
|
+
sortValueChanged = JSON.stringify(prevSortValue) !== JSON.stringify(currSortValue);
|
|
1882
|
+
isPriorityOrdering = !queryParams?.orderBy || queryParams.orderBy === "priority";
|
|
1883
|
+
}
|
|
1884
|
+
let shouldFire;
|
|
1885
|
+
if (affectedChildren) {
|
|
1886
|
+
shouldFire = positionChanged || isPriorityOrdering && sortValueChanged;
|
|
1887
|
+
} else {
|
|
1888
|
+
shouldFire = isPriorityOrdering && sortValueChanged;
|
|
1889
|
+
}
|
|
1890
|
+
if (shouldFire) {
|
|
1815
1891
|
this.fireChildMoved(view, key, childMovedSubs, newPrevKey, isVolatile, serverTimestamp);
|
|
1816
1892
|
}
|
|
1817
1893
|
}
|
|
@@ -1855,10 +1931,10 @@ var SubscriptionManager = class {
|
|
|
1855
1931
|
/**
|
|
1856
1932
|
* Fire child_removed callbacks for a child key.
|
|
1857
1933
|
*/
|
|
1858
|
-
fireChildRemoved(view, childKey, subs, isVolatile, serverTimestamp) {
|
|
1934
|
+
fireChildRemoved(view, childKey, subs, isVolatile, serverTimestamp, previousValue) {
|
|
1859
1935
|
if (subs.length === 0) return;
|
|
1860
1936
|
const childPath = joinPath(view.path, childKey);
|
|
1861
|
-
const snapshot = this.createSnapshot?.(childPath, null, isVolatile, serverTimestamp);
|
|
1937
|
+
const snapshot = this.createSnapshot?.(childPath, previousValue ?? null, isVolatile, serverTimestamp);
|
|
1862
1938
|
if (snapshot) {
|
|
1863
1939
|
for (const entry of subs) {
|
|
1864
1940
|
try {
|
|
@@ -1887,6 +1963,30 @@ var SubscriptionManager = class {
|
|
|
1887
1963
|
}
|
|
1888
1964
|
}
|
|
1889
1965
|
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Handle subscription revocation due to auth change.
|
|
1968
|
+
* The server has already removed the subscription, so we just clean up locally.
|
|
1969
|
+
* This is different from unsubscribeAll which sends an unsubscribe message.
|
|
1970
|
+
*/
|
|
1971
|
+
handleSubscriptionRevoked(path) {
|
|
1972
|
+
const normalizedPath = path;
|
|
1973
|
+
const queryIds = this.pathToQueryIds.get(normalizedPath);
|
|
1974
|
+
if (!queryIds || queryIds.size === 0) return;
|
|
1975
|
+
for (const queryId of queryIds) {
|
|
1976
|
+
const viewKey = this.makeViewKey(normalizedPath, queryId);
|
|
1977
|
+
const view = this.views.get(viewKey);
|
|
1978
|
+
if (view) {
|
|
1979
|
+
const tag = this.viewKeyToTag.get(viewKey);
|
|
1980
|
+
if (tag !== void 0) {
|
|
1981
|
+
this.tagToViewKey.delete(tag);
|
|
1982
|
+
this.viewKeyToTag.delete(viewKey);
|
|
1983
|
+
}
|
|
1984
|
+
view.clear();
|
|
1985
|
+
this.views.delete(viewKey);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
this.pathToQueryIds.delete(normalizedPath);
|
|
1989
|
+
}
|
|
1890
1990
|
/**
|
|
1891
1991
|
* Clear all subscriptions (e.g., on disconnect).
|
|
1892
1992
|
*/
|
|
@@ -1908,35 +2008,71 @@ var SubscriptionManager = class {
|
|
|
1908
2008
|
return queryIds !== void 0 && queryIds.size > 0;
|
|
1909
2009
|
}
|
|
1910
2010
|
/**
|
|
1911
|
-
* Check if a path is "covered" by an active subscription.
|
|
2011
|
+
* Check if a path is "covered" by an active subscription that has received data.
|
|
1912
2012
|
*
|
|
1913
2013
|
* A path is covered if:
|
|
1914
|
-
* - There's an active
|
|
1915
|
-
* - There's an active
|
|
2014
|
+
* - There's an active subscription at that exact path that has data, OR
|
|
2015
|
+
* - There's an active subscription at an ancestor path that has data
|
|
2016
|
+
*
|
|
2017
|
+
* Note: Any subscription type (value, child_added, child_moved, etc.) receives
|
|
2018
|
+
* the initial snapshot from the server and thus has cached data.
|
|
1916
2019
|
*/
|
|
1917
2020
|
isPathCovered(path) {
|
|
1918
2021
|
const normalized = normalizePath(path);
|
|
1919
|
-
if (this.
|
|
2022
|
+
if (this.hasActiveSubscriptionWithData(normalized)) {
|
|
1920
2023
|
return true;
|
|
1921
2024
|
}
|
|
1922
2025
|
const segments = normalized.split("/").filter((s) => s.length > 0);
|
|
1923
2026
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
1924
2027
|
const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
1925
|
-
if (this.
|
|
2028
|
+
if (this.hasActiveSubscriptionWithData(ancestorPath)) {
|
|
1926
2029
|
return true;
|
|
1927
2030
|
}
|
|
1928
2031
|
}
|
|
1929
|
-
if (normalized !== "/" && this.
|
|
2032
|
+
if (normalized !== "/" && this.hasActiveSubscriptionWithData("/")) {
|
|
1930
2033
|
return true;
|
|
1931
2034
|
}
|
|
1932
2035
|
return false;
|
|
1933
2036
|
}
|
|
1934
2037
|
/**
|
|
1935
|
-
* Check if there's
|
|
2038
|
+
* Check if there's an active subscription at a path that has data.
|
|
2039
|
+
* A View has data if it has received the initial snapshot OR has pending writes.
|
|
2040
|
+
* Any subscription type (value, child_added, child_moved, etc.) counts.
|
|
1936
2041
|
*/
|
|
1937
|
-
|
|
2042
|
+
hasActiveSubscriptionWithData(path) {
|
|
1938
2043
|
const views = this.getViewsAtPath(path);
|
|
1939
|
-
return views.some((view) => view.
|
|
2044
|
+
return views.some((view) => view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites()));
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Check if any ancestor path has a "complete" View (one that loadsAllData).
|
|
2048
|
+
* A complete View has no limits and no range filters, so it contains all data
|
|
2049
|
+
* for its subtree. Child subscriptions can use the ancestor's data instead
|
|
2050
|
+
* of creating their own server subscription.
|
|
2051
|
+
*
|
|
2052
|
+
* This matches Firebase's behavior where child listeners don't need their own
|
|
2053
|
+
* server subscription if an ancestor has an unlimited listener.
|
|
2054
|
+
*/
|
|
2055
|
+
hasAncestorCompleteView(path) {
|
|
2056
|
+
const normalized = normalizePath(path);
|
|
2057
|
+
const segments = normalized.split("/").filter((s) => s.length > 0);
|
|
2058
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
2059
|
+
const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
2060
|
+
const views = this.getViewsAtPath(ancestorPath);
|
|
2061
|
+
for (const view of views) {
|
|
2062
|
+
if (view.hasCallbacks() && view.loadsAllData() && view.hasReceivedInitialSnapshot) {
|
|
2063
|
+
return true;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
if (normalized !== "/") {
|
|
2068
|
+
const rootViews = this.getViewsAtPath("/");
|
|
2069
|
+
for (const view of rootViews) {
|
|
2070
|
+
if (view.hasCallbacks() && view.loadsAllData() && view.hasReceivedInitialSnapshot) {
|
|
2071
|
+
return true;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return false;
|
|
1940
2076
|
}
|
|
1941
2077
|
/**
|
|
1942
2078
|
* Get a cached value if the path is covered by an active subscription.
|
|
@@ -1959,7 +2095,7 @@ var SubscriptionManager = class {
|
|
|
1959
2095
|
const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
1960
2096
|
const ancestorViews = this.getViewsAtPath(ancestorPath);
|
|
1961
2097
|
for (const view of ancestorViews) {
|
|
1962
|
-
if (view.
|
|
2098
|
+
if (view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites())) {
|
|
1963
2099
|
const result = view.getCacheValue(normalized);
|
|
1964
2100
|
if (result.found) {
|
|
1965
2101
|
return result;
|
|
@@ -1970,7 +2106,7 @@ var SubscriptionManager = class {
|
|
|
1970
2106
|
if (normalized !== "/") {
|
|
1971
2107
|
const rootViews = this.getViewsAtPath("/");
|
|
1972
2108
|
for (const view of rootViews) {
|
|
1973
|
-
if (view.
|
|
2109
|
+
if (view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites())) {
|
|
1974
2110
|
const result = view.getCacheValue(normalized);
|
|
1975
2111
|
if (result.found) {
|
|
1976
2112
|
return result;
|
|
@@ -2079,8 +2215,11 @@ var SubscriptionManager = class {
|
|
|
2079
2215
|
const previousChildSet = new Set(previousOrder);
|
|
2080
2216
|
const isFirstSnapshot = !view.hasReceivedInitialSnapshot && !view.hasPendingWrites();
|
|
2081
2217
|
let previousCacheJson = null;
|
|
2218
|
+
let previousDisplayCache = null;
|
|
2082
2219
|
if (!isVolatile) {
|
|
2083
|
-
|
|
2220
|
+
const cache = view.getDisplayCache();
|
|
2221
|
+
previousDisplayCache = cache && typeof cache === "object" && !Array.isArray(cache) ? cache : null;
|
|
2222
|
+
previousCacheJson = this.serializeCacheForComparison(cache);
|
|
2084
2223
|
}
|
|
2085
2224
|
const affectedChildren = /* @__PURE__ */ new Set();
|
|
2086
2225
|
let isFullSnapshot = false;
|
|
@@ -2138,7 +2277,8 @@ var SubscriptionManager = class {
|
|
|
2138
2277
|
}
|
|
2139
2278
|
for (const key of previousOrder) {
|
|
2140
2279
|
if (!currentChildSet.has(key)) {
|
|
2141
|
-
|
|
2280
|
+
const prevValue = previousDisplayCache?.[key];
|
|
2281
|
+
this.fireChildRemoved(view, key, childRemovedSubs, isVolatile, serverTimestamp, prevValue);
|
|
2142
2282
|
}
|
|
2143
2283
|
}
|
|
2144
2284
|
} else {
|
|
@@ -2149,7 +2289,8 @@ var SubscriptionManager = class {
|
|
|
2149
2289
|
const prevKey = view.getPreviousChildKey(childKey);
|
|
2150
2290
|
this.fireChildAdded(view, childKey, childAddedSubs, prevKey, isVolatile, serverTimestamp);
|
|
2151
2291
|
} else if (wasPresent && !isPresent) {
|
|
2152
|
-
|
|
2292
|
+
const prevValue = previousDisplayCache?.[childKey];
|
|
2293
|
+
this.fireChildRemoved(view, childKey, childRemovedSubs, isVolatile, serverTimestamp, prevValue);
|
|
2153
2294
|
} else if (wasPresent && isPresent) {
|
|
2154
2295
|
const prevKey = view.getPreviousChildKey(childKey);
|
|
2155
2296
|
this.fireChildChanged(view, childKey, childChangedSubs, prevKey, isVolatile, serverTimestamp);
|
|
@@ -2160,6 +2301,8 @@ var SubscriptionManager = class {
|
|
|
2160
2301
|
previousOrder.forEach((key, idx) => previousPositions.set(key, idx));
|
|
2161
2302
|
const currentPositions = /* @__PURE__ */ new Map();
|
|
2162
2303
|
currentOrder.forEach((key, idx) => currentPositions.set(key, idx));
|
|
2304
|
+
const currentCache = view.getDisplayCache();
|
|
2305
|
+
const currentDisplayCache = currentCache && typeof currentCache === "object" && !Array.isArray(currentCache) ? currentCache : null;
|
|
2163
2306
|
this.detectAndFireMoves(
|
|
2164
2307
|
view,
|
|
2165
2308
|
previousOrder,
|
|
@@ -2171,7 +2314,9 @@ var SubscriptionManager = class {
|
|
|
2171
2314
|
childMovedSubs,
|
|
2172
2315
|
isVolatile,
|
|
2173
2316
|
serverTimestamp,
|
|
2174
|
-
isFullSnapshot ? void 0 : affectedChildren
|
|
2317
|
+
isFullSnapshot ? void 0 : affectedChildren,
|
|
2318
|
+
previousDisplayCache,
|
|
2319
|
+
currentDisplayCache
|
|
2175
2320
|
);
|
|
2176
2321
|
}
|
|
2177
2322
|
// ============================================
|
|
@@ -2190,8 +2335,12 @@ var SubscriptionManager = class {
|
|
|
2190
2335
|
views.push(view);
|
|
2191
2336
|
} else if (normalized.startsWith(viewPath + "/")) {
|
|
2192
2337
|
views.push(view);
|
|
2338
|
+
} else if (viewPath.startsWith(normalized + "/")) {
|
|
2339
|
+
views.push(view);
|
|
2193
2340
|
} else if (viewPath === "/") {
|
|
2194
2341
|
views.push(view);
|
|
2342
|
+
} else if (normalized === "/") {
|
|
2343
|
+
views.push(view);
|
|
2195
2344
|
}
|
|
2196
2345
|
}
|
|
2197
2346
|
return views;
|
|
@@ -2255,7 +2404,48 @@ var SubscriptionManager = class {
|
|
|
2255
2404
|
}
|
|
2256
2405
|
}
|
|
2257
2406
|
}
|
|
2258
|
-
this.
|
|
2407
|
+
this.fireChildEventsForAck(view, previousOrder, previousChildSet, currentOrder, currentChildSet, previousDisplayCache);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* Fire child events for ACK handling, skipping child_moved.
|
|
2412
|
+
* This is a variant of fireChildEvents that doesn't fire moves because:
|
|
2413
|
+
* 1. Moves were already fired optimistically
|
|
2414
|
+
* 2. If server modifies data, PUT event will fire correct moves
|
|
2415
|
+
* 3. ACK can arrive before PUT, causing incorrect intermediate state
|
|
2416
|
+
*/
|
|
2417
|
+
fireChildEventsForAck(view, previousOrder, previousChildSet, currentOrder, currentChildSet, previousDisplayCache) {
|
|
2418
|
+
const childAddedSubs = view.getCallbacks("child_added");
|
|
2419
|
+
const childChangedSubs = view.getCallbacks("child_changed");
|
|
2420
|
+
const childRemovedSubs = view.getCallbacks("child_removed");
|
|
2421
|
+
if (childAddedSubs.length === 0 && childChangedSubs.length === 0 && childRemovedSubs.length === 0) {
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
const displayCache = view.getDisplayCache();
|
|
2425
|
+
for (const key of currentOrder) {
|
|
2426
|
+
if (!previousChildSet.has(key)) {
|
|
2427
|
+
if (childAddedSubs.length > 0 && displayCache) {
|
|
2428
|
+
const prevKey = view.getPreviousChildKey(key);
|
|
2429
|
+
this.fireChildAdded(view, key, childAddedSubs, prevKey, false, void 0);
|
|
2430
|
+
}
|
|
2431
|
+
} else if (previousDisplayCache && childChangedSubs.length > 0 && displayCache) {
|
|
2432
|
+
const prevValue = previousDisplayCache[key];
|
|
2433
|
+
const currentValue = displayCache[key];
|
|
2434
|
+
const prevJson = this.serializeCacheForComparison(prevValue);
|
|
2435
|
+
const currJson = this.serializeCacheForComparison(currentValue);
|
|
2436
|
+
if (prevJson !== currJson) {
|
|
2437
|
+
const prevKey = view.getPreviousChildKey(key);
|
|
2438
|
+
this.fireChildChanged(view, key, childChangedSubs, prevKey, false, void 0);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
for (const key of previousOrder) {
|
|
2443
|
+
if (!currentChildSet.has(key)) {
|
|
2444
|
+
if (childRemovedSubs.length > 0) {
|
|
2445
|
+
const prevValue = previousDisplayCache?.[key];
|
|
2446
|
+
this.fireChildRemoved(view, key, childRemovedSubs, false, void 0, prevValue);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2259
2449
|
}
|
|
2260
2450
|
}
|
|
2261
2451
|
/**
|
|
@@ -2324,18 +2514,28 @@ var SubscriptionManager = class {
|
|
|
2324
2514
|
const updatedViews = [];
|
|
2325
2515
|
for (const view of affectedViews) {
|
|
2326
2516
|
let relativePath;
|
|
2517
|
+
let effectiveValue = value;
|
|
2327
2518
|
if (normalized === view.path) {
|
|
2328
2519
|
relativePath = "/";
|
|
2329
2520
|
} else if (view.path === "/") {
|
|
2330
2521
|
relativePath = normalized;
|
|
2331
|
-
} else {
|
|
2522
|
+
} else if (normalized.startsWith(view.path + "/")) {
|
|
2332
2523
|
relativePath = normalized.slice(view.path.length);
|
|
2524
|
+
} else if (view.path.startsWith(normalized + "/")) {
|
|
2525
|
+
const pathDiff = view.path.slice(normalized.length);
|
|
2526
|
+
effectiveValue = getValueAtPath(value, pathDiff);
|
|
2527
|
+
if (operation === "update" && effectiveValue === void 0) {
|
|
2528
|
+
continue;
|
|
2529
|
+
}
|
|
2530
|
+
relativePath = "/";
|
|
2531
|
+
} else {
|
|
2532
|
+
continue;
|
|
2333
2533
|
}
|
|
2334
2534
|
const previousDisplayCache = view.getDisplayCache();
|
|
2335
2535
|
const previousOrder = view.orderedChildren;
|
|
2336
2536
|
const previousChildSet = new Set(previousOrder);
|
|
2337
2537
|
const previousCacheJson = this.serializeCacheForComparison(previousDisplayCache);
|
|
2338
|
-
view.addPendingWriteData(requestId, relativePath,
|
|
2538
|
+
view.addPendingWriteData(requestId, relativePath, effectiveValue, operation);
|
|
2339
2539
|
const currentOrder = view.orderedChildren;
|
|
2340
2540
|
const currentChildSet = new Set(currentOrder);
|
|
2341
2541
|
const currentCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
|
|
@@ -2432,7 +2632,8 @@ var SubscriptionManager = class {
|
|
|
2432
2632
|
for (const key of previousOrder) {
|
|
2433
2633
|
if (!currentChildSet.has(key)) {
|
|
2434
2634
|
if (childRemovedSubs.length > 0) {
|
|
2435
|
-
const
|
|
2635
|
+
const prevValue = previousDisplayCache?.[key];
|
|
2636
|
+
const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue ?? null, isVolatile, serverTimestamp);
|
|
2436
2637
|
if (snapshot) {
|
|
2437
2638
|
for (const entry of childRemovedSubs) {
|
|
2438
2639
|
try {
|
|
@@ -2446,6 +2647,23 @@ var SubscriptionManager = class {
|
|
|
2446
2647
|
}
|
|
2447
2648
|
}
|
|
2448
2649
|
} else {
|
|
2650
|
+
if (childRemovedSubs.length > 0) {
|
|
2651
|
+
for (const key of previousOrder) {
|
|
2652
|
+
if (affectedChildren.has(key)) continue;
|
|
2653
|
+
if (currentChildSet.has(key)) continue;
|
|
2654
|
+
const prevValue = previousDisplayCache ? previousDisplayCache[key] : null;
|
|
2655
|
+
const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue, isVolatile, serverTimestamp);
|
|
2656
|
+
if (snapshot) {
|
|
2657
|
+
for (const entry of childRemovedSubs) {
|
|
2658
|
+
try {
|
|
2659
|
+
entry.callback(snapshot, void 0);
|
|
2660
|
+
} catch (err) {
|
|
2661
|
+
console.error("Error in child_removed callback:", err);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2449
2667
|
for (const key of affectedChildren) {
|
|
2450
2668
|
const wasPresent = previousChildSet.has(key);
|
|
2451
2669
|
const isPresent = currentChildSet.has(key);
|
|
@@ -2471,7 +2689,8 @@ var SubscriptionManager = class {
|
|
|
2471
2689
|
}
|
|
2472
2690
|
} else if (wasPresent && !isPresent) {
|
|
2473
2691
|
if (childRemovedSubs.length > 0) {
|
|
2474
|
-
const
|
|
2692
|
+
const prevValue = previousDisplayCache ? previousDisplayCache[key] : null;
|
|
2693
|
+
const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue, isVolatile, serverTimestamp);
|
|
2475
2694
|
if (snapshot) {
|
|
2476
2695
|
for (const entry of childRemovedSubs) {
|
|
2477
2696
|
try {
|
|
@@ -2504,18 +2723,24 @@ var SubscriptionManager = class {
|
|
|
2504
2723
|
}
|
|
2505
2724
|
}
|
|
2506
2725
|
}
|
|
2507
|
-
if (
|
|
2508
|
-
for (const key of
|
|
2726
|
+
if (childAddedSubs.length > 0 && displayCache) {
|
|
2727
|
+
for (const key of currentOrder) {
|
|
2728
|
+
if (previousChildSet.has(key)) continue;
|
|
2509
2729
|
if (affectedChildren.has(key)) continue;
|
|
2510
|
-
|
|
2511
|
-
const
|
|
2512
|
-
|
|
2730
|
+
const childValue = displayCache[key];
|
|
2731
|
+
const snapshot = this.createSnapshot?.(
|
|
2732
|
+
joinPath(view.path, key),
|
|
2733
|
+
childValue,
|
|
2734
|
+
isVolatile,
|
|
2735
|
+
serverTimestamp
|
|
2736
|
+
);
|
|
2513
2737
|
if (snapshot) {
|
|
2514
|
-
|
|
2738
|
+
const prevKey = view.getPreviousChildKey(key);
|
|
2739
|
+
for (const entry of childAddedSubs) {
|
|
2515
2740
|
try {
|
|
2516
|
-
entry.callback(snapshot,
|
|
2741
|
+
entry.callback(snapshot, prevKey);
|
|
2517
2742
|
} catch (err) {
|
|
2518
|
-
console.error("Error in
|
|
2743
|
+
console.error("Error in child_added callback:", err);
|
|
2519
2744
|
}
|
|
2520
2745
|
}
|
|
2521
2746
|
}
|
|
@@ -2538,7 +2763,9 @@ var SubscriptionManager = class {
|
|
|
2538
2763
|
childMovedSubs,
|
|
2539
2764
|
isVolatile,
|
|
2540
2765
|
serverTimestamp,
|
|
2541
|
-
isFullSnapshot ? void 0 : affectedChildren
|
|
2766
|
+
isFullSnapshot ? void 0 : affectedChildren,
|
|
2767
|
+
previousDisplayCache,
|
|
2768
|
+
displayCache
|
|
2542
2769
|
);
|
|
2543
2770
|
}
|
|
2544
2771
|
}
|
|
@@ -2761,26 +2988,26 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
2761
2988
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX] = this._query.orderByChildPath;
|
|
2762
2989
|
}
|
|
2763
2990
|
if (this._query.startAt !== void 0) {
|
|
2764
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAt.value;
|
|
2991
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAt.value ?? null;
|
|
2765
2992
|
if (this._query.startAt.key !== void 0) {
|
|
2766
2993
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.startAt.key;
|
|
2767
2994
|
}
|
|
2768
2995
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = true;
|
|
2769
2996
|
} else if (this._query.startAfter !== void 0) {
|
|
2770
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAfter.value;
|
|
2997
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAfter.value ?? null;
|
|
2771
2998
|
if (this._query.startAfter.key !== void 0) {
|
|
2772
2999
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.startAfter.key;
|
|
2773
3000
|
}
|
|
2774
3001
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = false;
|
|
2775
3002
|
}
|
|
2776
3003
|
if (this._query.endAt !== void 0) {
|
|
2777
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endAt.value;
|
|
3004
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endAt.value ?? null;
|
|
2778
3005
|
if (this._query.endAt.key !== void 0) {
|
|
2779
3006
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.endAt.key;
|
|
2780
3007
|
}
|
|
2781
3008
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_IS_INCLUSIVE] = true;
|
|
2782
3009
|
} else if (this._query.endBefore !== void 0) {
|
|
2783
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endBefore.value;
|
|
3010
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endBefore.value ?? null;
|
|
2784
3011
|
if (this._query.endBefore.key !== void 0) {
|
|
2785
3012
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.endBefore.key;
|
|
2786
3013
|
}
|
|
@@ -2939,17 +3166,39 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
2939
3166
|
}
|
|
2940
3167
|
/**
|
|
2941
3168
|
* Set the priority of the data at this location.
|
|
2942
|
-
*
|
|
3169
|
+
* Uses cached value for optimistic behavior (local effects are immediate).
|
|
3170
|
+
* The optimistic update happens synchronously, Promise resolves after server ack.
|
|
2943
3171
|
*/
|
|
2944
|
-
|
|
3172
|
+
setPriority(priority) {
|
|
2945
3173
|
validateNotInfoPath(this._path, "setPriority");
|
|
2946
|
-
const
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
3174
|
+
const { value: cachedValue, found } = this._db._getCachedValue(this._path);
|
|
3175
|
+
if (!found) {
|
|
3176
|
+
return this.once().then((snapshot) => {
|
|
3177
|
+
const actualValue2 = snapshot.val();
|
|
3178
|
+
if (actualValue2 === null || actualValue2 === void 0) {
|
|
3179
|
+
return this._db._sendSet(this._path, { ".priority": priority });
|
|
3180
|
+
}
|
|
3181
|
+
return this.setWithPriority(actualValue2, priority);
|
|
3182
|
+
});
|
|
2951
3183
|
}
|
|
2952
|
-
|
|
3184
|
+
let actualValue;
|
|
3185
|
+
if (cachedValue === null || cachedValue === void 0) {
|
|
3186
|
+
actualValue = null;
|
|
3187
|
+
} else if (typeof cachedValue === "object" && !Array.isArray(cachedValue)) {
|
|
3188
|
+
const obj = cachedValue;
|
|
3189
|
+
if (".value" in obj && Object.keys(obj).every((k) => k === ".value" || k === ".priority")) {
|
|
3190
|
+
actualValue = obj[".value"];
|
|
3191
|
+
} else {
|
|
3192
|
+
const { ".priority": _oldPriority, ...rest } = obj;
|
|
3193
|
+
actualValue = Object.keys(rest).length > 0 ? rest : null;
|
|
3194
|
+
}
|
|
3195
|
+
} else {
|
|
3196
|
+
actualValue = cachedValue;
|
|
3197
|
+
}
|
|
3198
|
+
if (actualValue === null || actualValue === void 0) {
|
|
3199
|
+
return this._db._sendSet(this._path, { ".priority": priority });
|
|
3200
|
+
}
|
|
3201
|
+
return this.setWithPriority(actualValue, priority);
|
|
2953
3202
|
}
|
|
2954
3203
|
/**
|
|
2955
3204
|
* Atomically modify the data at this location using optimistic concurrency.
|
|
@@ -2979,6 +3228,7 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
2979
3228
|
while (retries < maxRetries) {
|
|
2980
3229
|
const currentSnapshot = await this.once();
|
|
2981
3230
|
const currentValue = currentSnapshot.val();
|
|
3231
|
+
const rawValue = currentSnapshot.exportVal();
|
|
2982
3232
|
const newValue = updateFunction(currentValue);
|
|
2983
3233
|
if (newValue === void 0) {
|
|
2984
3234
|
return {
|
|
@@ -2987,13 +3237,18 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
2987
3237
|
};
|
|
2988
3238
|
}
|
|
2989
3239
|
let condition;
|
|
2990
|
-
if (isPrimitive(
|
|
2991
|
-
condition = { o: "c", p: this._path, v:
|
|
3240
|
+
if (isPrimitive(rawValue)) {
|
|
3241
|
+
condition = { o: "c", p: this._path, v: rawValue };
|
|
2992
3242
|
} else {
|
|
2993
|
-
const hash = await hashValue(
|
|
3243
|
+
const hash = await hashValue(rawValue);
|
|
2994
3244
|
condition = { o: "c", p: this._path, h: hash };
|
|
2995
3245
|
}
|
|
2996
|
-
|
|
3246
|
+
let valueToSet = newValue;
|
|
3247
|
+
const existingPriority = currentSnapshot.getPriority();
|
|
3248
|
+
if (existingPriority !== null && isPrimitive(newValue) && newValue !== null) {
|
|
3249
|
+
valueToSet = { ".priority": existingPriority, ".value": newValue };
|
|
3250
|
+
}
|
|
3251
|
+
const ops = [condition, { o: "s", p: this._path, v: valueToSet }];
|
|
2997
3252
|
try {
|
|
2998
3253
|
await this._db._sendTransaction(ops);
|
|
2999
3254
|
const finalSnapshot = await this.once();
|
|
@@ -3020,14 +3275,23 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3020
3275
|
/**
|
|
3021
3276
|
* Read the data at this location once.
|
|
3022
3277
|
*
|
|
3023
|
-
*
|
|
3278
|
+
* For 'value' events, this fetches data directly from the server.
|
|
3279
|
+
* For child events ('child_added', 'child_changed', 'child_removed', 'child_moved'),
|
|
3280
|
+
* this subscribes, waits for the first event, then unsubscribes.
|
|
3281
|
+
*
|
|
3282
|
+
* @param eventType - The event type
|
|
3024
3283
|
* @returns Promise that resolves to the DataSnapshot
|
|
3025
3284
|
*/
|
|
3026
3285
|
once(eventType = "value") {
|
|
3027
|
-
if (eventType
|
|
3028
|
-
|
|
3286
|
+
if (eventType === "value") {
|
|
3287
|
+
return this._db._sendOnce(this._path, this._buildQueryParams());
|
|
3029
3288
|
}
|
|
3030
|
-
return
|
|
3289
|
+
return new Promise((resolve) => {
|
|
3290
|
+
const unsubscribe = this.on(eventType, (snapshot) => {
|
|
3291
|
+
unsubscribe();
|
|
3292
|
+
resolve(snapshot);
|
|
3293
|
+
});
|
|
3294
|
+
});
|
|
3031
3295
|
}
|
|
3032
3296
|
// ============================================
|
|
3033
3297
|
// Subscriptions
|
|
@@ -3096,6 +3360,12 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3096
3360
|
*/
|
|
3097
3361
|
orderByChild(path) {
|
|
3098
3362
|
this._validateNoOrderBy("orderByChild");
|
|
3363
|
+
if (path.startsWith("$") || path.includes("/$")) {
|
|
3364
|
+
throw new LarkError(
|
|
3365
|
+
ErrorCode.INVALID_PATH,
|
|
3366
|
+
`orderByChild: Invalid path '${path}'. Paths cannot contain '$' prefix (reserved for internal use)`
|
|
3367
|
+
);
|
|
3368
|
+
}
|
|
3099
3369
|
return new _DatabaseReference(this._db, this._path, {
|
|
3100
3370
|
...this._query,
|
|
3101
3371
|
orderBy: "child",
|
|
@@ -3469,35 +3739,35 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3469
3739
|
hasParams = true;
|
|
3470
3740
|
}
|
|
3471
3741
|
if (this._query.startAt !== void 0) {
|
|
3472
|
-
params.startAt = this._query.startAt.value;
|
|
3742
|
+
params.startAt = this._query.startAt.value ?? null;
|
|
3473
3743
|
if (this._query.startAt.key !== void 0) {
|
|
3474
3744
|
params.startAtKey = this._query.startAt.key;
|
|
3475
3745
|
}
|
|
3476
3746
|
hasParams = true;
|
|
3477
3747
|
}
|
|
3478
3748
|
if (this._query.startAfter !== void 0) {
|
|
3479
|
-
params.startAfter = this._query.startAfter.value;
|
|
3749
|
+
params.startAfter = this._query.startAfter.value ?? null;
|
|
3480
3750
|
if (this._query.startAfter.key !== void 0) {
|
|
3481
3751
|
params.startAfterKey = this._query.startAfter.key;
|
|
3482
3752
|
}
|
|
3483
3753
|
hasParams = true;
|
|
3484
3754
|
}
|
|
3485
3755
|
if (this._query.endAt !== void 0) {
|
|
3486
|
-
params.endAt = this._query.endAt.value;
|
|
3756
|
+
params.endAt = this._query.endAt.value ?? null;
|
|
3487
3757
|
if (this._query.endAt.key !== void 0) {
|
|
3488
3758
|
params.endAtKey = this._query.endAt.key;
|
|
3489
3759
|
}
|
|
3490
3760
|
hasParams = true;
|
|
3491
3761
|
}
|
|
3492
3762
|
if (this._query.endBefore !== void 0) {
|
|
3493
|
-
params.endBefore = this._query.endBefore.value;
|
|
3763
|
+
params.endBefore = this._query.endBefore.value ?? null;
|
|
3494
3764
|
if (this._query.endBefore.key !== void 0) {
|
|
3495
3765
|
params.endBeforeKey = this._query.endBefore.key;
|
|
3496
3766
|
}
|
|
3497
3767
|
hasParams = true;
|
|
3498
3768
|
}
|
|
3499
3769
|
if (this._query.equalTo !== void 0) {
|
|
3500
|
-
params.equalTo = this._query.equalTo.value;
|
|
3770
|
+
params.equalTo = this._query.equalTo.value ?? null;
|
|
3501
3771
|
if (this._query.equalTo.key !== void 0) {
|
|
3502
3772
|
params.equalToKey = this._query.equalTo.key;
|
|
3503
3773
|
}
|
|
@@ -3516,6 +3786,13 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3516
3786
|
}
|
|
3517
3787
|
return `${baseUrl}${this._path}`;
|
|
3518
3788
|
}
|
|
3789
|
+
/**
|
|
3790
|
+
* Returns the URL for JSON serialization.
|
|
3791
|
+
* This allows refs to be serialized with JSON.stringify().
|
|
3792
|
+
*/
|
|
3793
|
+
toJSON() {
|
|
3794
|
+
return this.toString();
|
|
3795
|
+
}
|
|
3519
3796
|
};
|
|
3520
3797
|
var ThenableReference = class extends DatabaseReference {
|
|
3521
3798
|
constructor(db, path, promise) {
|
|
@@ -3536,11 +3813,17 @@ function isWrappedPrimitive(data) {
|
|
|
3536
3813
|
return false;
|
|
3537
3814
|
}
|
|
3538
3815
|
const keys = Object.keys(data);
|
|
3539
|
-
|
|
3816
|
+
if (keys.length === 2 && ".value" in data && ".priority" in data) {
|
|
3817
|
+
return true;
|
|
3818
|
+
}
|
|
3819
|
+
if (keys.length === 1 && ".value" in data) {
|
|
3820
|
+
return true;
|
|
3821
|
+
}
|
|
3822
|
+
return false;
|
|
3540
3823
|
}
|
|
3541
3824
|
function stripPriorityMetadata(data) {
|
|
3542
3825
|
if (data === null || data === void 0) {
|
|
3543
|
-
return
|
|
3826
|
+
return null;
|
|
3544
3827
|
}
|
|
3545
3828
|
if (typeof data !== "object") {
|
|
3546
3829
|
return data;
|
|
@@ -3556,7 +3839,10 @@ function stripPriorityMetadata(data) {
|
|
|
3556
3839
|
if (key === ".priority") {
|
|
3557
3840
|
continue;
|
|
3558
3841
|
}
|
|
3559
|
-
|
|
3842
|
+
const stripped = stripPriorityMetadata(value);
|
|
3843
|
+
if (stripped !== null) {
|
|
3844
|
+
result[key] = stripped;
|
|
3845
|
+
}
|
|
3560
3846
|
}
|
|
3561
3847
|
return Object.keys(result).length > 0 ? result : null;
|
|
3562
3848
|
}
|
|
@@ -3603,9 +3889,19 @@ var DataSnapshot = class _DataSnapshot {
|
|
|
3603
3889
|
}
|
|
3604
3890
|
/**
|
|
3605
3891
|
* Check if data exists at this location (is not null/undefined).
|
|
3892
|
+
* Returns false for priority-only nodes (only .priority, no actual value).
|
|
3606
3893
|
*/
|
|
3607
3894
|
exists() {
|
|
3608
|
-
|
|
3895
|
+
if (this._data === null || this._data === void 0) {
|
|
3896
|
+
return false;
|
|
3897
|
+
}
|
|
3898
|
+
if (typeof this._data === "object" && !Array.isArray(this._data)) {
|
|
3899
|
+
const keys = Object.keys(this._data);
|
|
3900
|
+
if (keys.length === 1 && keys[0] === ".priority") {
|
|
3901
|
+
return false;
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
return true;
|
|
3609
3905
|
}
|
|
3610
3906
|
/**
|
|
3611
3907
|
* Get a child snapshot at the specified path.
|
|
@@ -3752,24 +4048,6 @@ var DataSnapshot = class _DataSnapshot {
|
|
|
3752
4048
|
}
|
|
3753
4049
|
};
|
|
3754
4050
|
|
|
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
4051
|
// src/utils/volatile.ts
|
|
3774
4052
|
function isVolatilePath(path, patterns) {
|
|
3775
4053
|
if (!patterns || patterns.length === 0) {
|
|
@@ -3892,6 +4170,11 @@ var LarkDatabase = class {
|
|
|
3892
4170
|
this._coordinatorUrl = null;
|
|
3893
4171
|
this._volatilePaths = [];
|
|
3894
4172
|
this._transportType = null;
|
|
4173
|
+
// Auth state
|
|
4174
|
+
this._currentToken = null;
|
|
4175
|
+
// Token for auth (empty string = anonymous)
|
|
4176
|
+
this._isAnonymous = false;
|
|
4177
|
+
// True if connected anonymously
|
|
3895
4178
|
// Reconnection state
|
|
3896
4179
|
this._connectionId = null;
|
|
3897
4180
|
this._connectOptions = null;
|
|
@@ -3904,8 +4187,12 @@ var LarkDatabase = class {
|
|
|
3904
4187
|
this.disconnectCallbacks = /* @__PURE__ */ new Set();
|
|
3905
4188
|
this.errorCallbacks = /* @__PURE__ */ new Set();
|
|
3906
4189
|
this.reconnectingCallbacks = /* @__PURE__ */ new Set();
|
|
4190
|
+
this.authStateChangedCallbacks = /* @__PURE__ */ new Set();
|
|
3907
4191
|
// .info path subscriptions (handled locally, not sent to server)
|
|
3908
4192
|
this.infoSubscriptions = [];
|
|
4193
|
+
// Authentication promise - resolves when auth completes, allows operations to queue
|
|
4194
|
+
this.authenticationPromise = null;
|
|
4195
|
+
this.authenticationResolve = null;
|
|
3909
4196
|
this._serverTimeOffset = 0;
|
|
3910
4197
|
this.messageQueue = new MessageQueue();
|
|
3911
4198
|
this.subscriptionManager = new SubscriptionManager();
|
|
@@ -3915,10 +4202,11 @@ var LarkDatabase = class {
|
|
|
3915
4202
|
// Connection State
|
|
3916
4203
|
// ============================================
|
|
3917
4204
|
/**
|
|
3918
|
-
* Whether the database is
|
|
4205
|
+
* Whether the database is fully connected and authenticated.
|
|
4206
|
+
* Returns true when ready to perform database operations.
|
|
3919
4207
|
*/
|
|
3920
4208
|
get connected() {
|
|
3921
|
-
return this._state === "
|
|
4209
|
+
return this._state === "authenticated";
|
|
3922
4210
|
}
|
|
3923
4211
|
/**
|
|
3924
4212
|
* Whether the database is currently attempting to reconnect.
|
|
@@ -4003,16 +4291,27 @@ var LarkDatabase = class {
|
|
|
4003
4291
|
}
|
|
4004
4292
|
this._connectOptions = options;
|
|
4005
4293
|
this._intentionalDisconnect = false;
|
|
4294
|
+
this.authenticationPromise = new Promise((resolve) => {
|
|
4295
|
+
this.authenticationResolve = resolve;
|
|
4296
|
+
});
|
|
4006
4297
|
await this.performConnect(databaseId, options);
|
|
4007
4298
|
}
|
|
4008
4299
|
/**
|
|
4009
4300
|
* Internal connect implementation used by both initial connect and reconnect.
|
|
4301
|
+
* Implements the Join → Auth flow:
|
|
4302
|
+
* 1. Connect WebSocket
|
|
4303
|
+
* 2. Send join (identifies database)
|
|
4304
|
+
* 3. Send auth (authenticates user - required even for anonymous)
|
|
4010
4305
|
*/
|
|
4011
4306
|
async performConnect(databaseId, options, isReconnect = false) {
|
|
4012
4307
|
const previousState = this._state;
|
|
4013
4308
|
this._state = isReconnect ? "reconnecting" : "connecting";
|
|
4014
4309
|
this._databaseId = databaseId;
|
|
4015
4310
|
this._coordinatorUrl = options.coordinator || DEFAULT_COORDINATOR_URL;
|
|
4311
|
+
if (!isReconnect) {
|
|
4312
|
+
this._currentToken = options.token || "";
|
|
4313
|
+
this._isAnonymous = !options.token && options.anonymous !== false;
|
|
4314
|
+
}
|
|
4016
4315
|
try {
|
|
4017
4316
|
const coordinatorUrl = this._coordinatorUrl;
|
|
4018
4317
|
const coordinator = new Coordinator(coordinatorUrl);
|
|
@@ -4038,30 +4337,44 @@ var LarkDatabase = class {
|
|
|
4038
4337
|
);
|
|
4039
4338
|
this.transport = transportResult.transport;
|
|
4040
4339
|
this._transportType = transportResult.type;
|
|
4041
|
-
|
|
4340
|
+
this._state = "connected";
|
|
4341
|
+
const joinRequestId = this.messageQueue.nextRequestId();
|
|
4042
4342
|
const joinMessage = {
|
|
4043
4343
|
o: "j",
|
|
4044
|
-
|
|
4045
|
-
r:
|
|
4344
|
+
d: databaseId,
|
|
4345
|
+
r: joinRequestId
|
|
4046
4346
|
};
|
|
4047
4347
|
if (this._connectionId) {
|
|
4048
4348
|
joinMessage.pcid = this._connectionId;
|
|
4049
4349
|
}
|
|
4050
4350
|
this.send(joinMessage);
|
|
4051
|
-
const joinResponse = await this.messageQueue.registerRequest(
|
|
4351
|
+
const joinResponse = await this.messageQueue.registerRequest(joinRequestId);
|
|
4052
4352
|
this._volatilePaths = joinResponse.volatilePaths;
|
|
4053
4353
|
this._connectionId = joinResponse.connectionId;
|
|
4054
4354
|
if (joinResponse.serverTime != null) {
|
|
4055
4355
|
this._serverTimeOffset = joinResponse.serverTime - Date.now();
|
|
4056
4356
|
}
|
|
4057
|
-
|
|
4357
|
+
this._state = "joined";
|
|
4358
|
+
const authRequestId = this.messageQueue.nextRequestId();
|
|
4359
|
+
const authMessage = {
|
|
4360
|
+
o: "au",
|
|
4361
|
+
t: this._currentToken ?? "",
|
|
4362
|
+
r: authRequestId
|
|
4363
|
+
};
|
|
4364
|
+
this.send(authMessage);
|
|
4365
|
+
const authResponse = await this.messageQueue.registerRequest(authRequestId);
|
|
4058
4366
|
this._auth = {
|
|
4059
|
-
uid:
|
|
4060
|
-
provider:
|
|
4061
|
-
token:
|
|
4367
|
+
uid: authResponse.uid || "",
|
|
4368
|
+
provider: this._isAnonymous ? "anonymous" : "custom",
|
|
4369
|
+
token: {}
|
|
4370
|
+
// Token claims would need to be decoded from the token if needed
|
|
4062
4371
|
};
|
|
4063
|
-
this._state = "
|
|
4372
|
+
this._state = "authenticated";
|
|
4064
4373
|
this._reconnectAttempt = 0;
|
|
4374
|
+
if (this.authenticationResolve) {
|
|
4375
|
+
this.authenticationResolve();
|
|
4376
|
+
this.authenticationResolve = null;
|
|
4377
|
+
}
|
|
4065
4378
|
this.fireConnectionStateChange();
|
|
4066
4379
|
if (!isReconnect) {
|
|
4067
4380
|
this.subscriptionManager.initialize({
|
|
@@ -4074,6 +4387,7 @@ var LarkDatabase = class {
|
|
|
4074
4387
|
await this.restoreAfterReconnect();
|
|
4075
4388
|
}
|
|
4076
4389
|
this.connectCallbacks.forEach((cb) => cb());
|
|
4390
|
+
this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
|
|
4077
4391
|
} catch (error) {
|
|
4078
4392
|
if (isReconnect) {
|
|
4079
4393
|
this._state = "reconnecting";
|
|
@@ -4086,6 +4400,8 @@ var LarkDatabase = class {
|
|
|
4086
4400
|
this._connectOptions = null;
|
|
4087
4401
|
this._connectionId = null;
|
|
4088
4402
|
this._transportType = null;
|
|
4403
|
+
this._currentToken = null;
|
|
4404
|
+
this._isAnonymous = false;
|
|
4089
4405
|
this.transport?.close();
|
|
4090
4406
|
this.transport = null;
|
|
4091
4407
|
throw error;
|
|
@@ -4099,13 +4415,14 @@ var LarkDatabase = class {
|
|
|
4099
4415
|
if (this._state === "disconnected") {
|
|
4100
4416
|
return;
|
|
4101
4417
|
}
|
|
4102
|
-
const
|
|
4418
|
+
const wasAuthenticated = this._state === "authenticated";
|
|
4419
|
+
const wasPartiallyConnected = this._state === "connected" || this._state === "joined";
|
|
4103
4420
|
this._intentionalDisconnect = true;
|
|
4104
4421
|
if (this._reconnectTimer) {
|
|
4105
4422
|
clearTimeout(this._reconnectTimer);
|
|
4106
4423
|
this._reconnectTimer = null;
|
|
4107
4424
|
}
|
|
4108
|
-
if (
|
|
4425
|
+
if ((wasAuthenticated || wasPartiallyConnected) && this.transport) {
|
|
4109
4426
|
try {
|
|
4110
4427
|
const requestId = this.messageQueue.nextRequestId();
|
|
4111
4428
|
this.send({ o: "l", r: requestId });
|
|
@@ -4117,7 +4434,7 @@ var LarkDatabase = class {
|
|
|
4117
4434
|
}
|
|
4118
4435
|
}
|
|
4119
4436
|
this.cleanupFull();
|
|
4120
|
-
if (
|
|
4437
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4121
4438
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4122
4439
|
}
|
|
4123
4440
|
}
|
|
@@ -4126,7 +4443,7 @@ var LarkDatabase = class {
|
|
|
4126
4443
|
* Disconnects from the server but preserves subscriptions for later reconnection via goOnline().
|
|
4127
4444
|
*/
|
|
4128
4445
|
goOffline() {
|
|
4129
|
-
if (this._state === "connected" || this._state === "reconnecting") {
|
|
4446
|
+
if (this._state === "authenticated" || this._state === "joined" || this._state === "connected" || this._state === "reconnecting") {
|
|
4130
4447
|
this._intentionalDisconnect = true;
|
|
4131
4448
|
if (this._reconnectTimer) {
|
|
4132
4449
|
clearTimeout(this._reconnectTimer);
|
|
@@ -4157,7 +4474,7 @@ var LarkDatabase = class {
|
|
|
4157
4474
|
* Used for intentional disconnect.
|
|
4158
4475
|
*/
|
|
4159
4476
|
cleanupFull() {
|
|
4160
|
-
const
|
|
4477
|
+
const wasAuthenticated = this._state === "authenticated";
|
|
4161
4478
|
this.transport?.close();
|
|
4162
4479
|
this.transport = null;
|
|
4163
4480
|
this._state = "disconnected";
|
|
@@ -4168,11 +4485,15 @@ var LarkDatabase = class {
|
|
|
4168
4485
|
this._connectionId = null;
|
|
4169
4486
|
this._connectOptions = null;
|
|
4170
4487
|
this._transportType = null;
|
|
4488
|
+
this._currentToken = null;
|
|
4489
|
+
this._isAnonymous = false;
|
|
4171
4490
|
this._reconnectAttempt = 0;
|
|
4491
|
+
this.authenticationPromise = null;
|
|
4492
|
+
this.authenticationResolve = null;
|
|
4172
4493
|
this.subscriptionManager.clear();
|
|
4173
4494
|
this.messageQueue.rejectAll(new Error("Connection closed"));
|
|
4174
4495
|
this.pendingWrites.clear();
|
|
4175
|
-
if (
|
|
4496
|
+
if (wasAuthenticated) {
|
|
4176
4497
|
this.fireConnectionStateChange();
|
|
4177
4498
|
}
|
|
4178
4499
|
this.infoSubscriptions = [];
|
|
@@ -4205,7 +4526,7 @@ var LarkDatabase = class {
|
|
|
4205
4526
|
getInfoValue(path) {
|
|
4206
4527
|
const normalizedPath = normalizePath(path) || "/";
|
|
4207
4528
|
if (normalizedPath === "/.info/connected") {
|
|
4208
|
-
return this._state === "
|
|
4529
|
+
return this._state === "authenticated";
|
|
4209
4530
|
}
|
|
4210
4531
|
if (normalizedPath === "/.info/serverTimeOffset") {
|
|
4211
4532
|
return this._serverTimeOffset;
|
|
@@ -4272,6 +4593,9 @@ var LarkDatabase = class {
|
|
|
4272
4593
|
if (this._intentionalDisconnect || !this._databaseId || !this._connectOptions) {
|
|
4273
4594
|
return;
|
|
4274
4595
|
}
|
|
4596
|
+
this.authenticationPromise = new Promise((resolve) => {
|
|
4597
|
+
this.authenticationResolve = resolve;
|
|
4598
|
+
});
|
|
4275
4599
|
try {
|
|
4276
4600
|
await this.performConnect(this._databaseId, this._connectOptions, true);
|
|
4277
4601
|
} catch {
|
|
@@ -4430,6 +4754,9 @@ var LarkDatabase = class {
|
|
|
4430
4754
|
* @internal Send a transaction to the server.
|
|
4431
4755
|
*/
|
|
4432
4756
|
async _sendTransaction(ops) {
|
|
4757
|
+
if (!this.isAuthenticatedOrThrow()) {
|
|
4758
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4759
|
+
}
|
|
4433
4760
|
const requestId = this.messageQueue.nextRequestId();
|
|
4434
4761
|
this.pendingWrites.trackWrite(requestId, "transaction", "/", ops);
|
|
4435
4762
|
const message = {
|
|
@@ -4476,6 +4803,76 @@ var LarkDatabase = class {
|
|
|
4476
4803
|
this.reconnectingCallbacks.add(callback);
|
|
4477
4804
|
return () => this.reconnectingCallbacks.delete(callback);
|
|
4478
4805
|
}
|
|
4806
|
+
/**
|
|
4807
|
+
* Register a callback for auth state changes.
|
|
4808
|
+
* Fires when user signs in, signs out, or auth changes.
|
|
4809
|
+
* Returns an unsubscribe function.
|
|
4810
|
+
*/
|
|
4811
|
+
onAuthStateChanged(callback) {
|
|
4812
|
+
this.authStateChangedCallbacks.add(callback);
|
|
4813
|
+
return () => this.authStateChangedCallbacks.delete(callback);
|
|
4814
|
+
}
|
|
4815
|
+
// ============================================
|
|
4816
|
+
// Authentication Management
|
|
4817
|
+
// ============================================
|
|
4818
|
+
/**
|
|
4819
|
+
* Sign in with a new auth token while connected.
|
|
4820
|
+
* Changes the authenticated user without disconnecting.
|
|
4821
|
+
*
|
|
4822
|
+
* Note: Some subscriptions may be revoked if the new user doesn't have
|
|
4823
|
+
* permission. Listen for 'permission_denied' errors on your subscriptions.
|
|
4824
|
+
*
|
|
4825
|
+
* @param token - The auth token for the new user
|
|
4826
|
+
* @throws Error if not connected (must call connect() first)
|
|
4827
|
+
*/
|
|
4828
|
+
async signIn(token) {
|
|
4829
|
+
if (this._state !== "authenticated" && this._state !== "joined") {
|
|
4830
|
+
throw new LarkError("not_connected", "Must be connected first - call connect()");
|
|
4831
|
+
}
|
|
4832
|
+
const authRequestId = this.messageQueue.nextRequestId();
|
|
4833
|
+
const authMessage = {
|
|
4834
|
+
o: "au",
|
|
4835
|
+
t: token,
|
|
4836
|
+
r: authRequestId
|
|
4837
|
+
};
|
|
4838
|
+
this.send(authMessage);
|
|
4839
|
+
const authResponse = await this.messageQueue.registerRequest(authRequestId);
|
|
4840
|
+
this._currentToken = token;
|
|
4841
|
+
this._isAnonymous = false;
|
|
4842
|
+
this._auth = {
|
|
4843
|
+
uid: authResponse.uid || "",
|
|
4844
|
+
provider: "custom",
|
|
4845
|
+
token: {}
|
|
4846
|
+
};
|
|
4847
|
+
this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
|
|
4848
|
+
}
|
|
4849
|
+
/**
|
|
4850
|
+
* Sign out the current user.
|
|
4851
|
+
* Reverts to anonymous authentication.
|
|
4852
|
+
*
|
|
4853
|
+
* Note: Some subscriptions may be revoked if anonymous users don't have
|
|
4854
|
+
* permission. Listen for 'permission_denied' errors on your subscriptions.
|
|
4855
|
+
*/
|
|
4856
|
+
async signOut() {
|
|
4857
|
+
if (this._state !== "authenticated") {
|
|
4858
|
+
return;
|
|
4859
|
+
}
|
|
4860
|
+
const unauthRequestId = this.messageQueue.nextRequestId();
|
|
4861
|
+
const unauthMessage = {
|
|
4862
|
+
o: "ua",
|
|
4863
|
+
r: unauthRequestId
|
|
4864
|
+
};
|
|
4865
|
+
this.send(unauthMessage);
|
|
4866
|
+
const authResponse = await this.messageQueue.registerRequest(unauthRequestId);
|
|
4867
|
+
this._currentToken = "";
|
|
4868
|
+
this._isAnonymous = true;
|
|
4869
|
+
this._auth = {
|
|
4870
|
+
uid: authResponse.uid || "",
|
|
4871
|
+
provider: "anonymous",
|
|
4872
|
+
token: {}
|
|
4873
|
+
};
|
|
4874
|
+
this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
|
|
4875
|
+
}
|
|
4479
4876
|
// ============================================
|
|
4480
4877
|
// Internal: Message Handling
|
|
4481
4878
|
// ============================================
|
|
@@ -4487,6 +4884,9 @@ var LarkDatabase = class {
|
|
|
4487
4884
|
console.error("Failed to parse message:", data);
|
|
4488
4885
|
return;
|
|
4489
4886
|
}
|
|
4887
|
+
if (process.env.LARK_DEBUG) {
|
|
4888
|
+
console.log("[LARK] <<< SERVER:", JSON.stringify(message, null, 2));
|
|
4889
|
+
}
|
|
4490
4890
|
if (isPingMessage(message)) {
|
|
4491
4891
|
this.transport?.send(JSON.stringify({ o: "po" }));
|
|
4492
4892
|
return;
|
|
@@ -4496,6 +4896,12 @@ var LarkDatabase = class {
|
|
|
4496
4896
|
this.subscriptionManager.clearPendingWrite(message.a);
|
|
4497
4897
|
} else if (isNackMessage(message)) {
|
|
4498
4898
|
this.pendingWrites.onNack(message.n);
|
|
4899
|
+
if (message.e === "permission_denied" && message.sp) {
|
|
4900
|
+
const path = message.sp;
|
|
4901
|
+
console.warn(`Subscription revoked at ${path}: permission_denied`);
|
|
4902
|
+
this.subscriptionManager.handleSubscriptionRevoked(path);
|
|
4903
|
+
return;
|
|
4904
|
+
}
|
|
4499
4905
|
if (message.e !== "condition_failed") {
|
|
4500
4906
|
console.error(`Write failed (${message.e}): ${message.m || message.e}`);
|
|
4501
4907
|
}
|
|
@@ -4512,27 +4918,28 @@ var LarkDatabase = class {
|
|
|
4512
4918
|
if (this._state === "disconnected") {
|
|
4513
4919
|
return;
|
|
4514
4920
|
}
|
|
4515
|
-
const
|
|
4921
|
+
const wasAuthenticated = this._state === "authenticated";
|
|
4516
4922
|
const wasReconnecting = this._state === "reconnecting";
|
|
4923
|
+
const wasPartiallyConnected = this._state === "connected" || this._state === "joined";
|
|
4517
4924
|
if (this._intentionalDisconnect) {
|
|
4518
4925
|
this.cleanupFull();
|
|
4519
|
-
if (
|
|
4926
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4520
4927
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4521
4928
|
}
|
|
4522
4929
|
return;
|
|
4523
4930
|
}
|
|
4524
4931
|
const canReconnect = this._databaseId && this._connectOptions;
|
|
4525
|
-
if ((
|
|
4932
|
+
if ((wasAuthenticated || wasPartiallyConnected || wasReconnecting) && canReconnect) {
|
|
4526
4933
|
this._state = "reconnecting";
|
|
4527
4934
|
this.cleanupForReconnect();
|
|
4528
4935
|
this.reconnectingCallbacks.forEach((cb) => cb());
|
|
4529
|
-
if (
|
|
4936
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4530
4937
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4531
4938
|
}
|
|
4532
4939
|
this.scheduleReconnect();
|
|
4533
4940
|
} else {
|
|
4534
4941
|
this.cleanupFull();
|
|
4535
|
-
if (
|
|
4942
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4536
4943
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4537
4944
|
}
|
|
4538
4945
|
}
|
|
@@ -4543,10 +4950,48 @@ var LarkDatabase = class {
|
|
|
4543
4950
|
// ============================================
|
|
4544
4951
|
// Internal: Sending Messages
|
|
4545
4952
|
// ============================================
|
|
4953
|
+
/**
|
|
4954
|
+
* Check if authenticated synchronously.
|
|
4955
|
+
* Returns true if authenticated, false if connecting (should wait), throws if disconnected.
|
|
4956
|
+
*/
|
|
4957
|
+
isAuthenticatedOrThrow() {
|
|
4958
|
+
if (this._state === "authenticated") {
|
|
4959
|
+
return true;
|
|
4960
|
+
}
|
|
4961
|
+
if (this._state === "connecting" || this._state === "connected" || this._state === "joined" || this._state === "reconnecting") {
|
|
4962
|
+
return false;
|
|
4963
|
+
}
|
|
4964
|
+
throw new LarkError("not_connected", "Not connected - call connect() first");
|
|
4965
|
+
}
|
|
4966
|
+
/**
|
|
4967
|
+
* Wait for authentication to complete before performing an operation.
|
|
4968
|
+
* If already authenticated, returns immediately (synchronously).
|
|
4969
|
+
* If connecting/reconnecting, waits for auth to complete.
|
|
4970
|
+
* If disconnected and no connect in progress, throws.
|
|
4971
|
+
*
|
|
4972
|
+
* IMPORTANT: This returns a Promise only if waiting is needed.
|
|
4973
|
+
* Callers should use: `if (!this.isAuthenticatedOrThrow()) if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();`
|
|
4974
|
+
* to preserve synchronous execution when already authenticated.
|
|
4975
|
+
*/
|
|
4976
|
+
async waitForAuthenticated() {
|
|
4977
|
+
if (this._state === "authenticated") {
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4980
|
+
if (this._state === "connecting" || this._state === "connected" || this._state === "joined" || this._state === "reconnecting") {
|
|
4981
|
+
if (this.authenticationPromise) {
|
|
4982
|
+
await this.authenticationPromise;
|
|
4983
|
+
return;
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4986
|
+
throw new LarkError("not_connected", "Not connected - call connect() first");
|
|
4987
|
+
}
|
|
4546
4988
|
send(message) {
|
|
4547
4989
|
if (!this.transport || !this.transport.connected) {
|
|
4548
4990
|
throw new LarkError("not_connected", "Not connected to database");
|
|
4549
4991
|
}
|
|
4992
|
+
if (process.env.LARK_DEBUG) {
|
|
4993
|
+
console.log("[LARK] >>> CLIENT:", JSON.stringify(message, null, 2));
|
|
4994
|
+
}
|
|
4550
4995
|
this.transport.send(JSON.stringify(message));
|
|
4551
4996
|
}
|
|
4552
4997
|
/**
|
|
@@ -4554,6 +4999,7 @@ var LarkDatabase = class {
|
|
|
4554
4999
|
* Note: Priority is now part of the value (as .priority), not a separate field.
|
|
4555
5000
|
*/
|
|
4556
5001
|
async _sendSet(path, value) {
|
|
5002
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4557
5003
|
const normalizedPath = normalizePath(path) || "/";
|
|
4558
5004
|
validateWriteData(value, normalizedPath);
|
|
4559
5005
|
const requestId = this.messageQueue.nextRequestId();
|
|
@@ -4578,6 +5024,7 @@ var LarkDatabase = class {
|
|
|
4578
5024
|
* @internal Send an update operation.
|
|
4579
5025
|
*/
|
|
4580
5026
|
async _sendUpdate(path, values) {
|
|
5027
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4581
5028
|
const normalizedPath = normalizePath(path) || "/";
|
|
4582
5029
|
for (const [key, value] of Object.entries(values)) {
|
|
4583
5030
|
const fullPath = key.startsWith("/") ? key : `${normalizedPath}/${key}`;
|
|
@@ -4609,6 +5056,7 @@ var LarkDatabase = class {
|
|
|
4609
5056
|
* @internal Send a delete operation.
|
|
4610
5057
|
*/
|
|
4611
5058
|
async _sendDelete(path) {
|
|
5059
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4612
5060
|
const normalizedPath = normalizePath(path) || "/";
|
|
4613
5061
|
const requestId = this.messageQueue.nextRequestId();
|
|
4614
5062
|
const pendingWriteIds = this.subscriptionManager.getPendingWriteIdsForPath(normalizedPath);
|
|
@@ -4644,7 +5092,7 @@ var LarkDatabase = class {
|
|
|
4644
5092
|
_sendVolatileSet(path, value) {
|
|
4645
5093
|
const normalizedPath = normalizePath(path) || "/";
|
|
4646
5094
|
this.subscriptionManager.applyOptimisticWrite(normalizedPath, value, "", "set");
|
|
4647
|
-
if (!this.transport || !this.transport.connected) {
|
|
5095
|
+
if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
|
|
4648
5096
|
return;
|
|
4649
5097
|
}
|
|
4650
5098
|
const message = {
|
|
@@ -4660,7 +5108,7 @@ var LarkDatabase = class {
|
|
|
4660
5108
|
_sendVolatileUpdate(path, values) {
|
|
4661
5109
|
const normalizedPath = normalizePath(path) || "/";
|
|
4662
5110
|
this.subscriptionManager.applyOptimisticWrite(normalizedPath, values, "", "update");
|
|
4663
|
-
if (!this.transport || !this.transport.connected) {
|
|
5111
|
+
if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
|
|
4664
5112
|
return;
|
|
4665
5113
|
}
|
|
4666
5114
|
const message = {
|
|
@@ -4676,7 +5124,7 @@ var LarkDatabase = class {
|
|
|
4676
5124
|
_sendVolatileDelete(path) {
|
|
4677
5125
|
const normalizedPath = normalizePath(path) || "/";
|
|
4678
5126
|
this.subscriptionManager.applyOptimisticWrite(normalizedPath, null, "", "delete");
|
|
4679
|
-
if (!this.transport || !this.transport.connected) {
|
|
5127
|
+
if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
|
|
4680
5128
|
return;
|
|
4681
5129
|
}
|
|
4682
5130
|
const message = {
|
|
@@ -4715,6 +5163,7 @@ var LarkDatabase = class {
|
|
|
4715
5163
|
return new DataSnapshot(cached.value, path, this);
|
|
4716
5164
|
}
|
|
4717
5165
|
}
|
|
5166
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4718
5167
|
const requestId = this.messageQueue.nextRequestId();
|
|
4719
5168
|
const message = {
|
|
4720
5169
|
o: "o",
|
|
@@ -4731,6 +5180,7 @@ var LarkDatabase = class {
|
|
|
4731
5180
|
* @internal Send an onDisconnect operation.
|
|
4732
5181
|
*/
|
|
4733
5182
|
async _sendOnDisconnect(path, action, value) {
|
|
5183
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4734
5184
|
const requestId = this.messageQueue.nextRequestId();
|
|
4735
5185
|
const message = {
|
|
4736
5186
|
o: "od",
|
|
@@ -4744,11 +5194,20 @@ var LarkDatabase = class {
|
|
|
4744
5194
|
this.send(message);
|
|
4745
5195
|
await this.messageQueue.registerRequest(requestId);
|
|
4746
5196
|
}
|
|
5197
|
+
/**
|
|
5198
|
+
* @internal Get a cached value from the subscription manager.
|
|
5199
|
+
* Used for optimistic writes where we need the current value without a network fetch.
|
|
5200
|
+
*/
|
|
5201
|
+
_getCachedValue(path) {
|
|
5202
|
+
const normalizedPath = normalizePath(path) || "/";
|
|
5203
|
+
return this.subscriptionManager.getCachedValue(normalizedPath);
|
|
5204
|
+
}
|
|
4747
5205
|
/**
|
|
4748
5206
|
* @internal Send a subscribe message to server.
|
|
4749
5207
|
* Includes tag for non-default queries to enable proper event routing.
|
|
4750
5208
|
*/
|
|
4751
5209
|
async sendSubscribeMessage(path, eventTypes, queryParams, tag) {
|
|
5210
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4752
5211
|
const requestId = this.messageQueue.nextRequestId();
|
|
4753
5212
|
const message = {
|
|
4754
5213
|
o: "sb",
|
|
@@ -4766,6 +5225,7 @@ var LarkDatabase = class {
|
|
|
4766
5225
|
* Includes query params and tag so server can identify which specific subscription to remove.
|
|
4767
5226
|
*/
|
|
4768
5227
|
async sendUnsubscribeMessage(path, queryParams, tag) {
|
|
5228
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4769
5229
|
const requestId = this.messageQueue.nextRequestId();
|
|
4770
5230
|
const message = {
|
|
4771
5231
|
o: "us",
|