@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.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 null;
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
- if (hasRangeFilter) {
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 (!queryParams || queryParams.orderBy === "key" || !queryParams.orderBy && !queryParams.orderByChild) {
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
- const merged2 = { ...base, ...value };
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 = { ...current, ...value };
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 = value;
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 allEventTypes = view.getEventTypes();
1561
- this.sendSubscribe?.(normalizedPath, allEventTypes, view.queryParams ?? void 0, tag).catch((err) => {
1562
- console.error("Failed to subscribe:", err);
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
- * and caused a position change. Children that are merely "displaced" by
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
- if (oldPrevKey !== newPrevKey) {
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 'value' subscription at that exact path, OR
1915
- * - There's an active 'value' subscription at an ancestor path
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.hasValueSubscription(normalized)) {
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.hasValueSubscription(ancestorPath)) {
2028
+ if (this.hasActiveSubscriptionWithData(ancestorPath)) {
1926
2029
  return true;
1927
2030
  }
1928
2031
  }
1929
- if (normalized !== "/" && this.hasValueSubscription("/")) {
2032
+ if (normalized !== "/" && this.hasActiveSubscriptionWithData("/")) {
1930
2033
  return true;
1931
2034
  }
1932
2035
  return false;
1933
2036
  }
1934
2037
  /**
1935
- * Check if there's a 'value' subscription at a path (any query identifier).
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
- hasValueSubscription(path) {
2042
+ hasActiveSubscriptionWithData(path) {
1938
2043
  const views = this.getViewsAtPath(path);
1939
- return views.some((view) => view.hasCallbacksForType("value"));
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.hasCallbacksForType("value")) {
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.hasCallbacksForType("value")) {
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
- previousCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
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
- this.fireChildRemoved(view, key, childRemovedSubs, isVolatile, serverTimestamp);
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
- this.fireChildRemoved(view, childKey, childRemovedSubs, isVolatile, serverTimestamp);
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.fireChildEvents(view, previousOrder, previousChildSet, currentOrder, currentChildSet, "/", false, void 0, previousDisplayCache);
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, value, operation);
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 snapshot = this.createSnapshot?.(joinPath(view.path, key), null, isVolatile, serverTimestamp);
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 snapshot = this.createSnapshot?.(joinPath(view.path, key), null, isVolatile, serverTimestamp);
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 (childRemovedSubs.length > 0) {
2508
- for (const key of previousOrder) {
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
- if (currentChildSet.has(key)) continue;
2511
- const prevValue = previousDisplayCache ? previousDisplayCache[key] : null;
2512
- const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue, isVolatile, serverTimestamp);
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
- for (const entry of childRemovedSubs) {
2738
+ const prevKey = view.getPreviousChildKey(key);
2739
+ for (const entry of childAddedSubs) {
2515
2740
  try {
2516
- entry.callback(snapshot, void 0);
2741
+ entry.callback(snapshot, prevKey);
2517
2742
  } catch (err) {
2518
- console.error("Error in child_removed callback:", err);
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
- * Fetches current value and sets it with the new priority.
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
- async setPriority(priority) {
3172
+ setPriority(priority) {
2945
3173
  validateNotInfoPath(this._path, "setPriority");
2946
- const snapshot = await this.once();
2947
- const currentVal = snapshot.val();
2948
- if (currentVal === null || currentVal === void 0) {
2949
- await this._db._sendSet(this._path, { ".priority": priority });
2950
- return;
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
- await this.setWithPriority(currentVal, priority);
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(currentValue)) {
2991
- condition = { o: "c", p: this._path, v: currentValue };
3240
+ if (isPrimitive(rawValue)) {
3241
+ condition = { o: "c", p: this._path, v: rawValue };
2992
3242
  } else {
2993
- const hash = await hashValue(currentValue);
3243
+ const hash = await hashValue(rawValue);
2994
3244
  condition = { o: "c", p: this._path, h: hash };
2995
3245
  }
2996
- const ops = [condition, { o: "s", p: this._path, v: newValue }];
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
- * @param eventType - The event type (only 'value' is supported)
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 !== "value") {
3028
- throw new Error('once() only supports "value" event type');
3286
+ if (eventType === "value") {
3287
+ return this._db._sendOnce(this._path, this._buildQueryParams());
3029
3288
  }
3030
- return this._db._sendOnce(this._path, this._buildQueryParams());
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
- return keys.length === 2 && ".value" in data && ".priority" in data;
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 data;
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
- result[key] = stripPriorityMetadata(value);
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
- return this._data !== null && this._data !== void 0;
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 currently connected.
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 === "connected";
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
- const requestId = this.messageQueue.nextRequestId();
4340
+ this._state = "connected";
4341
+ const joinRequestId = this.messageQueue.nextRequestId();
4042
4342
  const joinMessage = {
4043
4343
  o: "j",
4044
- t: connectResponse.token,
4045
- r: requestId
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(requestId);
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
- const jwtPayload = decodeJwtPayload(connectResponse.token);
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: jwtPayload.sub,
4060
- provider: jwtPayload.provider,
4061
- token: jwtPayload.claims || {}
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 = "connected";
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 wasConnected = this._state === "connected";
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 (wasConnected && this.transport) {
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 (wasConnected) {
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 wasConnected = this._state === "connected";
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 (wasConnected) {
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 === "connected";
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 wasConnected = this._state === "connected";
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 (wasConnected) {
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 ((wasConnected || wasReconnecting) && canReconnect) {
4932
+ if ((wasAuthenticated || wasPartiallyConnected || wasReconnecting) && canReconnect) {
4526
4933
  this._state = "reconnecting";
4527
4934
  this.cleanupForReconnect();
4528
4935
  this.reconnectingCallbacks.forEach((cb) => cb());
4529
- if (wasConnected) {
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 (wasConnected) {
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",