@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.js
CHANGED
|
@@ -58,7 +58,8 @@ var ErrorCode = {
|
|
|
58
58
|
INVALID_OPERATION: "invalid_operation",
|
|
59
59
|
INTERNAL_ERROR: "internal_error",
|
|
60
60
|
CONDITION_FAILED: "condition_failed",
|
|
61
|
-
INVALID_QUERY: "invalid_query"
|
|
61
|
+
INVALID_QUERY: "invalid_query",
|
|
62
|
+
AUTH_REQUIRED: "auth_required"
|
|
62
63
|
};
|
|
63
64
|
var DEFAULT_COORDINATOR_URL = "https://db.lark.sh";
|
|
64
65
|
|
|
@@ -464,6 +465,9 @@ function isAckMessage(msg) {
|
|
|
464
465
|
function isJoinCompleteMessage(msg) {
|
|
465
466
|
return "jc" in msg;
|
|
466
467
|
}
|
|
468
|
+
function isAuthCompleteMessage(msg) {
|
|
469
|
+
return "ac" in msg;
|
|
470
|
+
}
|
|
467
471
|
function isNackMessage(msg) {
|
|
468
472
|
return "n" in msg;
|
|
469
473
|
}
|
|
@@ -529,6 +533,18 @@ var MessageQueue = class {
|
|
|
529
533
|
return true;
|
|
530
534
|
}
|
|
531
535
|
}
|
|
536
|
+
if (isAuthCompleteMessage(message)) {
|
|
537
|
+
const pending = this.pending.get(message.ac);
|
|
538
|
+
if (pending) {
|
|
539
|
+
clearTimeout(pending.timeout);
|
|
540
|
+
this.pending.delete(message.ac);
|
|
541
|
+
const response = {
|
|
542
|
+
uid: message.au || null
|
|
543
|
+
};
|
|
544
|
+
pending.resolve(response);
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
532
548
|
if (isAckMessage(message)) {
|
|
533
549
|
const pending = this.pending.get(message.a);
|
|
534
550
|
if (pending) {
|
|
@@ -552,7 +568,7 @@ var MessageQueue = class {
|
|
|
552
568
|
if (pending) {
|
|
553
569
|
clearTimeout(pending.timeout);
|
|
554
570
|
this.pending.delete(message.oc);
|
|
555
|
-
pending.resolve(message.ov);
|
|
571
|
+
pending.resolve(message.ov ?? null);
|
|
556
572
|
return true;
|
|
557
573
|
}
|
|
558
574
|
}
|
|
@@ -850,7 +866,7 @@ function getNestedValue(obj, path) {
|
|
|
850
866
|
}
|
|
851
867
|
function getSortValue(value, queryParams) {
|
|
852
868
|
if (!queryParams) {
|
|
853
|
-
return
|
|
869
|
+
return getNestedValue(value, ".priority");
|
|
854
870
|
}
|
|
855
871
|
if (queryParams.orderBy === "priority") {
|
|
856
872
|
return getNestedValue(value, ".priority");
|
|
@@ -865,13 +881,14 @@ function getSortValue(value, queryParams) {
|
|
|
865
881
|
return null;
|
|
866
882
|
}
|
|
867
883
|
const hasRangeFilter = queryParams.startAt !== void 0 || queryParams.startAfter !== void 0 || queryParams.endAt !== void 0 || queryParams.endBefore !== void 0 || queryParams.equalTo !== void 0;
|
|
868
|
-
|
|
884
|
+
const hasLimit = queryParams.limitToFirst !== void 0 || queryParams.limitToLast !== void 0;
|
|
885
|
+
if (hasRangeFilter || hasLimit) {
|
|
869
886
|
return getNestedValue(value, ".priority");
|
|
870
887
|
}
|
|
871
888
|
return null;
|
|
872
889
|
}
|
|
873
890
|
function compareEntries(a, b, queryParams) {
|
|
874
|
-
if (
|
|
891
|
+
if (queryParams?.orderBy === "key") {
|
|
875
892
|
return compareKeys(a.key, b.key);
|
|
876
893
|
}
|
|
877
894
|
const cmp = compareValues(a.sortValue, b.sortValue);
|
|
@@ -913,6 +930,39 @@ function getSortedKeys(data, queryParams) {
|
|
|
913
930
|
}
|
|
914
931
|
|
|
915
932
|
// src/connection/View.ts
|
|
933
|
+
function expandPathKeys(obj) {
|
|
934
|
+
const result = {};
|
|
935
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
936
|
+
if (key.includes("/")) {
|
|
937
|
+
const segments = key.split("/").filter((s) => s.length > 0);
|
|
938
|
+
let current = result;
|
|
939
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
940
|
+
const segment = segments[i];
|
|
941
|
+
if (!(segment in current) || typeof current[segment] !== "object" || current[segment] === null) {
|
|
942
|
+
current[segment] = {};
|
|
943
|
+
}
|
|
944
|
+
current = current[segment];
|
|
945
|
+
}
|
|
946
|
+
current[segments[segments.length - 1]] = value;
|
|
947
|
+
} else {
|
|
948
|
+
result[key] = value;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return result;
|
|
952
|
+
}
|
|
953
|
+
function deepMerge(target, source) {
|
|
954
|
+
const result = { ...target };
|
|
955
|
+
for (const [key, value] of Object.entries(source)) {
|
|
956
|
+
if (value === null) {
|
|
957
|
+
delete result[key];
|
|
958
|
+
} else if (value !== null && typeof value === "object" && !Array.isArray(value) && result[key] !== null && typeof result[key] === "object" && !Array.isArray(result[key])) {
|
|
959
|
+
result[key] = deepMerge(result[key], value);
|
|
960
|
+
} else {
|
|
961
|
+
result[key] = value;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return result;
|
|
965
|
+
}
|
|
916
966
|
var View = class {
|
|
917
967
|
constructor(path, queryParams) {
|
|
918
968
|
/** Event callbacks organized by event type */
|
|
@@ -1217,29 +1267,19 @@ var View = class {
|
|
|
1217
1267
|
return this.setAtPath(base, relativePath, value);
|
|
1218
1268
|
}
|
|
1219
1269
|
if (operation === "update") {
|
|
1270
|
+
const expanded = expandPathKeys(value);
|
|
1220
1271
|
if (relativePath === "/") {
|
|
1221
1272
|
if (base === null || base === void 0 || typeof base !== "object") {
|
|
1222
1273
|
base = {};
|
|
1223
1274
|
}
|
|
1224
|
-
|
|
1225
|
-
for (const key of Object.keys(merged2)) {
|
|
1226
|
-
if (merged2[key] === null) {
|
|
1227
|
-
delete merged2[key];
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
return merged2;
|
|
1275
|
+
return deepMerge(base, expanded);
|
|
1231
1276
|
}
|
|
1232
1277
|
const current = getValueAtPath(base, relativePath);
|
|
1233
1278
|
let merged;
|
|
1234
|
-
if (current && typeof current === "object") {
|
|
1235
|
-
merged =
|
|
1236
|
-
for (const key of Object.keys(merged)) {
|
|
1237
|
-
if (merged[key] === null) {
|
|
1238
|
-
delete merged[key];
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1279
|
+
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
1280
|
+
merged = deepMerge(current, expanded);
|
|
1241
1281
|
} else {
|
|
1242
|
-
merged =
|
|
1282
|
+
merged = expanded;
|
|
1243
1283
|
}
|
|
1244
1284
|
return this.setAtPath(base, relativePath, merged);
|
|
1245
1285
|
}
|
|
@@ -1329,7 +1369,7 @@ var View = class {
|
|
|
1329
1369
|
return { value: displayCache, found: true };
|
|
1330
1370
|
}
|
|
1331
1371
|
if (this._hasReceivedInitialSnapshot || this._pendingWriteData.length > 0) {
|
|
1332
|
-
return { value: displayCache, found: true };
|
|
1372
|
+
return { value: displayCache ?? null, found: true };
|
|
1333
1373
|
}
|
|
1334
1374
|
return { value: void 0, found: false };
|
|
1335
1375
|
}
|
|
@@ -1337,7 +1377,7 @@ var View = class {
|
|
|
1337
1377
|
const relativePath = this.path === "/" ? normalized : normalized.slice(this.path.length);
|
|
1338
1378
|
if (this._hasReceivedInitialSnapshot || this._pendingWriteData.length > 0) {
|
|
1339
1379
|
const extractedValue = getValueAtPath(displayCache, relativePath);
|
|
1340
|
-
return { value: extractedValue, found: true };
|
|
1380
|
+
return { value: extractedValue ?? null, found: true };
|
|
1341
1381
|
}
|
|
1342
1382
|
}
|
|
1343
1383
|
return { value: void 0, found: false };
|
|
@@ -1430,6 +1470,17 @@ var View = class {
|
|
|
1430
1470
|
if (!this.queryParams) return false;
|
|
1431
1471
|
return !!(this.queryParams.limitToFirst || this.queryParams.limitToLast);
|
|
1432
1472
|
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Check if this View loads all data (no limits, no range filters).
|
|
1475
|
+
* A View that loads all data can serve as a "complete" cache for child paths.
|
|
1476
|
+
* This matches Firebase's loadsAllData() semantics.
|
|
1477
|
+
*/
|
|
1478
|
+
loadsAllData() {
|
|
1479
|
+
if (!this.queryParams) return true;
|
|
1480
|
+
const hasLimit = !!(this.queryParams.limitToFirst || this.queryParams.limitToLast);
|
|
1481
|
+
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;
|
|
1482
|
+
return !hasLimit && !hasRangeFilter;
|
|
1483
|
+
}
|
|
1433
1484
|
// ============================================
|
|
1434
1485
|
// Pending Writes (for local-first)
|
|
1435
1486
|
// ============================================
|
|
@@ -1602,10 +1653,13 @@ var SubscriptionManager = class {
|
|
|
1602
1653
|
this.unsubscribeCallback(normalizedPath, eventType, callback, queryId);
|
|
1603
1654
|
};
|
|
1604
1655
|
if (isNewView || isNewEventType || queryParamsChanged) {
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1656
|
+
const hasAncestorComplete = this.hasAncestorCompleteView(normalizedPath);
|
|
1657
|
+
if (!hasAncestorComplete) {
|
|
1658
|
+
const allEventTypes = view.getEventTypes();
|
|
1659
|
+
this.sendSubscribe?.(normalizedPath, allEventTypes, view.queryParams ?? void 0, tag).catch((err) => {
|
|
1660
|
+
console.error("Failed to subscribe:", err);
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1609
1663
|
}
|
|
1610
1664
|
if (!isNewView && view.hasReceivedInitialSnapshot) {
|
|
1611
1665
|
this.fireInitialEventsToCallback(view, eventType, callback);
|
|
@@ -1833,18 +1887,23 @@ var SubscriptionManager = class {
|
|
|
1833
1887
|
}
|
|
1834
1888
|
}
|
|
1835
1889
|
/**
|
|
1836
|
-
* Detect and fire child_moved events for children that changed position.
|
|
1890
|
+
* Detect and fire child_moved events for children that changed position OR sort value.
|
|
1891
|
+
*
|
|
1892
|
+
* Firebase fires child_moved for ANY priority/sort value change, regardless of whether
|
|
1893
|
+
* the position actually changes. This is Case 2003 behavior.
|
|
1837
1894
|
*
|
|
1838
|
-
* IMPORTANT: child_moved should only fire for children whose VALUE changed
|
|
1839
|
-
*
|
|
1840
|
-
* another child moving should NOT fire child_moved.
|
|
1895
|
+
* IMPORTANT: child_moved should only fire for children whose VALUE or PRIORITY changed.
|
|
1896
|
+
* Children that are merely "displaced" by another child moving should NOT fire child_moved.
|
|
1841
1897
|
*
|
|
1842
1898
|
* @param affectedChildren - Only check these children for moves. If not provided,
|
|
1843
1899
|
* checks all children (for full snapshots where we compare values).
|
|
1900
|
+
* @param previousDisplayCache - Previous display cache for comparing sort values
|
|
1901
|
+
* @param currentDisplayCache - Current display cache for comparing sort values
|
|
1844
1902
|
*/
|
|
1845
|
-
detectAndFireMoves(view, previousOrder, currentOrder, previousPositions, currentPositions, previousChildSet, currentChildSet, childMovedSubs, isVolatile, serverTimestamp, affectedChildren) {
|
|
1903
|
+
detectAndFireMoves(view, previousOrder, currentOrder, previousPositions, currentPositions, previousChildSet, currentChildSet, childMovedSubs, isVolatile, serverTimestamp, affectedChildren, previousDisplayCache, currentDisplayCache) {
|
|
1846
1904
|
if (childMovedSubs.length === 0) return;
|
|
1847
1905
|
const childrenToCheck = affectedChildren ?? new Set(currentOrder);
|
|
1906
|
+
const queryParams = view.queryParams;
|
|
1848
1907
|
for (const key of childrenToCheck) {
|
|
1849
1908
|
if (!previousChildSet.has(key) || !currentChildSet.has(key)) {
|
|
1850
1909
|
continue;
|
|
@@ -1856,7 +1915,24 @@ var SubscriptionManager = class {
|
|
|
1856
1915
|
}
|
|
1857
1916
|
const oldPrevKey = oldPos > 0 ? previousOrder[oldPos - 1] : null;
|
|
1858
1917
|
const newPrevKey = newPos > 0 ? currentOrder[newPos - 1] : null;
|
|
1859
|
-
|
|
1918
|
+
let positionChanged = oldPrevKey !== newPrevKey;
|
|
1919
|
+
let sortValueChanged = false;
|
|
1920
|
+
let isPriorityOrdering = false;
|
|
1921
|
+
if (previousDisplayCache && currentDisplayCache) {
|
|
1922
|
+
const prevValue = previousDisplayCache[key];
|
|
1923
|
+
const currValue = currentDisplayCache[key];
|
|
1924
|
+
const prevSortValue = getSortValue(prevValue, queryParams);
|
|
1925
|
+
const currSortValue = getSortValue(currValue, queryParams);
|
|
1926
|
+
sortValueChanged = JSON.stringify(prevSortValue) !== JSON.stringify(currSortValue);
|
|
1927
|
+
isPriorityOrdering = !queryParams?.orderBy || queryParams.orderBy === "priority";
|
|
1928
|
+
}
|
|
1929
|
+
let shouldFire;
|
|
1930
|
+
if (affectedChildren) {
|
|
1931
|
+
shouldFire = positionChanged || isPriorityOrdering && sortValueChanged;
|
|
1932
|
+
} else {
|
|
1933
|
+
shouldFire = isPriorityOrdering && sortValueChanged;
|
|
1934
|
+
}
|
|
1935
|
+
if (shouldFire) {
|
|
1860
1936
|
this.fireChildMoved(view, key, childMovedSubs, newPrevKey, isVolatile, serverTimestamp);
|
|
1861
1937
|
}
|
|
1862
1938
|
}
|
|
@@ -1900,10 +1976,10 @@ var SubscriptionManager = class {
|
|
|
1900
1976
|
/**
|
|
1901
1977
|
* Fire child_removed callbacks for a child key.
|
|
1902
1978
|
*/
|
|
1903
|
-
fireChildRemoved(view, childKey, subs, isVolatile, serverTimestamp) {
|
|
1979
|
+
fireChildRemoved(view, childKey, subs, isVolatile, serverTimestamp, previousValue) {
|
|
1904
1980
|
if (subs.length === 0) return;
|
|
1905
1981
|
const childPath = joinPath(view.path, childKey);
|
|
1906
|
-
const snapshot = this.createSnapshot?.(childPath, null, isVolatile, serverTimestamp);
|
|
1982
|
+
const snapshot = this.createSnapshot?.(childPath, previousValue ?? null, isVolatile, serverTimestamp);
|
|
1907
1983
|
if (snapshot) {
|
|
1908
1984
|
for (const entry of subs) {
|
|
1909
1985
|
try {
|
|
@@ -1932,6 +2008,30 @@ var SubscriptionManager = class {
|
|
|
1932
2008
|
}
|
|
1933
2009
|
}
|
|
1934
2010
|
}
|
|
2011
|
+
/**
|
|
2012
|
+
* Handle subscription revocation due to auth change.
|
|
2013
|
+
* The server has already removed the subscription, so we just clean up locally.
|
|
2014
|
+
* This is different from unsubscribeAll which sends an unsubscribe message.
|
|
2015
|
+
*/
|
|
2016
|
+
handleSubscriptionRevoked(path) {
|
|
2017
|
+
const normalizedPath = path;
|
|
2018
|
+
const queryIds = this.pathToQueryIds.get(normalizedPath);
|
|
2019
|
+
if (!queryIds || queryIds.size === 0) return;
|
|
2020
|
+
for (const queryId of queryIds) {
|
|
2021
|
+
const viewKey = this.makeViewKey(normalizedPath, queryId);
|
|
2022
|
+
const view = this.views.get(viewKey);
|
|
2023
|
+
if (view) {
|
|
2024
|
+
const tag = this.viewKeyToTag.get(viewKey);
|
|
2025
|
+
if (tag !== void 0) {
|
|
2026
|
+
this.tagToViewKey.delete(tag);
|
|
2027
|
+
this.viewKeyToTag.delete(viewKey);
|
|
2028
|
+
}
|
|
2029
|
+
view.clear();
|
|
2030
|
+
this.views.delete(viewKey);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
this.pathToQueryIds.delete(normalizedPath);
|
|
2034
|
+
}
|
|
1935
2035
|
/**
|
|
1936
2036
|
* Clear all subscriptions (e.g., on disconnect).
|
|
1937
2037
|
*/
|
|
@@ -1953,35 +2053,71 @@ var SubscriptionManager = class {
|
|
|
1953
2053
|
return queryIds !== void 0 && queryIds.size > 0;
|
|
1954
2054
|
}
|
|
1955
2055
|
/**
|
|
1956
|
-
* Check if a path is "covered" by an active subscription.
|
|
2056
|
+
* Check if a path is "covered" by an active subscription that has received data.
|
|
1957
2057
|
*
|
|
1958
2058
|
* A path is covered if:
|
|
1959
|
-
* - There's an active
|
|
1960
|
-
* - There's an active
|
|
2059
|
+
* - There's an active subscription at that exact path that has data, OR
|
|
2060
|
+
* - There's an active subscription at an ancestor path that has data
|
|
2061
|
+
*
|
|
2062
|
+
* Note: Any subscription type (value, child_added, child_moved, etc.) receives
|
|
2063
|
+
* the initial snapshot from the server and thus has cached data.
|
|
1961
2064
|
*/
|
|
1962
2065
|
isPathCovered(path) {
|
|
1963
2066
|
const normalized = normalizePath(path);
|
|
1964
|
-
if (this.
|
|
2067
|
+
if (this.hasActiveSubscriptionWithData(normalized)) {
|
|
1965
2068
|
return true;
|
|
1966
2069
|
}
|
|
1967
2070
|
const segments = normalized.split("/").filter((s) => s.length > 0);
|
|
1968
2071
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
1969
2072
|
const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
1970
|
-
if (this.
|
|
2073
|
+
if (this.hasActiveSubscriptionWithData(ancestorPath)) {
|
|
1971
2074
|
return true;
|
|
1972
2075
|
}
|
|
1973
2076
|
}
|
|
1974
|
-
if (normalized !== "/" && this.
|
|
2077
|
+
if (normalized !== "/" && this.hasActiveSubscriptionWithData("/")) {
|
|
1975
2078
|
return true;
|
|
1976
2079
|
}
|
|
1977
2080
|
return false;
|
|
1978
2081
|
}
|
|
1979
2082
|
/**
|
|
1980
|
-
* Check if there's
|
|
2083
|
+
* Check if there's an active subscription at a path that has data.
|
|
2084
|
+
* A View has data if it has received the initial snapshot OR has pending writes.
|
|
2085
|
+
* Any subscription type (value, child_added, child_moved, etc.) counts.
|
|
1981
2086
|
*/
|
|
1982
|
-
|
|
2087
|
+
hasActiveSubscriptionWithData(path) {
|
|
1983
2088
|
const views = this.getViewsAtPath(path);
|
|
1984
|
-
return views.some((view) => view.
|
|
2089
|
+
return views.some((view) => view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites()));
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* Check if any ancestor path has a "complete" View (one that loadsAllData).
|
|
2093
|
+
* A complete View has no limits and no range filters, so it contains all data
|
|
2094
|
+
* for its subtree. Child subscriptions can use the ancestor's data instead
|
|
2095
|
+
* of creating their own server subscription.
|
|
2096
|
+
*
|
|
2097
|
+
* This matches Firebase's behavior where child listeners don't need their own
|
|
2098
|
+
* server subscription if an ancestor has an unlimited listener.
|
|
2099
|
+
*/
|
|
2100
|
+
hasAncestorCompleteView(path) {
|
|
2101
|
+
const normalized = normalizePath(path);
|
|
2102
|
+
const segments = normalized.split("/").filter((s) => s.length > 0);
|
|
2103
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
2104
|
+
const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
2105
|
+
const views = this.getViewsAtPath(ancestorPath);
|
|
2106
|
+
for (const view of views) {
|
|
2107
|
+
if (view.hasCallbacks() && view.loadsAllData() && view.hasReceivedInitialSnapshot) {
|
|
2108
|
+
return true;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
if (normalized !== "/") {
|
|
2113
|
+
const rootViews = this.getViewsAtPath("/");
|
|
2114
|
+
for (const view of rootViews) {
|
|
2115
|
+
if (view.hasCallbacks() && view.loadsAllData() && view.hasReceivedInitialSnapshot) {
|
|
2116
|
+
return true;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return false;
|
|
1985
2121
|
}
|
|
1986
2122
|
/**
|
|
1987
2123
|
* Get a cached value if the path is covered by an active subscription.
|
|
@@ -2004,7 +2140,7 @@ var SubscriptionManager = class {
|
|
|
2004
2140
|
const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
|
|
2005
2141
|
const ancestorViews = this.getViewsAtPath(ancestorPath);
|
|
2006
2142
|
for (const view of ancestorViews) {
|
|
2007
|
-
if (view.
|
|
2143
|
+
if (view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites())) {
|
|
2008
2144
|
const result = view.getCacheValue(normalized);
|
|
2009
2145
|
if (result.found) {
|
|
2010
2146
|
return result;
|
|
@@ -2015,7 +2151,7 @@ var SubscriptionManager = class {
|
|
|
2015
2151
|
if (normalized !== "/") {
|
|
2016
2152
|
const rootViews = this.getViewsAtPath("/");
|
|
2017
2153
|
for (const view of rootViews) {
|
|
2018
|
-
if (view.
|
|
2154
|
+
if (view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites())) {
|
|
2019
2155
|
const result = view.getCacheValue(normalized);
|
|
2020
2156
|
if (result.found) {
|
|
2021
2157
|
return result;
|
|
@@ -2124,8 +2260,11 @@ var SubscriptionManager = class {
|
|
|
2124
2260
|
const previousChildSet = new Set(previousOrder);
|
|
2125
2261
|
const isFirstSnapshot = !view.hasReceivedInitialSnapshot && !view.hasPendingWrites();
|
|
2126
2262
|
let previousCacheJson = null;
|
|
2263
|
+
let previousDisplayCache = null;
|
|
2127
2264
|
if (!isVolatile) {
|
|
2128
|
-
|
|
2265
|
+
const cache = view.getDisplayCache();
|
|
2266
|
+
previousDisplayCache = cache && typeof cache === "object" && !Array.isArray(cache) ? cache : null;
|
|
2267
|
+
previousCacheJson = this.serializeCacheForComparison(cache);
|
|
2129
2268
|
}
|
|
2130
2269
|
const affectedChildren = /* @__PURE__ */ new Set();
|
|
2131
2270
|
let isFullSnapshot = false;
|
|
@@ -2183,7 +2322,8 @@ var SubscriptionManager = class {
|
|
|
2183
2322
|
}
|
|
2184
2323
|
for (const key of previousOrder) {
|
|
2185
2324
|
if (!currentChildSet.has(key)) {
|
|
2186
|
-
|
|
2325
|
+
const prevValue = previousDisplayCache?.[key];
|
|
2326
|
+
this.fireChildRemoved(view, key, childRemovedSubs, isVolatile, serverTimestamp, prevValue);
|
|
2187
2327
|
}
|
|
2188
2328
|
}
|
|
2189
2329
|
} else {
|
|
@@ -2194,7 +2334,8 @@ var SubscriptionManager = class {
|
|
|
2194
2334
|
const prevKey = view.getPreviousChildKey(childKey);
|
|
2195
2335
|
this.fireChildAdded(view, childKey, childAddedSubs, prevKey, isVolatile, serverTimestamp);
|
|
2196
2336
|
} else if (wasPresent && !isPresent) {
|
|
2197
|
-
|
|
2337
|
+
const prevValue = previousDisplayCache?.[childKey];
|
|
2338
|
+
this.fireChildRemoved(view, childKey, childRemovedSubs, isVolatile, serverTimestamp, prevValue);
|
|
2198
2339
|
} else if (wasPresent && isPresent) {
|
|
2199
2340
|
const prevKey = view.getPreviousChildKey(childKey);
|
|
2200
2341
|
this.fireChildChanged(view, childKey, childChangedSubs, prevKey, isVolatile, serverTimestamp);
|
|
@@ -2205,6 +2346,8 @@ var SubscriptionManager = class {
|
|
|
2205
2346
|
previousOrder.forEach((key, idx) => previousPositions.set(key, idx));
|
|
2206
2347
|
const currentPositions = /* @__PURE__ */ new Map();
|
|
2207
2348
|
currentOrder.forEach((key, idx) => currentPositions.set(key, idx));
|
|
2349
|
+
const currentCache = view.getDisplayCache();
|
|
2350
|
+
const currentDisplayCache = currentCache && typeof currentCache === "object" && !Array.isArray(currentCache) ? currentCache : null;
|
|
2208
2351
|
this.detectAndFireMoves(
|
|
2209
2352
|
view,
|
|
2210
2353
|
previousOrder,
|
|
@@ -2216,7 +2359,9 @@ var SubscriptionManager = class {
|
|
|
2216
2359
|
childMovedSubs,
|
|
2217
2360
|
isVolatile,
|
|
2218
2361
|
serverTimestamp,
|
|
2219
|
-
isFullSnapshot ? void 0 : affectedChildren
|
|
2362
|
+
isFullSnapshot ? void 0 : affectedChildren,
|
|
2363
|
+
previousDisplayCache,
|
|
2364
|
+
currentDisplayCache
|
|
2220
2365
|
);
|
|
2221
2366
|
}
|
|
2222
2367
|
// ============================================
|
|
@@ -2235,8 +2380,12 @@ var SubscriptionManager = class {
|
|
|
2235
2380
|
views.push(view);
|
|
2236
2381
|
} else if (normalized.startsWith(viewPath + "/")) {
|
|
2237
2382
|
views.push(view);
|
|
2383
|
+
} else if (viewPath.startsWith(normalized + "/")) {
|
|
2384
|
+
views.push(view);
|
|
2238
2385
|
} else if (viewPath === "/") {
|
|
2239
2386
|
views.push(view);
|
|
2387
|
+
} else if (normalized === "/") {
|
|
2388
|
+
views.push(view);
|
|
2240
2389
|
}
|
|
2241
2390
|
}
|
|
2242
2391
|
return views;
|
|
@@ -2300,7 +2449,48 @@ var SubscriptionManager = class {
|
|
|
2300
2449
|
}
|
|
2301
2450
|
}
|
|
2302
2451
|
}
|
|
2303
|
-
this.
|
|
2452
|
+
this.fireChildEventsForAck(view, previousOrder, previousChildSet, currentOrder, currentChildSet, previousDisplayCache);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Fire child events for ACK handling, skipping child_moved.
|
|
2457
|
+
* This is a variant of fireChildEvents that doesn't fire moves because:
|
|
2458
|
+
* 1. Moves were already fired optimistically
|
|
2459
|
+
* 2. If server modifies data, PUT event will fire correct moves
|
|
2460
|
+
* 3. ACK can arrive before PUT, causing incorrect intermediate state
|
|
2461
|
+
*/
|
|
2462
|
+
fireChildEventsForAck(view, previousOrder, previousChildSet, currentOrder, currentChildSet, previousDisplayCache) {
|
|
2463
|
+
const childAddedSubs = view.getCallbacks("child_added");
|
|
2464
|
+
const childChangedSubs = view.getCallbacks("child_changed");
|
|
2465
|
+
const childRemovedSubs = view.getCallbacks("child_removed");
|
|
2466
|
+
if (childAddedSubs.length === 0 && childChangedSubs.length === 0 && childRemovedSubs.length === 0) {
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
const displayCache = view.getDisplayCache();
|
|
2470
|
+
for (const key of currentOrder) {
|
|
2471
|
+
if (!previousChildSet.has(key)) {
|
|
2472
|
+
if (childAddedSubs.length > 0 && displayCache) {
|
|
2473
|
+
const prevKey = view.getPreviousChildKey(key);
|
|
2474
|
+
this.fireChildAdded(view, key, childAddedSubs, prevKey, false, void 0);
|
|
2475
|
+
}
|
|
2476
|
+
} else if (previousDisplayCache && childChangedSubs.length > 0 && displayCache) {
|
|
2477
|
+
const prevValue = previousDisplayCache[key];
|
|
2478
|
+
const currentValue = displayCache[key];
|
|
2479
|
+
const prevJson = this.serializeCacheForComparison(prevValue);
|
|
2480
|
+
const currJson = this.serializeCacheForComparison(currentValue);
|
|
2481
|
+
if (prevJson !== currJson) {
|
|
2482
|
+
const prevKey = view.getPreviousChildKey(key);
|
|
2483
|
+
this.fireChildChanged(view, key, childChangedSubs, prevKey, false, void 0);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
for (const key of previousOrder) {
|
|
2488
|
+
if (!currentChildSet.has(key)) {
|
|
2489
|
+
if (childRemovedSubs.length > 0) {
|
|
2490
|
+
const prevValue = previousDisplayCache?.[key];
|
|
2491
|
+
this.fireChildRemoved(view, key, childRemovedSubs, false, void 0, prevValue);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2304
2494
|
}
|
|
2305
2495
|
}
|
|
2306
2496
|
/**
|
|
@@ -2369,18 +2559,28 @@ var SubscriptionManager = class {
|
|
|
2369
2559
|
const updatedViews = [];
|
|
2370
2560
|
for (const view of affectedViews) {
|
|
2371
2561
|
let relativePath;
|
|
2562
|
+
let effectiveValue = value;
|
|
2372
2563
|
if (normalized === view.path) {
|
|
2373
2564
|
relativePath = "/";
|
|
2374
2565
|
} else if (view.path === "/") {
|
|
2375
2566
|
relativePath = normalized;
|
|
2376
|
-
} else {
|
|
2567
|
+
} else if (normalized.startsWith(view.path + "/")) {
|
|
2377
2568
|
relativePath = normalized.slice(view.path.length);
|
|
2569
|
+
} else if (view.path.startsWith(normalized + "/")) {
|
|
2570
|
+
const pathDiff = view.path.slice(normalized.length);
|
|
2571
|
+
effectiveValue = getValueAtPath(value, pathDiff);
|
|
2572
|
+
if (operation === "update" && effectiveValue === void 0) {
|
|
2573
|
+
continue;
|
|
2574
|
+
}
|
|
2575
|
+
relativePath = "/";
|
|
2576
|
+
} else {
|
|
2577
|
+
continue;
|
|
2378
2578
|
}
|
|
2379
2579
|
const previousDisplayCache = view.getDisplayCache();
|
|
2380
2580
|
const previousOrder = view.orderedChildren;
|
|
2381
2581
|
const previousChildSet = new Set(previousOrder);
|
|
2382
2582
|
const previousCacheJson = this.serializeCacheForComparison(previousDisplayCache);
|
|
2383
|
-
view.addPendingWriteData(requestId, relativePath,
|
|
2583
|
+
view.addPendingWriteData(requestId, relativePath, effectiveValue, operation);
|
|
2384
2584
|
const currentOrder = view.orderedChildren;
|
|
2385
2585
|
const currentChildSet = new Set(currentOrder);
|
|
2386
2586
|
const currentCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
|
|
@@ -2477,7 +2677,8 @@ var SubscriptionManager = class {
|
|
|
2477
2677
|
for (const key of previousOrder) {
|
|
2478
2678
|
if (!currentChildSet.has(key)) {
|
|
2479
2679
|
if (childRemovedSubs.length > 0) {
|
|
2480
|
-
const
|
|
2680
|
+
const prevValue = previousDisplayCache?.[key];
|
|
2681
|
+
const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue ?? null, isVolatile, serverTimestamp);
|
|
2481
2682
|
if (snapshot) {
|
|
2482
2683
|
for (const entry of childRemovedSubs) {
|
|
2483
2684
|
try {
|
|
@@ -2491,6 +2692,23 @@ var SubscriptionManager = class {
|
|
|
2491
2692
|
}
|
|
2492
2693
|
}
|
|
2493
2694
|
} else {
|
|
2695
|
+
if (childRemovedSubs.length > 0) {
|
|
2696
|
+
for (const key of previousOrder) {
|
|
2697
|
+
if (affectedChildren.has(key)) continue;
|
|
2698
|
+
if (currentChildSet.has(key)) continue;
|
|
2699
|
+
const prevValue = previousDisplayCache ? previousDisplayCache[key] : null;
|
|
2700
|
+
const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue, isVolatile, serverTimestamp);
|
|
2701
|
+
if (snapshot) {
|
|
2702
|
+
for (const entry of childRemovedSubs) {
|
|
2703
|
+
try {
|
|
2704
|
+
entry.callback(snapshot, void 0);
|
|
2705
|
+
} catch (err) {
|
|
2706
|
+
console.error("Error in child_removed callback:", err);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2494
2712
|
for (const key of affectedChildren) {
|
|
2495
2713
|
const wasPresent = previousChildSet.has(key);
|
|
2496
2714
|
const isPresent = currentChildSet.has(key);
|
|
@@ -2516,7 +2734,8 @@ var SubscriptionManager = class {
|
|
|
2516
2734
|
}
|
|
2517
2735
|
} else if (wasPresent && !isPresent) {
|
|
2518
2736
|
if (childRemovedSubs.length > 0) {
|
|
2519
|
-
const
|
|
2737
|
+
const prevValue = previousDisplayCache ? previousDisplayCache[key] : null;
|
|
2738
|
+
const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue, isVolatile, serverTimestamp);
|
|
2520
2739
|
if (snapshot) {
|
|
2521
2740
|
for (const entry of childRemovedSubs) {
|
|
2522
2741
|
try {
|
|
@@ -2549,18 +2768,24 @@ var SubscriptionManager = class {
|
|
|
2549
2768
|
}
|
|
2550
2769
|
}
|
|
2551
2770
|
}
|
|
2552
|
-
if (
|
|
2553
|
-
for (const key of
|
|
2771
|
+
if (childAddedSubs.length > 0 && displayCache) {
|
|
2772
|
+
for (const key of currentOrder) {
|
|
2773
|
+
if (previousChildSet.has(key)) continue;
|
|
2554
2774
|
if (affectedChildren.has(key)) continue;
|
|
2555
|
-
|
|
2556
|
-
const
|
|
2557
|
-
|
|
2775
|
+
const childValue = displayCache[key];
|
|
2776
|
+
const snapshot = this.createSnapshot?.(
|
|
2777
|
+
joinPath(view.path, key),
|
|
2778
|
+
childValue,
|
|
2779
|
+
isVolatile,
|
|
2780
|
+
serverTimestamp
|
|
2781
|
+
);
|
|
2558
2782
|
if (snapshot) {
|
|
2559
|
-
|
|
2783
|
+
const prevKey = view.getPreviousChildKey(key);
|
|
2784
|
+
for (const entry of childAddedSubs) {
|
|
2560
2785
|
try {
|
|
2561
|
-
entry.callback(snapshot,
|
|
2786
|
+
entry.callback(snapshot, prevKey);
|
|
2562
2787
|
} catch (err) {
|
|
2563
|
-
console.error("Error in
|
|
2788
|
+
console.error("Error in child_added callback:", err);
|
|
2564
2789
|
}
|
|
2565
2790
|
}
|
|
2566
2791
|
}
|
|
@@ -2583,7 +2808,9 @@ var SubscriptionManager = class {
|
|
|
2583
2808
|
childMovedSubs,
|
|
2584
2809
|
isVolatile,
|
|
2585
2810
|
serverTimestamp,
|
|
2586
|
-
isFullSnapshot ? void 0 : affectedChildren
|
|
2811
|
+
isFullSnapshot ? void 0 : affectedChildren,
|
|
2812
|
+
previousDisplayCache,
|
|
2813
|
+
displayCache
|
|
2587
2814
|
);
|
|
2588
2815
|
}
|
|
2589
2816
|
}
|
|
@@ -2806,26 +3033,26 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
2806
3033
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX] = this._query.orderByChildPath;
|
|
2807
3034
|
}
|
|
2808
3035
|
if (this._query.startAt !== void 0) {
|
|
2809
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAt.value;
|
|
3036
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAt.value ?? null;
|
|
2810
3037
|
if (this._query.startAt.key !== void 0) {
|
|
2811
3038
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.startAt.key;
|
|
2812
3039
|
}
|
|
2813
3040
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = true;
|
|
2814
3041
|
} else if (this._query.startAfter !== void 0) {
|
|
2815
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAfter.value;
|
|
3042
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAfter.value ?? null;
|
|
2816
3043
|
if (this._query.startAfter.key !== void 0) {
|
|
2817
3044
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.startAfter.key;
|
|
2818
3045
|
}
|
|
2819
3046
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = false;
|
|
2820
3047
|
}
|
|
2821
3048
|
if (this._query.endAt !== void 0) {
|
|
2822
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endAt.value;
|
|
3049
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endAt.value ?? null;
|
|
2823
3050
|
if (this._query.endAt.key !== void 0) {
|
|
2824
3051
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.endAt.key;
|
|
2825
3052
|
}
|
|
2826
3053
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_IS_INCLUSIVE] = true;
|
|
2827
3054
|
} else if (this._query.endBefore !== void 0) {
|
|
2828
|
-
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endBefore.value;
|
|
3055
|
+
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endBefore.value ?? null;
|
|
2829
3056
|
if (this._query.endBefore.key !== void 0) {
|
|
2830
3057
|
queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.endBefore.key;
|
|
2831
3058
|
}
|
|
@@ -2984,17 +3211,39 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
2984
3211
|
}
|
|
2985
3212
|
/**
|
|
2986
3213
|
* Set the priority of the data at this location.
|
|
2987
|
-
*
|
|
3214
|
+
* Uses cached value for optimistic behavior (local effects are immediate).
|
|
3215
|
+
* The optimistic update happens synchronously, Promise resolves after server ack.
|
|
2988
3216
|
*/
|
|
2989
|
-
|
|
3217
|
+
setPriority(priority) {
|
|
2990
3218
|
validateNotInfoPath(this._path, "setPriority");
|
|
2991
|
-
const
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
3219
|
+
const { value: cachedValue, found } = this._db._getCachedValue(this._path);
|
|
3220
|
+
if (!found) {
|
|
3221
|
+
return this.once().then((snapshot) => {
|
|
3222
|
+
const actualValue2 = snapshot.val();
|
|
3223
|
+
if (actualValue2 === null || actualValue2 === void 0) {
|
|
3224
|
+
return this._db._sendSet(this._path, { ".priority": priority });
|
|
3225
|
+
}
|
|
3226
|
+
return this.setWithPriority(actualValue2, priority);
|
|
3227
|
+
});
|
|
2996
3228
|
}
|
|
2997
|
-
|
|
3229
|
+
let actualValue;
|
|
3230
|
+
if (cachedValue === null || cachedValue === void 0) {
|
|
3231
|
+
actualValue = null;
|
|
3232
|
+
} else if (typeof cachedValue === "object" && !Array.isArray(cachedValue)) {
|
|
3233
|
+
const obj = cachedValue;
|
|
3234
|
+
if (".value" in obj && Object.keys(obj).every((k) => k === ".value" || k === ".priority")) {
|
|
3235
|
+
actualValue = obj[".value"];
|
|
3236
|
+
} else {
|
|
3237
|
+
const { ".priority": _oldPriority, ...rest } = obj;
|
|
3238
|
+
actualValue = Object.keys(rest).length > 0 ? rest : null;
|
|
3239
|
+
}
|
|
3240
|
+
} else {
|
|
3241
|
+
actualValue = cachedValue;
|
|
3242
|
+
}
|
|
3243
|
+
if (actualValue === null || actualValue === void 0) {
|
|
3244
|
+
return this._db._sendSet(this._path, { ".priority": priority });
|
|
3245
|
+
}
|
|
3246
|
+
return this.setWithPriority(actualValue, priority);
|
|
2998
3247
|
}
|
|
2999
3248
|
/**
|
|
3000
3249
|
* Atomically modify the data at this location using optimistic concurrency.
|
|
@@ -3024,6 +3273,7 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3024
3273
|
while (retries < maxRetries) {
|
|
3025
3274
|
const currentSnapshot = await this.once();
|
|
3026
3275
|
const currentValue = currentSnapshot.val();
|
|
3276
|
+
const rawValue = currentSnapshot.exportVal();
|
|
3027
3277
|
const newValue = updateFunction(currentValue);
|
|
3028
3278
|
if (newValue === void 0) {
|
|
3029
3279
|
return {
|
|
@@ -3032,13 +3282,18 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3032
3282
|
};
|
|
3033
3283
|
}
|
|
3034
3284
|
let condition;
|
|
3035
|
-
if (isPrimitive(
|
|
3036
|
-
condition = { o: "c", p: this._path, v:
|
|
3285
|
+
if (isPrimitive(rawValue)) {
|
|
3286
|
+
condition = { o: "c", p: this._path, v: rawValue };
|
|
3037
3287
|
} else {
|
|
3038
|
-
const hash = await hashValue(
|
|
3288
|
+
const hash = await hashValue(rawValue);
|
|
3039
3289
|
condition = { o: "c", p: this._path, h: hash };
|
|
3040
3290
|
}
|
|
3041
|
-
|
|
3291
|
+
let valueToSet = newValue;
|
|
3292
|
+
const existingPriority = currentSnapshot.getPriority();
|
|
3293
|
+
if (existingPriority !== null && isPrimitive(newValue) && newValue !== null) {
|
|
3294
|
+
valueToSet = { ".priority": existingPriority, ".value": newValue };
|
|
3295
|
+
}
|
|
3296
|
+
const ops = [condition, { o: "s", p: this._path, v: valueToSet }];
|
|
3042
3297
|
try {
|
|
3043
3298
|
await this._db._sendTransaction(ops);
|
|
3044
3299
|
const finalSnapshot = await this.once();
|
|
@@ -3065,14 +3320,23 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3065
3320
|
/**
|
|
3066
3321
|
* Read the data at this location once.
|
|
3067
3322
|
*
|
|
3068
|
-
*
|
|
3323
|
+
* For 'value' events, this fetches data directly from the server.
|
|
3324
|
+
* For child events ('child_added', 'child_changed', 'child_removed', 'child_moved'),
|
|
3325
|
+
* this subscribes, waits for the first event, then unsubscribes.
|
|
3326
|
+
*
|
|
3327
|
+
* @param eventType - The event type
|
|
3069
3328
|
* @returns Promise that resolves to the DataSnapshot
|
|
3070
3329
|
*/
|
|
3071
3330
|
once(eventType = "value") {
|
|
3072
|
-
if (eventType
|
|
3073
|
-
|
|
3331
|
+
if (eventType === "value") {
|
|
3332
|
+
return this._db._sendOnce(this._path, this._buildQueryParams());
|
|
3074
3333
|
}
|
|
3075
|
-
return
|
|
3334
|
+
return new Promise((resolve) => {
|
|
3335
|
+
const unsubscribe = this.on(eventType, (snapshot) => {
|
|
3336
|
+
unsubscribe();
|
|
3337
|
+
resolve(snapshot);
|
|
3338
|
+
});
|
|
3339
|
+
});
|
|
3076
3340
|
}
|
|
3077
3341
|
// ============================================
|
|
3078
3342
|
// Subscriptions
|
|
@@ -3141,6 +3405,12 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3141
3405
|
*/
|
|
3142
3406
|
orderByChild(path) {
|
|
3143
3407
|
this._validateNoOrderBy("orderByChild");
|
|
3408
|
+
if (path.startsWith("$") || path.includes("/$")) {
|
|
3409
|
+
throw new LarkError(
|
|
3410
|
+
ErrorCode.INVALID_PATH,
|
|
3411
|
+
`orderByChild: Invalid path '${path}'. Paths cannot contain '$' prefix (reserved for internal use)`
|
|
3412
|
+
);
|
|
3413
|
+
}
|
|
3144
3414
|
return new _DatabaseReference(this._db, this._path, {
|
|
3145
3415
|
...this._query,
|
|
3146
3416
|
orderBy: "child",
|
|
@@ -3514,35 +3784,35 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3514
3784
|
hasParams = true;
|
|
3515
3785
|
}
|
|
3516
3786
|
if (this._query.startAt !== void 0) {
|
|
3517
|
-
params.startAt = this._query.startAt.value;
|
|
3787
|
+
params.startAt = this._query.startAt.value ?? null;
|
|
3518
3788
|
if (this._query.startAt.key !== void 0) {
|
|
3519
3789
|
params.startAtKey = this._query.startAt.key;
|
|
3520
3790
|
}
|
|
3521
3791
|
hasParams = true;
|
|
3522
3792
|
}
|
|
3523
3793
|
if (this._query.startAfter !== void 0) {
|
|
3524
|
-
params.startAfter = this._query.startAfter.value;
|
|
3794
|
+
params.startAfter = this._query.startAfter.value ?? null;
|
|
3525
3795
|
if (this._query.startAfter.key !== void 0) {
|
|
3526
3796
|
params.startAfterKey = this._query.startAfter.key;
|
|
3527
3797
|
}
|
|
3528
3798
|
hasParams = true;
|
|
3529
3799
|
}
|
|
3530
3800
|
if (this._query.endAt !== void 0) {
|
|
3531
|
-
params.endAt = this._query.endAt.value;
|
|
3801
|
+
params.endAt = this._query.endAt.value ?? null;
|
|
3532
3802
|
if (this._query.endAt.key !== void 0) {
|
|
3533
3803
|
params.endAtKey = this._query.endAt.key;
|
|
3534
3804
|
}
|
|
3535
3805
|
hasParams = true;
|
|
3536
3806
|
}
|
|
3537
3807
|
if (this._query.endBefore !== void 0) {
|
|
3538
|
-
params.endBefore = this._query.endBefore.value;
|
|
3808
|
+
params.endBefore = this._query.endBefore.value ?? null;
|
|
3539
3809
|
if (this._query.endBefore.key !== void 0) {
|
|
3540
3810
|
params.endBeforeKey = this._query.endBefore.key;
|
|
3541
3811
|
}
|
|
3542
3812
|
hasParams = true;
|
|
3543
3813
|
}
|
|
3544
3814
|
if (this._query.equalTo !== void 0) {
|
|
3545
|
-
params.equalTo = this._query.equalTo.value;
|
|
3815
|
+
params.equalTo = this._query.equalTo.value ?? null;
|
|
3546
3816
|
if (this._query.equalTo.key !== void 0) {
|
|
3547
3817
|
params.equalToKey = this._query.equalTo.key;
|
|
3548
3818
|
}
|
|
@@ -3561,6 +3831,13 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
3561
3831
|
}
|
|
3562
3832
|
return `${baseUrl}${this._path}`;
|
|
3563
3833
|
}
|
|
3834
|
+
/**
|
|
3835
|
+
* Returns the URL for JSON serialization.
|
|
3836
|
+
* This allows refs to be serialized with JSON.stringify().
|
|
3837
|
+
*/
|
|
3838
|
+
toJSON() {
|
|
3839
|
+
return this.toString();
|
|
3840
|
+
}
|
|
3564
3841
|
};
|
|
3565
3842
|
var ThenableReference = class extends DatabaseReference {
|
|
3566
3843
|
constructor(db, path, promise) {
|
|
@@ -3581,11 +3858,17 @@ function isWrappedPrimitive(data) {
|
|
|
3581
3858
|
return false;
|
|
3582
3859
|
}
|
|
3583
3860
|
const keys = Object.keys(data);
|
|
3584
|
-
|
|
3861
|
+
if (keys.length === 2 && ".value" in data && ".priority" in data) {
|
|
3862
|
+
return true;
|
|
3863
|
+
}
|
|
3864
|
+
if (keys.length === 1 && ".value" in data) {
|
|
3865
|
+
return true;
|
|
3866
|
+
}
|
|
3867
|
+
return false;
|
|
3585
3868
|
}
|
|
3586
3869
|
function stripPriorityMetadata(data) {
|
|
3587
3870
|
if (data === null || data === void 0) {
|
|
3588
|
-
return
|
|
3871
|
+
return null;
|
|
3589
3872
|
}
|
|
3590
3873
|
if (typeof data !== "object") {
|
|
3591
3874
|
return data;
|
|
@@ -3601,7 +3884,10 @@ function stripPriorityMetadata(data) {
|
|
|
3601
3884
|
if (key === ".priority") {
|
|
3602
3885
|
continue;
|
|
3603
3886
|
}
|
|
3604
|
-
|
|
3887
|
+
const stripped = stripPriorityMetadata(value);
|
|
3888
|
+
if (stripped !== null) {
|
|
3889
|
+
result[key] = stripped;
|
|
3890
|
+
}
|
|
3605
3891
|
}
|
|
3606
3892
|
return Object.keys(result).length > 0 ? result : null;
|
|
3607
3893
|
}
|
|
@@ -3648,9 +3934,19 @@ var DataSnapshot = class _DataSnapshot {
|
|
|
3648
3934
|
}
|
|
3649
3935
|
/**
|
|
3650
3936
|
* Check if data exists at this location (is not null/undefined).
|
|
3937
|
+
* Returns false for priority-only nodes (only .priority, no actual value).
|
|
3651
3938
|
*/
|
|
3652
3939
|
exists() {
|
|
3653
|
-
|
|
3940
|
+
if (this._data === null || this._data === void 0) {
|
|
3941
|
+
return false;
|
|
3942
|
+
}
|
|
3943
|
+
if (typeof this._data === "object" && !Array.isArray(this._data)) {
|
|
3944
|
+
const keys = Object.keys(this._data);
|
|
3945
|
+
if (keys.length === 1 && keys[0] === ".priority") {
|
|
3946
|
+
return false;
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
return true;
|
|
3654
3950
|
}
|
|
3655
3951
|
/**
|
|
3656
3952
|
* Get a child snapshot at the specified path.
|
|
@@ -3797,24 +4093,6 @@ var DataSnapshot = class _DataSnapshot {
|
|
|
3797
4093
|
}
|
|
3798
4094
|
};
|
|
3799
4095
|
|
|
3800
|
-
// src/utils/jwt.ts
|
|
3801
|
-
function decodeJwtPayload(token) {
|
|
3802
|
-
const parts = token.split(".");
|
|
3803
|
-
if (parts.length !== 3) {
|
|
3804
|
-
throw new Error("Invalid JWT format");
|
|
3805
|
-
}
|
|
3806
|
-
const payload = parts[1];
|
|
3807
|
-
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
3808
|
-
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
3809
|
-
let decoded;
|
|
3810
|
-
if (typeof atob === "function") {
|
|
3811
|
-
decoded = atob(padded);
|
|
3812
|
-
} else {
|
|
3813
|
-
decoded = Buffer.from(padded, "base64").toString("utf-8");
|
|
3814
|
-
}
|
|
3815
|
-
return JSON.parse(decoded);
|
|
3816
|
-
}
|
|
3817
|
-
|
|
3818
4096
|
// src/utils/volatile.ts
|
|
3819
4097
|
function isVolatilePath(path, patterns) {
|
|
3820
4098
|
if (!patterns || patterns.length === 0) {
|
|
@@ -3937,6 +4215,11 @@ var LarkDatabase = class {
|
|
|
3937
4215
|
this._coordinatorUrl = null;
|
|
3938
4216
|
this._volatilePaths = [];
|
|
3939
4217
|
this._transportType = null;
|
|
4218
|
+
// Auth state
|
|
4219
|
+
this._currentToken = null;
|
|
4220
|
+
// Token for auth (empty string = anonymous)
|
|
4221
|
+
this._isAnonymous = false;
|
|
4222
|
+
// True if connected anonymously
|
|
3940
4223
|
// Reconnection state
|
|
3941
4224
|
this._connectionId = null;
|
|
3942
4225
|
this._connectOptions = null;
|
|
@@ -3949,8 +4232,12 @@ var LarkDatabase = class {
|
|
|
3949
4232
|
this.disconnectCallbacks = /* @__PURE__ */ new Set();
|
|
3950
4233
|
this.errorCallbacks = /* @__PURE__ */ new Set();
|
|
3951
4234
|
this.reconnectingCallbacks = /* @__PURE__ */ new Set();
|
|
4235
|
+
this.authStateChangedCallbacks = /* @__PURE__ */ new Set();
|
|
3952
4236
|
// .info path subscriptions (handled locally, not sent to server)
|
|
3953
4237
|
this.infoSubscriptions = [];
|
|
4238
|
+
// Authentication promise - resolves when auth completes, allows operations to queue
|
|
4239
|
+
this.authenticationPromise = null;
|
|
4240
|
+
this.authenticationResolve = null;
|
|
3954
4241
|
this._serverTimeOffset = 0;
|
|
3955
4242
|
this.messageQueue = new MessageQueue();
|
|
3956
4243
|
this.subscriptionManager = new SubscriptionManager();
|
|
@@ -3960,10 +4247,11 @@ var LarkDatabase = class {
|
|
|
3960
4247
|
// Connection State
|
|
3961
4248
|
// ============================================
|
|
3962
4249
|
/**
|
|
3963
|
-
* Whether the database is
|
|
4250
|
+
* Whether the database is fully connected and authenticated.
|
|
4251
|
+
* Returns true when ready to perform database operations.
|
|
3964
4252
|
*/
|
|
3965
4253
|
get connected() {
|
|
3966
|
-
return this._state === "
|
|
4254
|
+
return this._state === "authenticated";
|
|
3967
4255
|
}
|
|
3968
4256
|
/**
|
|
3969
4257
|
* Whether the database is currently attempting to reconnect.
|
|
@@ -4048,16 +4336,27 @@ var LarkDatabase = class {
|
|
|
4048
4336
|
}
|
|
4049
4337
|
this._connectOptions = options;
|
|
4050
4338
|
this._intentionalDisconnect = false;
|
|
4339
|
+
this.authenticationPromise = new Promise((resolve) => {
|
|
4340
|
+
this.authenticationResolve = resolve;
|
|
4341
|
+
});
|
|
4051
4342
|
await this.performConnect(databaseId, options);
|
|
4052
4343
|
}
|
|
4053
4344
|
/**
|
|
4054
4345
|
* Internal connect implementation used by both initial connect and reconnect.
|
|
4346
|
+
* Implements the Join → Auth flow:
|
|
4347
|
+
* 1. Connect WebSocket
|
|
4348
|
+
* 2. Send join (identifies database)
|
|
4349
|
+
* 3. Send auth (authenticates user - required even for anonymous)
|
|
4055
4350
|
*/
|
|
4056
4351
|
async performConnect(databaseId, options, isReconnect = false) {
|
|
4057
4352
|
const previousState = this._state;
|
|
4058
4353
|
this._state = isReconnect ? "reconnecting" : "connecting";
|
|
4059
4354
|
this._databaseId = databaseId;
|
|
4060
4355
|
this._coordinatorUrl = options.coordinator || DEFAULT_COORDINATOR_URL;
|
|
4356
|
+
if (!isReconnect) {
|
|
4357
|
+
this._currentToken = options.token || "";
|
|
4358
|
+
this._isAnonymous = !options.token && options.anonymous !== false;
|
|
4359
|
+
}
|
|
4061
4360
|
try {
|
|
4062
4361
|
const coordinatorUrl = this._coordinatorUrl;
|
|
4063
4362
|
const coordinator = new Coordinator(coordinatorUrl);
|
|
@@ -4083,30 +4382,44 @@ var LarkDatabase = class {
|
|
|
4083
4382
|
);
|
|
4084
4383
|
this.transport = transportResult.transport;
|
|
4085
4384
|
this._transportType = transportResult.type;
|
|
4086
|
-
|
|
4385
|
+
this._state = "connected";
|
|
4386
|
+
const joinRequestId = this.messageQueue.nextRequestId();
|
|
4087
4387
|
const joinMessage = {
|
|
4088
4388
|
o: "j",
|
|
4089
|
-
|
|
4090
|
-
r:
|
|
4389
|
+
d: databaseId,
|
|
4390
|
+
r: joinRequestId
|
|
4091
4391
|
};
|
|
4092
4392
|
if (this._connectionId) {
|
|
4093
4393
|
joinMessage.pcid = this._connectionId;
|
|
4094
4394
|
}
|
|
4095
4395
|
this.send(joinMessage);
|
|
4096
|
-
const joinResponse = await this.messageQueue.registerRequest(
|
|
4396
|
+
const joinResponse = await this.messageQueue.registerRequest(joinRequestId);
|
|
4097
4397
|
this._volatilePaths = joinResponse.volatilePaths;
|
|
4098
4398
|
this._connectionId = joinResponse.connectionId;
|
|
4099
4399
|
if (joinResponse.serverTime != null) {
|
|
4100
4400
|
this._serverTimeOffset = joinResponse.serverTime - Date.now();
|
|
4101
4401
|
}
|
|
4102
|
-
|
|
4402
|
+
this._state = "joined";
|
|
4403
|
+
const authRequestId = this.messageQueue.nextRequestId();
|
|
4404
|
+
const authMessage = {
|
|
4405
|
+
o: "au",
|
|
4406
|
+
t: this._currentToken ?? "",
|
|
4407
|
+
r: authRequestId
|
|
4408
|
+
};
|
|
4409
|
+
this.send(authMessage);
|
|
4410
|
+
const authResponse = await this.messageQueue.registerRequest(authRequestId);
|
|
4103
4411
|
this._auth = {
|
|
4104
|
-
uid:
|
|
4105
|
-
provider:
|
|
4106
|
-
token:
|
|
4412
|
+
uid: authResponse.uid || "",
|
|
4413
|
+
provider: this._isAnonymous ? "anonymous" : "custom",
|
|
4414
|
+
token: {}
|
|
4415
|
+
// Token claims would need to be decoded from the token if needed
|
|
4107
4416
|
};
|
|
4108
|
-
this._state = "
|
|
4417
|
+
this._state = "authenticated";
|
|
4109
4418
|
this._reconnectAttempt = 0;
|
|
4419
|
+
if (this.authenticationResolve) {
|
|
4420
|
+
this.authenticationResolve();
|
|
4421
|
+
this.authenticationResolve = null;
|
|
4422
|
+
}
|
|
4110
4423
|
this.fireConnectionStateChange();
|
|
4111
4424
|
if (!isReconnect) {
|
|
4112
4425
|
this.subscriptionManager.initialize({
|
|
@@ -4119,6 +4432,7 @@ var LarkDatabase = class {
|
|
|
4119
4432
|
await this.restoreAfterReconnect();
|
|
4120
4433
|
}
|
|
4121
4434
|
this.connectCallbacks.forEach((cb) => cb());
|
|
4435
|
+
this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
|
|
4122
4436
|
} catch (error) {
|
|
4123
4437
|
if (isReconnect) {
|
|
4124
4438
|
this._state = "reconnecting";
|
|
@@ -4131,6 +4445,8 @@ var LarkDatabase = class {
|
|
|
4131
4445
|
this._connectOptions = null;
|
|
4132
4446
|
this._connectionId = null;
|
|
4133
4447
|
this._transportType = null;
|
|
4448
|
+
this._currentToken = null;
|
|
4449
|
+
this._isAnonymous = false;
|
|
4134
4450
|
this.transport?.close();
|
|
4135
4451
|
this.transport = null;
|
|
4136
4452
|
throw error;
|
|
@@ -4144,13 +4460,14 @@ var LarkDatabase = class {
|
|
|
4144
4460
|
if (this._state === "disconnected") {
|
|
4145
4461
|
return;
|
|
4146
4462
|
}
|
|
4147
|
-
const
|
|
4463
|
+
const wasAuthenticated = this._state === "authenticated";
|
|
4464
|
+
const wasPartiallyConnected = this._state === "connected" || this._state === "joined";
|
|
4148
4465
|
this._intentionalDisconnect = true;
|
|
4149
4466
|
if (this._reconnectTimer) {
|
|
4150
4467
|
clearTimeout(this._reconnectTimer);
|
|
4151
4468
|
this._reconnectTimer = null;
|
|
4152
4469
|
}
|
|
4153
|
-
if (
|
|
4470
|
+
if ((wasAuthenticated || wasPartiallyConnected) && this.transport) {
|
|
4154
4471
|
try {
|
|
4155
4472
|
const requestId = this.messageQueue.nextRequestId();
|
|
4156
4473
|
this.send({ o: "l", r: requestId });
|
|
@@ -4162,7 +4479,7 @@ var LarkDatabase = class {
|
|
|
4162
4479
|
}
|
|
4163
4480
|
}
|
|
4164
4481
|
this.cleanupFull();
|
|
4165
|
-
if (
|
|
4482
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4166
4483
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4167
4484
|
}
|
|
4168
4485
|
}
|
|
@@ -4171,7 +4488,7 @@ var LarkDatabase = class {
|
|
|
4171
4488
|
* Disconnects from the server but preserves subscriptions for later reconnection via goOnline().
|
|
4172
4489
|
*/
|
|
4173
4490
|
goOffline() {
|
|
4174
|
-
if (this._state === "connected" || this._state === "reconnecting") {
|
|
4491
|
+
if (this._state === "authenticated" || this._state === "joined" || this._state === "connected" || this._state === "reconnecting") {
|
|
4175
4492
|
this._intentionalDisconnect = true;
|
|
4176
4493
|
if (this._reconnectTimer) {
|
|
4177
4494
|
clearTimeout(this._reconnectTimer);
|
|
@@ -4202,7 +4519,7 @@ var LarkDatabase = class {
|
|
|
4202
4519
|
* Used for intentional disconnect.
|
|
4203
4520
|
*/
|
|
4204
4521
|
cleanupFull() {
|
|
4205
|
-
const
|
|
4522
|
+
const wasAuthenticated = this._state === "authenticated";
|
|
4206
4523
|
this.transport?.close();
|
|
4207
4524
|
this.transport = null;
|
|
4208
4525
|
this._state = "disconnected";
|
|
@@ -4213,11 +4530,15 @@ var LarkDatabase = class {
|
|
|
4213
4530
|
this._connectionId = null;
|
|
4214
4531
|
this._connectOptions = null;
|
|
4215
4532
|
this._transportType = null;
|
|
4533
|
+
this._currentToken = null;
|
|
4534
|
+
this._isAnonymous = false;
|
|
4216
4535
|
this._reconnectAttempt = 0;
|
|
4536
|
+
this.authenticationPromise = null;
|
|
4537
|
+
this.authenticationResolve = null;
|
|
4217
4538
|
this.subscriptionManager.clear();
|
|
4218
4539
|
this.messageQueue.rejectAll(new Error("Connection closed"));
|
|
4219
4540
|
this.pendingWrites.clear();
|
|
4220
|
-
if (
|
|
4541
|
+
if (wasAuthenticated) {
|
|
4221
4542
|
this.fireConnectionStateChange();
|
|
4222
4543
|
}
|
|
4223
4544
|
this.infoSubscriptions = [];
|
|
@@ -4250,7 +4571,7 @@ var LarkDatabase = class {
|
|
|
4250
4571
|
getInfoValue(path) {
|
|
4251
4572
|
const normalizedPath = normalizePath(path) || "/";
|
|
4252
4573
|
if (normalizedPath === "/.info/connected") {
|
|
4253
|
-
return this._state === "
|
|
4574
|
+
return this._state === "authenticated";
|
|
4254
4575
|
}
|
|
4255
4576
|
if (normalizedPath === "/.info/serverTimeOffset") {
|
|
4256
4577
|
return this._serverTimeOffset;
|
|
@@ -4317,6 +4638,9 @@ var LarkDatabase = class {
|
|
|
4317
4638
|
if (this._intentionalDisconnect || !this._databaseId || !this._connectOptions) {
|
|
4318
4639
|
return;
|
|
4319
4640
|
}
|
|
4641
|
+
this.authenticationPromise = new Promise((resolve) => {
|
|
4642
|
+
this.authenticationResolve = resolve;
|
|
4643
|
+
});
|
|
4320
4644
|
try {
|
|
4321
4645
|
await this.performConnect(this._databaseId, this._connectOptions, true);
|
|
4322
4646
|
} catch {
|
|
@@ -4475,6 +4799,9 @@ var LarkDatabase = class {
|
|
|
4475
4799
|
* @internal Send a transaction to the server.
|
|
4476
4800
|
*/
|
|
4477
4801
|
async _sendTransaction(ops) {
|
|
4802
|
+
if (!this.isAuthenticatedOrThrow()) {
|
|
4803
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4804
|
+
}
|
|
4478
4805
|
const requestId = this.messageQueue.nextRequestId();
|
|
4479
4806
|
this.pendingWrites.trackWrite(requestId, "transaction", "/", ops);
|
|
4480
4807
|
const message = {
|
|
@@ -4521,6 +4848,76 @@ var LarkDatabase = class {
|
|
|
4521
4848
|
this.reconnectingCallbacks.add(callback);
|
|
4522
4849
|
return () => this.reconnectingCallbacks.delete(callback);
|
|
4523
4850
|
}
|
|
4851
|
+
/**
|
|
4852
|
+
* Register a callback for auth state changes.
|
|
4853
|
+
* Fires when user signs in, signs out, or auth changes.
|
|
4854
|
+
* Returns an unsubscribe function.
|
|
4855
|
+
*/
|
|
4856
|
+
onAuthStateChanged(callback) {
|
|
4857
|
+
this.authStateChangedCallbacks.add(callback);
|
|
4858
|
+
return () => this.authStateChangedCallbacks.delete(callback);
|
|
4859
|
+
}
|
|
4860
|
+
// ============================================
|
|
4861
|
+
// Authentication Management
|
|
4862
|
+
// ============================================
|
|
4863
|
+
/**
|
|
4864
|
+
* Sign in with a new auth token while connected.
|
|
4865
|
+
* Changes the authenticated user without disconnecting.
|
|
4866
|
+
*
|
|
4867
|
+
* Note: Some subscriptions may be revoked if the new user doesn't have
|
|
4868
|
+
* permission. Listen for 'permission_denied' errors on your subscriptions.
|
|
4869
|
+
*
|
|
4870
|
+
* @param token - The auth token for the new user
|
|
4871
|
+
* @throws Error if not connected (must call connect() first)
|
|
4872
|
+
*/
|
|
4873
|
+
async signIn(token) {
|
|
4874
|
+
if (this._state !== "authenticated" && this._state !== "joined") {
|
|
4875
|
+
throw new LarkError("not_connected", "Must be connected first - call connect()");
|
|
4876
|
+
}
|
|
4877
|
+
const authRequestId = this.messageQueue.nextRequestId();
|
|
4878
|
+
const authMessage = {
|
|
4879
|
+
o: "au",
|
|
4880
|
+
t: token,
|
|
4881
|
+
r: authRequestId
|
|
4882
|
+
};
|
|
4883
|
+
this.send(authMessage);
|
|
4884
|
+
const authResponse = await this.messageQueue.registerRequest(authRequestId);
|
|
4885
|
+
this._currentToken = token;
|
|
4886
|
+
this._isAnonymous = false;
|
|
4887
|
+
this._auth = {
|
|
4888
|
+
uid: authResponse.uid || "",
|
|
4889
|
+
provider: "custom",
|
|
4890
|
+
token: {}
|
|
4891
|
+
};
|
|
4892
|
+
this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
|
|
4893
|
+
}
|
|
4894
|
+
/**
|
|
4895
|
+
* Sign out the current user.
|
|
4896
|
+
* Reverts to anonymous authentication.
|
|
4897
|
+
*
|
|
4898
|
+
* Note: Some subscriptions may be revoked if anonymous users don't have
|
|
4899
|
+
* permission. Listen for 'permission_denied' errors on your subscriptions.
|
|
4900
|
+
*/
|
|
4901
|
+
async signOut() {
|
|
4902
|
+
if (this._state !== "authenticated") {
|
|
4903
|
+
return;
|
|
4904
|
+
}
|
|
4905
|
+
const unauthRequestId = this.messageQueue.nextRequestId();
|
|
4906
|
+
const unauthMessage = {
|
|
4907
|
+
o: "ua",
|
|
4908
|
+
r: unauthRequestId
|
|
4909
|
+
};
|
|
4910
|
+
this.send(unauthMessage);
|
|
4911
|
+
const authResponse = await this.messageQueue.registerRequest(unauthRequestId);
|
|
4912
|
+
this._currentToken = "";
|
|
4913
|
+
this._isAnonymous = true;
|
|
4914
|
+
this._auth = {
|
|
4915
|
+
uid: authResponse.uid || "",
|
|
4916
|
+
provider: "anonymous",
|
|
4917
|
+
token: {}
|
|
4918
|
+
};
|
|
4919
|
+
this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
|
|
4920
|
+
}
|
|
4524
4921
|
// ============================================
|
|
4525
4922
|
// Internal: Message Handling
|
|
4526
4923
|
// ============================================
|
|
@@ -4532,6 +4929,9 @@ var LarkDatabase = class {
|
|
|
4532
4929
|
console.error("Failed to parse message:", data);
|
|
4533
4930
|
return;
|
|
4534
4931
|
}
|
|
4932
|
+
if (process.env.LARK_DEBUG) {
|
|
4933
|
+
console.log("[LARK] <<< SERVER:", JSON.stringify(message, null, 2));
|
|
4934
|
+
}
|
|
4535
4935
|
if (isPingMessage(message)) {
|
|
4536
4936
|
this.transport?.send(JSON.stringify({ o: "po" }));
|
|
4537
4937
|
return;
|
|
@@ -4541,6 +4941,12 @@ var LarkDatabase = class {
|
|
|
4541
4941
|
this.subscriptionManager.clearPendingWrite(message.a);
|
|
4542
4942
|
} else if (isNackMessage(message)) {
|
|
4543
4943
|
this.pendingWrites.onNack(message.n);
|
|
4944
|
+
if (message.e === "permission_denied" && message.sp) {
|
|
4945
|
+
const path = message.sp;
|
|
4946
|
+
console.warn(`Subscription revoked at ${path}: permission_denied`);
|
|
4947
|
+
this.subscriptionManager.handleSubscriptionRevoked(path);
|
|
4948
|
+
return;
|
|
4949
|
+
}
|
|
4544
4950
|
if (message.e !== "condition_failed") {
|
|
4545
4951
|
console.error(`Write failed (${message.e}): ${message.m || message.e}`);
|
|
4546
4952
|
}
|
|
@@ -4557,27 +4963,28 @@ var LarkDatabase = class {
|
|
|
4557
4963
|
if (this._state === "disconnected") {
|
|
4558
4964
|
return;
|
|
4559
4965
|
}
|
|
4560
|
-
const
|
|
4966
|
+
const wasAuthenticated = this._state === "authenticated";
|
|
4561
4967
|
const wasReconnecting = this._state === "reconnecting";
|
|
4968
|
+
const wasPartiallyConnected = this._state === "connected" || this._state === "joined";
|
|
4562
4969
|
if (this._intentionalDisconnect) {
|
|
4563
4970
|
this.cleanupFull();
|
|
4564
|
-
if (
|
|
4971
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4565
4972
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4566
4973
|
}
|
|
4567
4974
|
return;
|
|
4568
4975
|
}
|
|
4569
4976
|
const canReconnect = this._databaseId && this._connectOptions;
|
|
4570
|
-
if ((
|
|
4977
|
+
if ((wasAuthenticated || wasPartiallyConnected || wasReconnecting) && canReconnect) {
|
|
4571
4978
|
this._state = "reconnecting";
|
|
4572
4979
|
this.cleanupForReconnect();
|
|
4573
4980
|
this.reconnectingCallbacks.forEach((cb) => cb());
|
|
4574
|
-
if (
|
|
4981
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4575
4982
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4576
4983
|
}
|
|
4577
4984
|
this.scheduleReconnect();
|
|
4578
4985
|
} else {
|
|
4579
4986
|
this.cleanupFull();
|
|
4580
|
-
if (
|
|
4987
|
+
if (wasAuthenticated || wasPartiallyConnected) {
|
|
4581
4988
|
this.disconnectCallbacks.forEach((cb) => cb());
|
|
4582
4989
|
}
|
|
4583
4990
|
}
|
|
@@ -4588,10 +4995,48 @@ var LarkDatabase = class {
|
|
|
4588
4995
|
// ============================================
|
|
4589
4996
|
// Internal: Sending Messages
|
|
4590
4997
|
// ============================================
|
|
4998
|
+
/**
|
|
4999
|
+
* Check if authenticated synchronously.
|
|
5000
|
+
* Returns true if authenticated, false if connecting (should wait), throws if disconnected.
|
|
5001
|
+
*/
|
|
5002
|
+
isAuthenticatedOrThrow() {
|
|
5003
|
+
if (this._state === "authenticated") {
|
|
5004
|
+
return true;
|
|
5005
|
+
}
|
|
5006
|
+
if (this._state === "connecting" || this._state === "connected" || this._state === "joined" || this._state === "reconnecting") {
|
|
5007
|
+
return false;
|
|
5008
|
+
}
|
|
5009
|
+
throw new LarkError("not_connected", "Not connected - call connect() first");
|
|
5010
|
+
}
|
|
5011
|
+
/**
|
|
5012
|
+
* Wait for authentication to complete before performing an operation.
|
|
5013
|
+
* If already authenticated, returns immediately (synchronously).
|
|
5014
|
+
* If connecting/reconnecting, waits for auth to complete.
|
|
5015
|
+
* If disconnected and no connect in progress, throws.
|
|
5016
|
+
*
|
|
5017
|
+
* IMPORTANT: This returns a Promise only if waiting is needed.
|
|
5018
|
+
* Callers should use: `if (!this.isAuthenticatedOrThrow()) if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();`
|
|
5019
|
+
* to preserve synchronous execution when already authenticated.
|
|
5020
|
+
*/
|
|
5021
|
+
async waitForAuthenticated() {
|
|
5022
|
+
if (this._state === "authenticated") {
|
|
5023
|
+
return;
|
|
5024
|
+
}
|
|
5025
|
+
if (this._state === "connecting" || this._state === "connected" || this._state === "joined" || this._state === "reconnecting") {
|
|
5026
|
+
if (this.authenticationPromise) {
|
|
5027
|
+
await this.authenticationPromise;
|
|
5028
|
+
return;
|
|
5029
|
+
}
|
|
5030
|
+
}
|
|
5031
|
+
throw new LarkError("not_connected", "Not connected - call connect() first");
|
|
5032
|
+
}
|
|
4591
5033
|
send(message) {
|
|
4592
5034
|
if (!this.transport || !this.transport.connected) {
|
|
4593
5035
|
throw new LarkError("not_connected", "Not connected to database");
|
|
4594
5036
|
}
|
|
5037
|
+
if (process.env.LARK_DEBUG) {
|
|
5038
|
+
console.log("[LARK] >>> CLIENT:", JSON.stringify(message, null, 2));
|
|
5039
|
+
}
|
|
4595
5040
|
this.transport.send(JSON.stringify(message));
|
|
4596
5041
|
}
|
|
4597
5042
|
/**
|
|
@@ -4599,6 +5044,7 @@ var LarkDatabase = class {
|
|
|
4599
5044
|
* Note: Priority is now part of the value (as .priority), not a separate field.
|
|
4600
5045
|
*/
|
|
4601
5046
|
async _sendSet(path, value) {
|
|
5047
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4602
5048
|
const normalizedPath = normalizePath(path) || "/";
|
|
4603
5049
|
validateWriteData(value, normalizedPath);
|
|
4604
5050
|
const requestId = this.messageQueue.nextRequestId();
|
|
@@ -4623,6 +5069,7 @@ var LarkDatabase = class {
|
|
|
4623
5069
|
* @internal Send an update operation.
|
|
4624
5070
|
*/
|
|
4625
5071
|
async _sendUpdate(path, values) {
|
|
5072
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4626
5073
|
const normalizedPath = normalizePath(path) || "/";
|
|
4627
5074
|
for (const [key, value] of Object.entries(values)) {
|
|
4628
5075
|
const fullPath = key.startsWith("/") ? key : `${normalizedPath}/${key}`;
|
|
@@ -4654,6 +5101,7 @@ var LarkDatabase = class {
|
|
|
4654
5101
|
* @internal Send a delete operation.
|
|
4655
5102
|
*/
|
|
4656
5103
|
async _sendDelete(path) {
|
|
5104
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4657
5105
|
const normalizedPath = normalizePath(path) || "/";
|
|
4658
5106
|
const requestId = this.messageQueue.nextRequestId();
|
|
4659
5107
|
const pendingWriteIds = this.subscriptionManager.getPendingWriteIdsForPath(normalizedPath);
|
|
@@ -4689,7 +5137,7 @@ var LarkDatabase = class {
|
|
|
4689
5137
|
_sendVolatileSet(path, value) {
|
|
4690
5138
|
const normalizedPath = normalizePath(path) || "/";
|
|
4691
5139
|
this.subscriptionManager.applyOptimisticWrite(normalizedPath, value, "", "set");
|
|
4692
|
-
if (!this.transport || !this.transport.connected) {
|
|
5140
|
+
if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
|
|
4693
5141
|
return;
|
|
4694
5142
|
}
|
|
4695
5143
|
const message = {
|
|
@@ -4705,7 +5153,7 @@ var LarkDatabase = class {
|
|
|
4705
5153
|
_sendVolatileUpdate(path, values) {
|
|
4706
5154
|
const normalizedPath = normalizePath(path) || "/";
|
|
4707
5155
|
this.subscriptionManager.applyOptimisticWrite(normalizedPath, values, "", "update");
|
|
4708
|
-
if (!this.transport || !this.transport.connected) {
|
|
5156
|
+
if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
|
|
4709
5157
|
return;
|
|
4710
5158
|
}
|
|
4711
5159
|
const message = {
|
|
@@ -4721,7 +5169,7 @@ var LarkDatabase = class {
|
|
|
4721
5169
|
_sendVolatileDelete(path) {
|
|
4722
5170
|
const normalizedPath = normalizePath(path) || "/";
|
|
4723
5171
|
this.subscriptionManager.applyOptimisticWrite(normalizedPath, null, "", "delete");
|
|
4724
|
-
if (!this.transport || !this.transport.connected) {
|
|
5172
|
+
if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
|
|
4725
5173
|
return;
|
|
4726
5174
|
}
|
|
4727
5175
|
const message = {
|
|
@@ -4760,6 +5208,7 @@ var LarkDatabase = class {
|
|
|
4760
5208
|
return new DataSnapshot(cached.value, path, this);
|
|
4761
5209
|
}
|
|
4762
5210
|
}
|
|
5211
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4763
5212
|
const requestId = this.messageQueue.nextRequestId();
|
|
4764
5213
|
const message = {
|
|
4765
5214
|
o: "o",
|
|
@@ -4776,6 +5225,7 @@ var LarkDatabase = class {
|
|
|
4776
5225
|
* @internal Send an onDisconnect operation.
|
|
4777
5226
|
*/
|
|
4778
5227
|
async _sendOnDisconnect(path, action, value) {
|
|
5228
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4779
5229
|
const requestId = this.messageQueue.nextRequestId();
|
|
4780
5230
|
const message = {
|
|
4781
5231
|
o: "od",
|
|
@@ -4789,11 +5239,20 @@ var LarkDatabase = class {
|
|
|
4789
5239
|
this.send(message);
|
|
4790
5240
|
await this.messageQueue.registerRequest(requestId);
|
|
4791
5241
|
}
|
|
5242
|
+
/**
|
|
5243
|
+
* @internal Get a cached value from the subscription manager.
|
|
5244
|
+
* Used for optimistic writes where we need the current value without a network fetch.
|
|
5245
|
+
*/
|
|
5246
|
+
_getCachedValue(path) {
|
|
5247
|
+
const normalizedPath = normalizePath(path) || "/";
|
|
5248
|
+
return this.subscriptionManager.getCachedValue(normalizedPath);
|
|
5249
|
+
}
|
|
4792
5250
|
/**
|
|
4793
5251
|
* @internal Send a subscribe message to server.
|
|
4794
5252
|
* Includes tag for non-default queries to enable proper event routing.
|
|
4795
5253
|
*/
|
|
4796
5254
|
async sendSubscribeMessage(path, eventTypes, queryParams, tag) {
|
|
5255
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4797
5256
|
const requestId = this.messageQueue.nextRequestId();
|
|
4798
5257
|
const message = {
|
|
4799
5258
|
o: "sb",
|
|
@@ -4811,6 +5270,7 @@ var LarkDatabase = class {
|
|
|
4811
5270
|
* Includes query params and tag so server can identify which specific subscription to remove.
|
|
4812
5271
|
*/
|
|
4813
5272
|
async sendUnsubscribeMessage(path, queryParams, tag) {
|
|
5273
|
+
if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
|
|
4814
5274
|
const requestId = this.messageQueue.nextRequestId();
|
|
4815
5275
|
const message = {
|
|
4816
5276
|
o: "us",
|