@lark-sh/client 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -12,7 +12,9 @@ var ErrorCode = {
12
12
  INVALID_PATH: "invalid_path",
13
13
  INVALID_OPERATION: "invalid_operation",
14
14
  INTERNAL_ERROR: "internal_error",
15
- CONDITION_FAILED: "condition_failed"
15
+ CONDITION_FAILED: "condition_failed",
16
+ INVALID_QUERY: "invalid_query",
17
+ AUTH_REQUIRED: "auth_required"
16
18
  };
17
19
  var DEFAULT_COORDINATOR_URL = "https://db.lark.sh";
18
20
 
@@ -418,6 +420,9 @@ function isAckMessage(msg) {
418
420
  function isJoinCompleteMessage(msg) {
419
421
  return "jc" in msg;
420
422
  }
423
+ function isAuthCompleteMessage(msg) {
424
+ return "ac" in msg;
425
+ }
421
426
  function isNackMessage(msg) {
422
427
  return "n" in msg;
423
428
  }
@@ -476,7 +481,20 @@ var MessageQueue = class {
476
481
  this.pending.delete(message.jc);
477
482
  const response = {
478
483
  volatilePaths: message.vp || [],
479
- connectionId: message.cid || null
484
+ connectionId: message.cid || null,
485
+ serverTime: message.st ?? null
486
+ };
487
+ pending.resolve(response);
488
+ return true;
489
+ }
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
480
498
  };
481
499
  pending.resolve(response);
482
500
  return true;
@@ -505,7 +523,7 @@ var MessageQueue = class {
505
523
  if (pending) {
506
524
  clearTimeout(pending.timeout);
507
525
  this.pending.delete(message.oc);
508
- pending.resolve(message.ov);
526
+ pending.resolve(message.ov ?? null);
509
527
  return true;
510
528
  }
511
529
  }
@@ -803,7 +821,7 @@ function getNestedValue(obj, path) {
803
821
  }
804
822
  function getSortValue(value, queryParams) {
805
823
  if (!queryParams) {
806
- return null;
824
+ return getNestedValue(value, ".priority");
807
825
  }
808
826
  if (queryParams.orderBy === "priority") {
809
827
  return getNestedValue(value, ".priority");
@@ -814,10 +832,18 @@ function getSortValue(value, queryParams) {
814
832
  if (queryParams.orderBy === "value") {
815
833
  return value;
816
834
  }
835
+ if (queryParams.orderBy === "key") {
836
+ return null;
837
+ }
838
+ const hasRangeFilter = queryParams.startAt !== void 0 || queryParams.startAfter !== void 0 || queryParams.endAt !== void 0 || queryParams.endBefore !== void 0 || queryParams.equalTo !== void 0;
839
+ const hasLimit = queryParams.limitToFirst !== void 0 || queryParams.limitToLast !== void 0;
840
+ if (hasRangeFilter || hasLimit) {
841
+ return getNestedValue(value, ".priority");
842
+ }
817
843
  return null;
818
844
  }
819
845
  function compareEntries(a, b, queryParams) {
820
- if (!queryParams || queryParams.orderBy === "key" || !queryParams.orderBy && !queryParams.orderByChild) {
846
+ if (queryParams?.orderBy === "key") {
821
847
  return compareKeys(a.key, b.key);
822
848
  }
823
849
  const cmp = compareValues(a.sortValue, b.sortValue);
@@ -836,7 +862,11 @@ function createSortEntries(data, queryParams) {
836
862
  }
837
863
  const obj = data;
838
864
  const entries = [];
839
- for (const key of Object.keys(obj)) {
865
+ const keys = Object.keys(obj);
866
+ if (keys.length === 2 && ".value" in obj && ".priority" in obj) {
867
+ return [];
868
+ }
869
+ for (const key of keys) {
840
870
  if (key === ".priority") {
841
871
  continue;
842
872
  }
@@ -859,16 +889,17 @@ var View = class {
859
889
  constructor(path, queryParams) {
860
890
  /** Event callbacks organized by event type */
861
891
  this.eventCallbacks = /* @__PURE__ */ new Map();
862
- /** Child keys in sorted order */
892
+ /** Child keys in sorted order (computed from display cache) */
863
893
  this._orderedChildren = [];
864
- /** Local cache: stores the value at the subscription path */
865
- this._cache = void 0;
894
+ /** Server cache: what the server has told us (baseline) */
895
+ this._serverCache = null;
866
896
  /** Whether we've received the initial snapshot from the server */
867
897
  this._hasReceivedInitialSnapshot = false;
868
- /** Pending write request IDs for this View (for local-first recovery) */
869
- this._pendingWrites = /* @__PURE__ */ new Set();
870
- /** Whether this View is in recovery mode after a nack */
871
- this._recovering = false;
898
+ /** Pending write operations that haven't been ACKed yet (for local-first) */
899
+ this._pendingWriteData = [];
900
+ /** Cached display value (server + pending writes merged) - invalidated when either changes */
901
+ this._displayCacheValid = false;
902
+ this._displayCache = null;
872
903
  this.path = normalizePath(path);
873
904
  this._queryParams = queryParams ?? null;
874
905
  }
@@ -886,9 +917,8 @@ var View = class {
886
917
  return false;
887
918
  }
888
919
  this._queryParams = newParams;
889
- if (this._cache && typeof this._cache === "object" && this._cache !== null) {
890
- this._orderedChildren = getSortedKeys(this._cache, this._queryParams);
891
- }
920
+ this._displayCacheValid = false;
921
+ this.recomputeOrderedChildren();
892
922
  return true;
893
923
  }
894
924
  /**
@@ -962,138 +992,368 @@ var View = class {
962
992
  return entries !== void 0 && entries.length > 0;
963
993
  }
964
994
  // ============================================
965
- // Cache Management
995
+ // Cache Management (Dual Cache: Server + Pending Writes)
966
996
  // ============================================
967
997
  /**
968
- * Set the full cache value (used for initial snapshot).
969
- * Children are sorted using client-side sorting rules.
998
+ * Set the server cache (what the server told us).
999
+ * This is the baseline before pending writes are applied.
1000
+ * Deep clones the value to ensure each View has its own copy.
970
1001
  */
971
- setCache(value) {
972
- this._cache = value;
1002
+ setServerCache(value) {
1003
+ this._serverCache = this.deepClone(value);
973
1004
  this._hasReceivedInitialSnapshot = true;
974
- if (value && typeof value === "object" && value !== null && !Array.isArray(value)) {
975
- this._orderedChildren = getSortedKeys(value, this.queryParams);
976
- } else {
977
- this._orderedChildren = [];
1005
+ this.invalidateDisplayCache();
1006
+ }
1007
+ /**
1008
+ * Get the server cache (baseline without pending writes).
1009
+ */
1010
+ getServerCache() {
1011
+ return this._serverCache;
1012
+ }
1013
+ /**
1014
+ * Get the display cache (server + pending writes merged).
1015
+ * This is what should be shown to the user.
1016
+ */
1017
+ getDisplayCache() {
1018
+ if (!this._displayCacheValid) {
1019
+ this._displayCache = this.computeMergedCache();
1020
+ this._displayCacheValid = true;
978
1021
  }
1022
+ return this._displayCache;
979
1023
  }
980
1024
  /**
981
- * Get the full cached value at the subscription path.
1025
+ * Alias for getDisplayCache() - this is what callbacks receive.
982
1026
  */
983
1027
  getCache() {
984
- return this._cache;
1028
+ return this.getDisplayCache();
1029
+ }
1030
+ /**
1031
+ * Invalidate the display cache (call when serverCache or pendingWrites change).
1032
+ */
1033
+ invalidateDisplayCache() {
1034
+ this._displayCacheValid = false;
1035
+ this.recomputeOrderedChildren();
1036
+ }
1037
+ /**
1038
+ * Compute the merged cache (serverCache + pendingWrites), with query constraints applied.
1039
+ */
1040
+ computeMergedCache() {
1041
+ let result;
1042
+ if (this._pendingWriteData.length === 0) {
1043
+ result = this._serverCache;
1044
+ } else {
1045
+ result = this.deepClone(this._serverCache);
1046
+ for (const write of this._pendingWriteData) {
1047
+ result = this.applyWrite(result, write);
1048
+ }
1049
+ }
1050
+ return this.applyQueryConstraints(result);
1051
+ }
1052
+ /**
1053
+ * Apply query constraints (range filtering and limits) to data.
1054
+ * This ensures the displayCache only contains data matching the query.
1055
+ */
1056
+ applyQueryConstraints(data) {
1057
+ if (!this._queryParams) {
1058
+ return data;
1059
+ }
1060
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
1061
+ return data;
1062
+ }
1063
+ const obj = data;
1064
+ const hasRangeFilter = this._queryParams.startAt !== void 0 || this._queryParams.startAfter !== void 0 || this._queryParams.endAt !== void 0 || this._queryParams.endBefore !== void 0 || this._queryParams.equalTo !== void 0;
1065
+ const hasLimit = this._queryParams.limitToFirst !== void 0 || this._queryParams.limitToLast !== void 0;
1066
+ if (!hasRangeFilter && !hasLimit) {
1067
+ return data;
1068
+ }
1069
+ let entries = createSortEntries(obj, this._queryParams);
1070
+ if (hasRangeFilter) {
1071
+ entries = this.filterByRange(entries);
1072
+ }
1073
+ if (hasLimit) {
1074
+ entries = this.applyLimits(entries);
1075
+ }
1076
+ if (entries.length === 0) {
1077
+ return null;
1078
+ }
1079
+ const result = {};
1080
+ for (const entry of entries) {
1081
+ result[entry.key] = entry.value;
1082
+ }
1083
+ return result;
1084
+ }
1085
+ /**
1086
+ * Filter entries by range constraints (startAt, endAt, startAfter, endBefore, equalTo).
1087
+ */
1088
+ filterByRange(entries) {
1089
+ if (!this._queryParams) return entries;
1090
+ const {
1091
+ orderBy,
1092
+ startAt,
1093
+ startAtKey,
1094
+ startAfter,
1095
+ startAfterKey,
1096
+ endAt,
1097
+ endAtKey,
1098
+ endBefore,
1099
+ endBeforeKey,
1100
+ equalTo,
1101
+ equalToKey
1102
+ } = this._queryParams;
1103
+ const isOrderByKey = orderBy === "key";
1104
+ return entries.filter((entry) => {
1105
+ const compareValue = isOrderByKey ? entry.key : entry.sortValue;
1106
+ const compareFn = isOrderByKey ? (a, b) => compareKeys(a, b) : compareValues;
1107
+ if (equalTo !== void 0) {
1108
+ const cmp = compareFn(compareValue, equalTo);
1109
+ if (cmp !== 0) return false;
1110
+ if (equalToKey !== void 0 && !isOrderByKey) {
1111
+ const keyCmp = compareKeys(entry.key, equalToKey);
1112
+ if (keyCmp !== 0) return false;
1113
+ }
1114
+ return true;
1115
+ }
1116
+ if (startAt !== void 0) {
1117
+ const cmp = compareFn(compareValue, startAt);
1118
+ if (cmp < 0) return false;
1119
+ if (cmp === 0 && startAtKey !== void 0 && !isOrderByKey) {
1120
+ const keyCmp = compareKeys(entry.key, startAtKey);
1121
+ if (keyCmp < 0) return false;
1122
+ }
1123
+ }
1124
+ if (startAfter !== void 0) {
1125
+ const cmp = compareFn(compareValue, startAfter);
1126
+ if (cmp < 0) return false;
1127
+ if (cmp === 0) {
1128
+ if (startAfterKey !== void 0 && !isOrderByKey) {
1129
+ const keyCmp = compareKeys(entry.key, startAfterKey);
1130
+ if (keyCmp <= 0) return false;
1131
+ } else {
1132
+ return false;
1133
+ }
1134
+ }
1135
+ }
1136
+ if (endAt !== void 0) {
1137
+ const cmp = compareFn(compareValue, endAt);
1138
+ if (cmp > 0) return false;
1139
+ if (cmp === 0 && endAtKey !== void 0 && !isOrderByKey) {
1140
+ const keyCmp = compareKeys(entry.key, endAtKey);
1141
+ if (keyCmp > 0) return false;
1142
+ }
1143
+ }
1144
+ if (endBefore !== void 0) {
1145
+ const cmp = compareFn(compareValue, endBefore);
1146
+ if (cmp > 0) return false;
1147
+ if (cmp === 0) {
1148
+ if (endBeforeKey !== void 0 && !isOrderByKey) {
1149
+ const keyCmp = compareKeys(entry.key, endBeforeKey);
1150
+ if (keyCmp >= 0) return false;
1151
+ } else {
1152
+ return false;
1153
+ }
1154
+ }
1155
+ }
1156
+ return true;
1157
+ });
985
1158
  }
986
1159
  /**
987
- * Get a value from the cache at a relative or absolute path.
1160
+ * Apply limit constraints (limitToFirst, limitToLast) to entries.
1161
+ * Entries are already sorted, so we just slice.
1162
+ */
1163
+ applyLimits(entries) {
1164
+ if (!this._queryParams) return entries;
1165
+ const { limitToFirst, limitToLast } = this._queryParams;
1166
+ if (limitToFirst !== void 0) {
1167
+ return entries.slice(0, limitToFirst);
1168
+ }
1169
+ if (limitToLast !== void 0) {
1170
+ return entries.slice(-limitToLast);
1171
+ }
1172
+ return entries;
1173
+ }
1174
+ /**
1175
+ * Apply a single write operation to a value.
1176
+ */
1177
+ applyWrite(base, write) {
1178
+ const { relativePath, value, operation } = write;
1179
+ if (operation === "delete") {
1180
+ if (relativePath === "/") {
1181
+ return null;
1182
+ }
1183
+ return this.deleteAtPath(base, relativePath);
1184
+ }
1185
+ if (operation === "set") {
1186
+ if (relativePath === "/") {
1187
+ return value;
1188
+ }
1189
+ return this.setAtPath(base, relativePath, value);
1190
+ }
1191
+ if (operation === "update") {
1192
+ if (relativePath === "/") {
1193
+ if (base === null || base === void 0 || typeof base !== "object") {
1194
+ base = {};
1195
+ }
1196
+ const merged2 = { ...base, ...value };
1197
+ for (const key of Object.keys(merged2)) {
1198
+ if (merged2[key] === null) {
1199
+ delete merged2[key];
1200
+ }
1201
+ }
1202
+ return merged2;
1203
+ }
1204
+ const current = getValueAtPath(base, relativePath);
1205
+ let merged;
1206
+ if (current && typeof current === "object") {
1207
+ merged = { ...current, ...value };
1208
+ for (const key of Object.keys(merged)) {
1209
+ if (merged[key] === null) {
1210
+ delete merged[key];
1211
+ }
1212
+ }
1213
+ } else {
1214
+ merged = value;
1215
+ }
1216
+ return this.setAtPath(base, relativePath, merged);
1217
+ }
1218
+ return base;
1219
+ }
1220
+ /**
1221
+ * Set a value at a path in an object, creating intermediate objects as needed.
1222
+ */
1223
+ setAtPath(obj, path, value) {
1224
+ if (path === "/") return value;
1225
+ const segments = path.split("/").filter((s) => s.length > 0);
1226
+ if (segments.length === 0) return value;
1227
+ const result = obj === null || obj === void 0 || typeof obj !== "object" ? {} : { ...obj };
1228
+ let current = result;
1229
+ for (let i = 0; i < segments.length - 1; i++) {
1230
+ const key = segments[i];
1231
+ if (current[key] === null || current[key] === void 0 || typeof current[key] !== "object") {
1232
+ current[key] = {};
1233
+ } else {
1234
+ current[key] = { ...current[key] };
1235
+ }
1236
+ current = current[key];
1237
+ }
1238
+ const lastKey = segments[segments.length - 1];
1239
+ current[lastKey] = value;
1240
+ return result;
1241
+ }
1242
+ /**
1243
+ * Delete a value at a path in an object.
1244
+ */
1245
+ deleteAtPath(obj, path) {
1246
+ if (path === "/" || obj === null || obj === void 0 || typeof obj !== "object") {
1247
+ return null;
1248
+ }
1249
+ const segments = path.split("/").filter((s) => s.length > 0);
1250
+ if (segments.length === 0) return null;
1251
+ const result = { ...obj };
1252
+ if (segments.length === 1) {
1253
+ delete result[segments[0]];
1254
+ return Object.keys(result).length > 0 ? result : null;
1255
+ }
1256
+ let current = result;
1257
+ for (let i = 0; i < segments.length - 1; i++) {
1258
+ const key = segments[i];
1259
+ if (current[key] === null || current[key] === void 0 || typeof current[key] !== "object") {
1260
+ return result;
1261
+ }
1262
+ current[key] = { ...current[key] };
1263
+ current = current[key];
1264
+ }
1265
+ delete current[segments[segments.length - 1]];
1266
+ return result;
1267
+ }
1268
+ /**
1269
+ * Deep clone a value.
1270
+ */
1271
+ deepClone(value) {
1272
+ if (value === null || value === void 0) return value;
1273
+ if (typeof value !== "object") return value;
1274
+ if (Array.isArray(value)) return value.map((v) => this.deepClone(v));
1275
+ const result = {};
1276
+ for (const [k, v] of Object.entries(value)) {
1277
+ result[k] = this.deepClone(v);
1278
+ }
1279
+ return result;
1280
+ }
1281
+ /**
1282
+ * Recompute ordered children from display cache.
1283
+ */
1284
+ recomputeOrderedChildren() {
1285
+ const displayCache = this.getDisplayCache();
1286
+ if (displayCache && typeof displayCache === "object" && !Array.isArray(displayCache)) {
1287
+ this._orderedChildren = getSortedKeys(displayCache, this.queryParams);
1288
+ } else {
1289
+ this._orderedChildren = [];
1290
+ }
1291
+ }
1292
+ /**
1293
+ * Get a value from the display cache at a relative or absolute path.
988
1294
  * If the path is outside this View's scope, returns undefined.
989
1295
  */
990
1296
  getCacheValue(path) {
991
1297
  const normalized = normalizePath(path);
1298
+ const displayCache = this.getDisplayCache();
992
1299
  if (normalized === this.path) {
993
- if (this._cache !== void 0) {
994
- return { value: this._cache, found: true };
1300
+ if (displayCache !== void 0 && displayCache !== null) {
1301
+ return { value: displayCache, found: true };
1302
+ }
1303
+ if (this._hasReceivedInitialSnapshot || this._pendingWriteData.length > 0) {
1304
+ return { value: displayCache ?? null, found: true };
995
1305
  }
996
1306
  return { value: void 0, found: false };
997
1307
  }
998
1308
  if (normalized.startsWith(this.path + "/") || this.path === "/") {
999
1309
  const relativePath = this.path === "/" ? normalized : normalized.slice(this.path.length);
1000
- if (this._cache !== void 0) {
1001
- const extractedValue = getValueAtPath(this._cache, relativePath);
1002
- return { value: extractedValue, found: true };
1310
+ if (this._hasReceivedInitialSnapshot || this._pendingWriteData.length > 0) {
1311
+ const extractedValue = getValueAtPath(displayCache, relativePath);
1312
+ return { value: extractedValue ?? null, found: true };
1003
1313
  }
1004
1314
  }
1005
1315
  return { value: void 0, found: false };
1006
1316
  }
1007
1317
  /**
1008
- * Update a child value in the cache.
1318
+ * Update a child value in the SERVER cache.
1009
1319
  * relativePath should be relative to this View's path.
1010
- * Maintains sorted order of children using client-side sorting.
1320
+ * Called when server events (put/patch) arrive.
1011
1321
  */
1012
- updateCacheChild(relativePath, value) {
1322
+ updateServerCacheChild(relativePath, value) {
1013
1323
  if (relativePath === "/") {
1014
- this.setCache(value);
1324
+ this.setServerCache(value);
1015
1325
  return;
1016
1326
  }
1017
1327
  const segments = relativePath.split("/").filter((s) => s.length > 0);
1018
1328
  if (segments.length === 0) return;
1019
- const childKey = segments[0];
1020
- if (this._cache === null || this._cache === void 0 || typeof this._cache !== "object") {
1021
- this._cache = {};
1329
+ if (this._serverCache === null || this._serverCache === void 0 || typeof this._serverCache !== "object") {
1330
+ this._serverCache = {};
1022
1331
  }
1023
- const cache = this._cache;
1332
+ const cache = this._serverCache;
1024
1333
  if (segments.length === 1) {
1025
1334
  if (value === null) {
1026
- delete cache[childKey];
1027
- const idx = this._orderedChildren.indexOf(childKey);
1028
- if (idx !== -1) {
1029
- this._orderedChildren.splice(idx, 1);
1030
- }
1335
+ delete cache[segments[0]];
1031
1336
  } else {
1032
- const wasPresent = childKey in cache;
1033
- cache[childKey] = value;
1034
- if (!wasPresent) {
1035
- this.insertChildSorted(childKey, value);
1036
- } else {
1037
- this.resortChild(childKey);
1038
- }
1337
+ cache[segments[0]] = value;
1039
1338
  }
1040
1339
  } else {
1041
1340
  if (value === null) {
1042
1341
  setValueAtPath(cache, relativePath, void 0);
1043
1342
  } else {
1044
- const wasPresent = childKey in cache;
1045
- if (!wasPresent) {
1343
+ const childKey = segments[0];
1344
+ if (!(childKey in cache)) {
1046
1345
  cache[childKey] = {};
1047
1346
  }
1048
1347
  setValueAtPath(cache, relativePath, value);
1049
- if (!wasPresent) {
1050
- this.insertChildSorted(childKey, cache[childKey]);
1051
- } else {
1052
- this.resortChild(childKey);
1053
- }
1054
- }
1055
- }
1056
- }
1057
- /**
1058
- * Insert a child key at the correct sorted position.
1059
- */
1060
- insertChildSorted(key, value) {
1061
- if (this._orderedChildren.includes(key)) {
1062
- this.resortChild(key);
1063
- return;
1064
- }
1065
- const cache = this._cache;
1066
- const newSortValue = getSortValue(value, this.queryParams);
1067
- const newEntry = { key, value, sortValue: newSortValue };
1068
- let insertIdx = this._orderedChildren.length;
1069
- for (let i = 0; i < this._orderedChildren.length; i++) {
1070
- const existingKey = this._orderedChildren[i];
1071
- const existingValue = cache[existingKey];
1072
- const existingSortValue = getSortValue(existingValue, this.queryParams);
1073
- const existingEntry = { key: existingKey, value: existingValue, sortValue: existingSortValue };
1074
- if (compareEntries(newEntry, existingEntry, this.queryParams) < 0) {
1075
- insertIdx = i;
1076
- break;
1077
1348
  }
1078
1349
  }
1079
- this._orderedChildren.splice(insertIdx, 0, key);
1350
+ this.invalidateDisplayCache();
1080
1351
  }
1081
1352
  /**
1082
- * Re-sort a child that already exists (its sort value may have changed).
1353
+ * Remove a child from the SERVER cache.
1083
1354
  */
1084
- resortChild(key) {
1085
- const idx = this._orderedChildren.indexOf(key);
1086
- if (idx === -1) return;
1087
- this._orderedChildren.splice(idx, 1);
1088
- const cache = this._cache;
1089
- const value = cache[key];
1090
- this.insertChildSorted(key, value);
1091
- }
1092
- /**
1093
- * Remove a child from the cache.
1094
- */
1095
- removeCacheChild(relativePath) {
1096
- this.updateCacheChild(relativePath, null);
1355
+ removeServerCacheChild(relativePath) {
1356
+ this.updateServerCacheChild(relativePath, null);
1097
1357
  }
1098
1358
  /**
1099
1359
  * Check if we've received the initial snapshot.
@@ -1142,101 +1402,117 @@ var View = class {
1142
1402
  if (!this.queryParams) return false;
1143
1403
  return !!(this.queryParams.limitToFirst || this.queryParams.limitToLast);
1144
1404
  }
1405
+ /**
1406
+ * Check if this View loads all data (no limits, no range filters).
1407
+ * A View that loads all data can serve as a "complete" cache for child paths.
1408
+ * This matches Firebase's loadsAllData() semantics.
1409
+ */
1410
+ loadsAllData() {
1411
+ if (!this.queryParams) return true;
1412
+ const hasLimit = !!(this.queryParams.limitToFirst || this.queryParams.limitToLast);
1413
+ const hasRangeFilter = this.queryParams.startAt !== void 0 || this.queryParams.endAt !== void 0 || this.queryParams.equalTo !== void 0 || this.queryParams.startAfter !== void 0 || this.queryParams.endBefore !== void 0;
1414
+ return !hasLimit && !hasRangeFilter;
1415
+ }
1145
1416
  // ============================================
1146
1417
  // Pending Writes (for local-first)
1147
1418
  // ============================================
1148
1419
  /**
1149
1420
  * Get current pending write IDs (to include in pw field).
1150
- * Returns a copy of the set as an array.
1151
1421
  */
1152
1422
  getPendingWriteIds() {
1153
- return Array.from(this._pendingWrites);
1423
+ return this._pendingWriteData.filter((w) => w.requestId !== "").map((w) => w.requestId);
1154
1424
  }
1155
1425
  /**
1156
- * Add a pending write request ID.
1426
+ * Add a pending write with its data.
1427
+ * This is used for local-first writes - the write is applied to displayCache immediately.
1157
1428
  */
1158
- addPendingWrite(requestId) {
1159
- this._pendingWrites.add(requestId);
1429
+ addPendingWriteData(requestId, relativePath, value, operation) {
1430
+ this._pendingWriteData.push({ requestId, relativePath, value, operation });
1431
+ this.invalidateDisplayCache();
1160
1432
  }
1161
1433
  /**
1162
- * Remove a pending write (on ack or nack).
1434
+ * Remove a pending write by request ID (on ack or nack).
1435
+ * Returns true if the write was found and removed.
1163
1436
  */
1164
1437
  removePendingWrite(requestId) {
1165
- return this._pendingWrites.delete(requestId);
1438
+ const idx = this._pendingWriteData.findIndex((w) => w.requestId === requestId);
1439
+ if (idx === -1) return false;
1440
+ this._pendingWriteData.splice(idx, 1);
1441
+ this.invalidateDisplayCache();
1442
+ return true;
1166
1443
  }
1167
1444
  /**
1168
- * Clear all pending writes (on nack recovery).
1445
+ * Clear all pending writes.
1169
1446
  */
1170
1447
  clearPendingWrites() {
1171
- this._pendingWrites.clear();
1448
+ if (this._pendingWriteData.length > 0) {
1449
+ this._pendingWriteData = [];
1450
+ this.invalidateDisplayCache();
1451
+ }
1172
1452
  }
1173
1453
  /**
1174
1454
  * Check if this View has pending writes.
1175
1455
  */
1176
1456
  hasPendingWrites() {
1177
- return this._pendingWrites.size > 0;
1178
- }
1179
- /**
1180
- * Check if a specific write is pending.
1181
- */
1182
- isWritePending(requestId) {
1183
- return this._pendingWrites.has(requestId);
1184
- }
1185
- // ============================================
1186
- // Recovery State (for local-first nack handling)
1187
- // ============================================
1188
- /**
1189
- * Check if this View is in recovery mode.
1190
- */
1191
- get recovering() {
1192
- return this._recovering;
1457
+ return this._pendingWriteData.some((w) => w.requestId !== "");
1193
1458
  }
1194
1459
  /**
1195
- * Enter recovery mode (after a nack).
1460
+ * Get the pending write data (for debugging/testing).
1196
1461
  */
1197
- enterRecovery() {
1198
- this._recovering = true;
1199
- this.clearPendingWrites();
1462
+ getPendingWriteData() {
1463
+ return [...this._pendingWriteData];
1200
1464
  }
1201
1465
  /**
1202
- * Exit recovery mode (after fresh snapshot received).
1466
+ * Check if a specific write is pending.
1203
1467
  */
1204
- exitRecovery() {
1205
- this._recovering = false;
1468
+ isWritePending(requestId) {
1469
+ return this._pendingWriteData.some((w) => w.requestId === requestId);
1206
1470
  }
1207
1471
  // ============================================
1208
1472
  // Cleanup
1209
1473
  // ============================================
1210
1474
  /**
1211
- * Clear the cache but preserve callbacks and pending writes.
1475
+ * Clear the server cache but preserve callbacks and pending writes.
1212
1476
  * Used during reconnection.
1213
1477
  */
1214
- clearCache() {
1215
- this._cache = void 0;
1216
- this._orderedChildren = [];
1478
+ clearServerCache() {
1479
+ this._serverCache = null;
1217
1480
  this._hasReceivedInitialSnapshot = false;
1481
+ this.invalidateDisplayCache();
1218
1482
  }
1219
1483
  /**
1220
1484
  * Clear everything - used when unsubscribing completely.
1221
1485
  */
1222
1486
  clear() {
1223
1487
  this.eventCallbacks.clear();
1224
- this._cache = void 0;
1488
+ this._serverCache = null;
1225
1489
  this._orderedChildren = [];
1226
1490
  this._hasReceivedInitialSnapshot = false;
1227
- this._pendingWrites.clear();
1228
- this._recovering = false;
1491
+ this._pendingWriteData = [];
1492
+ this._displayCacheValid = false;
1493
+ this._displayCache = null;
1229
1494
  }
1230
1495
  };
1231
1496
 
1232
1497
  // src/connection/SubscriptionManager.ts
1233
1498
  var SubscriptionManager = class {
1234
1499
  constructor() {
1235
- // path -> View (one View per subscribed path)
1500
+ // viewKey (path:queryIdentifier) -> View
1501
+ // Each unique path+query combination gets its own View
1236
1502
  this.views = /* @__PURE__ */ new Map();
1503
+ // path -> Set of queryIdentifiers for that path
1504
+ // Used to find all Views at a path when events arrive
1505
+ this.pathToQueryIds = /* @__PURE__ */ new Map();
1506
+ // Tag counter for generating unique tags per non-default query
1507
+ // Tags are client-local and used to route server events to correct View
1508
+ this.nextTag = 1;
1509
+ // tag -> viewKey mapping for routing tagged events
1510
+ this.tagToViewKey = /* @__PURE__ */ new Map();
1511
+ // viewKey -> tag mapping for unsubscribe
1512
+ this.viewKeyToTag = /* @__PURE__ */ new Map();
1237
1513
  // Callback to send subscribe message to server
1238
1514
  this.sendSubscribe = null;
1239
- // Callback to send unsubscribe message to server
1515
+ // Callback to send unsubscribe message to server (includes queryParams and tag for server to identify subscription)
1240
1516
  this.sendUnsubscribe = null;
1241
1517
  // Callback to create DataSnapshot from event data
1242
1518
  this.createSnapshot = null;
@@ -1249,32 +1525,73 @@ var SubscriptionManager = class {
1249
1525
  this.sendUnsubscribe = options.sendUnsubscribe;
1250
1526
  this.createSnapshot = options.createSnapshot;
1251
1527
  }
1528
+ /**
1529
+ * Create a view key from path and query identifier.
1530
+ */
1531
+ makeViewKey(path, queryIdentifier) {
1532
+ return `${path}:${queryIdentifier}`;
1533
+ }
1534
+ /**
1535
+ * Get all Views at a given path (across all query identifiers).
1536
+ */
1537
+ getViewsAtPath(path) {
1538
+ const queryIds = this.pathToQueryIds.get(path);
1539
+ if (!queryIds || queryIds.size === 0) {
1540
+ return [];
1541
+ }
1542
+ const views = [];
1543
+ for (const queryId of queryIds) {
1544
+ const viewKey = this.makeViewKey(path, queryId);
1545
+ const view = this.views.get(viewKey);
1546
+ if (view) {
1547
+ views.push(view);
1548
+ }
1549
+ }
1550
+ return views;
1551
+ }
1252
1552
  /**
1253
1553
  * Subscribe to events at a path.
1254
1554
  * Returns an unsubscribe function.
1255
1555
  */
1256
- subscribe(path, eventType, callback, queryParams) {
1556
+ subscribe(path, eventType, callback, queryParams, queryIdentifier) {
1257
1557
  const normalizedPath = path;
1258
- let view = this.views.get(normalizedPath);
1558
+ const queryId = queryIdentifier ?? "default";
1559
+ const viewKey = this.makeViewKey(normalizedPath, queryId);
1560
+ const isNonDefaultQuery = queryId !== "default";
1561
+ let view = this.views.get(viewKey);
1259
1562
  const isNewView = !view;
1260
1563
  let queryParamsChanged = false;
1564
+ let tag;
1261
1565
  if (!view) {
1262
1566
  view = new View(normalizedPath, queryParams);
1263
- this.views.set(normalizedPath, view);
1567
+ this.views.set(viewKey, view);
1568
+ if (!this.pathToQueryIds.has(normalizedPath)) {
1569
+ this.pathToQueryIds.set(normalizedPath, /* @__PURE__ */ new Set());
1570
+ }
1571
+ this.pathToQueryIds.get(normalizedPath).add(queryId);
1572
+ if (isNonDefaultQuery) {
1573
+ tag = this.nextTag++;
1574
+ this.tagToViewKey.set(tag, viewKey);
1575
+ this.viewKeyToTag.set(viewKey, tag);
1576
+ }
1264
1577
  } else {
1265
1578
  queryParamsChanged = view.updateQueryParams(queryParams);
1579
+ tag = this.viewKeyToTag.get(viewKey);
1266
1580
  }
1267
1581
  const existingEventTypes = view.getEventTypes();
1268
1582
  const isNewEventType = !existingEventTypes.includes(eventType);
1269
1583
  const unsubscribe = view.addCallback(eventType, callback);
1270
1584
  const wrappedUnsubscribe = () => {
1271
- this.unsubscribeCallback(normalizedPath, eventType, callback);
1585
+ this.unsubscribeCallback(normalizedPath, eventType, callback, queryId);
1272
1586
  };
1273
1587
  if (isNewView || isNewEventType || queryParamsChanged) {
1274
- const allEventTypes = view.getEventTypes();
1275
- this.sendSubscribe?.(normalizedPath, allEventTypes, view.queryParams ?? void 0).catch((err) => {
1276
- console.error("Failed to subscribe:", err);
1277
- });
1588
+ const hasAncestorComplete = this.hasAncestorCompleteView(normalizedPath);
1589
+ if (!hasAncestorComplete) {
1590
+ const allEventTypes = view.getEventTypes();
1591
+ this.sendSubscribe?.(normalizedPath, allEventTypes, view.queryParams ?? void 0, tag).catch((err) => {
1592
+ console.error("Failed to subscribe:", err);
1593
+ });
1594
+ }
1278
1595
  }
1279
1596
  if (!isNewView && view.hasReceivedInitialSnapshot) {
1280
1597
  this.fireInitialEventsToCallback(view, eventType, callback);
@@ -1288,7 +1605,7 @@ var SubscriptionManager = class {
1288
1605
  fireInitialEventsToCallback(view, eventType, callback) {
1289
1606
  const cache = view.getCache();
1290
1607
  if (eventType === "value") {
1291
- const snapshot = this.createSnapshot?.(view.path, cache, false);
1608
+ const snapshot = this.createSnapshot?.(view.path, cache, false, void 0, view.orderedChildren);
1292
1609
  if (snapshot) {
1293
1610
  try {
1294
1611
  callback(snapshot, void 0);
@@ -1317,49 +1634,84 @@ var SubscriptionManager = class {
1317
1634
  /**
1318
1635
  * Remove a specific callback from a subscription.
1319
1636
  */
1320
- unsubscribeCallback(path, eventType, callback) {
1321
- const view = this.views.get(path);
1637
+ unsubscribeCallback(path, eventType, callback, queryIdentifier) {
1638
+ const queryId = queryIdentifier ?? "default";
1639
+ const viewKey = this.makeViewKey(path, queryId);
1640
+ const view = this.views.get(viewKey);
1322
1641
  if (!view) return;
1642
+ const queryParams = view.queryParams ?? void 0;
1643
+ const tag = this.viewKeyToTag.get(viewKey);
1323
1644
  view.removeCallback(eventType, callback);
1324
1645
  if (!view.hasCallbacks()) {
1325
- this.views.delete(path);
1326
- this.sendUnsubscribe?.(path).catch((err) => {
1646
+ this.views.delete(viewKey);
1647
+ const queryIds = this.pathToQueryIds.get(path);
1648
+ if (queryIds) {
1649
+ queryIds.delete(queryId);
1650
+ if (queryIds.size === 0) {
1651
+ this.pathToQueryIds.delete(path);
1652
+ }
1653
+ }
1654
+ if (tag !== void 0) {
1655
+ this.tagToViewKey.delete(tag);
1656
+ this.viewKeyToTag.delete(viewKey);
1657
+ }
1658
+ this.sendUnsubscribe?.(path, queryParams, tag).catch((err) => {
1327
1659
  console.error("Failed to unsubscribe:", err);
1328
1660
  });
1329
1661
  }
1330
1662
  }
1331
1663
  /**
1332
1664
  * Remove all subscriptions of a specific event type at a path.
1665
+ * This affects ALL Views at the path (across all query identifiers).
1333
1666
  */
1334
1667
  unsubscribeEventType(path, eventType) {
1335
1668
  const normalizedPath = path;
1336
- const view = this.views.get(normalizedPath);
1337
- if (!view) return;
1338
- view.removeAllCallbacks(eventType);
1339
- if (!view.hasCallbacks()) {
1340
- this.views.delete(normalizedPath);
1341
- this.sendUnsubscribe?.(normalizedPath).catch((err) => {
1342
- console.error("Failed to unsubscribe:", err);
1343
- });
1344
- } else {
1345
- const remainingEventTypes = view.getEventTypes();
1346
- this.sendSubscribe?.(normalizedPath, remainingEventTypes, view.queryParams ?? void 0).catch((err) => {
1347
- console.error("Failed to update subscription:", err);
1348
- });
1669
+ const queryIds = this.pathToQueryIds.get(normalizedPath);
1670
+ if (!queryIds) return;
1671
+ for (const queryId of Array.from(queryIds)) {
1672
+ const viewKey = this.makeViewKey(normalizedPath, queryId);
1673
+ const view = this.views.get(viewKey);
1674
+ if (!view) continue;
1675
+ const queryParams = view.queryParams ?? void 0;
1676
+ view.removeAllCallbacks(eventType);
1677
+ if (!view.hasCallbacks()) {
1678
+ this.views.delete(viewKey);
1679
+ queryIds.delete(queryId);
1680
+ this.sendUnsubscribe?.(normalizedPath, queryParams).catch((err) => {
1681
+ console.error("Failed to unsubscribe:", err);
1682
+ });
1683
+ } else {
1684
+ const remainingEventTypes = view.getEventTypes();
1685
+ this.sendSubscribe?.(normalizedPath, remainingEventTypes, queryParams).catch((err) => {
1686
+ console.error("Failed to update subscription:", err);
1687
+ });
1688
+ }
1689
+ }
1690
+ if (queryIds.size === 0) {
1691
+ this.pathToQueryIds.delete(normalizedPath);
1349
1692
  }
1350
1693
  }
1351
1694
  /**
1352
1695
  * Remove ALL subscriptions at a path.
1696
+ * This affects ALL Views at the path (across all query identifiers).
1353
1697
  */
1354
1698
  unsubscribeAll(path) {
1355
1699
  const normalizedPath = path;
1356
- const view = this.views.get(normalizedPath);
1357
- if (!view) return;
1358
- view.clear();
1359
- this.views.delete(normalizedPath);
1360
- this.sendUnsubscribe?.(normalizedPath).catch((err) => {
1361
- console.error("Failed to unsubscribe:", err);
1362
- });
1700
+ const queryIds = this.pathToQueryIds.get(normalizedPath);
1701
+ if (!queryIds || queryIds.size === 0) return;
1702
+ for (const queryId of queryIds) {
1703
+ const viewKey = this.makeViewKey(normalizedPath, queryId);
1704
+ const view = this.views.get(viewKey);
1705
+ if (view) {
1706
+ const queryParams = view.queryParams ?? void 0;
1707
+ view.clear();
1708
+ this.views.delete(viewKey);
1709
+ this.sendUnsubscribe?.(normalizedPath, queryParams).catch((err) => {
1710
+ console.error("Failed to unsubscribe:", err);
1711
+ });
1712
+ }
1713
+ }
1714
+ this.pathToQueryIds.delete(normalizedPath);
1363
1715
  }
1364
1716
  /**
1365
1717
  * Handle an incoming event message from the server.
@@ -1376,8 +1728,17 @@ var SubscriptionManager = class {
1376
1728
  console.warn("Unknown event type:", message.ev);
1377
1729
  }
1378
1730
  }
1731
+ /**
1732
+ * Get View by tag. Returns undefined if tag not found.
1733
+ */
1734
+ getViewByTag(tag) {
1735
+ const viewKey = this.tagToViewKey.get(tag);
1736
+ if (!viewKey) return void 0;
1737
+ return this.views.get(viewKey);
1738
+ }
1379
1739
  /**
1380
1740
  * Handle a 'put' event - single path change.
1741
+ * Routes to specific View by tag if present, otherwise to default View at path.
1381
1742
  */
1382
1743
  handlePutEvent(message) {
1383
1744
  const subscriptionPath = message.sp;
@@ -1385,21 +1746,24 @@ var SubscriptionManager = class {
1385
1746
  const value = message.v;
1386
1747
  const isVolatile = message.x ?? false;
1387
1748
  const serverTimestamp = message.ts;
1388
- const view = this.views.get(subscriptionPath);
1389
- if (!view) return;
1749
+ const tag = message.tag;
1390
1750
  if (value === void 0) return;
1391
- if (view.recovering) {
1392
- if (relativePath !== "/") {
1393
- return;
1751
+ if (tag !== void 0) {
1752
+ const view2 = this.getViewByTag(tag);
1753
+ if (view2) {
1754
+ this.applyServerUpdateToView(view2, [{ relativePath, value }], isVolatile, serverTimestamp);
1394
1755
  }
1395
- this.applyWriteToView(view, [{ relativePath, value }], isVolatile, serverTimestamp);
1396
- view.exitRecovery();
1397
1756
  return;
1398
1757
  }
1399
- this.applyWriteToView(view, [{ relativePath, value }], isVolatile, serverTimestamp);
1758
+ const defaultViewKey = this.makeViewKey(subscriptionPath, "default");
1759
+ const view = this.views.get(defaultViewKey);
1760
+ if (view) {
1761
+ this.applyServerUpdateToView(view, [{ relativePath, value }], isVolatile, serverTimestamp);
1762
+ }
1400
1763
  }
1401
1764
  /**
1402
1765
  * Handle a 'patch' event - multi-path change.
1766
+ * Routes to specific View by tag if present, otherwise to default View at path.
1403
1767
  */
1404
1768
  handlePatchEvent(message) {
1405
1769
  const subscriptionPath = message.sp;
@@ -1407,11 +1771,7 @@ var SubscriptionManager = class {
1407
1771
  const patches = message.v;
1408
1772
  const isVolatile = message.x ?? false;
1409
1773
  const serverTimestamp = message.ts;
1410
- const view = this.views.get(subscriptionPath);
1411
- if (!view) return;
1412
- if (view.recovering) {
1413
- return;
1414
- }
1774
+ const tag = message.tag;
1415
1775
  if (!patches) return;
1416
1776
  const updates = [];
1417
1777
  for (const [relativePath, patchValue] of Object.entries(patches)) {
@@ -1423,42 +1783,88 @@ var SubscriptionManager = class {
1423
1783
  }
1424
1784
  updates.push({ relativePath: fullRelativePath, value: patchValue });
1425
1785
  }
1426
- this.applyWriteToView(view, updates, isVolatile, serverTimestamp);
1786
+ if (tag !== void 0) {
1787
+ const view2 = this.getViewByTag(tag);
1788
+ if (view2) {
1789
+ this.applyServerUpdateToView(view2, updates, isVolatile, serverTimestamp);
1790
+ }
1791
+ return;
1792
+ }
1793
+ const defaultViewKey = this.makeViewKey(subscriptionPath, "default");
1794
+ const view = this.views.get(defaultViewKey);
1795
+ if (view) {
1796
+ this.applyServerUpdateToView(view, updates, isVolatile, serverTimestamp);
1797
+ }
1427
1798
  }
1428
1799
  /**
1429
1800
  * Handle a 'vb' (volatile batch) event - batched volatile updates across subscriptions.
1430
1801
  * Server batches volatile events in 50ms intervals to reduce message overhead.
1431
1802
  * Format: { ev: 'vb', b: { subscriptionPath: { relativePath: value } }, ts: timestamp }
1803
+ * Dispatches to ALL Views at each subscription path.
1432
1804
  */
1433
1805
  handleVolatileBatchEvent(message) {
1434
1806
  const batch = message.b;
1435
1807
  const serverTimestamp = message.ts;
1436
1808
  if (!batch) return;
1437
1809
  for (const [subscriptionPath, updates] of Object.entries(batch)) {
1438
- const view = this.views.get(subscriptionPath);
1439
- if (!view) continue;
1440
- if (view.recovering) continue;
1810
+ const views = this.getViewsAtPath(subscriptionPath);
1811
+ if (views.length === 0) continue;
1441
1812
  const updatesList = [];
1442
1813
  for (const [relativePath, value] of Object.entries(updates)) {
1443
1814
  updatesList.push({ relativePath, value });
1444
1815
  }
1445
- this.applyWriteToView(view, updatesList, true, serverTimestamp);
1816
+ for (const view of views) {
1817
+ this.applyServerUpdateToView(view, updatesList, true, serverTimestamp);
1818
+ }
1446
1819
  }
1447
1820
  }
1448
1821
  /**
1449
- * Detect and fire child_moved events for children that changed position.
1822
+ * Detect and fire child_moved events for children that changed position OR sort value.
1823
+ *
1824
+ * Firebase fires child_moved for ANY priority/sort value change, regardless of whether
1825
+ * the position actually changes. This is Case 2003 behavior.
1826
+ *
1827
+ * IMPORTANT: child_moved should only fire for children whose VALUE or PRIORITY changed.
1828
+ * Children that are merely "displaced" by another child moving should NOT fire child_moved.
1829
+ *
1830
+ * @param affectedChildren - Only check these children for moves. If not provided,
1831
+ * checks all children (for full snapshots where we compare values).
1832
+ * @param previousDisplayCache - Previous display cache for comparing sort values
1833
+ * @param currentDisplayCache - Current display cache for comparing sort values
1450
1834
  */
1451
- detectAndFireMoves(view, previousOrder, currentOrder, previousPositions, currentPositions, previousChildSet, currentChildSet, childMovedSubs, isVolatile, serverTimestamp) {
1835
+ detectAndFireMoves(view, previousOrder, currentOrder, previousPositions, currentPositions, previousChildSet, currentChildSet, childMovedSubs, isVolatile, serverTimestamp, affectedChildren, previousDisplayCache, currentDisplayCache) {
1452
1836
  if (childMovedSubs.length === 0) return;
1453
- for (const key of currentOrder) {
1837
+ const childrenToCheck = affectedChildren ?? new Set(currentOrder);
1838
+ const queryParams = view.queryParams;
1839
+ for (const key of childrenToCheck) {
1454
1840
  if (!previousChildSet.has(key) || !currentChildSet.has(key)) {
1455
1841
  continue;
1456
1842
  }
1457
1843
  const oldPos = previousPositions.get(key);
1458
1844
  const newPos = currentPositions.get(key);
1845
+ if (oldPos === void 0 || newPos === void 0) {
1846
+ continue;
1847
+ }
1459
1848
  const oldPrevKey = oldPos > 0 ? previousOrder[oldPos - 1] : null;
1460
1849
  const newPrevKey = newPos > 0 ? currentOrder[newPos - 1] : null;
1461
- if (oldPrevKey !== newPrevKey) {
1850
+ let positionChanged = oldPrevKey !== newPrevKey;
1851
+ let sortValueChanged = false;
1852
+ let isPriorityOrdering = false;
1853
+ if (previousDisplayCache && currentDisplayCache) {
1854
+ const prevValue = previousDisplayCache[key];
1855
+ const currValue = currentDisplayCache[key];
1856
+ const prevSortValue = getSortValue(prevValue, queryParams);
1857
+ const currSortValue = getSortValue(currValue, queryParams);
1858
+ sortValueChanged = JSON.stringify(prevSortValue) !== JSON.stringify(currSortValue);
1859
+ isPriorityOrdering = !queryParams?.orderBy || queryParams.orderBy === "priority";
1860
+ }
1861
+ let shouldFire;
1862
+ if (affectedChildren) {
1863
+ shouldFire = positionChanged || isPriorityOrdering && sortValueChanged;
1864
+ } else {
1865
+ shouldFire = isPriorityOrdering && sortValueChanged;
1866
+ }
1867
+ if (shouldFire) {
1462
1868
  this.fireChildMoved(view, key, childMovedSubs, newPrevKey, isVolatile, serverTimestamp);
1463
1869
  }
1464
1870
  }
@@ -1502,10 +1908,10 @@ var SubscriptionManager = class {
1502
1908
  /**
1503
1909
  * Fire child_removed callbacks for a child key.
1504
1910
  */
1505
- fireChildRemoved(view, childKey, subs, isVolatile, serverTimestamp) {
1911
+ fireChildRemoved(view, childKey, subs, isVolatile, serverTimestamp, previousValue) {
1506
1912
  if (subs.length === 0) return;
1507
1913
  const childPath = joinPath(view.path, childKey);
1508
- const snapshot = this.createSnapshot?.(childPath, null, isVolatile, serverTimestamp);
1914
+ const snapshot = this.createSnapshot?.(childPath, previousValue ?? null, isVolatile, serverTimestamp);
1509
1915
  if (snapshot) {
1510
1916
  for (const entry of subs) {
1511
1917
  try {
@@ -1534,6 +1940,30 @@ var SubscriptionManager = class {
1534
1940
  }
1535
1941
  }
1536
1942
  }
1943
+ /**
1944
+ * Handle subscription revocation due to auth change.
1945
+ * The server has already removed the subscription, so we just clean up locally.
1946
+ * This is different from unsubscribeAll which sends an unsubscribe message.
1947
+ */
1948
+ handleSubscriptionRevoked(path) {
1949
+ const normalizedPath = path;
1950
+ const queryIds = this.pathToQueryIds.get(normalizedPath);
1951
+ if (!queryIds || queryIds.size === 0) return;
1952
+ for (const queryId of queryIds) {
1953
+ const viewKey = this.makeViewKey(normalizedPath, queryId);
1954
+ const view = this.views.get(viewKey);
1955
+ if (view) {
1956
+ const tag = this.viewKeyToTag.get(viewKey);
1957
+ if (tag !== void 0) {
1958
+ this.tagToViewKey.delete(tag);
1959
+ this.viewKeyToTag.delete(viewKey);
1960
+ }
1961
+ view.clear();
1962
+ this.views.delete(viewKey);
1963
+ }
1964
+ }
1965
+ this.pathToQueryIds.delete(normalizedPath);
1966
+ }
1537
1967
  /**
1538
1968
  * Clear all subscriptions (e.g., on disconnect).
1539
1969
  */
@@ -1542,68 +1972,123 @@ var SubscriptionManager = class {
1542
1972
  view.clear();
1543
1973
  }
1544
1974
  this.views.clear();
1975
+ this.pathToQueryIds.clear();
1976
+ this.tagToViewKey.clear();
1977
+ this.viewKeyToTag.clear();
1978
+ this.nextTag = 1;
1545
1979
  }
1546
1980
  /**
1547
- * Check if there are any subscriptions at a path.
1981
+ * Check if there are any subscriptions at a path (across all query identifiers).
1548
1982
  */
1549
1983
  hasSubscriptions(path) {
1550
- return this.views.has(path);
1984
+ const queryIds = this.pathToQueryIds.get(path);
1985
+ return queryIds !== void 0 && queryIds.size > 0;
1551
1986
  }
1552
1987
  /**
1553
- * Check if a path is "covered" by an active subscription.
1988
+ * Check if a path is "covered" by an active subscription that has received data.
1554
1989
  *
1555
1990
  * A path is covered if:
1556
- * - There's an active 'value' subscription at that exact path, OR
1557
- * - There's an active 'value' subscription at an ancestor path
1991
+ * - There's an active subscription at that exact path that has data, OR
1992
+ * - There's an active subscription at an ancestor path that has data
1993
+ *
1994
+ * Note: Any subscription type (value, child_added, child_moved, etc.) receives
1995
+ * the initial snapshot from the server and thus has cached data.
1558
1996
  */
1559
1997
  isPathCovered(path) {
1560
1998
  const normalized = normalizePath(path);
1561
- if (this.hasValueSubscription(normalized)) {
1999
+ if (this.hasActiveSubscriptionWithData(normalized)) {
1562
2000
  return true;
1563
2001
  }
1564
2002
  const segments = normalized.split("/").filter((s) => s.length > 0);
1565
2003
  for (let i = segments.length - 1; i >= 0; i--) {
1566
2004
  const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
1567
- if (this.hasValueSubscription(ancestorPath)) {
2005
+ if (this.hasActiveSubscriptionWithData(ancestorPath)) {
1568
2006
  return true;
1569
2007
  }
1570
2008
  }
1571
- if (normalized !== "/" && this.hasValueSubscription("/")) {
2009
+ if (normalized !== "/" && this.hasActiveSubscriptionWithData("/")) {
1572
2010
  return true;
1573
2011
  }
1574
2012
  return false;
1575
2013
  }
1576
2014
  /**
1577
- * Check if there's a 'value' subscription at a path.
2015
+ * Check if there's an active subscription at a path that has data.
2016
+ * A View has data if it has received the initial snapshot OR has pending writes.
2017
+ * Any subscription type (value, child_added, child_moved, etc.) counts.
2018
+ */
2019
+ hasActiveSubscriptionWithData(path) {
2020
+ const views = this.getViewsAtPath(path);
2021
+ return views.some((view) => view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites()));
2022
+ }
2023
+ /**
2024
+ * Check if any ancestor path has a "complete" View (one that loadsAllData).
2025
+ * A complete View has no limits and no range filters, so it contains all data
2026
+ * for its subtree. Child subscriptions can use the ancestor's data instead
2027
+ * of creating their own server subscription.
2028
+ *
2029
+ * This matches Firebase's behavior where child listeners don't need their own
2030
+ * server subscription if an ancestor has an unlimited listener.
1578
2031
  */
1579
- hasValueSubscription(path) {
1580
- const view = this.views.get(path);
1581
- return view !== void 0 && view.hasCallbacksForType("value");
2032
+ hasAncestorCompleteView(path) {
2033
+ const normalized = normalizePath(path);
2034
+ const segments = normalized.split("/").filter((s) => s.length > 0);
2035
+ for (let i = segments.length - 1; i >= 0; i--) {
2036
+ const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
2037
+ const views = this.getViewsAtPath(ancestorPath);
2038
+ for (const view of views) {
2039
+ if (view.hasCallbacks() && view.loadsAllData() && view.hasReceivedInitialSnapshot) {
2040
+ return true;
2041
+ }
2042
+ }
2043
+ }
2044
+ if (normalized !== "/") {
2045
+ const rootViews = this.getViewsAtPath("/");
2046
+ for (const view of rootViews) {
2047
+ if (view.hasCallbacks() && view.loadsAllData() && view.hasReceivedInitialSnapshot) {
2048
+ return true;
2049
+ }
2050
+ }
2051
+ }
2052
+ return false;
1582
2053
  }
1583
2054
  /**
1584
2055
  * Get a cached value if the path is covered by an active subscription.
2056
+ * Returns the value from the first View that has it (typically the default/unfiltered one).
1585
2057
  */
1586
2058
  getCachedValue(path) {
1587
2059
  const normalized = normalizePath(path);
1588
2060
  if (!this.isPathCovered(normalized)) {
1589
2061
  return { value: void 0, found: false };
1590
2062
  }
1591
- const exactView = this.views.get(normalized);
1592
- if (exactView) {
1593
- return exactView.getCacheValue(normalized);
2063
+ const exactViews = this.getViewsAtPath(normalized);
2064
+ for (const view of exactViews) {
2065
+ const result = view.getCacheValue(normalized);
2066
+ if (result.found) {
2067
+ return result;
2068
+ }
1594
2069
  }
1595
2070
  const segments = normalized.split("/").filter((s) => s.length > 0);
1596
2071
  for (let i = segments.length - 1; i >= 0; i--) {
1597
2072
  const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
1598
- const ancestorView = this.views.get(ancestorPath);
1599
- if (ancestorView && ancestorView.hasCallbacksForType("value")) {
1600
- return ancestorView.getCacheValue(normalized);
2073
+ const ancestorViews = this.getViewsAtPath(ancestorPath);
2074
+ for (const view of ancestorViews) {
2075
+ if (view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites())) {
2076
+ const result = view.getCacheValue(normalized);
2077
+ if (result.found) {
2078
+ return result;
2079
+ }
2080
+ }
1601
2081
  }
1602
2082
  }
1603
2083
  if (normalized !== "/") {
1604
- const rootView = this.views.get("/");
1605
- if (rootView && rootView.hasCallbacksForType("value")) {
1606
- return rootView.getCacheValue(normalized);
2084
+ const rootViews = this.getViewsAtPath("/");
2085
+ for (const view of rootViews) {
2086
+ if (view.hasCallbacks() && (view.hasReceivedInitialSnapshot || view.hasPendingWrites())) {
2087
+ const result = view.getCacheValue(normalized);
2088
+ if (result.found) {
2089
+ return result;
2090
+ }
2091
+ }
1607
2092
  }
1608
2093
  }
1609
2094
  return { value: void 0, found: false };
@@ -1621,7 +2106,7 @@ var SubscriptionManager = class {
1621
2106
  */
1622
2107
  clearCacheOnly() {
1623
2108
  for (const view of this.views.values()) {
1624
- view.clearCache();
2109
+ view.clearServerCache();
1625
2110
  }
1626
2111
  }
1627
2112
  /**
@@ -1629,13 +2114,13 @@ var SubscriptionManager = class {
1629
2114
  * Used after reconnecting to restore subscriptions on the server.
1630
2115
  */
1631
2116
  async resubscribeAll() {
1632
- for (const [path, view] of this.views) {
2117
+ for (const view of this.views.values()) {
1633
2118
  const eventTypes = view.getEventTypes();
1634
2119
  if (eventTypes.length > 0) {
1635
2120
  try {
1636
- await this.sendSubscribe?.(path, eventTypes, view.queryParams ?? void 0);
2121
+ await this.sendSubscribe?.(view.path, eventTypes, view.queryParams ?? void 0);
1637
2122
  } catch (err) {
1638
- console.error(`Failed to resubscribe to ${path}:`, err);
2123
+ console.error(`Failed to resubscribe to ${view.path}:`, err);
1639
2124
  }
1640
2125
  }
1641
2126
  }
@@ -1645,18 +2130,22 @@ var SubscriptionManager = class {
1645
2130
  */
1646
2131
  getActiveSubscriptions() {
1647
2132
  const result = [];
1648
- for (const [path, view] of this.views) {
2133
+ for (const view of this.views.values()) {
1649
2134
  const eventTypes = view.getEventTypes();
1650
2135
  const queryParams = view.queryParams ?? void 0;
1651
- result.push({ path, eventTypes, queryParams });
2136
+ result.push({ path: view.path, eventTypes, queryParams });
1652
2137
  }
1653
2138
  return result;
1654
2139
  }
1655
2140
  /**
1656
2141
  * Get a View by path (for testing/debugging).
2142
+ * Returns the first View at this path (default query identifier).
1657
2143
  */
1658
2144
  getView(path) {
1659
- return this.views.get(path);
2145
+ const defaultView = this.views.get(this.makeViewKey(path, "default"));
2146
+ if (defaultView) return defaultView;
2147
+ const views = this.getViewsAtPath(path);
2148
+ return views.length > 0 ? views[0] : void 0;
1660
2149
  }
1661
2150
  // ============================================
1662
2151
  // Shared Write Application (used by server events and optimistic writes)
@@ -1698,19 +2187,22 @@ var SubscriptionManager = class {
1698
2187
  * @param isVolatile - Whether this is a volatile update
1699
2188
  * @param serverTimestamp - Optional server timestamp
1700
2189
  */
1701
- applyWriteToView(view, updates, isVolatile, serverTimestamp) {
2190
+ applyServerUpdateToView(view, updates, isVolatile, serverTimestamp) {
1702
2191
  const previousOrder = view.orderedChildren;
1703
2192
  const previousChildSet = new Set(previousOrder);
1704
- const isFirstSnapshot = !view.hasReceivedInitialSnapshot;
2193
+ const isFirstSnapshot = !view.hasReceivedInitialSnapshot && !view.hasPendingWrites();
1705
2194
  let previousCacheJson = null;
2195
+ let previousDisplayCache = null;
1706
2196
  if (!isVolatile) {
1707
- previousCacheJson = this.serializeCacheForComparison(view.getCache());
2197
+ const cache = view.getDisplayCache();
2198
+ previousDisplayCache = cache && typeof cache === "object" && !Array.isArray(cache) ? cache : null;
2199
+ previousCacheJson = this.serializeCacheForComparison(cache);
1708
2200
  }
1709
2201
  const affectedChildren = /* @__PURE__ */ new Set();
1710
2202
  let isFullSnapshot = false;
1711
2203
  for (const { relativePath, value } of updates) {
1712
2204
  if (relativePath === "/") {
1713
- view.setCache(value);
2205
+ view.setServerCache(value);
1714
2206
  isFullSnapshot = true;
1715
2207
  } else {
1716
2208
  const segments = relativePath.split("/").filter((s) => s.length > 0);
@@ -1718,24 +2210,24 @@ var SubscriptionManager = class {
1718
2210
  affectedChildren.add(segments[0]);
1719
2211
  }
1720
2212
  if (value === null) {
1721
- view.removeCacheChild(relativePath);
2213
+ view.removeServerCacheChild(relativePath);
1722
2214
  } else {
1723
- view.updateCacheChild(relativePath, value);
2215
+ view.updateServerCacheChild(relativePath, value);
1724
2216
  }
1725
2217
  }
1726
2218
  }
1727
2219
  const currentOrder = view.orderedChildren;
1728
2220
  const currentChildSet = new Set(currentOrder);
1729
2221
  if (!isVolatile && !isFirstSnapshot && previousCacheJson !== null) {
1730
- const currentCacheJson = this.serializeCacheForComparison(view.getCache());
2222
+ const currentCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
1731
2223
  if (previousCacheJson === currentCacheJson) {
1732
2224
  return;
1733
2225
  }
1734
2226
  }
1735
2227
  const valueSubs = view.getCallbacks("value");
1736
2228
  if (valueSubs.length > 0) {
1737
- const fullValue = view.getCache();
1738
- const snapshot = this.createSnapshot?.(view.path, fullValue, isVolatile, serverTimestamp);
2229
+ const fullValue = view.getDisplayCache();
2230
+ const snapshot = this.createSnapshot?.(view.path, fullValue, isVolatile, serverTimestamp, view.orderedChildren);
1739
2231
  if (snapshot) {
1740
2232
  for (const entry of valueSubs) {
1741
2233
  try {
@@ -1762,7 +2254,8 @@ var SubscriptionManager = class {
1762
2254
  }
1763
2255
  for (const key of previousOrder) {
1764
2256
  if (!currentChildSet.has(key)) {
1765
- this.fireChildRemoved(view, key, childRemovedSubs, isVolatile, serverTimestamp);
2257
+ const prevValue = previousDisplayCache?.[key];
2258
+ this.fireChildRemoved(view, key, childRemovedSubs, isVolatile, serverTimestamp, prevValue);
1766
2259
  }
1767
2260
  }
1768
2261
  } else {
@@ -1773,7 +2266,8 @@ var SubscriptionManager = class {
1773
2266
  const prevKey = view.getPreviousChildKey(childKey);
1774
2267
  this.fireChildAdded(view, childKey, childAddedSubs, prevKey, isVolatile, serverTimestamp);
1775
2268
  } else if (wasPresent && !isPresent) {
1776
- this.fireChildRemoved(view, childKey, childRemovedSubs, isVolatile, serverTimestamp);
2269
+ const prevValue = previousDisplayCache?.[childKey];
2270
+ this.fireChildRemoved(view, childKey, childRemovedSubs, isVolatile, serverTimestamp, prevValue);
1777
2271
  } else if (wasPresent && isPresent) {
1778
2272
  const prevKey = view.getPreviousChildKey(childKey);
1779
2273
  this.fireChildChanged(view, childKey, childChangedSubs, prevKey, isVolatile, serverTimestamp);
@@ -1784,6 +2278,8 @@ var SubscriptionManager = class {
1784
2278
  previousOrder.forEach((key, idx) => previousPositions.set(key, idx));
1785
2279
  const currentPositions = /* @__PURE__ */ new Map();
1786
2280
  currentOrder.forEach((key, idx) => currentPositions.set(key, idx));
2281
+ const currentCache = view.getDisplayCache();
2282
+ const currentDisplayCache = currentCache && typeof currentCache === "object" && !Array.isArray(currentCache) ? currentCache : null;
1787
2283
  this.detectAndFireMoves(
1788
2284
  view,
1789
2285
  previousOrder,
@@ -1794,7 +2290,10 @@ var SubscriptionManager = class {
1794
2290
  currentChildSet,
1795
2291
  childMovedSubs,
1796
2292
  isVolatile,
1797
- serverTimestamp
2293
+ serverTimestamp,
2294
+ isFullSnapshot ? void 0 : affectedChildren,
2295
+ previousDisplayCache,
2296
+ currentDisplayCache
1798
2297
  );
1799
2298
  }
1800
2299
  // ============================================
@@ -1807,13 +2306,18 @@ var SubscriptionManager = class {
1807
2306
  findViewsForWritePath(writePath) {
1808
2307
  const normalized = normalizePath(writePath);
1809
2308
  const views = [];
1810
- for (const [viewPath, view] of this.views) {
2309
+ for (const view of this.views.values()) {
2310
+ const viewPath = view.path;
1811
2311
  if (normalized === viewPath) {
1812
2312
  views.push(view);
1813
2313
  } else if (normalized.startsWith(viewPath + "/")) {
1814
2314
  views.push(view);
2315
+ } else if (viewPath.startsWith(normalized + "/")) {
2316
+ views.push(view);
1815
2317
  } else if (viewPath === "/") {
1816
2318
  views.push(view);
2319
+ } else if (normalized === "/") {
2320
+ views.push(view);
1817
2321
  }
1818
2322
  }
1819
2323
  return views;
@@ -1835,59 +2339,135 @@ var SubscriptionManager = class {
1835
2339
  /**
1836
2340
  * Track a pending write for all Views that cover the write path.
1837
2341
  */
1838
- trackPendingWrite(writePath, requestId) {
1839
- const views = this.findViewsForWritePath(writePath);
1840
- for (const view of views) {
1841
- view.addPendingWrite(requestId);
1842
- }
2342
+ /**
2343
+ * Track a pending write by request ID.
2344
+ * Note: With the dual-cache system, this is now handled by applyOptimisticWrite
2345
+ * via addPendingWriteData. This method is kept for API compatibility but is a no-op.
2346
+ */
2347
+ trackPendingWrite(_writePath, _requestId) {
1843
2348
  }
1844
2349
  /**
1845
2350
  * Clear a pending write from all Views (on ack).
2351
+ * Fires callbacks only if displayCache changed (e.g., server modified the data).
2352
+ * With PUT-before-ACK ordering, this handles the case where server data differs from optimistic.
1846
2353
  */
1847
2354
  clearPendingWrite(requestId) {
1848
2355
  for (const view of this.views.values()) {
2356
+ if (!view.isWritePending(requestId)) {
2357
+ continue;
2358
+ }
2359
+ const previousDisplayCache = view.getDisplayCache();
2360
+ const previousCacheJson = this.serializeCacheForComparison(previousDisplayCache);
2361
+ const previousOrder = view.orderedChildren;
2362
+ const previousChildSet = new Set(previousOrder);
1849
2363
  view.removePendingWrite(requestId);
1850
- }
2364
+ const currentCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
2365
+ if (previousCacheJson === currentCacheJson) {
2366
+ continue;
2367
+ }
2368
+ const currentOrder = view.orderedChildren;
2369
+ const currentChildSet = new Set(currentOrder);
2370
+ const valueSubs = view.getCallbacks("value");
2371
+ if (valueSubs.length > 0) {
2372
+ const fullValue = view.getDisplayCache();
2373
+ const snapshot = this.createSnapshot?.(view.path, fullValue, false, void 0, view.orderedChildren);
2374
+ if (snapshot) {
2375
+ for (const entry of valueSubs) {
2376
+ try {
2377
+ entry.callback(snapshot, void 0);
2378
+ } catch (err) {
2379
+ console.error("Error in value subscription callback:", err);
2380
+ }
2381
+ }
2382
+ }
2383
+ }
2384
+ this.fireChildEventsForAck(view, previousOrder, previousChildSet, currentOrder, currentChildSet, previousDisplayCache);
2385
+ }
1851
2386
  }
1852
2387
  /**
1853
- * Handle a nack for a pending write.
1854
- * Collects all tainted request IDs and puts affected Views into recovery mode.
1855
- * Returns the affected Views and all tainted request IDs for local rejection.
2388
+ * Fire child events for ACK handling, skipping child_moved.
2389
+ * This is a variant of fireChildEvents that doesn't fire moves because:
2390
+ * 1. Moves were already fired optimistically
2391
+ * 2. If server modifies data, PUT event will fire correct moves
2392
+ * 3. ACK can arrive before PUT, causing incorrect intermediate state
1856
2393
  */
1857
- handleWriteNack(requestId) {
1858
- const affectedViews = [];
1859
- const taintedIds = /* @__PURE__ */ new Set();
1860
- for (const view of this.views.values()) {
1861
- if (view.isWritePending(requestId)) {
1862
- affectedViews.push(view);
1863
- for (const id of view.getPendingWriteIds()) {
1864
- taintedIds.add(id);
2394
+ fireChildEventsForAck(view, previousOrder, previousChildSet, currentOrder, currentChildSet, previousDisplayCache) {
2395
+ const childAddedSubs = view.getCallbacks("child_added");
2396
+ const childChangedSubs = view.getCallbacks("child_changed");
2397
+ const childRemovedSubs = view.getCallbacks("child_removed");
2398
+ if (childAddedSubs.length === 0 && childChangedSubs.length === 0 && childRemovedSubs.length === 0) {
2399
+ return;
2400
+ }
2401
+ const displayCache = view.getDisplayCache();
2402
+ for (const key of currentOrder) {
2403
+ if (!previousChildSet.has(key)) {
2404
+ if (childAddedSubs.length > 0 && displayCache) {
2405
+ const prevKey = view.getPreviousChildKey(key);
2406
+ this.fireChildAdded(view, key, childAddedSubs, prevKey, false, void 0);
2407
+ }
2408
+ } else if (previousDisplayCache && childChangedSubs.length > 0 && displayCache) {
2409
+ const prevValue = previousDisplayCache[key];
2410
+ const currentValue = displayCache[key];
2411
+ const prevJson = this.serializeCacheForComparison(prevValue);
2412
+ const currJson = this.serializeCacheForComparison(currentValue);
2413
+ if (prevJson !== currJson) {
2414
+ const prevKey = view.getPreviousChildKey(key);
2415
+ this.fireChildChanged(view, key, childChangedSubs, prevKey, false, void 0);
1865
2416
  }
1866
2417
  }
1867
2418
  }
1868
- for (const view of affectedViews) {
1869
- view.enterRecovery();
2419
+ for (const key of previousOrder) {
2420
+ if (!currentChildSet.has(key)) {
2421
+ if (childRemovedSubs.length > 0) {
2422
+ const prevValue = previousDisplayCache?.[key];
2423
+ this.fireChildRemoved(view, key, childRemovedSubs, false, void 0, prevValue);
2424
+ }
2425
+ }
1870
2426
  }
1871
- return { affectedViews, taintedIds: Array.from(taintedIds) };
1872
2427
  }
1873
2428
  /**
1874
- * Check if any View covering a path is in recovery mode.
2429
+ * Handle a nack for a pending write.
2430
+ * Collects all tainted request IDs and puts affected Views into recovery mode.
2431
+ * Returns the affected Views and all tainted request IDs for local rejection.
1875
2432
  */
1876
- isPathInRecovery(writePath) {
1877
- const views = this.findViewsForWritePath(writePath);
1878
- return views.some((view) => view.recovering);
1879
- }
1880
2433
  /**
1881
- * Re-subscribe a specific View to get a fresh snapshot.
1882
- * Used during recovery after a nack.
2434
+ * Handle a write NACK by removing the pending write from affected Views.
2435
+ * With the dual-cache system, this automatically reverts to server truth.
2436
+ * Returns the affected Views so callbacks can be fired if data changed.
1883
2437
  */
1884
- async resubscribeView(path) {
1885
- const view = this.views.get(path);
1886
- if (!view) return;
1887
- const eventTypes = view.getEventTypes();
1888
- if (eventTypes.length > 0 && this.sendSubscribe) {
1889
- await this.sendSubscribe(path, eventTypes, view.queryParams ?? void 0);
2438
+ handleWriteNack(requestId) {
2439
+ const affectedViews = [];
2440
+ for (const view of this.views.values()) {
2441
+ if (view.isWritePending(requestId)) {
2442
+ const previousDisplayCache = view.getDisplayCache();
2443
+ const previousCacheJson = this.serializeCacheForComparison(previousDisplayCache);
2444
+ const previousOrder = view.orderedChildren;
2445
+ const previousChildSet = new Set(previousOrder);
2446
+ view.removePendingWrite(requestId);
2447
+ const currentCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
2448
+ if (previousCacheJson !== currentCacheJson) {
2449
+ affectedViews.push(view);
2450
+ const currentOrder = view.orderedChildren;
2451
+ const currentChildSet = new Set(currentOrder);
2452
+ const valueSubs = view.getCallbacks("value");
2453
+ if (valueSubs.length > 0) {
2454
+ const fullValue = view.getDisplayCache();
2455
+ const snapshot = this.createSnapshot?.(view.path, fullValue, false, void 0, view.orderedChildren);
2456
+ if (snapshot) {
2457
+ for (const entry of valueSubs) {
2458
+ try {
2459
+ entry.callback(snapshot, void 0);
2460
+ } catch (err) {
2461
+ console.error("Error in value subscription callback:", err);
2462
+ }
2463
+ }
2464
+ }
2465
+ }
2466
+ this.fireChildEvents(view, previousOrder, previousChildSet, currentOrder, currentChildSet, "/", false, void 0, previousDisplayCache);
2467
+ }
2468
+ }
1890
2469
  }
2470
+ return affectedViews;
1891
2471
  }
1892
2472
  // ============================================
1893
2473
  // Optimistic Writes (for local-first)
@@ -1910,34 +2490,262 @@ var SubscriptionManager = class {
1910
2490
  }
1911
2491
  const updatedViews = [];
1912
2492
  for (const view of affectedViews) {
1913
- if (!view.hasReceivedInitialSnapshot) {
1914
- continue;
1915
- }
1916
2493
  let relativePath;
2494
+ let effectiveValue = value;
1917
2495
  if (normalized === view.path) {
1918
2496
  relativePath = "/";
1919
2497
  } else if (view.path === "/") {
1920
2498
  relativePath = normalized;
1921
- } else {
2499
+ } else if (normalized.startsWith(view.path + "/")) {
1922
2500
  relativePath = normalized.slice(view.path.length);
1923
- }
1924
- const updates = [];
1925
- if (operation === "delete") {
1926
- updates.push({ relativePath, value: null });
1927
- } else if (operation === "update") {
1928
- const updateObj = value;
1929
- for (const [key, val] of Object.entries(updateObj)) {
1930
- const updatePath = relativePath === "/" ? "/" + key : relativePath + "/" + key;
1931
- updates.push({ relativePath: updatePath, value: val });
2501
+ } else if (view.path.startsWith(normalized + "/")) {
2502
+ const pathDiff = view.path.slice(normalized.length);
2503
+ effectiveValue = getValueAtPath(value, pathDiff);
2504
+ if (operation === "update" && effectiveValue === void 0) {
2505
+ continue;
1932
2506
  }
2507
+ relativePath = "/";
1933
2508
  } else {
1934
- updates.push({ relativePath, value });
2509
+ continue;
2510
+ }
2511
+ const previousDisplayCache = view.getDisplayCache();
2512
+ const previousOrder = view.orderedChildren;
2513
+ const previousChildSet = new Set(previousOrder);
2514
+ const previousCacheJson = this.serializeCacheForComparison(previousDisplayCache);
2515
+ view.addPendingWriteData(requestId, relativePath, effectiveValue, operation);
2516
+ const currentOrder = view.orderedChildren;
2517
+ const currentChildSet = new Set(currentOrder);
2518
+ const currentCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
2519
+ if (previousCacheJson === currentCacheJson) {
2520
+ continue;
1935
2521
  }
1936
- this.applyWriteToView(view, updates, false, void 0);
1937
2522
  updatedViews.push(view);
2523
+ const valueSubs = view.getCallbacks("value");
2524
+ if (valueSubs.length > 0) {
2525
+ const fullValue = view.getDisplayCache();
2526
+ const snapshot = this.createSnapshot?.(view.path, fullValue, false, void 0, view.orderedChildren);
2527
+ if (snapshot) {
2528
+ for (const entry of valueSubs) {
2529
+ try {
2530
+ entry.callback(snapshot, void 0);
2531
+ } catch (err) {
2532
+ console.error("Error in value subscription callback:", err);
2533
+ }
2534
+ }
2535
+ }
2536
+ }
2537
+ this.fireChildEvents(view, previousOrder, previousChildSet, currentOrder, currentChildSet, relativePath, false, void 0, previousDisplayCache);
1938
2538
  }
1939
2539
  return updatedViews;
1940
2540
  }
2541
+ /**
2542
+ * Fire child_added/child_changed/child_removed/child_moved events.
2543
+ * Extracted as helper since it's used by both server updates and optimistic writes.
2544
+ */
2545
+ fireChildEvents(view, previousOrder, previousChildSet, currentOrder, currentChildSet, affectedPath, isVolatile, serverTimestamp, previousDisplayCache) {
2546
+ const childAddedSubs = view.getCallbacks("child_added");
2547
+ const childChangedSubs = view.getCallbacks("child_changed");
2548
+ const childRemovedSubs = view.getCallbacks("child_removed");
2549
+ const childMovedSubs = view.getCallbacks("child_moved");
2550
+ if (childAddedSubs.length === 0 && childChangedSubs.length === 0 && childRemovedSubs.length === 0 && childMovedSubs.length === 0) {
2551
+ return;
2552
+ }
2553
+ const displayCache = view.getDisplayCache();
2554
+ const affectedChildren = /* @__PURE__ */ new Set();
2555
+ if (affectedPath !== "/") {
2556
+ const segments = affectedPath.split("/").filter((s) => s.length > 0);
2557
+ if (segments.length > 0) {
2558
+ affectedChildren.add(segments[0]);
2559
+ }
2560
+ }
2561
+ const isFullSnapshot = affectedPath === "/";
2562
+ if (isFullSnapshot) {
2563
+ for (const key of currentOrder) {
2564
+ if (!previousChildSet.has(key)) {
2565
+ if (childAddedSubs.length > 0 && displayCache) {
2566
+ const childValue = displayCache[key];
2567
+ const snapshot = this.createSnapshot?.(
2568
+ joinPath(view.path, key),
2569
+ childValue,
2570
+ isVolatile,
2571
+ serverTimestamp
2572
+ );
2573
+ if (snapshot) {
2574
+ const prevKey = view.getPreviousChildKey(key);
2575
+ for (const entry of childAddedSubs) {
2576
+ try {
2577
+ entry.callback(snapshot, prevKey);
2578
+ } catch (err) {
2579
+ console.error("Error in child_added callback:", err);
2580
+ }
2581
+ }
2582
+ }
2583
+ }
2584
+ } else if (previousDisplayCache && childChangedSubs.length > 0 && displayCache) {
2585
+ const prevValue = previousDisplayCache[key];
2586
+ const currentValue = displayCache[key];
2587
+ const prevJson = this.serializeCacheForComparison(prevValue);
2588
+ const currJson = this.serializeCacheForComparison(currentValue);
2589
+ if (prevJson !== currJson) {
2590
+ const snapshot = this.createSnapshot?.(
2591
+ joinPath(view.path, key),
2592
+ currentValue,
2593
+ isVolatile,
2594
+ serverTimestamp
2595
+ );
2596
+ if (snapshot) {
2597
+ const prevKey = view.getPreviousChildKey(key);
2598
+ for (const entry of childChangedSubs) {
2599
+ try {
2600
+ entry.callback(snapshot, prevKey);
2601
+ } catch (err) {
2602
+ console.error("Error in child_changed callback:", err);
2603
+ }
2604
+ }
2605
+ }
2606
+ }
2607
+ }
2608
+ }
2609
+ for (const key of previousOrder) {
2610
+ if (!currentChildSet.has(key)) {
2611
+ if (childRemovedSubs.length > 0) {
2612
+ const prevValue = previousDisplayCache?.[key];
2613
+ const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue ?? null, isVolatile, serverTimestamp);
2614
+ if (snapshot) {
2615
+ for (const entry of childRemovedSubs) {
2616
+ try {
2617
+ entry.callback(snapshot, void 0);
2618
+ } catch (err) {
2619
+ console.error("Error in child_removed callback:", err);
2620
+ }
2621
+ }
2622
+ }
2623
+ }
2624
+ }
2625
+ }
2626
+ } else {
2627
+ if (childRemovedSubs.length > 0) {
2628
+ for (const key of previousOrder) {
2629
+ if (affectedChildren.has(key)) continue;
2630
+ if (currentChildSet.has(key)) continue;
2631
+ const prevValue = previousDisplayCache ? previousDisplayCache[key] : null;
2632
+ const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue, isVolatile, serverTimestamp);
2633
+ if (snapshot) {
2634
+ for (const entry of childRemovedSubs) {
2635
+ try {
2636
+ entry.callback(snapshot, void 0);
2637
+ } catch (err) {
2638
+ console.error("Error in child_removed callback:", err);
2639
+ }
2640
+ }
2641
+ }
2642
+ }
2643
+ }
2644
+ for (const key of affectedChildren) {
2645
+ const wasPresent = previousChildSet.has(key);
2646
+ const isPresent = currentChildSet.has(key);
2647
+ if (!wasPresent && isPresent) {
2648
+ if (childAddedSubs.length > 0 && displayCache) {
2649
+ const childValue = displayCache[key];
2650
+ const snapshot = this.createSnapshot?.(
2651
+ joinPath(view.path, key),
2652
+ childValue,
2653
+ isVolatile,
2654
+ serverTimestamp
2655
+ );
2656
+ if (snapshot) {
2657
+ const prevKey = view.getPreviousChildKey(key);
2658
+ for (const entry of childAddedSubs) {
2659
+ try {
2660
+ entry.callback(snapshot, prevKey);
2661
+ } catch (err) {
2662
+ console.error("Error in child_added callback:", err);
2663
+ }
2664
+ }
2665
+ }
2666
+ }
2667
+ } else if (wasPresent && !isPresent) {
2668
+ if (childRemovedSubs.length > 0) {
2669
+ const prevValue = previousDisplayCache ? previousDisplayCache[key] : null;
2670
+ const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue, isVolatile, serverTimestamp);
2671
+ if (snapshot) {
2672
+ for (const entry of childRemovedSubs) {
2673
+ try {
2674
+ entry.callback(snapshot, void 0);
2675
+ } catch (err) {
2676
+ console.error("Error in child_removed callback:", err);
2677
+ }
2678
+ }
2679
+ }
2680
+ }
2681
+ } else if (wasPresent && isPresent) {
2682
+ if (childChangedSubs.length > 0 && displayCache) {
2683
+ const childValue = displayCache[key];
2684
+ const snapshot = this.createSnapshot?.(
2685
+ joinPath(view.path, key),
2686
+ childValue,
2687
+ isVolatile,
2688
+ serverTimestamp
2689
+ );
2690
+ if (snapshot) {
2691
+ const prevKey = view.getPreviousChildKey(key);
2692
+ for (const entry of childChangedSubs) {
2693
+ try {
2694
+ entry.callback(snapshot, prevKey);
2695
+ } catch (err) {
2696
+ console.error("Error in child_changed callback:", err);
2697
+ }
2698
+ }
2699
+ }
2700
+ }
2701
+ }
2702
+ }
2703
+ if (childAddedSubs.length > 0 && displayCache) {
2704
+ for (const key of currentOrder) {
2705
+ if (previousChildSet.has(key)) continue;
2706
+ if (affectedChildren.has(key)) continue;
2707
+ const childValue = displayCache[key];
2708
+ const snapshot = this.createSnapshot?.(
2709
+ joinPath(view.path, key),
2710
+ childValue,
2711
+ isVolatile,
2712
+ serverTimestamp
2713
+ );
2714
+ if (snapshot) {
2715
+ const prevKey = view.getPreviousChildKey(key);
2716
+ for (const entry of childAddedSubs) {
2717
+ try {
2718
+ entry.callback(snapshot, prevKey);
2719
+ } catch (err) {
2720
+ console.error("Error in child_added callback:", err);
2721
+ }
2722
+ }
2723
+ }
2724
+ }
2725
+ }
2726
+ }
2727
+ if (childMovedSubs.length > 0) {
2728
+ const previousPositions = /* @__PURE__ */ new Map();
2729
+ previousOrder.forEach((key, i) => previousPositions.set(key, i));
2730
+ const currentPositions = /* @__PURE__ */ new Map();
2731
+ currentOrder.forEach((key, i) => currentPositions.set(key, i));
2732
+ this.detectAndFireMoves(
2733
+ view,
2734
+ previousOrder,
2735
+ currentOrder,
2736
+ previousPositions,
2737
+ currentPositions,
2738
+ previousChildSet,
2739
+ currentChildSet,
2740
+ childMovedSubs,
2741
+ isVolatile,
2742
+ serverTimestamp,
2743
+ isFullSnapshot ? void 0 : affectedChildren,
2744
+ previousDisplayCache,
2745
+ displayCache
2746
+ );
2747
+ }
2748
+ }
1941
2749
  };
1942
2750
 
1943
2751
  // src/OnDisconnect.ts
@@ -2044,7 +2852,37 @@ function generatePushId() {
2044
2852
  }
2045
2853
 
2046
2854
  // src/DatabaseReference.ts
2855
+ function isInfoPath(path) {
2856
+ return path === "/.info" || path.startsWith("/.info/");
2857
+ }
2858
+ function validateNotInfoPath(path, operation) {
2859
+ if (isInfoPath(path)) {
2860
+ throw new LarkError(
2861
+ ErrorCode.INVALID_PATH,
2862
+ `Cannot ${operation} on .info paths - they are read-only system paths`
2863
+ );
2864
+ }
2865
+ }
2047
2866
  var DEFAULT_MAX_RETRIES = 25;
2867
+ var WIRE_PROTOCOL_CONSTANTS = {
2868
+ INDEX_START_VALUE: "sp",
2869
+ INDEX_START_NAME: "sn",
2870
+ INDEX_START_IS_INCLUSIVE: "sin",
2871
+ INDEX_END_VALUE: "ep",
2872
+ INDEX_END_NAME: "en",
2873
+ INDEX_END_IS_INCLUSIVE: "ein",
2874
+ LIMIT: "l",
2875
+ VIEW_FROM: "vf",
2876
+ INDEX: "i"
2877
+ };
2878
+ function objectToUniqueKey(obj) {
2879
+ const sortedKeys = Object.keys(obj).sort();
2880
+ const sorted = {};
2881
+ for (const key of sortedKeys) {
2882
+ sorted[key] = obj[key];
2883
+ }
2884
+ return JSON.stringify(sorted);
2885
+ }
2048
2886
  var DatabaseReference = class _DatabaseReference {
2049
2887
  constructor(db, path, query = {}) {
2050
2888
  this._db = db;
@@ -2077,6 +2915,110 @@ var DatabaseReference = class _DatabaseReference {
2077
2915
  get root() {
2078
2916
  return new _DatabaseReference(this._db, "");
2079
2917
  }
2918
+ /**
2919
+ * Get the LarkDatabase instance this reference belongs to.
2920
+ */
2921
+ get database() {
2922
+ return this._db;
2923
+ }
2924
+ /**
2925
+ * Get the underlying reference for a query.
2926
+ * For queries (created via orderBy*, limitTo*, startAt, etc.), this returns
2927
+ * a reference to the same path without query constraints.
2928
+ * For non-query references, this returns the reference itself.
2929
+ * This matches Firebase's Query.ref behavior.
2930
+ */
2931
+ get ref() {
2932
+ if (Object.keys(this._query).length === 0) {
2933
+ return this;
2934
+ }
2935
+ return new _DatabaseReference(this._db, this._path);
2936
+ }
2937
+ /**
2938
+ * Check if this reference/query is equal to another.
2939
+ * Two references are equal if they have the same database, path, and query constraints.
2940
+ */
2941
+ isEqual(other) {
2942
+ if (!other) return false;
2943
+ if (this._db !== other._db) return false;
2944
+ if (this._path !== other._path) return false;
2945
+ return JSON.stringify(this._query) === JSON.stringify(other._query);
2946
+ }
2947
+ /**
2948
+ * Get the unique identifier for this query's parameters.
2949
+ * Used to differentiate multiple queries on the same path.
2950
+ *
2951
+ * Returns "default" for non-query references (no constraints).
2952
+ * Returns a sorted JSON string of wire-format params for queries.
2953
+ *
2954
+ * This matches Firebase's queryIdentifier format for wire compatibility.
2955
+ */
2956
+ get queryIdentifier() {
2957
+ const queryObj = {};
2958
+ if (this._query.orderBy === "key") {
2959
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX] = ".key";
2960
+ } else if (this._query.orderBy === "value") {
2961
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX] = ".value";
2962
+ } else if (this._query.orderBy === "priority") {
2963
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX] = ".priority";
2964
+ } else if (this._query.orderBy === "child" && this._query.orderByChildPath) {
2965
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX] = this._query.orderByChildPath;
2966
+ }
2967
+ if (this._query.startAt !== void 0) {
2968
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAt.value ?? null;
2969
+ if (this._query.startAt.key !== void 0) {
2970
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.startAt.key;
2971
+ }
2972
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = true;
2973
+ } else if (this._query.startAfter !== void 0) {
2974
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAfter.value ?? null;
2975
+ if (this._query.startAfter.key !== void 0) {
2976
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.startAfter.key;
2977
+ }
2978
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = false;
2979
+ }
2980
+ if (this._query.endAt !== void 0) {
2981
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endAt.value ?? null;
2982
+ if (this._query.endAt.key !== void 0) {
2983
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.endAt.key;
2984
+ }
2985
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_IS_INCLUSIVE] = true;
2986
+ } else if (this._query.endBefore !== void 0) {
2987
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endBefore.value ?? null;
2988
+ if (this._query.endBefore.key !== void 0) {
2989
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.endBefore.key;
2990
+ }
2991
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_IS_INCLUSIVE] = false;
2992
+ }
2993
+ if (this._query.equalTo !== void 0) {
2994
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.equalTo.value;
2995
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.equalTo.value;
2996
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = true;
2997
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_IS_INCLUSIVE] = true;
2998
+ if (this._query.equalTo.key !== void 0) {
2999
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.equalTo.key;
3000
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.equalTo.key;
3001
+ }
3002
+ }
3003
+ if (this._query.limitToFirst !== void 0) {
3004
+ queryObj[WIRE_PROTOCOL_CONSTANTS.LIMIT] = this._query.limitToFirst;
3005
+ queryObj[WIRE_PROTOCOL_CONSTANTS.VIEW_FROM] = "l";
3006
+ } else if (this._query.limitToLast !== void 0) {
3007
+ queryObj[WIRE_PROTOCOL_CONSTANTS.LIMIT] = this._query.limitToLast;
3008
+ queryObj[WIRE_PROTOCOL_CONSTANTS.VIEW_FROM] = "r";
3009
+ }
3010
+ if (Object.keys(queryObj).length === 0) {
3011
+ return "default";
3012
+ }
3013
+ return objectToUniqueKey(queryObj);
3014
+ }
3015
+ /**
3016
+ * Get the data at this location. Alias for once('value').
3017
+ * This is Firebase's newer API for reading data.
3018
+ */
3019
+ async get() {
3020
+ return this.once("value");
3021
+ }
2080
3022
  // ============================================
2081
3023
  // Navigation
2082
3024
  // ============================================
@@ -2095,8 +3037,11 @@ var DatabaseReference = class _DatabaseReference {
2095
3037
  *
2096
3038
  * For volatile paths (high-frequency updates), this is fire-and-forget
2097
3039
  * and resolves immediately without waiting for server confirmation.
3040
+ *
3041
+ * @param value - The value to write
2098
3042
  */
2099
3043
  async set(value) {
3044
+ validateNotInfoPath(this._path, "set");
2100
3045
  if (this._db.isVolatilePath(this._path)) {
2101
3046
  this._db._sendVolatileSet(this._path, value);
2102
3047
  return;
@@ -2121,8 +3066,11 @@ var DatabaseReference = class _DatabaseReference {
2121
3066
  * '/leaderboard/alice': null // null = delete
2122
3067
  * });
2123
3068
  * ```
3069
+ *
3070
+ * @param values - The values to update
2124
3071
  */
2125
3072
  async update(values) {
3073
+ validateNotInfoPath(this._path, "update");
2126
3074
  const hasPathKeys = Object.keys(values).some((key) => key.startsWith("/"));
2127
3075
  if (hasPathKeys) {
2128
3076
  const ops = [];
@@ -2150,59 +3098,84 @@ var DatabaseReference = class _DatabaseReference {
2150
3098
  * For volatile paths, this is fire-and-forget.
2151
3099
  */
2152
3100
  async remove() {
3101
+ validateNotInfoPath(this._path, "remove");
2153
3102
  if (this._db.isVolatilePath(this._path)) {
2154
3103
  this._db._sendVolatileDelete(this._path);
2155
3104
  return;
2156
3105
  }
2157
3106
  await this._db._sendDelete(this._path);
2158
3107
  }
2159
- /**
2160
- * Generate a new child location with a unique key and optionally set its value.
2161
- *
2162
- * If value is provided, sets the value and returns a Promise that resolves
2163
- * to the new reference.
2164
- *
2165
- * If no value is provided, returns a reference immediately with a client-generated
2166
- * push key (you can then call set() on it).
2167
- */
2168
3108
  push(value) {
3109
+ validateNotInfoPath(this._path, "push");
2169
3110
  const key = generatePushId();
2170
3111
  const childRef = this.child(key);
2171
3112
  if (value === void 0) {
2172
- return childRef;
3113
+ return new ThenableReference(this._db, childRef.path);
2173
3114
  }
2174
- return childRef.set(value).then(() => childRef);
3115
+ const setPromise = childRef.set(value);
3116
+ const writePromise = setPromise.then(() => childRef);
3117
+ return new ThenableReference(this._db, childRef.path, writePromise);
2175
3118
  }
2176
3119
  /**
2177
3120
  * Set the data with a priority value for ordering.
2178
- * Priority is injected as `.priority` into the value object.
3121
+ *
3122
+ * For objects: injects `.priority` into the value object.
3123
+ * For primitives: wraps as `{ '.value': primitive, '.priority': priority }`.
3124
+ *
3125
+ * This follows Firebase's wire format for primitives with priority.
3126
+ *
3127
+ * @param value - The value to write
3128
+ * @param priority - The priority for ordering
2179
3129
  */
2180
3130
  async setWithPriority(value, priority) {
3131
+ validateNotInfoPath(this._path, "setWithPriority");
2181
3132
  if (value === null || value === void 0) {
2182
3133
  await this._db._sendSet(this._path, value);
2183
3134
  return;
2184
3135
  }
2185
3136
  if (typeof value !== "object" || Array.isArray(value)) {
2186
- throw new Error("Priority can only be set on object values");
3137
+ const wrappedValue = { ".value": value, ".priority": priority };
3138
+ await this._db._sendSet(this._path, wrappedValue);
3139
+ return;
2187
3140
  }
2188
3141
  const valueWithPriority = { ...value, ".priority": priority };
2189
3142
  await this._db._sendSet(this._path, valueWithPriority);
2190
3143
  }
2191
3144
  /**
2192
3145
  * Set the priority of the data at this location.
2193
- * Fetches current value and sets it with the new priority.
2194
- */
2195
- async setPriority(priority) {
2196
- const snapshot = await this.once();
2197
- const currentVal = snapshot.val();
2198
- if (currentVal === null || currentVal === void 0) {
2199
- await this._db._sendSet(this._path, { ".priority": priority });
2200
- return;
3146
+ * Uses cached value for optimistic behavior (local effects are immediate).
3147
+ * The optimistic update happens synchronously, Promise resolves after server ack.
3148
+ */
3149
+ setPriority(priority) {
3150
+ validateNotInfoPath(this._path, "setPriority");
3151
+ const { value: cachedValue, found } = this._db._getCachedValue(this._path);
3152
+ if (!found) {
3153
+ return this.once().then((snapshot) => {
3154
+ const actualValue2 = snapshot.val();
3155
+ if (actualValue2 === null || actualValue2 === void 0) {
3156
+ return this._db._sendSet(this._path, { ".priority": priority });
3157
+ }
3158
+ return this.setWithPriority(actualValue2, priority);
3159
+ });
2201
3160
  }
2202
- if (typeof currentVal !== "object" || Array.isArray(currentVal)) {
2203
- throw new Error("Priority can only be set on object values");
3161
+ let actualValue;
3162
+ if (cachedValue === null || cachedValue === void 0) {
3163
+ actualValue = null;
3164
+ } else if (typeof cachedValue === "object" && !Array.isArray(cachedValue)) {
3165
+ const obj = cachedValue;
3166
+ if (".value" in obj && Object.keys(obj).every((k) => k === ".value" || k === ".priority")) {
3167
+ actualValue = obj[".value"];
3168
+ } else {
3169
+ const { ".priority": _oldPriority, ...rest } = obj;
3170
+ actualValue = Object.keys(rest).length > 0 ? rest : null;
3171
+ }
3172
+ } else {
3173
+ actualValue = cachedValue;
2204
3174
  }
2205
- await this.setWithPriority(currentVal, priority);
3175
+ if (actualValue === null || actualValue === void 0) {
3176
+ return this._db._sendSet(this._path, { ".priority": priority });
3177
+ }
3178
+ return this.setWithPriority(actualValue, priority);
2206
3179
  }
2207
3180
  /**
2208
3181
  * Atomically modify the data at this location using optimistic concurrency.
@@ -2227,6 +3200,7 @@ var DatabaseReference = class _DatabaseReference {
2227
3200
  * @returns TransactionResult with committed status and final snapshot
2228
3201
  */
2229
3202
  async transaction(updateFunction, maxRetries = DEFAULT_MAX_RETRIES) {
3203
+ validateNotInfoPath(this._path, "transaction");
2230
3204
  let retries = 0;
2231
3205
  while (retries < maxRetries) {
2232
3206
  const currentSnapshot = await this.once();
@@ -2271,12 +3245,24 @@ var DatabaseReference = class _DatabaseReference {
2271
3245
  // ============================================
2272
3246
  /**
2273
3247
  * Read the data at this location once.
3248
+ *
3249
+ * For 'value' events, this fetches data directly from the server.
3250
+ * For child events ('child_added', 'child_changed', 'child_removed', 'child_moved'),
3251
+ * this subscribes, waits for the first event, then unsubscribes.
3252
+ *
3253
+ * @param eventType - The event type
3254
+ * @returns Promise that resolves to the DataSnapshot
2274
3255
  */
2275
- async once(eventType = "value") {
2276
- if (eventType !== "value") {
2277
- throw new Error('once() only supports "value" event type');
3256
+ once(eventType = "value") {
3257
+ if (eventType === "value") {
3258
+ return this._db._sendOnce(this._path, this._buildQueryParams());
2278
3259
  }
2279
- return this._db._sendOnce(this._path, this._buildQueryParams());
3260
+ return new Promise((resolve) => {
3261
+ const unsubscribe = this.on(eventType, (snapshot) => {
3262
+ unsubscribe();
3263
+ resolve(snapshot);
3264
+ });
3265
+ });
2280
3266
  }
2281
3267
  // ============================================
2282
3268
  // Subscriptions
@@ -2286,12 +3272,18 @@ var DatabaseReference = class _DatabaseReference {
2286
3272
  * Returns an unsubscribe function.
2287
3273
  */
2288
3274
  on(eventType, callback) {
2289
- return this._db._subscribe(this._path, eventType, callback, this._buildQueryParams());
3275
+ return this._db._subscribe(this._path, eventType, callback, this._buildQueryParams(), this.queryIdentifier);
2290
3276
  }
2291
3277
  /**
2292
3278
  * Unsubscribe from events.
2293
- * If eventType is specified, removes all listeners of that type.
2294
- * If no eventType, removes ALL listeners at this path.
3279
+ *
3280
+ * - `off()` - removes ALL listeners at this path
3281
+ * - `off('value')` - removes all 'value' listeners at this path
3282
+ *
3283
+ * Note: To unsubscribe a specific callback, use the unsubscribe function
3284
+ * returned by `on()`.
3285
+ *
3286
+ * @param eventType - Optional event type to unsubscribe from
2295
3287
  */
2296
3288
  off(eventType) {
2297
3289
  if (eventType) {
@@ -2316,6 +3308,9 @@ var DatabaseReference = class _DatabaseReference {
2316
3308
  * Order results by key.
2317
3309
  */
2318
3310
  orderByKey() {
3311
+ this._validateNoOrderBy("orderByKey");
3312
+ this._validateNoKeyParameterForOrderByKey("orderByKey");
3313
+ this._validateStringValuesForOrderByKey("orderByKey");
2319
3314
  return new _DatabaseReference(this._db, this._path, {
2320
3315
  ...this._query,
2321
3316
  orderBy: "key"
@@ -2325,6 +3320,7 @@ var DatabaseReference = class _DatabaseReference {
2325
3320
  * Order results by priority.
2326
3321
  */
2327
3322
  orderByPriority() {
3323
+ this._validateNoOrderBy("orderByPriority");
2328
3324
  return new _DatabaseReference(this._db, this._path, {
2329
3325
  ...this._query,
2330
3326
  orderBy: "priority"
@@ -2334,6 +3330,13 @@ var DatabaseReference = class _DatabaseReference {
2334
3330
  * Order results by a child key.
2335
3331
  */
2336
3332
  orderByChild(path) {
3333
+ this._validateNoOrderBy("orderByChild");
3334
+ if (path.startsWith("$") || path.includes("/$")) {
3335
+ throw new LarkError(
3336
+ ErrorCode.INVALID_PATH,
3337
+ `orderByChild: Invalid path '${path}'. Paths cannot contain '$' prefix (reserved for internal use)`
3338
+ );
3339
+ }
2337
3340
  return new _DatabaseReference(this._db, this._path, {
2338
3341
  ...this._query,
2339
3342
  orderBy: "child",
@@ -2344,6 +3347,7 @@ var DatabaseReference = class _DatabaseReference {
2344
3347
  * Order results by value.
2345
3348
  */
2346
3349
  orderByValue() {
3350
+ this._validateNoOrderBy("orderByValue");
2347
3351
  return new _DatabaseReference(this._db, this._path, {
2348
3352
  ...this._query,
2349
3353
  orderBy: "value"
@@ -2353,6 +3357,30 @@ var DatabaseReference = class _DatabaseReference {
2353
3357
  * Limit to the first N results.
2354
3358
  */
2355
3359
  limitToFirst(limit) {
3360
+ if (limit === void 0 || limit === null) {
3361
+ throw new LarkError(
3362
+ ErrorCode.INVALID_QUERY,
3363
+ "Query.limitToFirst: a limit must be provided"
3364
+ );
3365
+ }
3366
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0) {
3367
+ throw new LarkError(
3368
+ ErrorCode.INVALID_QUERY,
3369
+ "Query.limitToFirst: limit must be a positive integer"
3370
+ );
3371
+ }
3372
+ if (this._query.limitToFirst !== void 0) {
3373
+ throw new LarkError(
3374
+ ErrorCode.INVALID_QUERY,
3375
+ "Query.limitToFirst: limitToFirst() cannot be called multiple times"
3376
+ );
3377
+ }
3378
+ if (this._query.limitToLast !== void 0) {
3379
+ throw new LarkError(
3380
+ ErrorCode.INVALID_QUERY,
3381
+ "Query.limitToFirst: cannot use limitToFirst() with limitToLast()"
3382
+ );
3383
+ }
2356
3384
  return new _DatabaseReference(this._db, this._path, {
2357
3385
  ...this._query,
2358
3386
  limitToFirst: limit
@@ -2362,6 +3390,30 @@ var DatabaseReference = class _DatabaseReference {
2362
3390
  * Limit to the last N results.
2363
3391
  */
2364
3392
  limitToLast(limit) {
3393
+ if (limit === void 0 || limit === null) {
3394
+ throw new LarkError(
3395
+ ErrorCode.INVALID_QUERY,
3396
+ "Query.limitToLast: a limit must be provided"
3397
+ );
3398
+ }
3399
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0) {
3400
+ throw new LarkError(
3401
+ ErrorCode.INVALID_QUERY,
3402
+ "Query.limitToLast: limit must be a positive integer"
3403
+ );
3404
+ }
3405
+ if (this._query.limitToLast !== void 0) {
3406
+ throw new LarkError(
3407
+ ErrorCode.INVALID_QUERY,
3408
+ "Query.limitToLast: limitToLast() cannot be called multiple times"
3409
+ );
3410
+ }
3411
+ if (this._query.limitToFirst !== void 0) {
3412
+ throw new LarkError(
3413
+ ErrorCode.INVALID_QUERY,
3414
+ "Query.limitToLast: cannot use limitToLast() with limitToFirst()"
3415
+ );
3416
+ }
2365
3417
  return new _DatabaseReference(this._db, this._path, {
2366
3418
  ...this._query,
2367
3419
  limitToLast: limit
@@ -2369,30 +3421,260 @@ var DatabaseReference = class _DatabaseReference {
2369
3421
  }
2370
3422
  /**
2371
3423
  * Start at a specific value/key.
3424
+ * If no value is provided, starts at the beginning (null priority).
2372
3425
  */
2373
3426
  startAt(value, key) {
3427
+ if (this._query.startAt !== void 0 || this._query.startAfter !== void 0) {
3428
+ throw new LarkError(
3429
+ ErrorCode.INVALID_QUERY,
3430
+ "Query.startAt: startAt()/startAfter() cannot be called multiple times"
3431
+ );
3432
+ }
3433
+ if (this._query.equalTo !== void 0) {
3434
+ throw new LarkError(
3435
+ ErrorCode.INVALID_QUERY,
3436
+ "Query.startAt: cannot use startAt() with equalTo()"
3437
+ );
3438
+ }
3439
+ this._validateQueryValue("startAt", value, key);
2374
3440
  return new _DatabaseReference(this._db, this._path, {
2375
3441
  ...this._query,
2376
3442
  startAt: { value, key }
2377
3443
  });
2378
3444
  }
2379
3445
  /**
2380
- * End at a specific value/key.
3446
+ * Start after a specific value/key (exclusive).
3447
+ * Like startAt() but excludes the starting point.
3448
+ */
3449
+ startAfter(value, key) {
3450
+ if (this._query.startAfter !== void 0 || this._query.startAt !== void 0) {
3451
+ throw new LarkError(
3452
+ ErrorCode.INVALID_QUERY,
3453
+ "Query.startAfter: startAfter()/startAt() cannot be called multiple times"
3454
+ );
3455
+ }
3456
+ if (this._query.equalTo !== void 0) {
3457
+ throw new LarkError(
3458
+ ErrorCode.INVALID_QUERY,
3459
+ "Query.startAfter: cannot use startAfter() with equalTo()"
3460
+ );
3461
+ }
3462
+ this._validateQueryValue("startAfter", value, key);
3463
+ return new _DatabaseReference(this._db, this._path, {
3464
+ ...this._query,
3465
+ startAfter: { value, key }
3466
+ });
3467
+ }
3468
+ /**
3469
+ * End at a specific value/key.
3470
+ * If no value is provided, ends at the end (null priority).
3471
+ */
3472
+ endAt(value, key) {
3473
+ if (this._query.endAt !== void 0 || this._query.endBefore !== void 0) {
3474
+ throw new LarkError(
3475
+ ErrorCode.INVALID_QUERY,
3476
+ "Query.endAt: endAt()/endBefore() cannot be called multiple times"
3477
+ );
3478
+ }
3479
+ if (this._query.equalTo !== void 0) {
3480
+ throw new LarkError(
3481
+ ErrorCode.INVALID_QUERY,
3482
+ "Query.endAt: cannot use endAt() with equalTo()"
3483
+ );
3484
+ }
3485
+ this._validateQueryValue("endAt", value, key);
3486
+ return new _DatabaseReference(this._db, this._path, {
3487
+ ...this._query,
3488
+ endAt: { value, key }
3489
+ });
3490
+ }
3491
+ /**
3492
+ * End before a specific value/key (exclusive).
3493
+ * Like endAt() but excludes the ending point.
3494
+ */
3495
+ endBefore(value, key) {
3496
+ if (this._query.endBefore !== void 0 || this._query.endAt !== void 0) {
3497
+ throw new LarkError(
3498
+ ErrorCode.INVALID_QUERY,
3499
+ "Query.endBefore: endBefore()/endAt() cannot be called multiple times"
3500
+ );
3501
+ }
3502
+ if (this._query.equalTo !== void 0) {
3503
+ throw new LarkError(
3504
+ ErrorCode.INVALID_QUERY,
3505
+ "Query.endBefore: cannot use endBefore() with equalTo()"
3506
+ );
3507
+ }
3508
+ this._validateQueryValue("endBefore", value, key);
3509
+ return new _DatabaseReference(this._db, this._path, {
3510
+ ...this._query,
3511
+ endBefore: { value, key }
3512
+ });
3513
+ }
3514
+ /**
3515
+ * Filter to items equal to a specific value.
3516
+ */
3517
+ equalTo(value, key) {
3518
+ if (this._query.equalTo !== void 0) {
3519
+ throw new LarkError(
3520
+ ErrorCode.INVALID_QUERY,
3521
+ "Query.equalTo: equalTo() cannot be called multiple times"
3522
+ );
3523
+ }
3524
+ if (this._query.startAt !== void 0 || this._query.startAfter !== void 0) {
3525
+ throw new LarkError(
3526
+ ErrorCode.INVALID_QUERY,
3527
+ "Query.equalTo: cannot use equalTo() with startAt()/startAfter()"
3528
+ );
3529
+ }
3530
+ if (this._query.endAt !== void 0 || this._query.endBefore !== void 0) {
3531
+ throw new LarkError(
3532
+ ErrorCode.INVALID_QUERY,
3533
+ "Query.equalTo: cannot use equalTo() with endAt()/endBefore()"
3534
+ );
3535
+ }
3536
+ this._validateQueryValue("equalTo", value, key);
3537
+ return new _DatabaseReference(this._db, this._path, {
3538
+ ...this._query,
3539
+ equalTo: { value, key }
3540
+ });
3541
+ }
3542
+ // ============================================
3543
+ // Query Validation Helpers
3544
+ // ============================================
3545
+ /**
3546
+ * Validate that no orderBy has been set yet.
3547
+ */
3548
+ _validateNoOrderBy(methodName) {
3549
+ if (this._query.orderBy !== void 0) {
3550
+ const existingMethod = this._query.orderBy === "child" ? `orderByChild('${this._query.orderByChildPath}')` : `orderBy${this._query.orderBy.charAt(0).toUpperCase()}${this._query.orderBy.slice(1)}()`;
3551
+ throw new LarkError(
3552
+ ErrorCode.INVALID_QUERY,
3553
+ `Query.${methodName}: cannot use ${methodName}() with ${existingMethod}`
3554
+ );
3555
+ }
3556
+ }
3557
+ /**
3558
+ * Validate that no key parameter has been set on startAt/endAt/equalTo.
3559
+ * Used when calling orderByKey() after startAt/endAt/equalTo.
3560
+ */
3561
+ _validateNoKeyParameterForOrderByKey(methodName) {
3562
+ if (this._query.startAt?.key !== void 0) {
3563
+ throw new LarkError(
3564
+ ErrorCode.INVALID_QUERY,
3565
+ `Query.${methodName}: when using orderByKey(), you cannot specify a key parameter in startAt()`
3566
+ );
3567
+ }
3568
+ if (this._query.startAfter?.key !== void 0) {
3569
+ throw new LarkError(
3570
+ ErrorCode.INVALID_QUERY,
3571
+ `Query.${methodName}: when using orderByKey(), you cannot specify a key parameter in startAfter()`
3572
+ );
3573
+ }
3574
+ if (this._query.endAt?.key !== void 0) {
3575
+ throw new LarkError(
3576
+ ErrorCode.INVALID_QUERY,
3577
+ `Query.${methodName}: when using orderByKey(), you cannot specify a key parameter in endAt()`
3578
+ );
3579
+ }
3580
+ if (this._query.endBefore?.key !== void 0) {
3581
+ throw new LarkError(
3582
+ ErrorCode.INVALID_QUERY,
3583
+ `Query.${methodName}: when using orderByKey(), you cannot specify a key parameter in endBefore()`
3584
+ );
3585
+ }
3586
+ if (this._query.equalTo?.key !== void 0) {
3587
+ throw new LarkError(
3588
+ ErrorCode.INVALID_QUERY,
3589
+ `Query.${methodName}: when using orderByKey(), you cannot specify a key parameter in equalTo()`
3590
+ );
3591
+ }
3592
+ }
3593
+ /**
3594
+ * Validate that startAt/endAt/equalTo values are strings.
3595
+ * Used when calling orderByKey() after startAt/endAt/equalTo.
3596
+ */
3597
+ _validateStringValuesForOrderByKey(methodName) {
3598
+ if (this._query.startAt !== void 0 && typeof this._query.startAt.value !== "string") {
3599
+ throw new LarkError(
3600
+ ErrorCode.INVALID_QUERY,
3601
+ `Query.${methodName}: when using orderByKey(), startAt() value must be a string`
3602
+ );
3603
+ }
3604
+ if (this._query.startAfter !== void 0 && typeof this._query.startAfter.value !== "string") {
3605
+ throw new LarkError(
3606
+ ErrorCode.INVALID_QUERY,
3607
+ `Query.${methodName}: when using orderByKey(), startAfter() value must be a string`
3608
+ );
3609
+ }
3610
+ if (this._query.endAt !== void 0 && typeof this._query.endAt.value !== "string") {
3611
+ throw new LarkError(
3612
+ ErrorCode.INVALID_QUERY,
3613
+ `Query.${methodName}: when using orderByKey(), endAt() value must be a string`
3614
+ );
3615
+ }
3616
+ if (this._query.endBefore !== void 0 && typeof this._query.endBefore.value !== "string") {
3617
+ throw new LarkError(
3618
+ ErrorCode.INVALID_QUERY,
3619
+ `Query.${methodName}: when using orderByKey(), endBefore() value must be a string`
3620
+ );
3621
+ }
3622
+ if (this._query.equalTo !== void 0 && typeof this._query.equalTo.value !== "string") {
3623
+ throw new LarkError(
3624
+ ErrorCode.INVALID_QUERY,
3625
+ `Query.${methodName}: when using orderByKey(), equalTo() value must be a string`
3626
+ );
3627
+ }
3628
+ }
3629
+ /**
3630
+ * Validate value and key types for query methods (startAt, endAt, equalTo, etc.)
2381
3631
  */
2382
- endAt(value, key) {
2383
- return new _DatabaseReference(this._db, this._path, {
2384
- ...this._query,
2385
- endAt: { value, key }
2386
- });
3632
+ _validateQueryValue(methodName, value, key) {
3633
+ if (key !== void 0) {
3634
+ this._validateKeyFormat(methodName, key);
3635
+ }
3636
+ if (this._query.orderBy === "key") {
3637
+ if (typeof value !== "string") {
3638
+ throw new LarkError(
3639
+ ErrorCode.INVALID_QUERY,
3640
+ `Query.${methodName}: when using orderByKey(), the value must be a string`
3641
+ );
3642
+ }
3643
+ if (key !== void 0) {
3644
+ throw new LarkError(
3645
+ ErrorCode.INVALID_QUERY,
3646
+ `Query.${methodName}: when using orderByKey(), you cannot specify a key parameter`
3647
+ );
3648
+ }
3649
+ return;
3650
+ }
3651
+ if (this._query.orderBy === "priority") {
3652
+ if (typeof value === "boolean") {
3653
+ throw new LarkError(
3654
+ ErrorCode.INVALID_QUERY,
3655
+ `Query.${methodName}: when using orderByPriority(), the value must be a valid priority (null, number, or string)`
3656
+ );
3657
+ }
3658
+ }
3659
+ if (value !== null && typeof value === "object") {
3660
+ throw new LarkError(
3661
+ ErrorCode.INVALID_QUERY,
3662
+ `Query.${methodName}: the value argument cannot be an object`
3663
+ );
3664
+ }
2387
3665
  }
2388
3666
  /**
2389
- * Filter to items equal to a specific value.
3667
+ * Validate that a key is a valid Firebase key format.
3668
+ * Invalid characters: . $ # [ ] /
3669
+ * Also cannot start or end with .
2390
3670
  */
2391
- equalTo(value, key) {
2392
- return new _DatabaseReference(this._db, this._path, {
2393
- ...this._query,
2394
- equalTo: { value, key }
2395
- });
3671
+ _validateKeyFormat(methodName, key) {
3672
+ if (/[.#$\[\]\/]/.test(key)) {
3673
+ throw new LarkError(
3674
+ ErrorCode.INVALID_QUERY,
3675
+ `Query.${methodName}: invalid key "${key}" - keys cannot contain . # $ [ ] /`
3676
+ );
3677
+ }
2396
3678
  }
2397
3679
  // ============================================
2398
3680
  // Internal Helpers
@@ -2428,21 +3710,35 @@ var DatabaseReference = class _DatabaseReference {
2428
3710
  hasParams = true;
2429
3711
  }
2430
3712
  if (this._query.startAt !== void 0) {
2431
- params.startAt = this._query.startAt.value;
3713
+ params.startAt = this._query.startAt.value ?? null;
2432
3714
  if (this._query.startAt.key !== void 0) {
2433
3715
  params.startAtKey = this._query.startAt.key;
2434
3716
  }
2435
3717
  hasParams = true;
2436
3718
  }
3719
+ if (this._query.startAfter !== void 0) {
3720
+ params.startAfter = this._query.startAfter.value ?? null;
3721
+ if (this._query.startAfter.key !== void 0) {
3722
+ params.startAfterKey = this._query.startAfter.key;
3723
+ }
3724
+ hasParams = true;
3725
+ }
2437
3726
  if (this._query.endAt !== void 0) {
2438
- params.endAt = this._query.endAt.value;
3727
+ params.endAt = this._query.endAt.value ?? null;
2439
3728
  if (this._query.endAt.key !== void 0) {
2440
3729
  params.endAtKey = this._query.endAt.key;
2441
3730
  }
2442
3731
  hasParams = true;
2443
3732
  }
3733
+ if (this._query.endBefore !== void 0) {
3734
+ params.endBefore = this._query.endBefore.value ?? null;
3735
+ if (this._query.endBefore.key !== void 0) {
3736
+ params.endBeforeKey = this._query.endBefore.key;
3737
+ }
3738
+ hasParams = true;
3739
+ }
2444
3740
  if (this._query.equalTo !== void 0) {
2445
- params.equalTo = this._query.equalTo.value;
3741
+ params.equalTo = this._query.equalTo.value ?? null;
2446
3742
  if (this._query.equalTo.key !== void 0) {
2447
3743
  params.equalToKey = this._query.equalTo.key;
2448
3744
  }
@@ -2461,9 +3757,63 @@ var DatabaseReference = class _DatabaseReference {
2461
3757
  }
2462
3758
  return `${baseUrl}${this._path}`;
2463
3759
  }
3760
+ /**
3761
+ * Returns the URL for JSON serialization.
3762
+ * This allows refs to be serialized with JSON.stringify().
3763
+ */
3764
+ toJSON() {
3765
+ return this.toString();
3766
+ }
3767
+ };
3768
+ var ThenableReference = class extends DatabaseReference {
3769
+ constructor(db, path, promise) {
3770
+ super(db, path);
3771
+ this._promise = promise ?? Promise.resolve(this);
3772
+ }
3773
+ then(onfulfilled, onrejected) {
3774
+ return this._promise.then(onfulfilled, onrejected);
3775
+ }
3776
+ catch(onrejected) {
3777
+ return this._promise.catch(onrejected);
3778
+ }
2464
3779
  };
2465
3780
 
2466
3781
  // src/DataSnapshot.ts
3782
+ function isWrappedPrimitive(data) {
3783
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
3784
+ return false;
3785
+ }
3786
+ const keys = Object.keys(data);
3787
+ if (keys.length === 2 && ".value" in data && ".priority" in data) {
3788
+ return true;
3789
+ }
3790
+ if (keys.length === 1 && ".value" in data) {
3791
+ return true;
3792
+ }
3793
+ return false;
3794
+ }
3795
+ function stripPriorityMetadata(data) {
3796
+ if (data === null || data === void 0) {
3797
+ return null;
3798
+ }
3799
+ if (typeof data !== "object") {
3800
+ return data;
3801
+ }
3802
+ if (Array.isArray(data)) {
3803
+ return data.map(stripPriorityMetadata);
3804
+ }
3805
+ if (isWrappedPrimitive(data)) {
3806
+ return stripPriorityMetadata(data[".value"]);
3807
+ }
3808
+ const result = {};
3809
+ for (const [key, value] of Object.entries(data)) {
3810
+ if (key === ".priority") {
3811
+ continue;
3812
+ }
3813
+ result[key] = stripPriorityMetadata(value);
3814
+ }
3815
+ return Object.keys(result).length > 0 ? result : null;
3816
+ }
2467
3817
  var DataSnapshot = class _DataSnapshot {
2468
3818
  constructor(data, path, db, options = {}) {
2469
3819
  this._data = data;
@@ -2471,6 +3821,8 @@ var DataSnapshot = class _DataSnapshot {
2471
3821
  this._db = db;
2472
3822
  this._volatile = options.volatile ?? false;
2473
3823
  this._serverTimestamp = options.serverTimestamp ?? null;
3824
+ this._queryParams = options.queryParams ?? null;
3825
+ this._orderedKeys = options.orderedKeys ?? null;
2474
3826
  }
2475
3827
  /**
2476
3828
  * Get a DatabaseReference for the location of this snapshot.
@@ -2485,69 +3837,128 @@ var DataSnapshot = class _DataSnapshot {
2485
3837
  return getKey(this._path);
2486
3838
  }
2487
3839
  /**
2488
- * Get the data value, with `.priority` stripped if present.
2489
- * Priority is internal metadata and should not be visible to app code.
3840
+ * Get the data value, with metadata stripped.
3841
+ *
3842
+ * - Strips `.priority` from objects (it's metadata, not user data)
3843
+ * - Unwraps `{ '.value': x, '.priority': y }` to just `x` (wrapped primitives)
3844
+ *
3845
+ * @typeParam T - Optional type for the returned value. Defaults to `any`.
3846
+ * @example
3847
+ * ```typescript
3848
+ * // Untyped (returns any)
3849
+ * const data = snapshot.val();
3850
+ *
3851
+ * // Typed (returns User)
3852
+ * const user = snapshot.val<User>();
3853
+ * ```
2490
3854
  */
2491
3855
  val() {
2492
- if (this._data && typeof this._data === "object" && !Array.isArray(this._data)) {
2493
- const data = this._data;
2494
- if (".priority" in data) {
2495
- const { ".priority": _, ...rest } = data;
2496
- return Object.keys(rest).length > 0 ? rest : Object.keys(data).length === 1 ? null : rest;
2497
- }
2498
- }
2499
- return this._data;
3856
+ return stripPriorityMetadata(this._data);
2500
3857
  }
2501
3858
  /**
2502
3859
  * Check if data exists at this location (is not null/undefined).
3860
+ * Returns false for priority-only nodes (only .priority, no actual value).
2503
3861
  */
2504
3862
  exists() {
2505
- return this._data !== null && this._data !== void 0;
3863
+ if (this._data === null || this._data === void 0) {
3864
+ return false;
3865
+ }
3866
+ if (typeof this._data === "object" && !Array.isArray(this._data)) {
3867
+ const keys = Object.keys(this._data);
3868
+ if (keys.length === 1 && keys[0] === ".priority") {
3869
+ return false;
3870
+ }
3871
+ }
3872
+ return true;
2506
3873
  }
2507
3874
  /**
2508
3875
  * Get a child snapshot at the specified path.
3876
+ *
3877
+ * Special handling:
3878
+ * - `.priority` returns the priority value (Firebase compatible)
3879
+ * - For wrapped primitives, only `.priority` returns data; other paths return null
3880
+ * - Non-existent paths return a snapshot with val() === null (Firebase compatible)
2509
3881
  */
2510
3882
  child(path) {
2511
3883
  const childPath = joinPath(this._path, path);
3884
+ if (path === ".priority") {
3885
+ const priority = this.getPriority();
3886
+ return new _DataSnapshot(priority, childPath, this._db, {
3887
+ volatile: this._volatile,
3888
+ serverTimestamp: this._serverTimestamp
3889
+ });
3890
+ }
3891
+ if (isWrappedPrimitive(this._data)) {
3892
+ return new _DataSnapshot(null, childPath, this._db, {
3893
+ volatile: this._volatile,
3894
+ serverTimestamp: this._serverTimestamp
3895
+ });
3896
+ }
2512
3897
  const childData = getValueAtPath(this._data, path);
2513
- return new _DataSnapshot(childData, childPath, this._db, {
3898
+ return new _DataSnapshot(childData ?? null, childPath, this._db, {
2514
3899
  volatile: this._volatile,
2515
3900
  serverTimestamp: this._serverTimestamp
2516
3901
  });
2517
3902
  }
2518
3903
  /**
2519
3904
  * Check if this snapshot has any children.
3905
+ * Excludes `.priority` metadata from consideration.
3906
+ * Wrapped primitives have no children.
2520
3907
  */
2521
3908
  hasChildren() {
2522
3909
  if (typeof this._data !== "object" || this._data === null) {
2523
3910
  return false;
2524
3911
  }
2525
- return Object.keys(this._data).length > 0;
3912
+ if (isWrappedPrimitive(this._data)) {
3913
+ return false;
3914
+ }
3915
+ const keys = Object.keys(this._data).filter((k) => k !== ".priority");
3916
+ return keys.length > 0;
2526
3917
  }
2527
3918
  /**
2528
3919
  * Check if this snapshot has a specific child.
3920
+ * `.priority` is always accessible if it exists.
3921
+ * Wrapped primitives have no children except `.priority`.
2529
3922
  */
2530
3923
  hasChild(path) {
3924
+ if (path === ".priority") {
3925
+ return this.getPriority() !== null;
3926
+ }
3927
+ if (isWrappedPrimitive(this._data)) {
3928
+ return false;
3929
+ }
2531
3930
  const childData = getValueAtPath(this._data, path);
2532
3931
  return childData !== void 0 && childData !== null;
2533
3932
  }
2534
3933
  /**
2535
3934
  * Get the number of children.
3935
+ * Excludes `.priority` metadata from count.
3936
+ * Wrapped primitives have 0 children.
2536
3937
  */
2537
3938
  numChildren() {
2538
3939
  if (typeof this._data !== "object" || this._data === null) {
2539
3940
  return 0;
2540
3941
  }
2541
- return Object.keys(this._data).length;
3942
+ if (isWrappedPrimitive(this._data)) {
3943
+ return 0;
3944
+ }
3945
+ return Object.keys(this._data).filter((k) => k !== ".priority").length;
2542
3946
  }
2543
3947
  /**
2544
- * Iterate over children. Return true from callback to stop iteration.
3948
+ * Iterate over children in the correct order.
3949
+ * Uses pre-computed orderedKeys if available (from subscription View),
3950
+ * otherwise computes sorted keys based on query params.
3951
+ * Excludes `.priority` metadata from iteration.
3952
+ * Wrapped primitives have no children to iterate.
2545
3953
  */
2546
3954
  forEach(callback) {
2547
3955
  if (typeof this._data !== "object" || this._data === null) {
2548
3956
  return;
2549
3957
  }
2550
- const keys = Object.keys(this._data);
3958
+ if (isWrappedPrimitive(this._data)) {
3959
+ return;
3960
+ }
3961
+ const keys = this._orderedKeys ?? getSortedKeys(this._data, this._queryParams);
2551
3962
  for (const key of keys) {
2552
3963
  const childSnap = this.child(key);
2553
3964
  if (callback(childSnap) === true) {
@@ -2585,31 +3996,26 @@ var DataSnapshot = class _DataSnapshot {
2585
3996
  return this._serverTimestamp;
2586
3997
  }
2587
3998
  /**
2588
- * Export the snapshot data as JSON (alias for val()).
3999
+ * Export the snapshot data with priority metadata intact.
4000
+ * Unlike val(), this preserves `.value` and `.priority` wrappers.
4001
+ * Useful for serializing data while preserving priorities.
4002
+ *
4003
+ * @typeParam T - Optional type for the returned value. Defaults to `any`.
4004
+ */
4005
+ exportVal() {
4006
+ return this._data;
4007
+ }
4008
+ /**
4009
+ * Export the snapshot data as JSON.
4010
+ * Same as exportVal() - preserves priority metadata.
4011
+ *
4012
+ * @typeParam T - Optional type for the returned value. Defaults to `any`.
2589
4013
  */
2590
4014
  toJSON() {
2591
4015
  return this._data;
2592
4016
  }
2593
4017
  };
2594
4018
 
2595
- // src/utils/jwt.ts
2596
- function decodeJwtPayload(token) {
2597
- const parts = token.split(".");
2598
- if (parts.length !== 3) {
2599
- throw new Error("Invalid JWT format");
2600
- }
2601
- const payload = parts[1];
2602
- const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
2603
- const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
2604
- let decoded;
2605
- if (typeof atob === "function") {
2606
- decoded = atob(padded);
2607
- } else {
2608
- decoded = Buffer.from(padded, "base64").toString("utf-8");
2609
- }
2610
- return JSON.parse(decoded);
2611
- }
2612
-
2613
4019
  // src/utils/volatile.ts
2614
4020
  function isVolatilePath(path, patterns) {
2615
4021
  if (!patterns || patterns.length === 0) {
@@ -2633,9 +4039,97 @@ function isVolatilePath(path, patterns) {
2633
4039
  }
2634
4040
 
2635
4041
  // src/LarkDatabase.ts
4042
+ function validateWriteData(data, path = "") {
4043
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
4044
+ return;
4045
+ }
4046
+ const obj = data;
4047
+ const keys = Object.keys(obj);
4048
+ if (".value" in obj) {
4049
+ const otherKeys = keys.filter((k) => k !== ".value" && k !== ".priority");
4050
+ if (otherKeys.length > 0) {
4051
+ const location = path || "/";
4052
+ throw new LarkError(
4053
+ "invalid_data",
4054
+ `Data at ${location} contains ".value" alongside other children (${otherKeys.join(", ")}). ".value" can only be used with ".priority" for primitives with priority.`
4055
+ );
4056
+ }
4057
+ }
4058
+ for (const key of keys) {
4059
+ if (key !== ".priority" && key !== ".value") {
4060
+ validateWriteData(obj[key], path ? `${path}/${key}` : `/${key}`);
4061
+ }
4062
+ }
4063
+ }
2636
4064
  var RECONNECT_BASE_DELAY_MS = 1e3;
2637
4065
  var RECONNECT_MAX_DELAY_MS = 3e4;
2638
4066
  var RECONNECT_JITTER_FACTOR = 0.5;
4067
+ function isServerValue(value) {
4068
+ return value !== null && typeof value === "object" && !Array.isArray(value) && ".sv" in value;
4069
+ }
4070
+ function resolveServerValuesLocally(value, currentValue) {
4071
+ if (value === null || value === void 0) {
4072
+ return value;
4073
+ }
4074
+ if (isServerValue(value)) {
4075
+ const sv = value[".sv"];
4076
+ if (sv === "timestamp") {
4077
+ return Date.now();
4078
+ }
4079
+ if (typeof sv === "object" && sv !== null && "increment" in sv) {
4080
+ const delta = sv.increment;
4081
+ if (typeof currentValue === "number") {
4082
+ return currentValue + delta;
4083
+ }
4084
+ return delta;
4085
+ }
4086
+ return value;
4087
+ }
4088
+ if (typeof value === "object" && !Array.isArray(value)) {
4089
+ const result = {};
4090
+ const currentObj = currentValue !== null && typeof currentValue === "object" && !Array.isArray(currentValue) ? currentValue : {};
4091
+ for (const [key, val] of Object.entries(value)) {
4092
+ result[key] = resolveServerValuesLocally(val, currentObj[key]);
4093
+ }
4094
+ return result;
4095
+ }
4096
+ if (Array.isArray(value)) {
4097
+ return value.map((item, index) => {
4098
+ const currentArr = Array.isArray(currentValue) ? currentValue : [];
4099
+ return resolveServerValuesLocally(item, currentArr[index]);
4100
+ });
4101
+ }
4102
+ return value;
4103
+ }
4104
+ var ServerValue = {
4105
+ /**
4106
+ * A placeholder value for auto-populating the current server timestamp.
4107
+ * The server will replace this with the actual Unix timestamp in milliseconds.
4108
+ *
4109
+ * @example
4110
+ * ```javascript
4111
+ * import { ServerValue } from '@lark-sh/client';
4112
+ * await ref.set({ createdAt: ServerValue.TIMESTAMP });
4113
+ * // Server stores: { createdAt: 1704067200000 }
4114
+ * ```
4115
+ */
4116
+ TIMESTAMP: { ".sv": "timestamp" },
4117
+ /**
4118
+ * Returns a placeholder value for atomically incrementing a numeric field.
4119
+ * If the field doesn't exist or isn't a number, it's treated as 0.
4120
+ *
4121
+ * @param delta - The amount to increment by (can be negative to decrement)
4122
+ * @returns A server value placeholder
4123
+ *
4124
+ * @example
4125
+ * ```javascript
4126
+ * import { ServerValue } from '@lark-sh/client';
4127
+ * await ref.child('score').set(ServerValue.increment(10));
4128
+ * // Atomically adds 10 to the current score
4129
+ * ```
4130
+ */
4131
+ increment: (delta) => ({ ".sv": { increment: delta } })
4132
+ };
2639
4133
  var LarkDatabase = class {
2640
4134
  constructor() {
2641
4135
  this._state = "disconnected";
@@ -2644,6 +4138,11 @@ var LarkDatabase = class {
2644
4138
  this._coordinatorUrl = null;
2645
4139
  this._volatilePaths = [];
2646
4140
  this._transportType = null;
4141
+ // Auth state
4142
+ this._currentToken = null;
4143
+ // Token for auth (empty string = anonymous)
4144
+ this._isAnonymous = false;
4145
+ // True if connected anonymously
2647
4146
  // Reconnection state
2648
4147
  this._connectionId = null;
2649
4148
  this._connectOptions = null;
@@ -2656,6 +4155,13 @@ var LarkDatabase = class {
2656
4155
  this.disconnectCallbacks = /* @__PURE__ */ new Set();
2657
4156
  this.errorCallbacks = /* @__PURE__ */ new Set();
2658
4157
  this.reconnectingCallbacks = /* @__PURE__ */ new Set();
4158
+ this.authStateChangedCallbacks = /* @__PURE__ */ new Set();
4159
+ // .info path subscriptions (handled locally, not sent to server)
4160
+ this.infoSubscriptions = [];
4161
+ // Authentication promise - resolves when auth completes, allows operations to queue
4162
+ this.authenticationPromise = null;
4163
+ this.authenticationResolve = null;
4164
+ this._serverTimeOffset = 0;
2659
4165
  this.messageQueue = new MessageQueue();
2660
4166
  this.subscriptionManager = new SubscriptionManager();
2661
4167
  this.pendingWrites = new PendingWriteManager();
@@ -2664,10 +4170,11 @@ var LarkDatabase = class {
2664
4170
  // Connection State
2665
4171
  // ============================================
2666
4172
  /**
2667
- * Whether the database is currently connected.
4173
+ * Whether the database is fully connected and authenticated.
4174
+ * Returns true when ready to perform database operations.
2668
4175
  */
2669
4176
  get connected() {
2670
- return this._state === "connected";
4177
+ return this._state === "authenticated";
2671
4178
  }
2672
4179
  /**
2673
4180
  * Whether the database is currently attempting to reconnect.
@@ -2710,6 +4217,13 @@ var LarkDatabase = class {
2710
4217
  get transportType() {
2711
4218
  return this._transportType;
2712
4219
  }
4220
+ /**
4221
+ * Get the estimated server time offset in milliseconds.
4222
+ * Add this to Date.now() to get approximate server time.
4223
+ */
4224
+ get serverTimeOffset() {
4225
+ return this._serverTimeOffset;
4226
+ }
2713
4227
  /**
2714
4228
  * Check if there are any pending writes waiting for acknowledgment.
2715
4229
  * Useful for showing "saving..." indicators in UI.
@@ -2745,16 +4259,27 @@ var LarkDatabase = class {
2745
4259
  }
2746
4260
  this._connectOptions = options;
2747
4261
  this._intentionalDisconnect = false;
4262
+ this.authenticationPromise = new Promise((resolve) => {
4263
+ this.authenticationResolve = resolve;
4264
+ });
2748
4265
  await this.performConnect(databaseId, options);
2749
4266
  }
2750
4267
  /**
2751
4268
  * Internal connect implementation used by both initial connect and reconnect.
4269
+ * Implements the Join → Auth flow:
4270
+ * 1. Connect WebSocket
4271
+ * 2. Send join (identifies database)
4272
+ * 3. Send auth (authenticates user - required even for anonymous)
2752
4273
  */
2753
4274
  async performConnect(databaseId, options, isReconnect = false) {
2754
4275
  const previousState = this._state;
2755
4276
  this._state = isReconnect ? "reconnecting" : "connecting";
2756
4277
  this._databaseId = databaseId;
2757
4278
  this._coordinatorUrl = options.coordinator || DEFAULT_COORDINATOR_URL;
4279
+ if (!isReconnect) {
4280
+ this._currentToken = options.token || "";
4281
+ this._isAnonymous = !options.token && options.anonymous !== false;
4282
+ }
2758
4283
  try {
2759
4284
  const coordinatorUrl = this._coordinatorUrl;
2760
4285
  const coordinator = new Coordinator(coordinatorUrl);
@@ -2780,27 +4305,45 @@ var LarkDatabase = class {
2780
4305
  );
2781
4306
  this.transport = transportResult.transport;
2782
4307
  this._transportType = transportResult.type;
2783
- const requestId = this.messageQueue.nextRequestId();
4308
+ this._state = "connected";
4309
+ const joinRequestId = this.messageQueue.nextRequestId();
2784
4310
  const joinMessage = {
2785
4311
  o: "j",
2786
- t: connectResponse.token,
2787
- r: requestId
4312
+ d: databaseId,
4313
+ r: joinRequestId
2788
4314
  };
2789
4315
  if (this._connectionId) {
2790
4316
  joinMessage.pcid = this._connectionId;
2791
4317
  }
2792
4318
  this.send(joinMessage);
2793
- const joinResponse = await this.messageQueue.registerRequest(requestId);
4319
+ const joinResponse = await this.messageQueue.registerRequest(joinRequestId);
2794
4320
  this._volatilePaths = joinResponse.volatilePaths;
2795
4321
  this._connectionId = joinResponse.connectionId;
2796
- const jwtPayload = decodeJwtPayload(connectResponse.token);
4322
+ if (joinResponse.serverTime != null) {
4323
+ this._serverTimeOffset = joinResponse.serverTime - Date.now();
4324
+ }
4325
+ this._state = "joined";
4326
+ const authRequestId = this.messageQueue.nextRequestId();
4327
+ const authMessage = {
4328
+ o: "au",
4329
+ t: this._currentToken ?? "",
4330
+ r: authRequestId
4331
+ };
4332
+ this.send(authMessage);
4333
+ const authResponse = await this.messageQueue.registerRequest(authRequestId);
2797
4334
  this._auth = {
2798
- uid: jwtPayload.sub,
2799
- provider: jwtPayload.provider,
2800
- token: jwtPayload.claims || {}
4335
+ uid: authResponse.uid || "",
4336
+ provider: this._isAnonymous ? "anonymous" : "custom",
4337
+ token: {}
4338
+ // Token claims would need to be decoded from the token if needed
2801
4339
  };
2802
- this._state = "connected";
4340
+ this._state = "authenticated";
2803
4341
  this._reconnectAttempt = 0;
4342
+ if (this.authenticationResolve) {
4343
+ this.authenticationResolve();
4344
+ this.authenticationResolve = null;
4345
+ }
4346
+ this.fireConnectionStateChange();
2804
4347
  if (!isReconnect) {
2805
4348
  this.subscriptionManager.initialize({
2806
4349
  sendSubscribe: this.sendSubscribeMessage.bind(this),
@@ -2812,6 +4355,7 @@ var LarkDatabase = class {
2812
4355
  await this.restoreAfterReconnect();
2813
4356
  }
2814
4357
  this.connectCallbacks.forEach((cb) => cb());
4358
+ this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
2815
4359
  } catch (error) {
2816
4360
  if (isReconnect) {
2817
4361
  this._state = "reconnecting";
@@ -2824,6 +4368,8 @@ var LarkDatabase = class {
2824
4368
  this._connectOptions = null;
2825
4369
  this._connectionId = null;
2826
4370
  this._transportType = null;
4371
+ this._currentToken = null;
4372
+ this._isAnonymous = false;
2827
4373
  this.transport?.close();
2828
4374
  this.transport = null;
2829
4375
  throw error;
@@ -2837,13 +4383,14 @@ var LarkDatabase = class {
2837
4383
  if (this._state === "disconnected") {
2838
4384
  return;
2839
4385
  }
2840
- const wasConnected = this._state === "connected";
4386
+ const wasAuthenticated = this._state === "authenticated";
4387
+ const wasPartiallyConnected = this._state === "connected" || this._state === "joined";
2841
4388
  this._intentionalDisconnect = true;
2842
4389
  if (this._reconnectTimer) {
2843
4390
  clearTimeout(this._reconnectTimer);
2844
4391
  this._reconnectTimer = null;
2845
4392
  }
2846
- if (wasConnected && this.transport) {
4393
+ if ((wasAuthenticated || wasPartiallyConnected) && this.transport) {
2847
4394
  try {
2848
4395
  const requestId = this.messageQueue.nextRequestId();
2849
4396
  this.send({ o: "l", r: requestId });
@@ -2855,15 +4402,47 @@ var LarkDatabase = class {
2855
4402
  }
2856
4403
  }
2857
4404
  this.cleanupFull();
2858
- if (wasConnected) {
4405
+ if (wasAuthenticated || wasPartiallyConnected) {
2859
4406
  this.disconnectCallbacks.forEach((cb) => cb());
2860
4407
  }
2861
4408
  }
4409
+ /**
4410
+ * Temporarily disable the connection.
4411
+ * Disconnects from the server but preserves subscriptions for later reconnection via goOnline().
4412
+ */
4413
+ goOffline() {
4414
+ if (this._state === "authenticated" || this._state === "joined" || this._state === "connected" || this._state === "reconnecting") {
4415
+ this._intentionalDisconnect = true;
4416
+ if (this._reconnectTimer) {
4417
+ clearTimeout(this._reconnectTimer);
4418
+ this._reconnectTimer = null;
4419
+ }
4420
+ this.transport?.close();
4421
+ this.transport = null;
4422
+ this._state = "disconnected";
4423
+ this.subscriptionManager.clearCacheOnly();
4424
+ this.fireConnectionStateChange();
4425
+ }
4426
+ }
4427
+ /**
4428
+ * Re-enable the connection after goOffline().
4429
+ * Reconnects to the database and restores subscriptions.
4430
+ */
4431
+ goOnline() {
4432
+ this._intentionalDisconnect = false;
4433
+ if (this._state === "disconnected" && this._databaseId && this._connectOptions) {
4434
+ this._state = "reconnecting";
4435
+ this.reconnectingCallbacks.forEach((cb) => cb());
4436
+ this._reconnectAttempt = 0;
4437
+ this.attemptReconnect();
4438
+ }
4439
+ }
2862
4440
  /**
2863
4441
  * Full cleanup - clears all state including subscriptions.
2864
4442
  * Used for intentional disconnect.
2865
4443
  */
2866
4444
  cleanupFull() {
4445
+ const wasAuthenticated = this._state === "authenticated";
2867
4446
  this.transport?.close();
2868
4447
  this.transport = null;
2869
4448
  this._state = "disconnected";
@@ -2874,10 +4453,18 @@ var LarkDatabase = class {
2874
4453
  this._connectionId = null;
2875
4454
  this._connectOptions = null;
2876
4455
  this._transportType = null;
4456
+ this._currentToken = null;
4457
+ this._isAnonymous = false;
2877
4458
  this._reconnectAttempt = 0;
4459
+ this.authenticationPromise = null;
4460
+ this.authenticationResolve = null;
2878
4461
  this.subscriptionManager.clear();
2879
4462
  this.messageQueue.rejectAll(new Error("Connection closed"));
2880
4463
  this.pendingWrites.clear();
4464
+ if (wasAuthenticated) {
4465
+ this.fireConnectionStateChange();
4466
+ }
4467
+ this.infoSubscriptions = [];
2881
4468
  }
2882
4469
  /**
2883
4470
  * Partial cleanup - preserves state needed for reconnect.
@@ -2889,6 +4476,67 @@ var LarkDatabase = class {
2889
4476
  this._auth = null;
2890
4477
  this.subscriptionManager.clearCacheOnly();
2891
4478
  this.messageQueue.rejectAll(new Error("Connection closed"));
4479
+ this.fireConnectionStateChange();
4480
+ }
4481
+ // ============================================
4482
+ // .info Path Handling
4483
+ // ============================================
4484
+ /**
4485
+ * Check if a path is a .info path (handled locally).
4486
+ */
4487
+ isInfoPath(path) {
4488
+ const normalizedPath = normalizePath(path) || "/";
4489
+ return normalizedPath === "/.info" || normalizedPath.startsWith("/.info/");
4490
+ }
4491
+ /**
4492
+ * Get the current value for a .info path.
4493
+ */
4494
+ getInfoValue(path) {
4495
+ const normalizedPath = normalizePath(path) || "/";
4496
+ if (normalizedPath === "/.info/connected") {
4497
+ return this._state === "authenticated";
4498
+ }
4499
+ if (normalizedPath === "/.info/serverTimeOffset") {
4500
+ return this._serverTimeOffset;
4501
+ }
4502
+ return null;
4503
+ }
4504
+ /**
4505
+ * Subscribe to a .info path.
4506
+ * Returns an unsubscribe function.
4507
+ */
4508
+ subscribeToInfo(path, callback) {
4509
+ const normalizedPath = normalizePath(path) || "/";
4510
+ const subscription = { path: normalizedPath, callback };
4511
+ this.infoSubscriptions.push(subscription);
4512
+ const value = this.getInfoValue(normalizedPath);
4513
+ const snapshot = new DataSnapshot(value, normalizedPath, this);
4514
+ setTimeout(() => callback(snapshot), 0);
4515
+ return () => {
4516
+ const index = this.infoSubscriptions.indexOf(subscription);
4517
+ if (index !== -1) {
4518
+ this.infoSubscriptions.splice(index, 1);
4519
+ }
4520
+ };
4521
+ }
4522
+ /**
4523
+ * Fire events for .info path changes.
4524
+ */
4525
+ fireInfoEvents(path) {
4526
+ const normalizedPath = normalizePath(path) || "/";
4527
+ const value = this.getInfoValue(normalizedPath);
4528
+ for (const sub of this.infoSubscriptions) {
4529
+ if (sub.path === normalizedPath) {
4530
+ const snapshot = new DataSnapshot(value, normalizedPath, this);
4531
+ sub.callback(snapshot);
4532
+ }
4533
+ }
4534
+ }
4535
+ /**
4536
+ * Fire connection state change events to .info/connected subscribers.
4537
+ */
4538
+ fireConnectionStateChange() {
4539
+ this.fireInfoEvents("/.info/connected");
2892
4540
  }
2893
4541
  /**
2894
4542
  * Schedule a reconnection attempt with exponential backoff.
@@ -2913,6 +4561,9 @@ var LarkDatabase = class {
2913
4561
  if (this._intentionalDisconnect || !this._databaseId || !this._connectOptions) {
2914
4562
  return;
2915
4563
  }
4564
+ this.authenticationPromise = new Promise((resolve) => {
4565
+ this.authenticationResolve = resolve;
4566
+ });
2916
4567
  try {
2917
4568
  await this.performConnect(this._databaseId, this._connectOptions, true);
2918
4569
  } catch {
@@ -3071,6 +4722,9 @@ var LarkDatabase = class {
3071
4722
  * @internal Send a transaction to the server.
3072
4723
  */
3073
4724
  async _sendTransaction(ops) {
4725
+ if (!this.isAuthenticatedOrThrow()) {
4726
+ if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
4727
+ }
3074
4728
  const requestId = this.messageQueue.nextRequestId();
3075
4729
  this.pendingWrites.trackWrite(requestId, "transaction", "/", ops);
3076
4730
  const message = {
@@ -3117,6 +4771,76 @@ var LarkDatabase = class {
3117
4771
  this.reconnectingCallbacks.add(callback);
3118
4772
  return () => this.reconnectingCallbacks.delete(callback);
3119
4773
  }
4774
+ /**
4775
+ * Register a callback for auth state changes.
4776
+ * Fires when user signs in, signs out, or auth changes.
4777
+ * Returns an unsubscribe function.
4778
+ */
4779
+ onAuthStateChanged(callback) {
4780
+ this.authStateChangedCallbacks.add(callback);
4781
+ return () => this.authStateChangedCallbacks.delete(callback);
4782
+ }
4783
+ // ============================================
4784
+ // Authentication Management
4785
+ // ============================================
4786
+ /**
4787
+ * Sign in with a new auth token while connected.
4788
+ * Changes the authenticated user without disconnecting.
4789
+ *
4790
+ * Note: Some subscriptions may be revoked if the new user doesn't have
4791
+ * permission. Listen for 'permission_denied' errors on your subscriptions.
4792
+ *
4793
+ * @param token - The auth token for the new user
4794
+ * @throws Error if not connected (must call connect() first)
4795
+ */
4796
+ async signIn(token) {
4797
+ if (this._state !== "authenticated" && this._state !== "joined") {
4798
+ throw new LarkError("not_connected", "Must be connected first - call connect()");
4799
+ }
4800
+ const authRequestId = this.messageQueue.nextRequestId();
4801
+ const authMessage = {
4802
+ o: "au",
4803
+ t: token,
4804
+ r: authRequestId
4805
+ };
4806
+ this.send(authMessage);
4807
+ const authResponse = await this.messageQueue.registerRequest(authRequestId);
4808
+ this._currentToken = token;
4809
+ this._isAnonymous = false;
4810
+ this._auth = {
4811
+ uid: authResponse.uid || "",
4812
+ provider: "custom",
4813
+ token: {}
4814
+ };
4815
+ this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
4816
+ }
4817
+ /**
4818
+ * Sign out the current user.
4819
+ * Reverts to anonymous authentication.
4820
+ *
4821
+ * Note: Some subscriptions may be revoked if anonymous users don't have
4822
+ * permission. Listen for 'permission_denied' errors on your subscriptions.
4823
+ */
4824
+ async signOut() {
4825
+ if (this._state !== "authenticated") {
4826
+ return;
4827
+ }
4828
+ const unauthRequestId = this.messageQueue.nextRequestId();
4829
+ const unauthMessage = {
4830
+ o: "ua",
4831
+ r: unauthRequestId
4832
+ };
4833
+ this.send(unauthMessage);
4834
+ const authResponse = await this.messageQueue.registerRequest(unauthRequestId);
4835
+ this._currentToken = "";
4836
+ this._isAnonymous = true;
4837
+ this._auth = {
4838
+ uid: authResponse.uid || "",
4839
+ provider: "anonymous",
4840
+ token: {}
4841
+ };
4842
+ this.authStateChangedCallbacks.forEach((cb) => cb(this._auth));
4843
+ }
3120
4844
  // ============================================
3121
4845
  // Internal: Message Handling
3122
4846
  // ============================================
@@ -3128,6 +4852,9 @@ var LarkDatabase = class {
3128
4852
  console.error("Failed to parse message:", data);
3129
4853
  return;
3130
4854
  }
4855
+ if (process.env.LARK_DEBUG) {
4856
+ console.log("[LARK] <<< SERVER:", JSON.stringify(message, null, 2));
4857
+ }
3131
4858
  if (isPingMessage(message)) {
3132
4859
  this.transport?.send(JSON.stringify({ o: "po" }));
3133
4860
  return;
@@ -3137,28 +4864,16 @@ var LarkDatabase = class {
3137
4864
  this.subscriptionManager.clearPendingWrite(message.a);
3138
4865
  } else if (isNackMessage(message)) {
3139
4866
  this.pendingWrites.onNack(message.n);
4867
+ if (message.e === "permission_denied" && message.sp) {
4868
+ const path = message.sp;
4869
+ console.warn(`Subscription revoked at ${path}: permission_denied`);
4870
+ this.subscriptionManager.handleSubscriptionRevoked(path);
4871
+ return;
4872
+ }
3140
4873
  if (message.e !== "condition_failed") {
3141
4874
  console.error(`Write failed (${message.e}): ${message.m || message.e}`);
3142
- const { affectedViews, taintedIds } = this.subscriptionManager.handleWriteNack(message.n);
3143
- if (affectedViews.length > 0) {
3144
- for (const id of taintedIds) {
3145
- if (id !== message.n) {
3146
- this.messageQueue.rejectLocally(
3147
- id,
3148
- new LarkError("write_tainted", "Write cancelled: depends on failed write")
3149
- );
3150
- this.pendingWrites.onNack(id);
3151
- }
3152
- }
3153
- for (const view of affectedViews) {
3154
- this.subscriptionManager.resubscribeView(view.path).catch((err) => {
3155
- console.error(`Failed to re-subscribe ${view.path} during recovery:`, err);
3156
- });
3157
- }
3158
- }
3159
- } else {
3160
- this.subscriptionManager.clearPendingWrite(message.n);
3161
4875
  }
4876
+ this.subscriptionManager.handleWriteNack(message.n);
3162
4877
  }
3163
4878
  if (this.messageQueue.handleMessage(message)) {
3164
4879
  return;
@@ -3171,27 +4886,28 @@ var LarkDatabase = class {
3171
4886
  if (this._state === "disconnected") {
3172
4887
  return;
3173
4888
  }
3174
- const wasConnected = this._state === "connected";
4889
+ const wasAuthenticated = this._state === "authenticated";
3175
4890
  const wasReconnecting = this._state === "reconnecting";
4891
+ const wasPartiallyConnected = this._state === "connected" || this._state === "joined";
3176
4892
  if (this._intentionalDisconnect) {
3177
4893
  this.cleanupFull();
3178
- if (wasConnected) {
4894
+ if (wasAuthenticated || wasPartiallyConnected) {
3179
4895
  this.disconnectCallbacks.forEach((cb) => cb());
3180
4896
  }
3181
4897
  return;
3182
4898
  }
3183
4899
  const canReconnect = this._databaseId && this._connectOptions;
3184
- if ((wasConnected || wasReconnecting) && canReconnect) {
4900
+ if ((wasAuthenticated || wasPartiallyConnected || wasReconnecting) && canReconnect) {
3185
4901
  this._state = "reconnecting";
3186
4902
  this.cleanupForReconnect();
3187
4903
  this.reconnectingCallbacks.forEach((cb) => cb());
3188
- if (wasConnected) {
4904
+ if (wasAuthenticated || wasPartiallyConnected) {
3189
4905
  this.disconnectCallbacks.forEach((cb) => cb());
3190
4906
  }
3191
4907
  this.scheduleReconnect();
3192
4908
  } else {
3193
4909
  this.cleanupFull();
3194
- if (wasConnected) {
4910
+ if (wasAuthenticated || wasPartiallyConnected) {
3195
4911
  this.disconnectCallbacks.forEach((cb) => cb());
3196
4912
  }
3197
4913
  }
@@ -3202,10 +4918,48 @@ var LarkDatabase = class {
3202
4918
  // ============================================
3203
4919
  // Internal: Sending Messages
3204
4920
  // ============================================
4921
+ /**
4922
+ * Check if authenticated synchronously.
4923
+ * Returns true if authenticated, false if connecting (should wait), throws if disconnected.
4924
+ */
4925
+ isAuthenticatedOrThrow() {
4926
+ if (this._state === "authenticated") {
4927
+ return true;
4928
+ }
4929
+ if (this._state === "connecting" || this._state === "connected" || this._state === "joined" || this._state === "reconnecting") {
4930
+ return false;
4931
+ }
4932
+ throw new LarkError("not_connected", "Not connected - call connect() first");
4933
+ }
4934
+ /**
4935
+ * Wait for authentication to complete before performing an operation.
4936
+ * If already authenticated, returns immediately (synchronously).
4937
+ * If connecting/reconnecting, waits for auth to complete.
4938
+ * If disconnected and no connect in progress, throws.
4939
+ *
4940
+ * IMPORTANT: This returns a Promise only if waiting is needed.
4941
+ * Callers should use: `if (!this.isAuthenticatedOrThrow()) if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();`
4942
+ * to preserve synchronous execution when already authenticated.
4943
+ */
4944
+ async waitForAuthenticated() {
4945
+ if (this._state === "authenticated") {
4946
+ return;
4947
+ }
4948
+ if (this._state === "connecting" || this._state === "connected" || this._state === "joined" || this._state === "reconnecting") {
4949
+ if (this.authenticationPromise) {
4950
+ await this.authenticationPromise;
4951
+ return;
4952
+ }
4953
+ }
4954
+ throw new LarkError("not_connected", "Not connected - call connect() first");
4955
+ }
3205
4956
  send(message) {
3206
4957
  if (!this.transport || !this.transport.connected) {
3207
4958
  throw new LarkError("not_connected", "Not connected to database");
3208
4959
  }
4960
+ if (process.env.LARK_DEBUG) {
4961
+ console.log("[LARK] >>> CLIENT:", JSON.stringify(message, null, 2));
4962
+ }
3209
4963
  this.transport.send(JSON.stringify(message));
3210
4964
  }
3211
4965
  /**
@@ -3213,19 +4967,21 @@ var LarkDatabase = class {
3213
4967
  * Note: Priority is now part of the value (as .priority), not a separate field.
3214
4968
  */
3215
4969
  async _sendSet(path, value) {
4970
+ if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
3216
4971
  const normalizedPath = normalizePath(path) || "/";
3217
- if (this.subscriptionManager.isPathInRecovery(normalizedPath)) {
3218
- throw new LarkError("view_recovering", "Cannot write: View is recovering from failed write");
3219
- }
4972
+ validateWriteData(value, normalizedPath);
3220
4973
  const requestId = this.messageQueue.nextRequestId();
3221
4974
  const pendingWriteIds = this.subscriptionManager.getPendingWriteIdsForPath(normalizedPath);
3222
4975
  this.pendingWrites.trackWrite(requestId, "set", normalizedPath, value);
3223
4976
  this.subscriptionManager.trackPendingWrite(normalizedPath, requestId);
3224
- this.subscriptionManager.applyOptimisticWrite(normalizedPath, value, requestId, "set");
4977
+ const { value: currentValue } = this.subscriptionManager.getCachedValue(normalizedPath);
4978
+ const resolvedValue = resolveServerValuesLocally(value, currentValue);
4979
+ this.subscriptionManager.applyOptimisticWrite(normalizedPath, resolvedValue, requestId, "set");
3225
4980
  const message = {
3226
4981
  o: "s",
3227
4982
  p: normalizedPath,
3228
4983
  v: value,
4984
+ // Send original value with ServerValue placeholders to server
3229
4985
  r: requestId,
3230
4986
  pw: pendingWriteIds.length > 0 ? pendingWriteIds : void 0
3231
4987
  };
@@ -3236,19 +4992,28 @@ var LarkDatabase = class {
3236
4992
  * @internal Send an update operation.
3237
4993
  */
3238
4994
  async _sendUpdate(path, values) {
4995
+ if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
3239
4996
  const normalizedPath = normalizePath(path) || "/";
3240
- if (this.subscriptionManager.isPathInRecovery(normalizedPath)) {
3241
- throw new LarkError("view_recovering", "Cannot write: View is recovering from failed write");
4997
+ for (const [key, value] of Object.entries(values)) {
4998
+ const fullPath = key.startsWith("/") ? key : `${normalizedPath}/${key}`;
4999
+ validateWriteData(value, fullPath);
3242
5000
  }
3243
5001
  const requestId = this.messageQueue.nextRequestId();
3244
5002
  const pendingWriteIds = this.subscriptionManager.getPendingWriteIdsForPath(normalizedPath);
3245
5003
  this.pendingWrites.trackWrite(requestId, "update", normalizedPath, values);
3246
5004
  this.subscriptionManager.trackPendingWrite(normalizedPath, requestId);
3247
- this.subscriptionManager.applyOptimisticWrite(normalizedPath, values, requestId, "update");
5005
+ const resolvedValues = {};
5006
+ for (const [key, value] of Object.entries(values)) {
5007
+ const fullPath = key.startsWith("/") ? key : `${normalizedPath}/${key}`;
5008
+ const { value: currentValue } = this.subscriptionManager.getCachedValue(fullPath);
5009
+ resolvedValues[key] = resolveServerValuesLocally(value, currentValue);
5010
+ }
5011
+ this.subscriptionManager.applyOptimisticWrite(normalizedPath, resolvedValues, requestId, "update");
3248
5012
  const message = {
3249
5013
  o: "u",
3250
5014
  p: normalizedPath,
3251
5015
  v: values,
5016
+ // Send original value with ServerValue placeholders to server
3252
5017
  r: requestId,
3253
5018
  pw: pendingWriteIds.length > 0 ? pendingWriteIds : void 0
3254
5019
  };
@@ -3259,10 +5024,8 @@ var LarkDatabase = class {
3259
5024
  * @internal Send a delete operation.
3260
5025
  */
3261
5026
  async _sendDelete(path) {
5027
+ if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
3262
5028
  const normalizedPath = normalizePath(path) || "/";
3263
- if (this.subscriptionManager.isPathInRecovery(normalizedPath)) {
3264
- throw new LarkError("view_recovering", "Cannot write: View is recovering from failed write");
3265
- }
3266
5029
  const requestId = this.messageQueue.nextRequestId();
3267
5030
  const pendingWriteIds = this.subscriptionManager.getPendingWriteIdsForPath(normalizedPath);
3268
5031
  this.pendingWrites.trackWrite(requestId, "delete", normalizedPath);
@@ -3297,7 +5060,7 @@ var LarkDatabase = class {
3297
5060
  _sendVolatileSet(path, value) {
3298
5061
  const normalizedPath = normalizePath(path) || "/";
3299
5062
  this.subscriptionManager.applyOptimisticWrite(normalizedPath, value, "", "set");
3300
- if (!this.transport || !this.transport.connected) {
5063
+ if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
3301
5064
  return;
3302
5065
  }
3303
5066
  const message = {
@@ -3313,7 +5076,7 @@ var LarkDatabase = class {
3313
5076
  _sendVolatileUpdate(path, values) {
3314
5077
  const normalizedPath = normalizePath(path) || "/";
3315
5078
  this.subscriptionManager.applyOptimisticWrite(normalizedPath, values, "", "update");
3316
- if (!this.transport || !this.transport.connected) {
5079
+ if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
3317
5080
  return;
3318
5081
  }
3319
5082
  const message = {
@@ -3329,7 +5092,7 @@ var LarkDatabase = class {
3329
5092
  _sendVolatileDelete(path) {
3330
5093
  const normalizedPath = normalizePath(path) || "/";
3331
5094
  this.subscriptionManager.applyOptimisticWrite(normalizedPath, null, "", "delete");
3332
- if (!this.transport || !this.transport.connected) {
5095
+ if (this._state !== "authenticated" || !this.transport || !this.transport.connected) {
3333
5096
  return;
3334
5097
  }
3335
5098
  const message = {
@@ -3358,12 +5121,17 @@ var LarkDatabase = class {
3358
5121
  */
3359
5122
  async _sendOnce(path, query) {
3360
5123
  const normalizedPath = normalizePath(path) || "/";
5124
+ if (this.isInfoPath(normalizedPath)) {
5125
+ const value2 = this.getInfoValue(normalizedPath);
5126
+ return new DataSnapshot(value2, path, this);
5127
+ }
3361
5128
  if (!query) {
3362
5129
  const cached = this.subscriptionManager.getCachedValue(normalizedPath);
3363
5130
  if (cached.found) {
3364
5131
  return new DataSnapshot(cached.value, path, this);
3365
5132
  }
3366
5133
  }
5134
+ if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
3367
5135
  const requestId = this.messageQueue.nextRequestId();
3368
5136
  const message = {
3369
5137
  o: "o",
@@ -3374,12 +5142,13 @@ var LarkDatabase = class {
3374
5142
  };
3375
5143
  this.send(message);
3376
5144
  const value = await this.messageQueue.registerRequest(requestId);
3377
- return new DataSnapshot(value, path, this);
5145
+ return new DataSnapshot(value, path, this, { queryParams: query ?? null });
3378
5146
  }
3379
5147
  /**
3380
5148
  * @internal Send an onDisconnect operation.
3381
5149
  */
3382
5150
  async _sendOnDisconnect(path, action, value) {
5151
+ if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
3383
5152
  const requestId = this.messageQueue.nextRequestId();
3384
5153
  const message = {
3385
5154
  o: "od",
@@ -3393,30 +5162,45 @@ var LarkDatabase = class {
3393
5162
  this.send(message);
3394
5163
  await this.messageQueue.registerRequest(requestId);
3395
5164
  }
5165
+ /**
5166
+ * @internal Get a cached value from the subscription manager.
5167
+ * Used for optimistic writes where we need the current value without a network fetch.
5168
+ */
5169
+ _getCachedValue(path) {
5170
+ const normalizedPath = normalizePath(path) || "/";
5171
+ return this.subscriptionManager.getCachedValue(normalizedPath);
5172
+ }
3396
5173
  /**
3397
5174
  * @internal Send a subscribe message to server.
5175
+ * Includes tag for non-default queries to enable proper event routing.
3398
5176
  */
3399
- async sendSubscribeMessage(path, eventTypes, queryParams) {
5177
+ async sendSubscribeMessage(path, eventTypes, queryParams, tag) {
5178
+ if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
3400
5179
  const requestId = this.messageQueue.nextRequestId();
3401
5180
  const message = {
3402
5181
  o: "sb",
3403
5182
  p: normalizePath(path) || "/",
3404
5183
  e: eventTypes,
3405
5184
  r: requestId,
3406
- ...queryParams
5185
+ ...queryParams,
5186
+ ...tag !== void 0 ? { tag } : {}
3407
5187
  };
3408
5188
  this.send(message);
3409
5189
  await this.messageQueue.registerRequest(requestId);
3410
5190
  }
3411
5191
  /**
3412
5192
  * @internal Send an unsubscribe message to server.
5193
+ * Includes query params and tag so server can identify which specific subscription to remove.
3413
5194
  */
3414
- async sendUnsubscribeMessage(path) {
5195
+ async sendUnsubscribeMessage(path, queryParams, tag) {
5196
+ if (!this.isAuthenticatedOrThrow()) await this.waitForAuthenticated();
3415
5197
  const requestId = this.messageQueue.nextRequestId();
3416
5198
  const message = {
3417
5199
  o: "us",
3418
5200
  p: normalizePath(path) || "/",
3419
- r: requestId
5201
+ r: requestId,
5202
+ ...queryParams,
5203
+ ...tag !== void 0 ? { tag } : {}
3420
5204
  };
3421
5205
  this.send(message);
3422
5206
  await this.messageQueue.registerRequest(requestId);
@@ -3424,10 +5208,11 @@ var LarkDatabase = class {
3424
5208
  /**
3425
5209
  * @internal Create a DataSnapshot from event data.
3426
5210
  */
3427
- createSnapshot(path, value, volatile, serverTimestamp) {
5211
+ createSnapshot(path, value, volatile, serverTimestamp, orderedKeys) {
3428
5212
  return new DataSnapshot(value, path, this, {
3429
5213
  volatile,
3430
- serverTimestamp: serverTimestamp ?? null
5214
+ serverTimestamp: serverTimestamp ?? null,
5215
+ orderedKeys: orderedKeys ?? null
3431
5216
  });
3432
5217
  }
3433
5218
  // ============================================
@@ -3436,8 +5221,11 @@ var LarkDatabase = class {
3436
5221
  /**
3437
5222
  * @internal Subscribe to events at a path.
3438
5223
  */
3439
- _subscribe(path, eventType, callback, queryParams) {
3440
- return this.subscriptionManager.subscribe(path, eventType, callback, queryParams);
5224
+ _subscribe(path, eventType, callback, queryParams, queryIdentifier) {
5225
+ if (this.isInfoPath(path)) {
5226
+ return this.subscribeToInfo(path, callback);
5227
+ }
5228
+ return this.subscriptionManager.subscribe(path, eventType, callback, queryParams, queryIdentifier);
3441
5229
  }
3442
5230
  /**
3443
5231
  * @internal Unsubscribe from a specific event type at a path.
@@ -3452,6 +5240,11 @@ var LarkDatabase = class {
3452
5240
  this.subscriptionManager.unsubscribeAll(path);
3453
5241
  }
3454
5242
  };
5243
+ /**
5244
+ * Server values that are resolved by the server when a write is committed.
5245
+ * Alias for the exported ServerValue object.
5246
+ */
5247
+ LarkDatabase.ServerValue = ServerValue;
3455
5248
  export {
3456
5249
  DataSnapshot,
3457
5250
  DatabaseReference,
@@ -3459,6 +5252,8 @@ export {
3459
5252
  LarkError,
3460
5253
  OnDisconnect,
3461
5254
  PendingWriteManager,
5255
+ ServerValue,
5256
+ ThenableReference,
3462
5257
  generatePushId,
3463
5258
  isVolatilePath
3464
5259
  };