@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.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 null;
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
- if (hasRangeFilter) {
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 (!queryParams || queryParams.orderBy === "key" || !queryParams.orderBy && !queryParams.orderByChild) {
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
- const merged2 = { ...base, ...value };
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 = { ...current, ...value };
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 = value;
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 allEventTypes = view.getEventTypes();
1606
- this.sendSubscribe?.(normalizedPath, allEventTypes, view.queryParams ?? void 0, tag).catch((err) => {
1607
- console.error("Failed to subscribe:", err);
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
- * and caused a position change. Children that are merely "displaced" by
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
- if (oldPrevKey !== newPrevKey) {
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 'value' subscription at that exact path, OR
1960
- * - There's an active 'value' subscription at an ancestor path
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.hasValueSubscription(normalized)) {
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.hasValueSubscription(ancestorPath)) {
2073
+ if (this.hasActiveSubscriptionWithData(ancestorPath)) {
1971
2074
  return true;
1972
2075
  }
1973
2076
  }
1974
- if (normalized !== "/" && this.hasValueSubscription("/")) {
2077
+ if (normalized !== "/" && this.hasActiveSubscriptionWithData("/")) {
1975
2078
  return true;
1976
2079
  }
1977
2080
  return false;
1978
2081
  }
1979
2082
  /**
1980
- * Check if there's a 'value' subscription at a path (any query identifier).
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
- hasValueSubscription(path) {
2087
+ hasActiveSubscriptionWithData(path) {
1983
2088
  const views = this.getViewsAtPath(path);
1984
- return views.some((view) => view.hasCallbacksForType("value"));
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.hasCallbacksForType("value")) {
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.hasCallbacksForType("value")) {
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
- previousCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
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
- this.fireChildRemoved(view, key, childRemovedSubs, isVolatile, serverTimestamp);
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
- this.fireChildRemoved(view, childKey, childRemovedSubs, isVolatile, serverTimestamp);
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.fireChildEvents(view, previousOrder, previousChildSet, currentOrder, currentChildSet, "/", false, void 0, previousDisplayCache);
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, value, operation);
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 snapshot = this.createSnapshot?.(joinPath(view.path, key), null, isVolatile, serverTimestamp);
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 snapshot = this.createSnapshot?.(joinPath(view.path, key), null, isVolatile, serverTimestamp);
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 (childRemovedSubs.length > 0) {
2553
- for (const key of previousOrder) {
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
- if (currentChildSet.has(key)) continue;
2556
- const prevValue = previousDisplayCache ? previousDisplayCache[key] : null;
2557
- const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue, isVolatile, serverTimestamp);
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
- for (const entry of childRemovedSubs) {
2783
+ const prevKey = view.getPreviousChildKey(key);
2784
+ for (const entry of childAddedSubs) {
2560
2785
  try {
2561
- entry.callback(snapshot, void 0);
2786
+ entry.callback(snapshot, prevKey);
2562
2787
  } catch (err) {
2563
- console.error("Error in child_removed callback:", err);
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
- * Fetches current value and sets it with the new priority.
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
- async setPriority(priority) {
3217
+ setPriority(priority) {
2990
3218
  validateNotInfoPath(this._path, "setPriority");
2991
- const snapshot = await this.once();
2992
- const currentVal = snapshot.val();
2993
- if (currentVal === null || currentVal === void 0) {
2994
- await this._db._sendSet(this._path, { ".priority": priority });
2995
- return;
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
- await this.setWithPriority(currentVal, priority);
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(currentValue)) {
3036
- condition = { o: "c", p: this._path, v: currentValue };
3285
+ if (isPrimitive(rawValue)) {
3286
+ condition = { o: "c", p: this._path, v: rawValue };
3037
3287
  } else {
3038
- const hash = await hashValue(currentValue);
3288
+ const hash = await hashValue(rawValue);
3039
3289
  condition = { o: "c", p: this._path, h: hash };
3040
3290
  }
3041
- const ops = [condition, { o: "s", p: this._path, v: newValue }];
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
- * @param eventType - The event type (only 'value' is supported)
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 !== "value") {
3073
- throw new Error('once() only supports "value" event type');
3331
+ if (eventType === "value") {
3332
+ return this._db._sendOnce(this._path, this._buildQueryParams());
3074
3333
  }
3075
- return this._db._sendOnce(this._path, this._buildQueryParams());
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
- return keys.length === 2 && ".value" in data && ".priority" in data;
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 data;
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
- result[key] = stripPriorityMetadata(value);
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
- return this._data !== null && this._data !== void 0;
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 currently connected.
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 === "connected";
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
- const requestId = this.messageQueue.nextRequestId();
4385
+ this._state = "connected";
4386
+ const joinRequestId = this.messageQueue.nextRequestId();
4087
4387
  const joinMessage = {
4088
4388
  o: "j",
4089
- t: connectResponse.token,
4090
- r: requestId
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(requestId);
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
- const jwtPayload = decodeJwtPayload(connectResponse.token);
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: jwtPayload.sub,
4105
- provider: jwtPayload.provider,
4106
- token: jwtPayload.claims || {}
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 = "connected";
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 wasConnected = this._state === "connected";
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 (wasConnected && this.transport) {
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 (wasConnected) {
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 wasConnected = this._state === "connected";
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 (wasConnected) {
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 === "connected";
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 wasConnected = this._state === "connected";
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 (wasConnected) {
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 ((wasConnected || wasReconnecting) && canReconnect) {
4977
+ if ((wasAuthenticated || wasPartiallyConnected || wasReconnecting) && canReconnect) {
4571
4978
  this._state = "reconnecting";
4572
4979
  this.cleanupForReconnect();
4573
4980
  this.reconnectingCallbacks.forEach((cb) => cb());
4574
- if (wasConnected) {
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 (wasConnected) {
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",