@lark-sh/client 0.1.10 → 0.1.11

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,8 @@ 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"
16
17
  };
17
18
  var DEFAULT_COORDINATOR_URL = "https://db.lark.sh";
18
19
 
@@ -476,7 +477,8 @@ var MessageQueue = class {
476
477
  this.pending.delete(message.jc);
477
478
  const response = {
478
479
  volatilePaths: message.vp || [],
479
- connectionId: message.cid || null
480
+ connectionId: message.cid || null,
481
+ serverTime: message.st ?? null
480
482
  };
481
483
  pending.resolve(response);
482
484
  return true;
@@ -814,6 +816,13 @@ function getSortValue(value, queryParams) {
814
816
  if (queryParams.orderBy === "value") {
815
817
  return value;
816
818
  }
819
+ if (queryParams.orderBy === "key") {
820
+ return null;
821
+ }
822
+ const hasRangeFilter = queryParams.startAt !== void 0 || queryParams.startAfter !== void 0 || queryParams.endAt !== void 0 || queryParams.endBefore !== void 0 || queryParams.equalTo !== void 0;
823
+ if (hasRangeFilter) {
824
+ return getNestedValue(value, ".priority");
825
+ }
817
826
  return null;
818
827
  }
819
828
  function compareEntries(a, b, queryParams) {
@@ -836,7 +845,11 @@ function createSortEntries(data, queryParams) {
836
845
  }
837
846
  const obj = data;
838
847
  const entries = [];
839
- for (const key of Object.keys(obj)) {
848
+ const keys = Object.keys(obj);
849
+ if (keys.length === 2 && ".value" in obj && ".priority" in obj) {
850
+ return [];
851
+ }
852
+ for (const key of keys) {
840
853
  if (key === ".priority") {
841
854
  continue;
842
855
  }
@@ -859,16 +872,17 @@ var View = class {
859
872
  constructor(path, queryParams) {
860
873
  /** Event callbacks organized by event type */
861
874
  this.eventCallbacks = /* @__PURE__ */ new Map();
862
- /** Child keys in sorted order */
875
+ /** Child keys in sorted order (computed from display cache) */
863
876
  this._orderedChildren = [];
864
- /** Local cache: stores the value at the subscription path */
865
- this._cache = void 0;
877
+ /** Server cache: what the server has told us (baseline) */
878
+ this._serverCache = null;
866
879
  /** Whether we've received the initial snapshot from the server */
867
880
  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;
881
+ /** Pending write operations that haven't been ACKed yet (for local-first) */
882
+ this._pendingWriteData = [];
883
+ /** Cached display value (server + pending writes merged) - invalidated when either changes */
884
+ this._displayCacheValid = false;
885
+ this._displayCache = null;
872
886
  this.path = normalizePath(path);
873
887
  this._queryParams = queryParams ?? null;
874
888
  }
@@ -886,9 +900,8 @@ var View = class {
886
900
  return false;
887
901
  }
888
902
  this._queryParams = newParams;
889
- if (this._cache && typeof this._cache === "object" && this._cache !== null) {
890
- this._orderedChildren = getSortedKeys(this._cache, this._queryParams);
891
- }
903
+ this._displayCacheValid = false;
904
+ this.recomputeOrderedChildren();
892
905
  return true;
893
906
  }
894
907
  /**
@@ -962,138 +975,368 @@ var View = class {
962
975
  return entries !== void 0 && entries.length > 0;
963
976
  }
964
977
  // ============================================
965
- // Cache Management
978
+ // Cache Management (Dual Cache: Server + Pending Writes)
966
979
  // ============================================
967
980
  /**
968
- * Set the full cache value (used for initial snapshot).
969
- * Children are sorted using client-side sorting rules.
981
+ * Set the server cache (what the server told us).
982
+ * This is the baseline before pending writes are applied.
983
+ * Deep clones the value to ensure each View has its own copy.
970
984
  */
971
- setCache(value) {
972
- this._cache = value;
985
+ setServerCache(value) {
986
+ this._serverCache = this.deepClone(value);
973
987
  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 = [];
988
+ this.invalidateDisplayCache();
989
+ }
990
+ /**
991
+ * Get the server cache (baseline without pending writes).
992
+ */
993
+ getServerCache() {
994
+ return this._serverCache;
995
+ }
996
+ /**
997
+ * Get the display cache (server + pending writes merged).
998
+ * This is what should be shown to the user.
999
+ */
1000
+ getDisplayCache() {
1001
+ if (!this._displayCacheValid) {
1002
+ this._displayCache = this.computeMergedCache();
1003
+ this._displayCacheValid = true;
978
1004
  }
1005
+ return this._displayCache;
979
1006
  }
980
1007
  /**
981
- * Get the full cached value at the subscription path.
1008
+ * Alias for getDisplayCache() - this is what callbacks receive.
982
1009
  */
983
1010
  getCache() {
984
- return this._cache;
1011
+ return this.getDisplayCache();
1012
+ }
1013
+ /**
1014
+ * Invalidate the display cache (call when serverCache or pendingWrites change).
1015
+ */
1016
+ invalidateDisplayCache() {
1017
+ this._displayCacheValid = false;
1018
+ this.recomputeOrderedChildren();
1019
+ }
1020
+ /**
1021
+ * Compute the merged cache (serverCache + pendingWrites), with query constraints applied.
1022
+ */
1023
+ computeMergedCache() {
1024
+ let result;
1025
+ if (this._pendingWriteData.length === 0) {
1026
+ result = this._serverCache;
1027
+ } else {
1028
+ result = this.deepClone(this._serverCache);
1029
+ for (const write of this._pendingWriteData) {
1030
+ result = this.applyWrite(result, write);
1031
+ }
1032
+ }
1033
+ return this.applyQueryConstraints(result);
1034
+ }
1035
+ /**
1036
+ * Apply query constraints (range filtering and limits) to data.
1037
+ * This ensures the displayCache only contains data matching the query.
1038
+ */
1039
+ applyQueryConstraints(data) {
1040
+ if (!this._queryParams) {
1041
+ return data;
1042
+ }
1043
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
1044
+ return data;
1045
+ }
1046
+ const obj = data;
1047
+ 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;
1048
+ const hasLimit = this._queryParams.limitToFirst !== void 0 || this._queryParams.limitToLast !== void 0;
1049
+ if (!hasRangeFilter && !hasLimit) {
1050
+ return data;
1051
+ }
1052
+ let entries = createSortEntries(obj, this._queryParams);
1053
+ if (hasRangeFilter) {
1054
+ entries = this.filterByRange(entries);
1055
+ }
1056
+ if (hasLimit) {
1057
+ entries = this.applyLimits(entries);
1058
+ }
1059
+ if (entries.length === 0) {
1060
+ return null;
1061
+ }
1062
+ const result = {};
1063
+ for (const entry of entries) {
1064
+ result[entry.key] = entry.value;
1065
+ }
1066
+ return result;
1067
+ }
1068
+ /**
1069
+ * Filter entries by range constraints (startAt, endAt, startAfter, endBefore, equalTo).
1070
+ */
1071
+ filterByRange(entries) {
1072
+ if (!this._queryParams) return entries;
1073
+ const {
1074
+ orderBy,
1075
+ startAt,
1076
+ startAtKey,
1077
+ startAfter,
1078
+ startAfterKey,
1079
+ endAt,
1080
+ endAtKey,
1081
+ endBefore,
1082
+ endBeforeKey,
1083
+ equalTo,
1084
+ equalToKey
1085
+ } = this._queryParams;
1086
+ const isOrderByKey = orderBy === "key";
1087
+ return entries.filter((entry) => {
1088
+ const compareValue = isOrderByKey ? entry.key : entry.sortValue;
1089
+ const compareFn = isOrderByKey ? (a, b) => compareKeys(a, b) : compareValues;
1090
+ if (equalTo !== void 0) {
1091
+ const cmp = compareFn(compareValue, equalTo);
1092
+ if (cmp !== 0) return false;
1093
+ if (equalToKey !== void 0 && !isOrderByKey) {
1094
+ const keyCmp = compareKeys(entry.key, equalToKey);
1095
+ if (keyCmp !== 0) return false;
1096
+ }
1097
+ return true;
1098
+ }
1099
+ if (startAt !== void 0) {
1100
+ const cmp = compareFn(compareValue, startAt);
1101
+ if (cmp < 0) return false;
1102
+ if (cmp === 0 && startAtKey !== void 0 && !isOrderByKey) {
1103
+ const keyCmp = compareKeys(entry.key, startAtKey);
1104
+ if (keyCmp < 0) return false;
1105
+ }
1106
+ }
1107
+ if (startAfter !== void 0) {
1108
+ const cmp = compareFn(compareValue, startAfter);
1109
+ if (cmp < 0) return false;
1110
+ if (cmp === 0) {
1111
+ if (startAfterKey !== void 0 && !isOrderByKey) {
1112
+ const keyCmp = compareKeys(entry.key, startAfterKey);
1113
+ if (keyCmp <= 0) return false;
1114
+ } else {
1115
+ return false;
1116
+ }
1117
+ }
1118
+ }
1119
+ if (endAt !== void 0) {
1120
+ const cmp = compareFn(compareValue, endAt);
1121
+ if (cmp > 0) return false;
1122
+ if (cmp === 0 && endAtKey !== void 0 && !isOrderByKey) {
1123
+ const keyCmp = compareKeys(entry.key, endAtKey);
1124
+ if (keyCmp > 0) return false;
1125
+ }
1126
+ }
1127
+ if (endBefore !== void 0) {
1128
+ const cmp = compareFn(compareValue, endBefore);
1129
+ if (cmp > 0) return false;
1130
+ if (cmp === 0) {
1131
+ if (endBeforeKey !== void 0 && !isOrderByKey) {
1132
+ const keyCmp = compareKeys(entry.key, endBeforeKey);
1133
+ if (keyCmp >= 0) return false;
1134
+ } else {
1135
+ return false;
1136
+ }
1137
+ }
1138
+ }
1139
+ return true;
1140
+ });
1141
+ }
1142
+ /**
1143
+ * Apply limit constraints (limitToFirst, limitToLast) to entries.
1144
+ * Entries are already sorted, so we just slice.
1145
+ */
1146
+ applyLimits(entries) {
1147
+ if (!this._queryParams) return entries;
1148
+ const { limitToFirst, limitToLast } = this._queryParams;
1149
+ if (limitToFirst !== void 0) {
1150
+ return entries.slice(0, limitToFirst);
1151
+ }
1152
+ if (limitToLast !== void 0) {
1153
+ return entries.slice(-limitToLast);
1154
+ }
1155
+ return entries;
1156
+ }
1157
+ /**
1158
+ * Apply a single write operation to a value.
1159
+ */
1160
+ applyWrite(base, write) {
1161
+ const { relativePath, value, operation } = write;
1162
+ if (operation === "delete") {
1163
+ if (relativePath === "/") {
1164
+ return null;
1165
+ }
1166
+ return this.deleteAtPath(base, relativePath);
1167
+ }
1168
+ if (operation === "set") {
1169
+ if (relativePath === "/") {
1170
+ return value;
1171
+ }
1172
+ return this.setAtPath(base, relativePath, value);
1173
+ }
1174
+ if (operation === "update") {
1175
+ if (relativePath === "/") {
1176
+ if (base === null || base === void 0 || typeof base !== "object") {
1177
+ base = {};
1178
+ }
1179
+ const merged2 = { ...base, ...value };
1180
+ for (const key of Object.keys(merged2)) {
1181
+ if (merged2[key] === null) {
1182
+ delete merged2[key];
1183
+ }
1184
+ }
1185
+ return merged2;
1186
+ }
1187
+ const current = getValueAtPath(base, relativePath);
1188
+ let merged;
1189
+ if (current && typeof current === "object") {
1190
+ merged = { ...current, ...value };
1191
+ for (const key of Object.keys(merged)) {
1192
+ if (merged[key] === null) {
1193
+ delete merged[key];
1194
+ }
1195
+ }
1196
+ } else {
1197
+ merged = value;
1198
+ }
1199
+ return this.setAtPath(base, relativePath, merged);
1200
+ }
1201
+ return base;
1202
+ }
1203
+ /**
1204
+ * Set a value at a path in an object, creating intermediate objects as needed.
1205
+ */
1206
+ setAtPath(obj, path, value) {
1207
+ if (path === "/") return value;
1208
+ const segments = path.split("/").filter((s) => s.length > 0);
1209
+ if (segments.length === 0) return value;
1210
+ const result = obj === null || obj === void 0 || typeof obj !== "object" ? {} : { ...obj };
1211
+ let current = result;
1212
+ for (let i = 0; i < segments.length - 1; i++) {
1213
+ const key = segments[i];
1214
+ if (current[key] === null || current[key] === void 0 || typeof current[key] !== "object") {
1215
+ current[key] = {};
1216
+ } else {
1217
+ current[key] = { ...current[key] };
1218
+ }
1219
+ current = current[key];
1220
+ }
1221
+ const lastKey = segments[segments.length - 1];
1222
+ current[lastKey] = value;
1223
+ return result;
1224
+ }
1225
+ /**
1226
+ * Delete a value at a path in an object.
1227
+ */
1228
+ deleteAtPath(obj, path) {
1229
+ if (path === "/" || obj === null || obj === void 0 || typeof obj !== "object") {
1230
+ return null;
1231
+ }
1232
+ const segments = path.split("/").filter((s) => s.length > 0);
1233
+ if (segments.length === 0) return null;
1234
+ const result = { ...obj };
1235
+ if (segments.length === 1) {
1236
+ delete result[segments[0]];
1237
+ return Object.keys(result).length > 0 ? result : null;
1238
+ }
1239
+ let current = result;
1240
+ for (let i = 0; i < segments.length - 1; i++) {
1241
+ const key = segments[i];
1242
+ if (current[key] === null || current[key] === void 0 || typeof current[key] !== "object") {
1243
+ return result;
1244
+ }
1245
+ current[key] = { ...current[key] };
1246
+ current = current[key];
1247
+ }
1248
+ delete current[segments[segments.length - 1]];
1249
+ return result;
1250
+ }
1251
+ /**
1252
+ * Deep clone a value.
1253
+ */
1254
+ deepClone(value) {
1255
+ if (value === null || value === void 0) return value;
1256
+ if (typeof value !== "object") return value;
1257
+ if (Array.isArray(value)) return value.map((v) => this.deepClone(v));
1258
+ const result = {};
1259
+ for (const [k, v] of Object.entries(value)) {
1260
+ result[k] = this.deepClone(v);
1261
+ }
1262
+ return result;
1263
+ }
1264
+ /**
1265
+ * Recompute ordered children from display cache.
1266
+ */
1267
+ recomputeOrderedChildren() {
1268
+ const displayCache = this.getDisplayCache();
1269
+ if (displayCache && typeof displayCache === "object" && !Array.isArray(displayCache)) {
1270
+ this._orderedChildren = getSortedKeys(displayCache, this.queryParams);
1271
+ } else {
1272
+ this._orderedChildren = [];
1273
+ }
985
1274
  }
986
1275
  /**
987
- * Get a value from the cache at a relative or absolute path.
1276
+ * Get a value from the display cache at a relative or absolute path.
988
1277
  * If the path is outside this View's scope, returns undefined.
989
1278
  */
990
1279
  getCacheValue(path) {
991
1280
  const normalized = normalizePath(path);
1281
+ const displayCache = this.getDisplayCache();
992
1282
  if (normalized === this.path) {
993
- if (this._cache !== void 0) {
994
- return { value: this._cache, found: true };
1283
+ if (displayCache !== void 0 && displayCache !== null) {
1284
+ return { value: displayCache, found: true };
1285
+ }
1286
+ if (this._hasReceivedInitialSnapshot || this._pendingWriteData.length > 0) {
1287
+ return { value: displayCache, found: true };
995
1288
  }
996
1289
  return { value: void 0, found: false };
997
1290
  }
998
1291
  if (normalized.startsWith(this.path + "/") || this.path === "/") {
999
1292
  const relativePath = this.path === "/" ? normalized : normalized.slice(this.path.length);
1000
- if (this._cache !== void 0) {
1001
- const extractedValue = getValueAtPath(this._cache, relativePath);
1293
+ if (this._hasReceivedInitialSnapshot || this._pendingWriteData.length > 0) {
1294
+ const extractedValue = getValueAtPath(displayCache, relativePath);
1002
1295
  return { value: extractedValue, found: true };
1003
1296
  }
1004
1297
  }
1005
1298
  return { value: void 0, found: false };
1006
1299
  }
1007
1300
  /**
1008
- * Update a child value in the cache.
1301
+ * Update a child value in the SERVER cache.
1009
1302
  * relativePath should be relative to this View's path.
1010
- * Maintains sorted order of children using client-side sorting.
1303
+ * Called when server events (put/patch) arrive.
1011
1304
  */
1012
- updateCacheChild(relativePath, value) {
1305
+ updateServerCacheChild(relativePath, value) {
1013
1306
  if (relativePath === "/") {
1014
- this.setCache(value);
1307
+ this.setServerCache(value);
1015
1308
  return;
1016
1309
  }
1017
1310
  const segments = relativePath.split("/").filter((s) => s.length > 0);
1018
1311
  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 = {};
1312
+ if (this._serverCache === null || this._serverCache === void 0 || typeof this._serverCache !== "object") {
1313
+ this._serverCache = {};
1022
1314
  }
1023
- const cache = this._cache;
1315
+ const cache = this._serverCache;
1024
1316
  if (segments.length === 1) {
1025
1317
  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
- }
1318
+ delete cache[segments[0]];
1031
1319
  } 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
- }
1320
+ cache[segments[0]] = value;
1039
1321
  }
1040
1322
  } else {
1041
1323
  if (value === null) {
1042
1324
  setValueAtPath(cache, relativePath, void 0);
1043
1325
  } else {
1044
- const wasPresent = childKey in cache;
1045
- if (!wasPresent) {
1326
+ const childKey = segments[0];
1327
+ if (!(childKey in cache)) {
1046
1328
  cache[childKey] = {};
1047
1329
  }
1048
1330
  setValueAtPath(cache, relativePath, value);
1049
- if (!wasPresent) {
1050
- this.insertChildSorted(childKey, cache[childKey]);
1051
- } else {
1052
- this.resortChild(childKey);
1053
- }
1054
1331
  }
1055
1332
  }
1333
+ this.invalidateDisplayCache();
1056
1334
  }
1057
1335
  /**
1058
- * Insert a child key at the correct sorted position.
1336
+ * Remove a child from the SERVER cache.
1059
1337
  */
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
- }
1078
- }
1079
- this._orderedChildren.splice(insertIdx, 0, key);
1080
- }
1081
- /**
1082
- * Re-sort a child that already exists (its sort value may have changed).
1083
- */
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);
1338
+ removeServerCacheChild(relativePath) {
1339
+ this.updateServerCacheChild(relativePath, null);
1097
1340
  }
1098
1341
  /**
1099
1342
  * Check if we've received the initial snapshot.
@@ -1147,96 +1390,101 @@ var View = class {
1147
1390
  // ============================================
1148
1391
  /**
1149
1392
  * Get current pending write IDs (to include in pw field).
1150
- * Returns a copy of the set as an array.
1151
1393
  */
1152
1394
  getPendingWriteIds() {
1153
- return Array.from(this._pendingWrites);
1395
+ return this._pendingWriteData.filter((w) => w.requestId !== "").map((w) => w.requestId);
1154
1396
  }
1155
1397
  /**
1156
- * Add a pending write request ID.
1398
+ * Add a pending write with its data.
1399
+ * This is used for local-first writes - the write is applied to displayCache immediately.
1157
1400
  */
1158
- addPendingWrite(requestId) {
1159
- this._pendingWrites.add(requestId);
1401
+ addPendingWriteData(requestId, relativePath, value, operation) {
1402
+ this._pendingWriteData.push({ requestId, relativePath, value, operation });
1403
+ this.invalidateDisplayCache();
1160
1404
  }
1161
1405
  /**
1162
- * Remove a pending write (on ack or nack).
1406
+ * Remove a pending write by request ID (on ack or nack).
1407
+ * Returns true if the write was found and removed.
1163
1408
  */
1164
1409
  removePendingWrite(requestId) {
1165
- return this._pendingWrites.delete(requestId);
1410
+ const idx = this._pendingWriteData.findIndex((w) => w.requestId === requestId);
1411
+ if (idx === -1) return false;
1412
+ this._pendingWriteData.splice(idx, 1);
1413
+ this.invalidateDisplayCache();
1414
+ return true;
1166
1415
  }
1167
1416
  /**
1168
- * Clear all pending writes (on nack recovery).
1417
+ * Clear all pending writes.
1169
1418
  */
1170
1419
  clearPendingWrites() {
1171
- this._pendingWrites.clear();
1420
+ if (this._pendingWriteData.length > 0) {
1421
+ this._pendingWriteData = [];
1422
+ this.invalidateDisplayCache();
1423
+ }
1172
1424
  }
1173
1425
  /**
1174
1426
  * Check if this View has pending writes.
1175
1427
  */
1176
1428
  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;
1429
+ return this._pendingWriteData.some((w) => w.requestId !== "");
1193
1430
  }
1194
1431
  /**
1195
- * Enter recovery mode (after a nack).
1432
+ * Get the pending write data (for debugging/testing).
1196
1433
  */
1197
- enterRecovery() {
1198
- this._recovering = true;
1199
- this.clearPendingWrites();
1434
+ getPendingWriteData() {
1435
+ return [...this._pendingWriteData];
1200
1436
  }
1201
1437
  /**
1202
- * Exit recovery mode (after fresh snapshot received).
1438
+ * Check if a specific write is pending.
1203
1439
  */
1204
- exitRecovery() {
1205
- this._recovering = false;
1440
+ isWritePending(requestId) {
1441
+ return this._pendingWriteData.some((w) => w.requestId === requestId);
1206
1442
  }
1207
1443
  // ============================================
1208
1444
  // Cleanup
1209
1445
  // ============================================
1210
1446
  /**
1211
- * Clear the cache but preserve callbacks and pending writes.
1447
+ * Clear the server cache but preserve callbacks and pending writes.
1212
1448
  * Used during reconnection.
1213
1449
  */
1214
- clearCache() {
1215
- this._cache = void 0;
1216
- this._orderedChildren = [];
1450
+ clearServerCache() {
1451
+ this._serverCache = null;
1217
1452
  this._hasReceivedInitialSnapshot = false;
1453
+ this.invalidateDisplayCache();
1218
1454
  }
1219
1455
  /**
1220
1456
  * Clear everything - used when unsubscribing completely.
1221
1457
  */
1222
1458
  clear() {
1223
1459
  this.eventCallbacks.clear();
1224
- this._cache = void 0;
1460
+ this._serverCache = null;
1225
1461
  this._orderedChildren = [];
1226
1462
  this._hasReceivedInitialSnapshot = false;
1227
- this._pendingWrites.clear();
1228
- this._recovering = false;
1463
+ this._pendingWriteData = [];
1464
+ this._displayCacheValid = false;
1465
+ this._displayCache = null;
1229
1466
  }
1230
1467
  };
1231
1468
 
1232
1469
  // src/connection/SubscriptionManager.ts
1233
1470
  var SubscriptionManager = class {
1234
1471
  constructor() {
1235
- // path -> View (one View per subscribed path)
1472
+ // viewKey (path:queryIdentifier) -> View
1473
+ // Each unique path+query combination gets its own View
1236
1474
  this.views = /* @__PURE__ */ new Map();
1475
+ // path -> Set of queryIdentifiers for that path
1476
+ // Used to find all Views at a path when events arrive
1477
+ this.pathToQueryIds = /* @__PURE__ */ new Map();
1478
+ // Tag counter for generating unique tags per non-default query
1479
+ // Tags are client-local and used to route server events to correct View
1480
+ this.nextTag = 1;
1481
+ // tag -> viewKey mapping for routing tagged events
1482
+ this.tagToViewKey = /* @__PURE__ */ new Map();
1483
+ // viewKey -> tag mapping for unsubscribe
1484
+ this.viewKeyToTag = /* @__PURE__ */ new Map();
1237
1485
  // Callback to send subscribe message to server
1238
1486
  this.sendSubscribe = null;
1239
- // Callback to send unsubscribe message to server
1487
+ // Callback to send unsubscribe message to server (includes queryParams and tag for server to identify subscription)
1240
1488
  this.sendUnsubscribe = null;
1241
1489
  // Callback to create DataSnapshot from event data
1242
1490
  this.createSnapshot = null;
@@ -1249,30 +1497,68 @@ var SubscriptionManager = class {
1249
1497
  this.sendUnsubscribe = options.sendUnsubscribe;
1250
1498
  this.createSnapshot = options.createSnapshot;
1251
1499
  }
1500
+ /**
1501
+ * Create a view key from path and query identifier.
1502
+ */
1503
+ makeViewKey(path, queryIdentifier) {
1504
+ return `${path}:${queryIdentifier}`;
1505
+ }
1506
+ /**
1507
+ * Get all Views at a given path (across all query identifiers).
1508
+ */
1509
+ getViewsAtPath(path) {
1510
+ const queryIds = this.pathToQueryIds.get(path);
1511
+ if (!queryIds || queryIds.size === 0) {
1512
+ return [];
1513
+ }
1514
+ const views = [];
1515
+ for (const queryId of queryIds) {
1516
+ const viewKey = this.makeViewKey(path, queryId);
1517
+ const view = this.views.get(viewKey);
1518
+ if (view) {
1519
+ views.push(view);
1520
+ }
1521
+ }
1522
+ return views;
1523
+ }
1252
1524
  /**
1253
1525
  * Subscribe to events at a path.
1254
1526
  * Returns an unsubscribe function.
1255
1527
  */
1256
- subscribe(path, eventType, callback, queryParams) {
1528
+ subscribe(path, eventType, callback, queryParams, queryIdentifier) {
1257
1529
  const normalizedPath = path;
1258
- let view = this.views.get(normalizedPath);
1530
+ const queryId = queryIdentifier ?? "default";
1531
+ const viewKey = this.makeViewKey(normalizedPath, queryId);
1532
+ const isNonDefaultQuery = queryId !== "default";
1533
+ let view = this.views.get(viewKey);
1259
1534
  const isNewView = !view;
1260
1535
  let queryParamsChanged = false;
1536
+ let tag;
1261
1537
  if (!view) {
1262
1538
  view = new View(normalizedPath, queryParams);
1263
- this.views.set(normalizedPath, view);
1539
+ this.views.set(viewKey, view);
1540
+ if (!this.pathToQueryIds.has(normalizedPath)) {
1541
+ this.pathToQueryIds.set(normalizedPath, /* @__PURE__ */ new Set());
1542
+ }
1543
+ this.pathToQueryIds.get(normalizedPath).add(queryId);
1544
+ if (isNonDefaultQuery) {
1545
+ tag = this.nextTag++;
1546
+ this.tagToViewKey.set(tag, viewKey);
1547
+ this.viewKeyToTag.set(viewKey, tag);
1548
+ }
1264
1549
  } else {
1265
1550
  queryParamsChanged = view.updateQueryParams(queryParams);
1551
+ tag = this.viewKeyToTag.get(viewKey);
1266
1552
  }
1267
1553
  const existingEventTypes = view.getEventTypes();
1268
1554
  const isNewEventType = !existingEventTypes.includes(eventType);
1269
1555
  const unsubscribe = view.addCallback(eventType, callback);
1270
1556
  const wrappedUnsubscribe = () => {
1271
- this.unsubscribeCallback(normalizedPath, eventType, callback);
1557
+ this.unsubscribeCallback(normalizedPath, eventType, callback, queryId);
1272
1558
  };
1273
1559
  if (isNewView || isNewEventType || queryParamsChanged) {
1274
1560
  const allEventTypes = view.getEventTypes();
1275
- this.sendSubscribe?.(normalizedPath, allEventTypes, view.queryParams ?? void 0).catch((err) => {
1561
+ this.sendSubscribe?.(normalizedPath, allEventTypes, view.queryParams ?? void 0, tag).catch((err) => {
1276
1562
  console.error("Failed to subscribe:", err);
1277
1563
  });
1278
1564
  }
@@ -1288,7 +1574,7 @@ var SubscriptionManager = class {
1288
1574
  fireInitialEventsToCallback(view, eventType, callback) {
1289
1575
  const cache = view.getCache();
1290
1576
  if (eventType === "value") {
1291
- const snapshot = this.createSnapshot?.(view.path, cache, false);
1577
+ const snapshot = this.createSnapshot?.(view.path, cache, false, void 0, view.orderedChildren);
1292
1578
  if (snapshot) {
1293
1579
  try {
1294
1580
  callback(snapshot, void 0);
@@ -1317,49 +1603,84 @@ var SubscriptionManager = class {
1317
1603
  /**
1318
1604
  * Remove a specific callback from a subscription.
1319
1605
  */
1320
- unsubscribeCallback(path, eventType, callback) {
1321
- const view = this.views.get(path);
1606
+ unsubscribeCallback(path, eventType, callback, queryIdentifier) {
1607
+ const queryId = queryIdentifier ?? "default";
1608
+ const viewKey = this.makeViewKey(path, queryId);
1609
+ const view = this.views.get(viewKey);
1322
1610
  if (!view) return;
1611
+ const queryParams = view.queryParams ?? void 0;
1612
+ const tag = this.viewKeyToTag.get(viewKey);
1323
1613
  view.removeCallback(eventType, callback);
1324
1614
  if (!view.hasCallbacks()) {
1325
- this.views.delete(path);
1326
- this.sendUnsubscribe?.(path).catch((err) => {
1615
+ this.views.delete(viewKey);
1616
+ const queryIds = this.pathToQueryIds.get(path);
1617
+ if (queryIds) {
1618
+ queryIds.delete(queryId);
1619
+ if (queryIds.size === 0) {
1620
+ this.pathToQueryIds.delete(path);
1621
+ }
1622
+ }
1623
+ if (tag !== void 0) {
1624
+ this.tagToViewKey.delete(tag);
1625
+ this.viewKeyToTag.delete(viewKey);
1626
+ }
1627
+ this.sendUnsubscribe?.(path, queryParams, tag).catch((err) => {
1327
1628
  console.error("Failed to unsubscribe:", err);
1328
1629
  });
1329
1630
  }
1330
1631
  }
1331
1632
  /**
1332
1633
  * Remove all subscriptions of a specific event type at a path.
1634
+ * This affects ALL Views at the path (across all query identifiers).
1333
1635
  */
1334
1636
  unsubscribeEventType(path, eventType) {
1335
1637
  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
- });
1638
+ const queryIds = this.pathToQueryIds.get(normalizedPath);
1639
+ if (!queryIds) return;
1640
+ for (const queryId of Array.from(queryIds)) {
1641
+ const viewKey = this.makeViewKey(normalizedPath, queryId);
1642
+ const view = this.views.get(viewKey);
1643
+ if (!view) continue;
1644
+ const queryParams = view.queryParams ?? void 0;
1645
+ view.removeAllCallbacks(eventType);
1646
+ if (!view.hasCallbacks()) {
1647
+ this.views.delete(viewKey);
1648
+ queryIds.delete(queryId);
1649
+ this.sendUnsubscribe?.(normalizedPath, queryParams).catch((err) => {
1650
+ console.error("Failed to unsubscribe:", err);
1651
+ });
1652
+ } else {
1653
+ const remainingEventTypes = view.getEventTypes();
1654
+ this.sendSubscribe?.(normalizedPath, remainingEventTypes, queryParams).catch((err) => {
1655
+ console.error("Failed to update subscription:", err);
1656
+ });
1657
+ }
1658
+ }
1659
+ if (queryIds.size === 0) {
1660
+ this.pathToQueryIds.delete(normalizedPath);
1349
1661
  }
1350
1662
  }
1351
1663
  /**
1352
1664
  * Remove ALL subscriptions at a path.
1665
+ * This affects ALL Views at the path (across all query identifiers).
1353
1666
  */
1354
1667
  unsubscribeAll(path) {
1355
1668
  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
- });
1669
+ const queryIds = this.pathToQueryIds.get(normalizedPath);
1670
+ if (!queryIds || queryIds.size === 0) return;
1671
+ for (const queryId of queryIds) {
1672
+ const viewKey = this.makeViewKey(normalizedPath, queryId);
1673
+ const view = this.views.get(viewKey);
1674
+ if (view) {
1675
+ const queryParams = view.queryParams ?? void 0;
1676
+ view.clear();
1677
+ this.views.delete(viewKey);
1678
+ this.sendUnsubscribe?.(normalizedPath, queryParams).catch((err) => {
1679
+ console.error("Failed to unsubscribe:", err);
1680
+ });
1681
+ }
1682
+ }
1683
+ this.pathToQueryIds.delete(normalizedPath);
1363
1684
  }
1364
1685
  /**
1365
1686
  * Handle an incoming event message from the server.
@@ -1376,8 +1697,17 @@ var SubscriptionManager = class {
1376
1697
  console.warn("Unknown event type:", message.ev);
1377
1698
  }
1378
1699
  }
1700
+ /**
1701
+ * Get View by tag. Returns undefined if tag not found.
1702
+ */
1703
+ getViewByTag(tag) {
1704
+ const viewKey = this.tagToViewKey.get(tag);
1705
+ if (!viewKey) return void 0;
1706
+ return this.views.get(viewKey);
1707
+ }
1379
1708
  /**
1380
1709
  * Handle a 'put' event - single path change.
1710
+ * Routes to specific View by tag if present, otherwise to default View at path.
1381
1711
  */
1382
1712
  handlePutEvent(message) {
1383
1713
  const subscriptionPath = message.sp;
@@ -1385,21 +1715,24 @@ var SubscriptionManager = class {
1385
1715
  const value = message.v;
1386
1716
  const isVolatile = message.x ?? false;
1387
1717
  const serverTimestamp = message.ts;
1388
- const view = this.views.get(subscriptionPath);
1389
- if (!view) return;
1718
+ const tag = message.tag;
1390
1719
  if (value === void 0) return;
1391
- if (view.recovering) {
1392
- if (relativePath !== "/") {
1393
- return;
1720
+ if (tag !== void 0) {
1721
+ const view2 = this.getViewByTag(tag);
1722
+ if (view2) {
1723
+ this.applyServerUpdateToView(view2, [{ relativePath, value }], isVolatile, serverTimestamp);
1394
1724
  }
1395
- this.applyWriteToView(view, [{ relativePath, value }], isVolatile, serverTimestamp);
1396
- view.exitRecovery();
1397
1725
  return;
1398
1726
  }
1399
- this.applyWriteToView(view, [{ relativePath, value }], isVolatile, serverTimestamp);
1727
+ const defaultViewKey = this.makeViewKey(subscriptionPath, "default");
1728
+ const view = this.views.get(defaultViewKey);
1729
+ if (view) {
1730
+ this.applyServerUpdateToView(view, [{ relativePath, value }], isVolatile, serverTimestamp);
1731
+ }
1400
1732
  }
1401
1733
  /**
1402
1734
  * Handle a 'patch' event - multi-path change.
1735
+ * Routes to specific View by tag if present, otherwise to default View at path.
1403
1736
  */
1404
1737
  handlePatchEvent(message) {
1405
1738
  const subscriptionPath = message.sp;
@@ -1407,11 +1740,7 @@ var SubscriptionManager = class {
1407
1740
  const patches = message.v;
1408
1741
  const isVolatile = message.x ?? false;
1409
1742
  const serverTimestamp = message.ts;
1410
- const view = this.views.get(subscriptionPath);
1411
- if (!view) return;
1412
- if (view.recovering) {
1413
- return;
1414
- }
1743
+ const tag = message.tag;
1415
1744
  if (!patches) return;
1416
1745
  const updates = [];
1417
1746
  for (const [relativePath, patchValue] of Object.entries(patches)) {
@@ -1423,39 +1752,63 @@ var SubscriptionManager = class {
1423
1752
  }
1424
1753
  updates.push({ relativePath: fullRelativePath, value: patchValue });
1425
1754
  }
1426
- this.applyWriteToView(view, updates, isVolatile, serverTimestamp);
1755
+ if (tag !== void 0) {
1756
+ const view2 = this.getViewByTag(tag);
1757
+ if (view2) {
1758
+ this.applyServerUpdateToView(view2, updates, isVolatile, serverTimestamp);
1759
+ }
1760
+ return;
1761
+ }
1762
+ const defaultViewKey = this.makeViewKey(subscriptionPath, "default");
1763
+ const view = this.views.get(defaultViewKey);
1764
+ if (view) {
1765
+ this.applyServerUpdateToView(view, updates, isVolatile, serverTimestamp);
1766
+ }
1427
1767
  }
1428
1768
  /**
1429
1769
  * Handle a 'vb' (volatile batch) event - batched volatile updates across subscriptions.
1430
1770
  * Server batches volatile events in 50ms intervals to reduce message overhead.
1431
1771
  * Format: { ev: 'vb', b: { subscriptionPath: { relativePath: value } }, ts: timestamp }
1772
+ * Dispatches to ALL Views at each subscription path.
1432
1773
  */
1433
1774
  handleVolatileBatchEvent(message) {
1434
1775
  const batch = message.b;
1435
1776
  const serverTimestamp = message.ts;
1436
1777
  if (!batch) return;
1437
1778
  for (const [subscriptionPath, updates] of Object.entries(batch)) {
1438
- const view = this.views.get(subscriptionPath);
1439
- if (!view) continue;
1440
- if (view.recovering) continue;
1779
+ const views = this.getViewsAtPath(subscriptionPath);
1780
+ if (views.length === 0) continue;
1441
1781
  const updatesList = [];
1442
1782
  for (const [relativePath, value] of Object.entries(updates)) {
1443
1783
  updatesList.push({ relativePath, value });
1444
1784
  }
1445
- this.applyWriteToView(view, updatesList, true, serverTimestamp);
1785
+ for (const view of views) {
1786
+ this.applyServerUpdateToView(view, updatesList, true, serverTimestamp);
1787
+ }
1446
1788
  }
1447
1789
  }
1448
1790
  /**
1449
1791
  * Detect and fire child_moved events for children that changed position.
1792
+ *
1793
+ * IMPORTANT: child_moved should only fire for children whose VALUE changed
1794
+ * and caused a position change. Children that are merely "displaced" by
1795
+ * another child moving should NOT fire child_moved.
1796
+ *
1797
+ * @param affectedChildren - Only check these children for moves. If not provided,
1798
+ * checks all children (for full snapshots where we compare values).
1450
1799
  */
1451
- detectAndFireMoves(view, previousOrder, currentOrder, previousPositions, currentPositions, previousChildSet, currentChildSet, childMovedSubs, isVolatile, serverTimestamp) {
1800
+ detectAndFireMoves(view, previousOrder, currentOrder, previousPositions, currentPositions, previousChildSet, currentChildSet, childMovedSubs, isVolatile, serverTimestamp, affectedChildren) {
1452
1801
  if (childMovedSubs.length === 0) return;
1453
- for (const key of currentOrder) {
1802
+ const childrenToCheck = affectedChildren ?? new Set(currentOrder);
1803
+ for (const key of childrenToCheck) {
1454
1804
  if (!previousChildSet.has(key) || !currentChildSet.has(key)) {
1455
1805
  continue;
1456
1806
  }
1457
1807
  const oldPos = previousPositions.get(key);
1458
1808
  const newPos = currentPositions.get(key);
1809
+ if (oldPos === void 0 || newPos === void 0) {
1810
+ continue;
1811
+ }
1459
1812
  const oldPrevKey = oldPos > 0 ? previousOrder[oldPos - 1] : null;
1460
1813
  const newPrevKey = newPos > 0 ? currentOrder[newPos - 1] : null;
1461
1814
  if (oldPrevKey !== newPrevKey) {
@@ -1542,12 +1895,17 @@ var SubscriptionManager = class {
1542
1895
  view.clear();
1543
1896
  }
1544
1897
  this.views.clear();
1898
+ this.pathToQueryIds.clear();
1899
+ this.tagToViewKey.clear();
1900
+ this.viewKeyToTag.clear();
1901
+ this.nextTag = 1;
1545
1902
  }
1546
1903
  /**
1547
- * Check if there are any subscriptions at a path.
1904
+ * Check if there are any subscriptions at a path (across all query identifiers).
1548
1905
  */
1549
1906
  hasSubscriptions(path) {
1550
- return this.views.has(path);
1907
+ const queryIds = this.pathToQueryIds.get(path);
1908
+ return queryIds !== void 0 && queryIds.size > 0;
1551
1909
  }
1552
1910
  /**
1553
1911
  * Check if a path is "covered" by an active subscription.
@@ -1574,36 +1932,50 @@ var SubscriptionManager = class {
1574
1932
  return false;
1575
1933
  }
1576
1934
  /**
1577
- * Check if there's a 'value' subscription at a path.
1935
+ * Check if there's a 'value' subscription at a path (any query identifier).
1578
1936
  */
1579
1937
  hasValueSubscription(path) {
1580
- const view = this.views.get(path);
1581
- return view !== void 0 && view.hasCallbacksForType("value");
1938
+ const views = this.getViewsAtPath(path);
1939
+ return views.some((view) => view.hasCallbacksForType("value"));
1582
1940
  }
1583
1941
  /**
1584
1942
  * Get a cached value if the path is covered by an active subscription.
1943
+ * Returns the value from the first View that has it (typically the default/unfiltered one).
1585
1944
  */
1586
1945
  getCachedValue(path) {
1587
1946
  const normalized = normalizePath(path);
1588
1947
  if (!this.isPathCovered(normalized)) {
1589
1948
  return { value: void 0, found: false };
1590
1949
  }
1591
- const exactView = this.views.get(normalized);
1592
- if (exactView) {
1593
- return exactView.getCacheValue(normalized);
1950
+ const exactViews = this.getViewsAtPath(normalized);
1951
+ for (const view of exactViews) {
1952
+ const result = view.getCacheValue(normalized);
1953
+ if (result.found) {
1954
+ return result;
1955
+ }
1594
1956
  }
1595
1957
  const segments = normalized.split("/").filter((s) => s.length > 0);
1596
1958
  for (let i = segments.length - 1; i >= 0; i--) {
1597
1959
  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);
1960
+ const ancestorViews = this.getViewsAtPath(ancestorPath);
1961
+ for (const view of ancestorViews) {
1962
+ if (view.hasCallbacksForType("value")) {
1963
+ const result = view.getCacheValue(normalized);
1964
+ if (result.found) {
1965
+ return result;
1966
+ }
1967
+ }
1601
1968
  }
1602
1969
  }
1603
1970
  if (normalized !== "/") {
1604
- const rootView = this.views.get("/");
1605
- if (rootView && rootView.hasCallbacksForType("value")) {
1606
- return rootView.getCacheValue(normalized);
1971
+ const rootViews = this.getViewsAtPath("/");
1972
+ for (const view of rootViews) {
1973
+ if (view.hasCallbacksForType("value")) {
1974
+ const result = view.getCacheValue(normalized);
1975
+ if (result.found) {
1976
+ return result;
1977
+ }
1978
+ }
1607
1979
  }
1608
1980
  }
1609
1981
  return { value: void 0, found: false };
@@ -1621,7 +1993,7 @@ var SubscriptionManager = class {
1621
1993
  */
1622
1994
  clearCacheOnly() {
1623
1995
  for (const view of this.views.values()) {
1624
- view.clearCache();
1996
+ view.clearServerCache();
1625
1997
  }
1626
1998
  }
1627
1999
  /**
@@ -1629,13 +2001,13 @@ var SubscriptionManager = class {
1629
2001
  * Used after reconnecting to restore subscriptions on the server.
1630
2002
  */
1631
2003
  async resubscribeAll() {
1632
- for (const [path, view] of this.views) {
2004
+ for (const view of this.views.values()) {
1633
2005
  const eventTypes = view.getEventTypes();
1634
2006
  if (eventTypes.length > 0) {
1635
2007
  try {
1636
- await this.sendSubscribe?.(path, eventTypes, view.queryParams ?? void 0);
2008
+ await this.sendSubscribe?.(view.path, eventTypes, view.queryParams ?? void 0);
1637
2009
  } catch (err) {
1638
- console.error(`Failed to resubscribe to ${path}:`, err);
2010
+ console.error(`Failed to resubscribe to ${view.path}:`, err);
1639
2011
  }
1640
2012
  }
1641
2013
  }
@@ -1645,18 +2017,22 @@ var SubscriptionManager = class {
1645
2017
  */
1646
2018
  getActiveSubscriptions() {
1647
2019
  const result = [];
1648
- for (const [path, view] of this.views) {
2020
+ for (const view of this.views.values()) {
1649
2021
  const eventTypes = view.getEventTypes();
1650
2022
  const queryParams = view.queryParams ?? void 0;
1651
- result.push({ path, eventTypes, queryParams });
2023
+ result.push({ path: view.path, eventTypes, queryParams });
1652
2024
  }
1653
2025
  return result;
1654
2026
  }
1655
2027
  /**
1656
2028
  * Get a View by path (for testing/debugging).
2029
+ * Returns the first View at this path (default query identifier).
1657
2030
  */
1658
2031
  getView(path) {
1659
- return this.views.get(path);
2032
+ const defaultView = this.views.get(this.makeViewKey(path, "default"));
2033
+ if (defaultView) return defaultView;
2034
+ const views = this.getViewsAtPath(path);
2035
+ return views.length > 0 ? views[0] : void 0;
1660
2036
  }
1661
2037
  // ============================================
1662
2038
  // Shared Write Application (used by server events and optimistic writes)
@@ -1698,19 +2074,19 @@ var SubscriptionManager = class {
1698
2074
  * @param isVolatile - Whether this is a volatile update
1699
2075
  * @param serverTimestamp - Optional server timestamp
1700
2076
  */
1701
- applyWriteToView(view, updates, isVolatile, serverTimestamp) {
2077
+ applyServerUpdateToView(view, updates, isVolatile, serverTimestamp) {
1702
2078
  const previousOrder = view.orderedChildren;
1703
2079
  const previousChildSet = new Set(previousOrder);
1704
- const isFirstSnapshot = !view.hasReceivedInitialSnapshot;
2080
+ const isFirstSnapshot = !view.hasReceivedInitialSnapshot && !view.hasPendingWrites();
1705
2081
  let previousCacheJson = null;
1706
2082
  if (!isVolatile) {
1707
- previousCacheJson = this.serializeCacheForComparison(view.getCache());
2083
+ previousCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
1708
2084
  }
1709
2085
  const affectedChildren = /* @__PURE__ */ new Set();
1710
2086
  let isFullSnapshot = false;
1711
2087
  for (const { relativePath, value } of updates) {
1712
2088
  if (relativePath === "/") {
1713
- view.setCache(value);
2089
+ view.setServerCache(value);
1714
2090
  isFullSnapshot = true;
1715
2091
  } else {
1716
2092
  const segments = relativePath.split("/").filter((s) => s.length > 0);
@@ -1718,24 +2094,24 @@ var SubscriptionManager = class {
1718
2094
  affectedChildren.add(segments[0]);
1719
2095
  }
1720
2096
  if (value === null) {
1721
- view.removeCacheChild(relativePath);
2097
+ view.removeServerCacheChild(relativePath);
1722
2098
  } else {
1723
- view.updateCacheChild(relativePath, value);
2099
+ view.updateServerCacheChild(relativePath, value);
1724
2100
  }
1725
2101
  }
1726
2102
  }
1727
2103
  const currentOrder = view.orderedChildren;
1728
2104
  const currentChildSet = new Set(currentOrder);
1729
2105
  if (!isVolatile && !isFirstSnapshot && previousCacheJson !== null) {
1730
- const currentCacheJson = this.serializeCacheForComparison(view.getCache());
2106
+ const currentCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
1731
2107
  if (previousCacheJson === currentCacheJson) {
1732
2108
  return;
1733
2109
  }
1734
2110
  }
1735
2111
  const valueSubs = view.getCallbacks("value");
1736
2112
  if (valueSubs.length > 0) {
1737
- const fullValue = view.getCache();
1738
- const snapshot = this.createSnapshot?.(view.path, fullValue, isVolatile, serverTimestamp);
2113
+ const fullValue = view.getDisplayCache();
2114
+ const snapshot = this.createSnapshot?.(view.path, fullValue, isVolatile, serverTimestamp, view.orderedChildren);
1739
2115
  if (snapshot) {
1740
2116
  for (const entry of valueSubs) {
1741
2117
  try {
@@ -1794,7 +2170,8 @@ var SubscriptionManager = class {
1794
2170
  currentChildSet,
1795
2171
  childMovedSubs,
1796
2172
  isVolatile,
1797
- serverTimestamp
2173
+ serverTimestamp,
2174
+ isFullSnapshot ? void 0 : affectedChildren
1798
2175
  );
1799
2176
  }
1800
2177
  // ============================================
@@ -1807,7 +2184,8 @@ var SubscriptionManager = class {
1807
2184
  findViewsForWritePath(writePath) {
1808
2185
  const normalized = normalizePath(writePath);
1809
2186
  const views = [];
1810
- for (const [viewPath, view] of this.views) {
2187
+ for (const view of this.views.values()) {
2188
+ const viewPath = view.path;
1811
2189
  if (normalized === viewPath) {
1812
2190
  views.push(view);
1813
2191
  } else if (normalized.startsWith(viewPath + "/")) {
@@ -1835,18 +2213,49 @@ var SubscriptionManager = class {
1835
2213
  /**
1836
2214
  * Track a pending write for all Views that cover the write path.
1837
2215
  */
1838
- trackPendingWrite(writePath, requestId) {
1839
- const views = this.findViewsForWritePath(writePath);
1840
- for (const view of views) {
1841
- view.addPendingWrite(requestId);
1842
- }
2216
+ /**
2217
+ * Track a pending write by request ID.
2218
+ * Note: With the dual-cache system, this is now handled by applyOptimisticWrite
2219
+ * via addPendingWriteData. This method is kept for API compatibility but is a no-op.
2220
+ */
2221
+ trackPendingWrite(_writePath, _requestId) {
1843
2222
  }
1844
2223
  /**
1845
2224
  * Clear a pending write from all Views (on ack).
2225
+ * Fires callbacks only if displayCache changed (e.g., server modified the data).
2226
+ * With PUT-before-ACK ordering, this handles the case where server data differs from optimistic.
1846
2227
  */
1847
2228
  clearPendingWrite(requestId) {
1848
2229
  for (const view of this.views.values()) {
2230
+ if (!view.isWritePending(requestId)) {
2231
+ continue;
2232
+ }
2233
+ const previousDisplayCache = view.getDisplayCache();
2234
+ const previousCacheJson = this.serializeCacheForComparison(previousDisplayCache);
2235
+ const previousOrder = view.orderedChildren;
2236
+ const previousChildSet = new Set(previousOrder);
1849
2237
  view.removePendingWrite(requestId);
2238
+ const currentCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
2239
+ if (previousCacheJson === currentCacheJson) {
2240
+ continue;
2241
+ }
2242
+ const currentOrder = view.orderedChildren;
2243
+ const currentChildSet = new Set(currentOrder);
2244
+ const valueSubs = view.getCallbacks("value");
2245
+ if (valueSubs.length > 0) {
2246
+ const fullValue = view.getDisplayCache();
2247
+ const snapshot = this.createSnapshot?.(view.path, fullValue, false, void 0, view.orderedChildren);
2248
+ if (snapshot) {
2249
+ for (const entry of valueSubs) {
2250
+ try {
2251
+ entry.callback(snapshot, void 0);
2252
+ } catch (err) {
2253
+ console.error("Error in value subscription callback:", err);
2254
+ }
2255
+ }
2256
+ }
2257
+ }
2258
+ this.fireChildEvents(view, previousOrder, previousChildSet, currentOrder, currentChildSet, "/", false, void 0, previousDisplayCache);
1850
2259
  }
1851
2260
  }
1852
2261
  /**
@@ -1854,40 +2263,44 @@ var SubscriptionManager = class {
1854
2263
  * Collects all tainted request IDs and puts affected Views into recovery mode.
1855
2264
  * Returns the affected Views and all tainted request IDs for local rejection.
1856
2265
  */
2266
+ /**
2267
+ * Handle a write NACK by removing the pending write from affected Views.
2268
+ * With the dual-cache system, this automatically reverts to server truth.
2269
+ * Returns the affected Views so callbacks can be fired if data changed.
2270
+ */
1857
2271
  handleWriteNack(requestId) {
1858
2272
  const affectedViews = [];
1859
- const taintedIds = /* @__PURE__ */ new Set();
1860
2273
  for (const view of this.views.values()) {
1861
2274
  if (view.isWritePending(requestId)) {
1862
- affectedViews.push(view);
1863
- for (const id of view.getPendingWriteIds()) {
1864
- taintedIds.add(id);
2275
+ const previousDisplayCache = view.getDisplayCache();
2276
+ const previousCacheJson = this.serializeCacheForComparison(previousDisplayCache);
2277
+ const previousOrder = view.orderedChildren;
2278
+ const previousChildSet = new Set(previousOrder);
2279
+ view.removePendingWrite(requestId);
2280
+ const currentCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
2281
+ if (previousCacheJson !== currentCacheJson) {
2282
+ affectedViews.push(view);
2283
+ const currentOrder = view.orderedChildren;
2284
+ const currentChildSet = new Set(currentOrder);
2285
+ const valueSubs = view.getCallbacks("value");
2286
+ if (valueSubs.length > 0) {
2287
+ const fullValue = view.getDisplayCache();
2288
+ const snapshot = this.createSnapshot?.(view.path, fullValue, false, void 0, view.orderedChildren);
2289
+ if (snapshot) {
2290
+ for (const entry of valueSubs) {
2291
+ try {
2292
+ entry.callback(snapshot, void 0);
2293
+ } catch (err) {
2294
+ console.error("Error in value subscription callback:", err);
2295
+ }
2296
+ }
2297
+ }
2298
+ }
2299
+ this.fireChildEvents(view, previousOrder, previousChildSet, currentOrder, currentChildSet, "/", false, void 0, previousDisplayCache);
1865
2300
  }
1866
2301
  }
1867
2302
  }
1868
- for (const view of affectedViews) {
1869
- view.enterRecovery();
1870
- }
1871
- return { affectedViews, taintedIds: Array.from(taintedIds) };
1872
- }
1873
- /**
1874
- * Check if any View covering a path is in recovery mode.
1875
- */
1876
- isPathInRecovery(writePath) {
1877
- const views = this.findViewsForWritePath(writePath);
1878
- return views.some((view) => view.recovering);
1879
- }
1880
- /**
1881
- * Re-subscribe a specific View to get a fresh snapshot.
1882
- * Used during recovery after a nack.
1883
- */
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);
1890
- }
2303
+ return affectedViews;
1891
2304
  }
1892
2305
  // ============================================
1893
2306
  // Optimistic Writes (for local-first)
@@ -1910,9 +2323,6 @@ var SubscriptionManager = class {
1910
2323
  }
1911
2324
  const updatedViews = [];
1912
2325
  for (const view of affectedViews) {
1913
- if (!view.hasReceivedInitialSnapshot) {
1914
- continue;
1915
- }
1916
2326
  let relativePath;
1917
2327
  if (normalized === view.path) {
1918
2328
  relativePath = "/";
@@ -1921,23 +2331,217 @@ var SubscriptionManager = class {
1921
2331
  } else {
1922
2332
  relativePath = normalized.slice(view.path.length);
1923
2333
  }
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 });
1932
- }
1933
- } else {
1934
- updates.push({ relativePath, value });
2334
+ const previousDisplayCache = view.getDisplayCache();
2335
+ const previousOrder = view.orderedChildren;
2336
+ const previousChildSet = new Set(previousOrder);
2337
+ const previousCacheJson = this.serializeCacheForComparison(previousDisplayCache);
2338
+ view.addPendingWriteData(requestId, relativePath, value, operation);
2339
+ const currentOrder = view.orderedChildren;
2340
+ const currentChildSet = new Set(currentOrder);
2341
+ const currentCacheJson = this.serializeCacheForComparison(view.getDisplayCache());
2342
+ if (previousCacheJson === currentCacheJson) {
2343
+ continue;
1935
2344
  }
1936
- this.applyWriteToView(view, updates, false, void 0);
1937
2345
  updatedViews.push(view);
2346
+ const valueSubs = view.getCallbacks("value");
2347
+ if (valueSubs.length > 0) {
2348
+ const fullValue = view.getDisplayCache();
2349
+ const snapshot = this.createSnapshot?.(view.path, fullValue, false, void 0, view.orderedChildren);
2350
+ if (snapshot) {
2351
+ for (const entry of valueSubs) {
2352
+ try {
2353
+ entry.callback(snapshot, void 0);
2354
+ } catch (err) {
2355
+ console.error("Error in value subscription callback:", err);
2356
+ }
2357
+ }
2358
+ }
2359
+ }
2360
+ this.fireChildEvents(view, previousOrder, previousChildSet, currentOrder, currentChildSet, relativePath, false, void 0, previousDisplayCache);
1938
2361
  }
1939
2362
  return updatedViews;
1940
2363
  }
2364
+ /**
2365
+ * Fire child_added/child_changed/child_removed/child_moved events.
2366
+ * Extracted as helper since it's used by both server updates and optimistic writes.
2367
+ */
2368
+ fireChildEvents(view, previousOrder, previousChildSet, currentOrder, currentChildSet, affectedPath, isVolatile, serverTimestamp, previousDisplayCache) {
2369
+ const childAddedSubs = view.getCallbacks("child_added");
2370
+ const childChangedSubs = view.getCallbacks("child_changed");
2371
+ const childRemovedSubs = view.getCallbacks("child_removed");
2372
+ const childMovedSubs = view.getCallbacks("child_moved");
2373
+ if (childAddedSubs.length === 0 && childChangedSubs.length === 0 && childRemovedSubs.length === 0 && childMovedSubs.length === 0) {
2374
+ return;
2375
+ }
2376
+ const displayCache = view.getDisplayCache();
2377
+ const affectedChildren = /* @__PURE__ */ new Set();
2378
+ if (affectedPath !== "/") {
2379
+ const segments = affectedPath.split("/").filter((s) => s.length > 0);
2380
+ if (segments.length > 0) {
2381
+ affectedChildren.add(segments[0]);
2382
+ }
2383
+ }
2384
+ const isFullSnapshot = affectedPath === "/";
2385
+ if (isFullSnapshot) {
2386
+ for (const key of currentOrder) {
2387
+ if (!previousChildSet.has(key)) {
2388
+ if (childAddedSubs.length > 0 && displayCache) {
2389
+ const childValue = displayCache[key];
2390
+ const snapshot = this.createSnapshot?.(
2391
+ joinPath(view.path, key),
2392
+ childValue,
2393
+ isVolatile,
2394
+ serverTimestamp
2395
+ );
2396
+ if (snapshot) {
2397
+ const prevKey = view.getPreviousChildKey(key);
2398
+ for (const entry of childAddedSubs) {
2399
+ try {
2400
+ entry.callback(snapshot, prevKey);
2401
+ } catch (err) {
2402
+ console.error("Error in child_added callback:", err);
2403
+ }
2404
+ }
2405
+ }
2406
+ }
2407
+ } else if (previousDisplayCache && childChangedSubs.length > 0 && displayCache) {
2408
+ const prevValue = previousDisplayCache[key];
2409
+ const currentValue = displayCache[key];
2410
+ const prevJson = this.serializeCacheForComparison(prevValue);
2411
+ const currJson = this.serializeCacheForComparison(currentValue);
2412
+ if (prevJson !== currJson) {
2413
+ const snapshot = this.createSnapshot?.(
2414
+ joinPath(view.path, key),
2415
+ currentValue,
2416
+ isVolatile,
2417
+ serverTimestamp
2418
+ );
2419
+ if (snapshot) {
2420
+ const prevKey = view.getPreviousChildKey(key);
2421
+ for (const entry of childChangedSubs) {
2422
+ try {
2423
+ entry.callback(snapshot, prevKey);
2424
+ } catch (err) {
2425
+ console.error("Error in child_changed callback:", err);
2426
+ }
2427
+ }
2428
+ }
2429
+ }
2430
+ }
2431
+ }
2432
+ for (const key of previousOrder) {
2433
+ if (!currentChildSet.has(key)) {
2434
+ if (childRemovedSubs.length > 0) {
2435
+ const snapshot = this.createSnapshot?.(joinPath(view.path, key), null, isVolatile, serverTimestamp);
2436
+ if (snapshot) {
2437
+ for (const entry of childRemovedSubs) {
2438
+ try {
2439
+ entry.callback(snapshot, void 0);
2440
+ } catch (err) {
2441
+ console.error("Error in child_removed callback:", err);
2442
+ }
2443
+ }
2444
+ }
2445
+ }
2446
+ }
2447
+ }
2448
+ } else {
2449
+ for (const key of affectedChildren) {
2450
+ const wasPresent = previousChildSet.has(key);
2451
+ const isPresent = currentChildSet.has(key);
2452
+ if (!wasPresent && isPresent) {
2453
+ if (childAddedSubs.length > 0 && displayCache) {
2454
+ const childValue = displayCache[key];
2455
+ const snapshot = this.createSnapshot?.(
2456
+ joinPath(view.path, key),
2457
+ childValue,
2458
+ isVolatile,
2459
+ serverTimestamp
2460
+ );
2461
+ if (snapshot) {
2462
+ const prevKey = view.getPreviousChildKey(key);
2463
+ for (const entry of childAddedSubs) {
2464
+ try {
2465
+ entry.callback(snapshot, prevKey);
2466
+ } catch (err) {
2467
+ console.error("Error in child_added callback:", err);
2468
+ }
2469
+ }
2470
+ }
2471
+ }
2472
+ } else if (wasPresent && !isPresent) {
2473
+ if (childRemovedSubs.length > 0) {
2474
+ const snapshot = this.createSnapshot?.(joinPath(view.path, key), null, isVolatile, serverTimestamp);
2475
+ if (snapshot) {
2476
+ for (const entry of childRemovedSubs) {
2477
+ try {
2478
+ entry.callback(snapshot, void 0);
2479
+ } catch (err) {
2480
+ console.error("Error in child_removed callback:", err);
2481
+ }
2482
+ }
2483
+ }
2484
+ }
2485
+ } else if (wasPresent && isPresent) {
2486
+ if (childChangedSubs.length > 0 && displayCache) {
2487
+ const childValue = displayCache[key];
2488
+ const snapshot = this.createSnapshot?.(
2489
+ joinPath(view.path, key),
2490
+ childValue,
2491
+ isVolatile,
2492
+ serverTimestamp
2493
+ );
2494
+ if (snapshot) {
2495
+ const prevKey = view.getPreviousChildKey(key);
2496
+ for (const entry of childChangedSubs) {
2497
+ try {
2498
+ entry.callback(snapshot, prevKey);
2499
+ } catch (err) {
2500
+ console.error("Error in child_changed callback:", err);
2501
+ }
2502
+ }
2503
+ }
2504
+ }
2505
+ }
2506
+ }
2507
+ if (childRemovedSubs.length > 0) {
2508
+ for (const key of previousOrder) {
2509
+ if (affectedChildren.has(key)) continue;
2510
+ if (currentChildSet.has(key)) continue;
2511
+ const prevValue = previousDisplayCache ? previousDisplayCache[key] : null;
2512
+ const snapshot = this.createSnapshot?.(joinPath(view.path, key), prevValue, isVolatile, serverTimestamp);
2513
+ if (snapshot) {
2514
+ for (const entry of childRemovedSubs) {
2515
+ try {
2516
+ entry.callback(snapshot, void 0);
2517
+ } catch (err) {
2518
+ console.error("Error in child_removed callback:", err);
2519
+ }
2520
+ }
2521
+ }
2522
+ }
2523
+ }
2524
+ }
2525
+ if (childMovedSubs.length > 0) {
2526
+ const previousPositions = /* @__PURE__ */ new Map();
2527
+ previousOrder.forEach((key, i) => previousPositions.set(key, i));
2528
+ const currentPositions = /* @__PURE__ */ new Map();
2529
+ currentOrder.forEach((key, i) => currentPositions.set(key, i));
2530
+ this.detectAndFireMoves(
2531
+ view,
2532
+ previousOrder,
2533
+ currentOrder,
2534
+ previousPositions,
2535
+ currentPositions,
2536
+ previousChildSet,
2537
+ currentChildSet,
2538
+ childMovedSubs,
2539
+ isVolatile,
2540
+ serverTimestamp,
2541
+ isFullSnapshot ? void 0 : affectedChildren
2542
+ );
2543
+ }
2544
+ }
1941
2545
  };
1942
2546
 
1943
2547
  // src/OnDisconnect.ts
@@ -2044,7 +2648,37 @@ function generatePushId() {
2044
2648
  }
2045
2649
 
2046
2650
  // src/DatabaseReference.ts
2651
+ function isInfoPath(path) {
2652
+ return path === "/.info" || path.startsWith("/.info/");
2653
+ }
2654
+ function validateNotInfoPath(path, operation) {
2655
+ if (isInfoPath(path)) {
2656
+ throw new LarkError(
2657
+ ErrorCode.INVALID_PATH,
2658
+ `Cannot ${operation} on .info paths - they are read-only system paths`
2659
+ );
2660
+ }
2661
+ }
2047
2662
  var DEFAULT_MAX_RETRIES = 25;
2663
+ var WIRE_PROTOCOL_CONSTANTS = {
2664
+ INDEX_START_VALUE: "sp",
2665
+ INDEX_START_NAME: "sn",
2666
+ INDEX_START_IS_INCLUSIVE: "sin",
2667
+ INDEX_END_VALUE: "ep",
2668
+ INDEX_END_NAME: "en",
2669
+ INDEX_END_IS_INCLUSIVE: "ein",
2670
+ LIMIT: "l",
2671
+ VIEW_FROM: "vf",
2672
+ INDEX: "i"
2673
+ };
2674
+ function objectToUniqueKey(obj) {
2675
+ const sortedKeys = Object.keys(obj).sort();
2676
+ const sorted = {};
2677
+ for (const key of sortedKeys) {
2678
+ sorted[key] = obj[key];
2679
+ }
2680
+ return JSON.stringify(sorted);
2681
+ }
2048
2682
  var DatabaseReference = class _DatabaseReference {
2049
2683
  constructor(db, path, query = {}) {
2050
2684
  this._db = db;
@@ -2077,6 +2711,110 @@ var DatabaseReference = class _DatabaseReference {
2077
2711
  get root() {
2078
2712
  return new _DatabaseReference(this._db, "");
2079
2713
  }
2714
+ /**
2715
+ * Get the LarkDatabase instance this reference belongs to.
2716
+ */
2717
+ get database() {
2718
+ return this._db;
2719
+ }
2720
+ /**
2721
+ * Get the underlying reference for a query.
2722
+ * For queries (created via orderBy*, limitTo*, startAt, etc.), this returns
2723
+ * a reference to the same path without query constraints.
2724
+ * For non-query references, this returns the reference itself.
2725
+ * This matches Firebase's Query.ref behavior.
2726
+ */
2727
+ get ref() {
2728
+ if (Object.keys(this._query).length === 0) {
2729
+ return this;
2730
+ }
2731
+ return new _DatabaseReference(this._db, this._path);
2732
+ }
2733
+ /**
2734
+ * Check if this reference/query is equal to another.
2735
+ * Two references are equal if they have the same database, path, and query constraints.
2736
+ */
2737
+ isEqual(other) {
2738
+ if (!other) return false;
2739
+ if (this._db !== other._db) return false;
2740
+ if (this._path !== other._path) return false;
2741
+ return JSON.stringify(this._query) === JSON.stringify(other._query);
2742
+ }
2743
+ /**
2744
+ * Get the unique identifier for this query's parameters.
2745
+ * Used to differentiate multiple queries on the same path.
2746
+ *
2747
+ * Returns "default" for non-query references (no constraints).
2748
+ * Returns a sorted JSON string of wire-format params for queries.
2749
+ *
2750
+ * This matches Firebase's queryIdentifier format for wire compatibility.
2751
+ */
2752
+ get queryIdentifier() {
2753
+ const queryObj = {};
2754
+ if (this._query.orderBy === "key") {
2755
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX] = ".key";
2756
+ } else if (this._query.orderBy === "value") {
2757
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX] = ".value";
2758
+ } else if (this._query.orderBy === "priority") {
2759
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX] = ".priority";
2760
+ } else if (this._query.orderBy === "child" && this._query.orderByChildPath) {
2761
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX] = this._query.orderByChildPath;
2762
+ }
2763
+ if (this._query.startAt !== void 0) {
2764
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAt.value;
2765
+ if (this._query.startAt.key !== void 0) {
2766
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.startAt.key;
2767
+ }
2768
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = true;
2769
+ } else if (this._query.startAfter !== void 0) {
2770
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.startAfter.value;
2771
+ if (this._query.startAfter.key !== void 0) {
2772
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.startAfter.key;
2773
+ }
2774
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = false;
2775
+ }
2776
+ if (this._query.endAt !== void 0) {
2777
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endAt.value;
2778
+ if (this._query.endAt.key !== void 0) {
2779
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.endAt.key;
2780
+ }
2781
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_IS_INCLUSIVE] = true;
2782
+ } else if (this._query.endBefore !== void 0) {
2783
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.endBefore.value;
2784
+ if (this._query.endBefore.key !== void 0) {
2785
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.endBefore.key;
2786
+ }
2787
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_IS_INCLUSIVE] = false;
2788
+ }
2789
+ if (this._query.equalTo !== void 0) {
2790
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this._query.equalTo.value;
2791
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this._query.equalTo.value;
2792
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_IS_INCLUSIVE] = true;
2793
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_IS_INCLUSIVE] = true;
2794
+ if (this._query.equalTo.key !== void 0) {
2795
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this._query.equalTo.key;
2796
+ queryObj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this._query.equalTo.key;
2797
+ }
2798
+ }
2799
+ if (this._query.limitToFirst !== void 0) {
2800
+ queryObj[WIRE_PROTOCOL_CONSTANTS.LIMIT] = this._query.limitToFirst;
2801
+ queryObj[WIRE_PROTOCOL_CONSTANTS.VIEW_FROM] = "l";
2802
+ } else if (this._query.limitToLast !== void 0) {
2803
+ queryObj[WIRE_PROTOCOL_CONSTANTS.LIMIT] = this._query.limitToLast;
2804
+ queryObj[WIRE_PROTOCOL_CONSTANTS.VIEW_FROM] = "r";
2805
+ }
2806
+ if (Object.keys(queryObj).length === 0) {
2807
+ return "default";
2808
+ }
2809
+ return objectToUniqueKey(queryObj);
2810
+ }
2811
+ /**
2812
+ * Get the data at this location. Alias for once('value').
2813
+ * This is Firebase's newer API for reading data.
2814
+ */
2815
+ async get() {
2816
+ return this.once("value");
2817
+ }
2080
2818
  // ============================================
2081
2819
  // Navigation
2082
2820
  // ============================================
@@ -2095,8 +2833,11 @@ var DatabaseReference = class _DatabaseReference {
2095
2833
  *
2096
2834
  * For volatile paths (high-frequency updates), this is fire-and-forget
2097
2835
  * and resolves immediately without waiting for server confirmation.
2836
+ *
2837
+ * @param value - The value to write
2098
2838
  */
2099
2839
  async set(value) {
2840
+ validateNotInfoPath(this._path, "set");
2100
2841
  if (this._db.isVolatilePath(this._path)) {
2101
2842
  this._db._sendVolatileSet(this._path, value);
2102
2843
  return;
@@ -2121,8 +2862,11 @@ var DatabaseReference = class _DatabaseReference {
2121
2862
  * '/leaderboard/alice': null // null = delete
2122
2863
  * });
2123
2864
  * ```
2865
+ *
2866
+ * @param values - The values to update
2124
2867
  */
2125
2868
  async update(values) {
2869
+ validateNotInfoPath(this._path, "update");
2126
2870
  const hasPathKeys = Object.keys(values).some((key) => key.startsWith("/"));
2127
2871
  if (hasPathKeys) {
2128
2872
  const ops = [];
@@ -2150,40 +2894,45 @@ var DatabaseReference = class _DatabaseReference {
2150
2894
  * For volatile paths, this is fire-and-forget.
2151
2895
  */
2152
2896
  async remove() {
2897
+ validateNotInfoPath(this._path, "remove");
2153
2898
  if (this._db.isVolatilePath(this._path)) {
2154
2899
  this._db._sendVolatileDelete(this._path);
2155
2900
  return;
2156
2901
  }
2157
2902
  await this._db._sendDelete(this._path);
2158
2903
  }
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
2904
  push(value) {
2905
+ validateNotInfoPath(this._path, "push");
2169
2906
  const key = generatePushId();
2170
2907
  const childRef = this.child(key);
2171
2908
  if (value === void 0) {
2172
- return childRef;
2909
+ return new ThenableReference(this._db, childRef.path);
2173
2910
  }
2174
- return childRef.set(value).then(() => childRef);
2911
+ const setPromise = childRef.set(value);
2912
+ const writePromise = setPromise.then(() => childRef);
2913
+ return new ThenableReference(this._db, childRef.path, writePromise);
2175
2914
  }
2176
2915
  /**
2177
2916
  * Set the data with a priority value for ordering.
2178
- * Priority is injected as `.priority` into the value object.
2917
+ *
2918
+ * For objects: injects `.priority` into the value object.
2919
+ * For primitives: wraps as `{ '.value': primitive, '.priority': priority }`.
2920
+ *
2921
+ * This follows Firebase's wire format for primitives with priority.
2922
+ *
2923
+ * @param value - The value to write
2924
+ * @param priority - The priority for ordering
2179
2925
  */
2180
2926
  async setWithPriority(value, priority) {
2927
+ validateNotInfoPath(this._path, "setWithPriority");
2181
2928
  if (value === null || value === void 0) {
2182
2929
  await this._db._sendSet(this._path, value);
2183
2930
  return;
2184
2931
  }
2185
2932
  if (typeof value !== "object" || Array.isArray(value)) {
2186
- throw new Error("Priority can only be set on object values");
2933
+ const wrappedValue = { ".value": value, ".priority": priority };
2934
+ await this._db._sendSet(this._path, wrappedValue);
2935
+ return;
2187
2936
  }
2188
2937
  const valueWithPriority = { ...value, ".priority": priority };
2189
2938
  await this._db._sendSet(this._path, valueWithPriority);
@@ -2193,15 +2942,13 @@ var DatabaseReference = class _DatabaseReference {
2193
2942
  * Fetches current value and sets it with the new priority.
2194
2943
  */
2195
2944
  async setPriority(priority) {
2945
+ validateNotInfoPath(this._path, "setPriority");
2196
2946
  const snapshot = await this.once();
2197
2947
  const currentVal = snapshot.val();
2198
2948
  if (currentVal === null || currentVal === void 0) {
2199
2949
  await this._db._sendSet(this._path, { ".priority": priority });
2200
2950
  return;
2201
2951
  }
2202
- if (typeof currentVal !== "object" || Array.isArray(currentVal)) {
2203
- throw new Error("Priority can only be set on object values");
2204
- }
2205
2952
  await this.setWithPriority(currentVal, priority);
2206
2953
  }
2207
2954
  /**
@@ -2227,6 +2974,7 @@ var DatabaseReference = class _DatabaseReference {
2227
2974
  * @returns TransactionResult with committed status and final snapshot
2228
2975
  */
2229
2976
  async transaction(updateFunction, maxRetries = DEFAULT_MAX_RETRIES) {
2977
+ validateNotInfoPath(this._path, "transaction");
2230
2978
  let retries = 0;
2231
2979
  while (retries < maxRetries) {
2232
2980
  const currentSnapshot = await this.once();
@@ -2271,8 +3019,11 @@ var DatabaseReference = class _DatabaseReference {
2271
3019
  // ============================================
2272
3020
  /**
2273
3021
  * Read the data at this location once.
3022
+ *
3023
+ * @param eventType - The event type (only 'value' is supported)
3024
+ * @returns Promise that resolves to the DataSnapshot
2274
3025
  */
2275
- async once(eventType = "value") {
3026
+ once(eventType = "value") {
2276
3027
  if (eventType !== "value") {
2277
3028
  throw new Error('once() only supports "value" event type');
2278
3029
  }
@@ -2286,12 +3037,18 @@ var DatabaseReference = class _DatabaseReference {
2286
3037
  * Returns an unsubscribe function.
2287
3038
  */
2288
3039
  on(eventType, callback) {
2289
- return this._db._subscribe(this._path, eventType, callback, this._buildQueryParams());
3040
+ return this._db._subscribe(this._path, eventType, callback, this._buildQueryParams(), this.queryIdentifier);
2290
3041
  }
2291
3042
  /**
2292
3043
  * Unsubscribe from events.
2293
- * If eventType is specified, removes all listeners of that type.
2294
- * If no eventType, removes ALL listeners at this path.
3044
+ *
3045
+ * - `off()` - removes ALL listeners at this path
3046
+ * - `off('value')` - removes all 'value' listeners at this path
3047
+ *
3048
+ * Note: To unsubscribe a specific callback, use the unsubscribe function
3049
+ * returned by `on()`.
3050
+ *
3051
+ * @param eventType - Optional event type to unsubscribe from
2295
3052
  */
2296
3053
  off(eventType) {
2297
3054
  if (eventType) {
@@ -2316,6 +3073,9 @@ var DatabaseReference = class _DatabaseReference {
2316
3073
  * Order results by key.
2317
3074
  */
2318
3075
  orderByKey() {
3076
+ this._validateNoOrderBy("orderByKey");
3077
+ this._validateNoKeyParameterForOrderByKey("orderByKey");
3078
+ this._validateStringValuesForOrderByKey("orderByKey");
2319
3079
  return new _DatabaseReference(this._db, this._path, {
2320
3080
  ...this._query,
2321
3081
  orderBy: "key"
@@ -2325,6 +3085,7 @@ var DatabaseReference = class _DatabaseReference {
2325
3085
  * Order results by priority.
2326
3086
  */
2327
3087
  orderByPriority() {
3088
+ this._validateNoOrderBy("orderByPriority");
2328
3089
  return new _DatabaseReference(this._db, this._path, {
2329
3090
  ...this._query,
2330
3091
  orderBy: "priority"
@@ -2334,6 +3095,7 @@ var DatabaseReference = class _DatabaseReference {
2334
3095
  * Order results by a child key.
2335
3096
  */
2336
3097
  orderByChild(path) {
3098
+ this._validateNoOrderBy("orderByChild");
2337
3099
  return new _DatabaseReference(this._db, this._path, {
2338
3100
  ...this._query,
2339
3101
  orderBy: "child",
@@ -2344,57 +3106,336 @@ var DatabaseReference = class _DatabaseReference {
2344
3106
  * Order results by value.
2345
3107
  */
2346
3108
  orderByValue() {
3109
+ this._validateNoOrderBy("orderByValue");
3110
+ return new _DatabaseReference(this._db, this._path, {
3111
+ ...this._query,
3112
+ orderBy: "value"
3113
+ });
3114
+ }
3115
+ /**
3116
+ * Limit to the first N results.
3117
+ */
3118
+ limitToFirst(limit) {
3119
+ if (limit === void 0 || limit === null) {
3120
+ throw new LarkError(
3121
+ ErrorCode.INVALID_QUERY,
3122
+ "Query.limitToFirst: a limit must be provided"
3123
+ );
3124
+ }
3125
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0) {
3126
+ throw new LarkError(
3127
+ ErrorCode.INVALID_QUERY,
3128
+ "Query.limitToFirst: limit must be a positive integer"
3129
+ );
3130
+ }
3131
+ if (this._query.limitToFirst !== void 0) {
3132
+ throw new LarkError(
3133
+ ErrorCode.INVALID_QUERY,
3134
+ "Query.limitToFirst: limitToFirst() cannot be called multiple times"
3135
+ );
3136
+ }
3137
+ if (this._query.limitToLast !== void 0) {
3138
+ throw new LarkError(
3139
+ ErrorCode.INVALID_QUERY,
3140
+ "Query.limitToFirst: cannot use limitToFirst() with limitToLast()"
3141
+ );
3142
+ }
3143
+ return new _DatabaseReference(this._db, this._path, {
3144
+ ...this._query,
3145
+ limitToFirst: limit
3146
+ });
3147
+ }
3148
+ /**
3149
+ * Limit to the last N results.
3150
+ */
3151
+ limitToLast(limit) {
3152
+ if (limit === void 0 || limit === null) {
3153
+ throw new LarkError(
3154
+ ErrorCode.INVALID_QUERY,
3155
+ "Query.limitToLast: a limit must be provided"
3156
+ );
3157
+ }
3158
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0) {
3159
+ throw new LarkError(
3160
+ ErrorCode.INVALID_QUERY,
3161
+ "Query.limitToLast: limit must be a positive integer"
3162
+ );
3163
+ }
3164
+ if (this._query.limitToLast !== void 0) {
3165
+ throw new LarkError(
3166
+ ErrorCode.INVALID_QUERY,
3167
+ "Query.limitToLast: limitToLast() cannot be called multiple times"
3168
+ );
3169
+ }
3170
+ if (this._query.limitToFirst !== void 0) {
3171
+ throw new LarkError(
3172
+ ErrorCode.INVALID_QUERY,
3173
+ "Query.limitToLast: cannot use limitToLast() with limitToFirst()"
3174
+ );
3175
+ }
2347
3176
  return new _DatabaseReference(this._db, this._path, {
2348
3177
  ...this._query,
2349
- orderBy: "value"
3178
+ limitToLast: limit
2350
3179
  });
2351
3180
  }
2352
3181
  /**
2353
- * Limit to the first N results.
3182
+ * Start at a specific value/key.
3183
+ * If no value is provided, starts at the beginning (null priority).
2354
3184
  */
2355
- limitToFirst(limit) {
3185
+ startAt(value, key) {
3186
+ if (this._query.startAt !== void 0 || this._query.startAfter !== void 0) {
3187
+ throw new LarkError(
3188
+ ErrorCode.INVALID_QUERY,
3189
+ "Query.startAt: startAt()/startAfter() cannot be called multiple times"
3190
+ );
3191
+ }
3192
+ if (this._query.equalTo !== void 0) {
3193
+ throw new LarkError(
3194
+ ErrorCode.INVALID_QUERY,
3195
+ "Query.startAt: cannot use startAt() with equalTo()"
3196
+ );
3197
+ }
3198
+ this._validateQueryValue("startAt", value, key);
2356
3199
  return new _DatabaseReference(this._db, this._path, {
2357
3200
  ...this._query,
2358
- limitToFirst: limit
3201
+ startAt: { value, key }
2359
3202
  });
2360
3203
  }
2361
3204
  /**
2362
- * Limit to the last N results.
3205
+ * Start after a specific value/key (exclusive).
3206
+ * Like startAt() but excludes the starting point.
2363
3207
  */
2364
- limitToLast(limit) {
3208
+ startAfter(value, key) {
3209
+ if (this._query.startAfter !== void 0 || this._query.startAt !== void 0) {
3210
+ throw new LarkError(
3211
+ ErrorCode.INVALID_QUERY,
3212
+ "Query.startAfter: startAfter()/startAt() cannot be called multiple times"
3213
+ );
3214
+ }
3215
+ if (this._query.equalTo !== void 0) {
3216
+ throw new LarkError(
3217
+ ErrorCode.INVALID_QUERY,
3218
+ "Query.startAfter: cannot use startAfter() with equalTo()"
3219
+ );
3220
+ }
3221
+ this._validateQueryValue("startAfter", value, key);
2365
3222
  return new _DatabaseReference(this._db, this._path, {
2366
3223
  ...this._query,
2367
- limitToLast: limit
3224
+ startAfter: { value, key }
2368
3225
  });
2369
3226
  }
2370
3227
  /**
2371
- * Start at a specific value/key.
3228
+ * End at a specific value/key.
3229
+ * If no value is provided, ends at the end (null priority).
2372
3230
  */
2373
- startAt(value, key) {
3231
+ endAt(value, key) {
3232
+ if (this._query.endAt !== void 0 || this._query.endBefore !== void 0) {
3233
+ throw new LarkError(
3234
+ ErrorCode.INVALID_QUERY,
3235
+ "Query.endAt: endAt()/endBefore() cannot be called multiple times"
3236
+ );
3237
+ }
3238
+ if (this._query.equalTo !== void 0) {
3239
+ throw new LarkError(
3240
+ ErrorCode.INVALID_QUERY,
3241
+ "Query.endAt: cannot use endAt() with equalTo()"
3242
+ );
3243
+ }
3244
+ this._validateQueryValue("endAt", value, key);
2374
3245
  return new _DatabaseReference(this._db, this._path, {
2375
3246
  ...this._query,
2376
- startAt: { value, key }
3247
+ endAt: { value, key }
2377
3248
  });
2378
3249
  }
2379
3250
  /**
2380
- * End at a specific value/key.
3251
+ * End before a specific value/key (exclusive).
3252
+ * Like endAt() but excludes the ending point.
2381
3253
  */
2382
- endAt(value, key) {
3254
+ endBefore(value, key) {
3255
+ if (this._query.endBefore !== void 0 || this._query.endAt !== void 0) {
3256
+ throw new LarkError(
3257
+ ErrorCode.INVALID_QUERY,
3258
+ "Query.endBefore: endBefore()/endAt() cannot be called multiple times"
3259
+ );
3260
+ }
3261
+ if (this._query.equalTo !== void 0) {
3262
+ throw new LarkError(
3263
+ ErrorCode.INVALID_QUERY,
3264
+ "Query.endBefore: cannot use endBefore() with equalTo()"
3265
+ );
3266
+ }
3267
+ this._validateQueryValue("endBefore", value, key);
2383
3268
  return new _DatabaseReference(this._db, this._path, {
2384
3269
  ...this._query,
2385
- endAt: { value, key }
3270
+ endBefore: { value, key }
2386
3271
  });
2387
3272
  }
2388
3273
  /**
2389
3274
  * Filter to items equal to a specific value.
2390
3275
  */
2391
3276
  equalTo(value, key) {
3277
+ if (this._query.equalTo !== void 0) {
3278
+ throw new LarkError(
3279
+ ErrorCode.INVALID_QUERY,
3280
+ "Query.equalTo: equalTo() cannot be called multiple times"
3281
+ );
3282
+ }
3283
+ if (this._query.startAt !== void 0 || this._query.startAfter !== void 0) {
3284
+ throw new LarkError(
3285
+ ErrorCode.INVALID_QUERY,
3286
+ "Query.equalTo: cannot use equalTo() with startAt()/startAfter()"
3287
+ );
3288
+ }
3289
+ if (this._query.endAt !== void 0 || this._query.endBefore !== void 0) {
3290
+ throw new LarkError(
3291
+ ErrorCode.INVALID_QUERY,
3292
+ "Query.equalTo: cannot use equalTo() with endAt()/endBefore()"
3293
+ );
3294
+ }
3295
+ this._validateQueryValue("equalTo", value, key);
2392
3296
  return new _DatabaseReference(this._db, this._path, {
2393
3297
  ...this._query,
2394
3298
  equalTo: { value, key }
2395
3299
  });
2396
3300
  }
2397
3301
  // ============================================
3302
+ // Query Validation Helpers
3303
+ // ============================================
3304
+ /**
3305
+ * Validate that no orderBy has been set yet.
3306
+ */
3307
+ _validateNoOrderBy(methodName) {
3308
+ if (this._query.orderBy !== void 0) {
3309
+ const existingMethod = this._query.orderBy === "child" ? `orderByChild('${this._query.orderByChildPath}')` : `orderBy${this._query.orderBy.charAt(0).toUpperCase()}${this._query.orderBy.slice(1)}()`;
3310
+ throw new LarkError(
3311
+ ErrorCode.INVALID_QUERY,
3312
+ `Query.${methodName}: cannot use ${methodName}() with ${existingMethod}`
3313
+ );
3314
+ }
3315
+ }
3316
+ /**
3317
+ * Validate that no key parameter has been set on startAt/endAt/equalTo.
3318
+ * Used when calling orderByKey() after startAt/endAt/equalTo.
3319
+ */
3320
+ _validateNoKeyParameterForOrderByKey(methodName) {
3321
+ if (this._query.startAt?.key !== void 0) {
3322
+ throw new LarkError(
3323
+ ErrorCode.INVALID_QUERY,
3324
+ `Query.${methodName}: when using orderByKey(), you cannot specify a key parameter in startAt()`
3325
+ );
3326
+ }
3327
+ if (this._query.startAfter?.key !== void 0) {
3328
+ throw new LarkError(
3329
+ ErrorCode.INVALID_QUERY,
3330
+ `Query.${methodName}: when using orderByKey(), you cannot specify a key parameter in startAfter()`
3331
+ );
3332
+ }
3333
+ if (this._query.endAt?.key !== void 0) {
3334
+ throw new LarkError(
3335
+ ErrorCode.INVALID_QUERY,
3336
+ `Query.${methodName}: when using orderByKey(), you cannot specify a key parameter in endAt()`
3337
+ );
3338
+ }
3339
+ if (this._query.endBefore?.key !== void 0) {
3340
+ throw new LarkError(
3341
+ ErrorCode.INVALID_QUERY,
3342
+ `Query.${methodName}: when using orderByKey(), you cannot specify a key parameter in endBefore()`
3343
+ );
3344
+ }
3345
+ if (this._query.equalTo?.key !== void 0) {
3346
+ throw new LarkError(
3347
+ ErrorCode.INVALID_QUERY,
3348
+ `Query.${methodName}: when using orderByKey(), you cannot specify a key parameter in equalTo()`
3349
+ );
3350
+ }
3351
+ }
3352
+ /**
3353
+ * Validate that startAt/endAt/equalTo values are strings.
3354
+ * Used when calling orderByKey() after startAt/endAt/equalTo.
3355
+ */
3356
+ _validateStringValuesForOrderByKey(methodName) {
3357
+ if (this._query.startAt !== void 0 && typeof this._query.startAt.value !== "string") {
3358
+ throw new LarkError(
3359
+ ErrorCode.INVALID_QUERY,
3360
+ `Query.${methodName}: when using orderByKey(), startAt() value must be a string`
3361
+ );
3362
+ }
3363
+ if (this._query.startAfter !== void 0 && typeof this._query.startAfter.value !== "string") {
3364
+ throw new LarkError(
3365
+ ErrorCode.INVALID_QUERY,
3366
+ `Query.${methodName}: when using orderByKey(), startAfter() value must be a string`
3367
+ );
3368
+ }
3369
+ if (this._query.endAt !== void 0 && typeof this._query.endAt.value !== "string") {
3370
+ throw new LarkError(
3371
+ ErrorCode.INVALID_QUERY,
3372
+ `Query.${methodName}: when using orderByKey(), endAt() value must be a string`
3373
+ );
3374
+ }
3375
+ if (this._query.endBefore !== void 0 && typeof this._query.endBefore.value !== "string") {
3376
+ throw new LarkError(
3377
+ ErrorCode.INVALID_QUERY,
3378
+ `Query.${methodName}: when using orderByKey(), endBefore() value must be a string`
3379
+ );
3380
+ }
3381
+ if (this._query.equalTo !== void 0 && typeof this._query.equalTo.value !== "string") {
3382
+ throw new LarkError(
3383
+ ErrorCode.INVALID_QUERY,
3384
+ `Query.${methodName}: when using orderByKey(), equalTo() value must be a string`
3385
+ );
3386
+ }
3387
+ }
3388
+ /**
3389
+ * Validate value and key types for query methods (startAt, endAt, equalTo, etc.)
3390
+ */
3391
+ _validateQueryValue(methodName, value, key) {
3392
+ if (key !== void 0) {
3393
+ this._validateKeyFormat(methodName, key);
3394
+ }
3395
+ if (this._query.orderBy === "key") {
3396
+ if (typeof value !== "string") {
3397
+ throw new LarkError(
3398
+ ErrorCode.INVALID_QUERY,
3399
+ `Query.${methodName}: when using orderByKey(), the value must be a string`
3400
+ );
3401
+ }
3402
+ if (key !== void 0) {
3403
+ throw new LarkError(
3404
+ ErrorCode.INVALID_QUERY,
3405
+ `Query.${methodName}: when using orderByKey(), you cannot specify a key parameter`
3406
+ );
3407
+ }
3408
+ return;
3409
+ }
3410
+ if (this._query.orderBy === "priority") {
3411
+ if (typeof value === "boolean") {
3412
+ throw new LarkError(
3413
+ ErrorCode.INVALID_QUERY,
3414
+ `Query.${methodName}: when using orderByPriority(), the value must be a valid priority (null, number, or string)`
3415
+ );
3416
+ }
3417
+ }
3418
+ if (value !== null && typeof value === "object") {
3419
+ throw new LarkError(
3420
+ ErrorCode.INVALID_QUERY,
3421
+ `Query.${methodName}: the value argument cannot be an object`
3422
+ );
3423
+ }
3424
+ }
3425
+ /**
3426
+ * Validate that a key is a valid Firebase key format.
3427
+ * Invalid characters: . $ # [ ] /
3428
+ * Also cannot start or end with .
3429
+ */
3430
+ _validateKeyFormat(methodName, key) {
3431
+ if (/[.#$\[\]\/]/.test(key)) {
3432
+ throw new LarkError(
3433
+ ErrorCode.INVALID_QUERY,
3434
+ `Query.${methodName}: invalid key "${key}" - keys cannot contain . # $ [ ] /`
3435
+ );
3436
+ }
3437
+ }
3438
+ // ============================================
2398
3439
  // Internal Helpers
2399
3440
  // ============================================
2400
3441
  /**
@@ -2434,6 +3475,13 @@ var DatabaseReference = class _DatabaseReference {
2434
3475
  }
2435
3476
  hasParams = true;
2436
3477
  }
3478
+ if (this._query.startAfter !== void 0) {
3479
+ params.startAfter = this._query.startAfter.value;
3480
+ if (this._query.startAfter.key !== void 0) {
3481
+ params.startAfterKey = this._query.startAfter.key;
3482
+ }
3483
+ hasParams = true;
3484
+ }
2437
3485
  if (this._query.endAt !== void 0) {
2438
3486
  params.endAt = this._query.endAt.value;
2439
3487
  if (this._query.endAt.key !== void 0) {
@@ -2441,6 +3489,13 @@ var DatabaseReference = class _DatabaseReference {
2441
3489
  }
2442
3490
  hasParams = true;
2443
3491
  }
3492
+ if (this._query.endBefore !== void 0) {
3493
+ params.endBefore = this._query.endBefore.value;
3494
+ if (this._query.endBefore.key !== void 0) {
3495
+ params.endBeforeKey = this._query.endBefore.key;
3496
+ }
3497
+ hasParams = true;
3498
+ }
2444
3499
  if (this._query.equalTo !== void 0) {
2445
3500
  params.equalTo = this._query.equalTo.value;
2446
3501
  if (this._query.equalTo.key !== void 0) {
@@ -2462,8 +3517,49 @@ var DatabaseReference = class _DatabaseReference {
2462
3517
  return `${baseUrl}${this._path}`;
2463
3518
  }
2464
3519
  };
3520
+ var ThenableReference = class extends DatabaseReference {
3521
+ constructor(db, path, promise) {
3522
+ super(db, path);
3523
+ this._promise = promise ?? Promise.resolve(this);
3524
+ }
3525
+ then(onfulfilled, onrejected) {
3526
+ return this._promise.then(onfulfilled, onrejected);
3527
+ }
3528
+ catch(onrejected) {
3529
+ return this._promise.catch(onrejected);
3530
+ }
3531
+ };
2465
3532
 
2466
3533
  // src/DataSnapshot.ts
3534
+ function isWrappedPrimitive(data) {
3535
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
3536
+ return false;
3537
+ }
3538
+ const keys = Object.keys(data);
3539
+ return keys.length === 2 && ".value" in data && ".priority" in data;
3540
+ }
3541
+ function stripPriorityMetadata(data) {
3542
+ if (data === null || data === void 0) {
3543
+ return data;
3544
+ }
3545
+ if (typeof data !== "object") {
3546
+ return data;
3547
+ }
3548
+ if (Array.isArray(data)) {
3549
+ return data.map(stripPriorityMetadata);
3550
+ }
3551
+ if (isWrappedPrimitive(data)) {
3552
+ return stripPriorityMetadata(data[".value"]);
3553
+ }
3554
+ const result = {};
3555
+ for (const [key, value] of Object.entries(data)) {
3556
+ if (key === ".priority") {
3557
+ continue;
3558
+ }
3559
+ result[key] = stripPriorityMetadata(value);
3560
+ }
3561
+ return Object.keys(result).length > 0 ? result : null;
3562
+ }
2467
3563
  var DataSnapshot = class _DataSnapshot {
2468
3564
  constructor(data, path, db, options = {}) {
2469
3565
  this._data = data;
@@ -2471,6 +3567,8 @@ var DataSnapshot = class _DataSnapshot {
2471
3567
  this._db = db;
2472
3568
  this._volatile = options.volatile ?? false;
2473
3569
  this._serverTimestamp = options.serverTimestamp ?? null;
3570
+ this._queryParams = options.queryParams ?? null;
3571
+ this._orderedKeys = options.orderedKeys ?? null;
2474
3572
  }
2475
3573
  /**
2476
3574
  * Get a DatabaseReference for the location of this snapshot.
@@ -2485,18 +3583,23 @@ var DataSnapshot = class _DataSnapshot {
2485
3583
  return getKey(this._path);
2486
3584
  }
2487
3585
  /**
2488
- * Get the data value, with `.priority` stripped if present.
2489
- * Priority is internal metadata and should not be visible to app code.
3586
+ * Get the data value, with metadata stripped.
3587
+ *
3588
+ * - Strips `.priority` from objects (it's metadata, not user data)
3589
+ * - Unwraps `{ '.value': x, '.priority': y }` to just `x` (wrapped primitives)
3590
+ *
3591
+ * @typeParam T - Optional type for the returned value. Defaults to `any`.
3592
+ * @example
3593
+ * ```typescript
3594
+ * // Untyped (returns any)
3595
+ * const data = snapshot.val();
3596
+ *
3597
+ * // Typed (returns User)
3598
+ * const user = snapshot.val<User>();
3599
+ * ```
2490
3600
  */
2491
3601
  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;
3602
+ return stripPriorityMetadata(this._data);
2500
3603
  }
2501
3604
  /**
2502
3605
  * Check if data exists at this location (is not null/undefined).
@@ -2506,48 +3609,92 @@ var DataSnapshot = class _DataSnapshot {
2506
3609
  }
2507
3610
  /**
2508
3611
  * Get a child snapshot at the specified path.
3612
+ *
3613
+ * Special handling:
3614
+ * - `.priority` returns the priority value (Firebase compatible)
3615
+ * - For wrapped primitives, only `.priority` returns data; other paths return null
3616
+ * - Non-existent paths return a snapshot with val() === null (Firebase compatible)
2509
3617
  */
2510
3618
  child(path) {
2511
3619
  const childPath = joinPath(this._path, path);
3620
+ if (path === ".priority") {
3621
+ const priority = this.getPriority();
3622
+ return new _DataSnapshot(priority, childPath, this._db, {
3623
+ volatile: this._volatile,
3624
+ serverTimestamp: this._serverTimestamp
3625
+ });
3626
+ }
3627
+ if (isWrappedPrimitive(this._data)) {
3628
+ return new _DataSnapshot(null, childPath, this._db, {
3629
+ volatile: this._volatile,
3630
+ serverTimestamp: this._serverTimestamp
3631
+ });
3632
+ }
2512
3633
  const childData = getValueAtPath(this._data, path);
2513
- return new _DataSnapshot(childData, childPath, this._db, {
3634
+ return new _DataSnapshot(childData ?? null, childPath, this._db, {
2514
3635
  volatile: this._volatile,
2515
3636
  serverTimestamp: this._serverTimestamp
2516
3637
  });
2517
3638
  }
2518
3639
  /**
2519
3640
  * Check if this snapshot has any children.
3641
+ * Excludes `.priority` metadata from consideration.
3642
+ * Wrapped primitives have no children.
2520
3643
  */
2521
3644
  hasChildren() {
2522
3645
  if (typeof this._data !== "object" || this._data === null) {
2523
3646
  return false;
2524
3647
  }
2525
- return Object.keys(this._data).length > 0;
3648
+ if (isWrappedPrimitive(this._data)) {
3649
+ return false;
3650
+ }
3651
+ const keys = Object.keys(this._data).filter((k) => k !== ".priority");
3652
+ return keys.length > 0;
2526
3653
  }
2527
3654
  /**
2528
3655
  * Check if this snapshot has a specific child.
3656
+ * `.priority` is always accessible if it exists.
3657
+ * Wrapped primitives have no children except `.priority`.
2529
3658
  */
2530
3659
  hasChild(path) {
3660
+ if (path === ".priority") {
3661
+ return this.getPriority() !== null;
3662
+ }
3663
+ if (isWrappedPrimitive(this._data)) {
3664
+ return false;
3665
+ }
2531
3666
  const childData = getValueAtPath(this._data, path);
2532
3667
  return childData !== void 0 && childData !== null;
2533
3668
  }
2534
3669
  /**
2535
3670
  * Get the number of children.
3671
+ * Excludes `.priority` metadata from count.
3672
+ * Wrapped primitives have 0 children.
2536
3673
  */
2537
3674
  numChildren() {
2538
3675
  if (typeof this._data !== "object" || this._data === null) {
2539
3676
  return 0;
2540
3677
  }
2541
- return Object.keys(this._data).length;
3678
+ if (isWrappedPrimitive(this._data)) {
3679
+ return 0;
3680
+ }
3681
+ return Object.keys(this._data).filter((k) => k !== ".priority").length;
2542
3682
  }
2543
3683
  /**
2544
- * Iterate over children. Return true from callback to stop iteration.
3684
+ * Iterate over children in the correct order.
3685
+ * Uses pre-computed orderedKeys if available (from subscription View),
3686
+ * otherwise computes sorted keys based on query params.
3687
+ * Excludes `.priority` metadata from iteration.
3688
+ * Wrapped primitives have no children to iterate.
2545
3689
  */
2546
3690
  forEach(callback) {
2547
3691
  if (typeof this._data !== "object" || this._data === null) {
2548
3692
  return;
2549
3693
  }
2550
- const keys = Object.keys(this._data);
3694
+ if (isWrappedPrimitive(this._data)) {
3695
+ return;
3696
+ }
3697
+ const keys = this._orderedKeys ?? getSortedKeys(this._data, this._queryParams);
2551
3698
  for (const key of keys) {
2552
3699
  const childSnap = this.child(key);
2553
3700
  if (callback(childSnap) === true) {
@@ -2585,7 +3732,20 @@ var DataSnapshot = class _DataSnapshot {
2585
3732
  return this._serverTimestamp;
2586
3733
  }
2587
3734
  /**
2588
- * Export the snapshot data as JSON (alias for val()).
3735
+ * Export the snapshot data with priority metadata intact.
3736
+ * Unlike val(), this preserves `.value` and `.priority` wrappers.
3737
+ * Useful for serializing data while preserving priorities.
3738
+ *
3739
+ * @typeParam T - Optional type for the returned value. Defaults to `any`.
3740
+ */
3741
+ exportVal() {
3742
+ return this._data;
3743
+ }
3744
+ /**
3745
+ * Export the snapshot data as JSON.
3746
+ * Same as exportVal() - preserves priority metadata.
3747
+ *
3748
+ * @typeParam T - Optional type for the returned value. Defaults to `any`.
2589
3749
  */
2590
3750
  toJSON() {
2591
3751
  return this._data;
@@ -2633,9 +3793,97 @@ function isVolatilePath(path, patterns) {
2633
3793
  }
2634
3794
 
2635
3795
  // src/LarkDatabase.ts
3796
+ function validateWriteData(data, path = "") {
3797
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
3798
+ return;
3799
+ }
3800
+ const obj = data;
3801
+ const keys = Object.keys(obj);
3802
+ if (".value" in obj) {
3803
+ const otherKeys = keys.filter((k) => k !== ".value" && k !== ".priority");
3804
+ if (otherKeys.length > 0) {
3805
+ const location = path || "/";
3806
+ throw new LarkError(
3807
+ "invalid_data",
3808
+ `Data at ${location} contains ".value" alongside other children (${otherKeys.join(", ")}). ".value" can only be used with ".priority" for primitives with priority.`
3809
+ );
3810
+ }
3811
+ }
3812
+ for (const key of keys) {
3813
+ if (key !== ".priority" && key !== ".value") {
3814
+ validateWriteData(obj[key], path ? `${path}/${key}` : `/${key}`);
3815
+ }
3816
+ }
3817
+ }
2636
3818
  var RECONNECT_BASE_DELAY_MS = 1e3;
2637
3819
  var RECONNECT_MAX_DELAY_MS = 3e4;
2638
3820
  var RECONNECT_JITTER_FACTOR = 0.5;
3821
+ function isServerValue(value) {
3822
+ return value !== null && typeof value === "object" && !Array.isArray(value) && ".sv" in value;
3823
+ }
3824
+ function resolveServerValuesLocally(value, currentValue) {
3825
+ if (value === null || value === void 0) {
3826
+ return value;
3827
+ }
3828
+ if (isServerValue(value)) {
3829
+ const sv = value[".sv"];
3830
+ if (sv === "timestamp") {
3831
+ return Date.now();
3832
+ }
3833
+ if (typeof sv === "object" && sv !== null && "increment" in sv) {
3834
+ const delta = sv.increment;
3835
+ if (typeof currentValue === "number") {
3836
+ return currentValue + delta;
3837
+ }
3838
+ return delta;
3839
+ }
3840
+ return value;
3841
+ }
3842
+ if (typeof value === "object" && !Array.isArray(value)) {
3843
+ const result = {};
3844
+ const currentObj = currentValue !== null && typeof currentValue === "object" && !Array.isArray(currentValue) ? currentValue : {};
3845
+ for (const [key, val] of Object.entries(value)) {
3846
+ result[key] = resolveServerValuesLocally(val, currentObj[key]);
3847
+ }
3848
+ return result;
3849
+ }
3850
+ if (Array.isArray(value)) {
3851
+ return value.map((item, index) => {
3852
+ const currentArr = Array.isArray(currentValue) ? currentValue : [];
3853
+ return resolveServerValuesLocally(item, currentArr[index]);
3854
+ });
3855
+ }
3856
+ return value;
3857
+ }
3858
+ var ServerValue = {
3859
+ /**
3860
+ * A placeholder value for auto-populating the current server timestamp.
3861
+ * The server will replace this with the actual Unix timestamp in milliseconds.
3862
+ *
3863
+ * @example
3864
+ * ```javascript
3865
+ * import { ServerValue } from '@lark-sh/client';
3866
+ * await ref.set({ createdAt: ServerValue.TIMESTAMP });
3867
+ * // Server stores: { createdAt: 1704067200000 }
3868
+ * ```
3869
+ */
3870
+ TIMESTAMP: { ".sv": "timestamp" },
3871
+ /**
3872
+ * Returns a placeholder value for atomically incrementing a numeric field.
3873
+ * If the field doesn't exist or isn't a number, it's treated as 0.
3874
+ *
3875
+ * @param delta - The amount to increment by (can be negative to decrement)
3876
+ * @returns A server value placeholder
3877
+ *
3878
+ * @example
3879
+ * ```javascript
3880
+ * import { ServerValue } from '@lark-sh/client';
3881
+ * await ref.child('score').set(ServerValue.increment(10));
3882
+ * // Atomically adds 10 to the current score
3883
+ * ```
3884
+ */
3885
+ increment: (delta) => ({ ".sv": { increment: delta } })
3886
+ };
2639
3887
  var LarkDatabase = class {
2640
3888
  constructor() {
2641
3889
  this._state = "disconnected";
@@ -2656,6 +3904,9 @@ var LarkDatabase = class {
2656
3904
  this.disconnectCallbacks = /* @__PURE__ */ new Set();
2657
3905
  this.errorCallbacks = /* @__PURE__ */ new Set();
2658
3906
  this.reconnectingCallbacks = /* @__PURE__ */ new Set();
3907
+ // .info path subscriptions (handled locally, not sent to server)
3908
+ this.infoSubscriptions = [];
3909
+ this._serverTimeOffset = 0;
2659
3910
  this.messageQueue = new MessageQueue();
2660
3911
  this.subscriptionManager = new SubscriptionManager();
2661
3912
  this.pendingWrites = new PendingWriteManager();
@@ -2710,6 +3961,13 @@ var LarkDatabase = class {
2710
3961
  get transportType() {
2711
3962
  return this._transportType;
2712
3963
  }
3964
+ /**
3965
+ * Get the estimated server time offset in milliseconds.
3966
+ * Add this to Date.now() to get approximate server time.
3967
+ */
3968
+ get serverTimeOffset() {
3969
+ return this._serverTimeOffset;
3970
+ }
2713
3971
  /**
2714
3972
  * Check if there are any pending writes waiting for acknowledgment.
2715
3973
  * Useful for showing "saving..." indicators in UI.
@@ -2793,6 +4051,9 @@ var LarkDatabase = class {
2793
4051
  const joinResponse = await this.messageQueue.registerRequest(requestId);
2794
4052
  this._volatilePaths = joinResponse.volatilePaths;
2795
4053
  this._connectionId = joinResponse.connectionId;
4054
+ if (joinResponse.serverTime != null) {
4055
+ this._serverTimeOffset = joinResponse.serverTime - Date.now();
4056
+ }
2796
4057
  const jwtPayload = decodeJwtPayload(connectResponse.token);
2797
4058
  this._auth = {
2798
4059
  uid: jwtPayload.sub,
@@ -2801,6 +4062,7 @@ var LarkDatabase = class {
2801
4062
  };
2802
4063
  this._state = "connected";
2803
4064
  this._reconnectAttempt = 0;
4065
+ this.fireConnectionStateChange();
2804
4066
  if (!isReconnect) {
2805
4067
  this.subscriptionManager.initialize({
2806
4068
  sendSubscribe: this.sendSubscribeMessage.bind(this),
@@ -2859,11 +4121,43 @@ var LarkDatabase = class {
2859
4121
  this.disconnectCallbacks.forEach((cb) => cb());
2860
4122
  }
2861
4123
  }
4124
+ /**
4125
+ * Temporarily disable the connection.
4126
+ * Disconnects from the server but preserves subscriptions for later reconnection via goOnline().
4127
+ */
4128
+ goOffline() {
4129
+ if (this._state === "connected" || this._state === "reconnecting") {
4130
+ this._intentionalDisconnect = true;
4131
+ if (this._reconnectTimer) {
4132
+ clearTimeout(this._reconnectTimer);
4133
+ this._reconnectTimer = null;
4134
+ }
4135
+ this.transport?.close();
4136
+ this.transport = null;
4137
+ this._state = "disconnected";
4138
+ this.subscriptionManager.clearCacheOnly();
4139
+ this.fireConnectionStateChange();
4140
+ }
4141
+ }
4142
+ /**
4143
+ * Re-enable the connection after goOffline().
4144
+ * Reconnects to the database and restores subscriptions.
4145
+ */
4146
+ goOnline() {
4147
+ this._intentionalDisconnect = false;
4148
+ if (this._state === "disconnected" && this._databaseId && this._connectOptions) {
4149
+ this._state = "reconnecting";
4150
+ this.reconnectingCallbacks.forEach((cb) => cb());
4151
+ this._reconnectAttempt = 0;
4152
+ this.attemptReconnect();
4153
+ }
4154
+ }
2862
4155
  /**
2863
4156
  * Full cleanup - clears all state including subscriptions.
2864
4157
  * Used for intentional disconnect.
2865
4158
  */
2866
4159
  cleanupFull() {
4160
+ const wasConnected = this._state === "connected";
2867
4161
  this.transport?.close();
2868
4162
  this.transport = null;
2869
4163
  this._state = "disconnected";
@@ -2878,6 +4172,10 @@ var LarkDatabase = class {
2878
4172
  this.subscriptionManager.clear();
2879
4173
  this.messageQueue.rejectAll(new Error("Connection closed"));
2880
4174
  this.pendingWrites.clear();
4175
+ if (wasConnected) {
4176
+ this.fireConnectionStateChange();
4177
+ }
4178
+ this.infoSubscriptions = [];
2881
4179
  }
2882
4180
  /**
2883
4181
  * Partial cleanup - preserves state needed for reconnect.
@@ -2889,6 +4187,67 @@ var LarkDatabase = class {
2889
4187
  this._auth = null;
2890
4188
  this.subscriptionManager.clearCacheOnly();
2891
4189
  this.messageQueue.rejectAll(new Error("Connection closed"));
4190
+ this.fireConnectionStateChange();
4191
+ }
4192
+ // ============================================
4193
+ // .info Path Handling
4194
+ // ============================================
4195
+ /**
4196
+ * Check if a path is a .info path (handled locally).
4197
+ */
4198
+ isInfoPath(path) {
4199
+ const normalizedPath = normalizePath(path) || "/";
4200
+ return normalizedPath === "/.info" || normalizedPath.startsWith("/.info/");
4201
+ }
4202
+ /**
4203
+ * Get the current value for a .info path.
4204
+ */
4205
+ getInfoValue(path) {
4206
+ const normalizedPath = normalizePath(path) || "/";
4207
+ if (normalizedPath === "/.info/connected") {
4208
+ return this._state === "connected";
4209
+ }
4210
+ if (normalizedPath === "/.info/serverTimeOffset") {
4211
+ return this._serverTimeOffset;
4212
+ }
4213
+ return null;
4214
+ }
4215
+ /**
4216
+ * Subscribe to a .info path.
4217
+ * Returns an unsubscribe function.
4218
+ */
4219
+ subscribeToInfo(path, callback) {
4220
+ const normalizedPath = normalizePath(path) || "/";
4221
+ const subscription = { path: normalizedPath, callback };
4222
+ this.infoSubscriptions.push(subscription);
4223
+ const value = this.getInfoValue(normalizedPath);
4224
+ const snapshot = new DataSnapshot(value, normalizedPath, this);
4225
+ setTimeout(() => callback(snapshot), 0);
4226
+ return () => {
4227
+ const index = this.infoSubscriptions.indexOf(subscription);
4228
+ if (index !== -1) {
4229
+ this.infoSubscriptions.splice(index, 1);
4230
+ }
4231
+ };
4232
+ }
4233
+ /**
4234
+ * Fire events for .info path changes.
4235
+ */
4236
+ fireInfoEvents(path) {
4237
+ const normalizedPath = normalizePath(path) || "/";
4238
+ const value = this.getInfoValue(normalizedPath);
4239
+ for (const sub of this.infoSubscriptions) {
4240
+ if (sub.path === normalizedPath) {
4241
+ const snapshot = new DataSnapshot(value, normalizedPath, this);
4242
+ sub.callback(snapshot);
4243
+ }
4244
+ }
4245
+ }
4246
+ /**
4247
+ * Fire connection state change events to .info/connected subscribers.
4248
+ */
4249
+ fireConnectionStateChange() {
4250
+ this.fireInfoEvents("/.info/connected");
2892
4251
  }
2893
4252
  /**
2894
4253
  * Schedule a reconnection attempt with exponential backoff.
@@ -3139,26 +4498,8 @@ var LarkDatabase = class {
3139
4498
  this.pendingWrites.onNack(message.n);
3140
4499
  if (message.e !== "condition_failed") {
3141
4500
  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
4501
  }
4502
+ this.subscriptionManager.handleWriteNack(message.n);
3162
4503
  }
3163
4504
  if (this.messageQueue.handleMessage(message)) {
3164
4505
  return;
@@ -3214,18 +4555,19 @@ var LarkDatabase = class {
3214
4555
  */
3215
4556
  async _sendSet(path, value) {
3216
4557
  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
- }
4558
+ validateWriteData(value, normalizedPath);
3220
4559
  const requestId = this.messageQueue.nextRequestId();
3221
4560
  const pendingWriteIds = this.subscriptionManager.getPendingWriteIdsForPath(normalizedPath);
3222
4561
  this.pendingWrites.trackWrite(requestId, "set", normalizedPath, value);
3223
4562
  this.subscriptionManager.trackPendingWrite(normalizedPath, requestId);
3224
- this.subscriptionManager.applyOptimisticWrite(normalizedPath, value, requestId, "set");
4563
+ const { value: currentValue } = this.subscriptionManager.getCachedValue(normalizedPath);
4564
+ const resolvedValue = resolveServerValuesLocally(value, currentValue);
4565
+ this.subscriptionManager.applyOptimisticWrite(normalizedPath, resolvedValue, requestId, "set");
3225
4566
  const message = {
3226
4567
  o: "s",
3227
4568
  p: normalizedPath,
3228
4569
  v: value,
4570
+ // Send original value with ServerValue placeholders to server
3229
4571
  r: requestId,
3230
4572
  pw: pendingWriteIds.length > 0 ? pendingWriteIds : void 0
3231
4573
  };
@@ -3237,18 +4579,26 @@ var LarkDatabase = class {
3237
4579
  */
3238
4580
  async _sendUpdate(path, values) {
3239
4581
  const normalizedPath = normalizePath(path) || "/";
3240
- if (this.subscriptionManager.isPathInRecovery(normalizedPath)) {
3241
- throw new LarkError("view_recovering", "Cannot write: View is recovering from failed write");
4582
+ for (const [key, value] of Object.entries(values)) {
4583
+ const fullPath = key.startsWith("/") ? key : `${normalizedPath}/${key}`;
4584
+ validateWriteData(value, fullPath);
3242
4585
  }
3243
4586
  const requestId = this.messageQueue.nextRequestId();
3244
4587
  const pendingWriteIds = this.subscriptionManager.getPendingWriteIdsForPath(normalizedPath);
3245
4588
  this.pendingWrites.trackWrite(requestId, "update", normalizedPath, values);
3246
4589
  this.subscriptionManager.trackPendingWrite(normalizedPath, requestId);
3247
- this.subscriptionManager.applyOptimisticWrite(normalizedPath, values, requestId, "update");
4590
+ const resolvedValues = {};
4591
+ for (const [key, value] of Object.entries(values)) {
4592
+ const fullPath = key.startsWith("/") ? key : `${normalizedPath}/${key}`;
4593
+ const { value: currentValue } = this.subscriptionManager.getCachedValue(fullPath);
4594
+ resolvedValues[key] = resolveServerValuesLocally(value, currentValue);
4595
+ }
4596
+ this.subscriptionManager.applyOptimisticWrite(normalizedPath, resolvedValues, requestId, "update");
3248
4597
  const message = {
3249
4598
  o: "u",
3250
4599
  p: normalizedPath,
3251
4600
  v: values,
4601
+ // Send original value with ServerValue placeholders to server
3252
4602
  r: requestId,
3253
4603
  pw: pendingWriteIds.length > 0 ? pendingWriteIds : void 0
3254
4604
  };
@@ -3260,9 +4610,6 @@ var LarkDatabase = class {
3260
4610
  */
3261
4611
  async _sendDelete(path) {
3262
4612
  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
4613
  const requestId = this.messageQueue.nextRequestId();
3267
4614
  const pendingWriteIds = this.subscriptionManager.getPendingWriteIdsForPath(normalizedPath);
3268
4615
  this.pendingWrites.trackWrite(requestId, "delete", normalizedPath);
@@ -3358,6 +4705,10 @@ var LarkDatabase = class {
3358
4705
  */
3359
4706
  async _sendOnce(path, query) {
3360
4707
  const normalizedPath = normalizePath(path) || "/";
4708
+ if (this.isInfoPath(normalizedPath)) {
4709
+ const value2 = this.getInfoValue(normalizedPath);
4710
+ return new DataSnapshot(value2, path, this);
4711
+ }
3361
4712
  if (!query) {
3362
4713
  const cached = this.subscriptionManager.getCachedValue(normalizedPath);
3363
4714
  if (cached.found) {
@@ -3374,7 +4725,7 @@ var LarkDatabase = class {
3374
4725
  };
3375
4726
  this.send(message);
3376
4727
  const value = await this.messageQueue.registerRequest(requestId);
3377
- return new DataSnapshot(value, path, this);
4728
+ return new DataSnapshot(value, path, this, { queryParams: query ?? null });
3378
4729
  }
3379
4730
  /**
3380
4731
  * @internal Send an onDisconnect operation.
@@ -3395,28 +4746,33 @@ var LarkDatabase = class {
3395
4746
  }
3396
4747
  /**
3397
4748
  * @internal Send a subscribe message to server.
4749
+ * Includes tag for non-default queries to enable proper event routing.
3398
4750
  */
3399
- async sendSubscribeMessage(path, eventTypes, queryParams) {
4751
+ async sendSubscribeMessage(path, eventTypes, queryParams, tag) {
3400
4752
  const requestId = this.messageQueue.nextRequestId();
3401
4753
  const message = {
3402
4754
  o: "sb",
3403
4755
  p: normalizePath(path) || "/",
3404
4756
  e: eventTypes,
3405
4757
  r: requestId,
3406
- ...queryParams
4758
+ ...queryParams,
4759
+ ...tag !== void 0 ? { tag } : {}
3407
4760
  };
3408
4761
  this.send(message);
3409
4762
  await this.messageQueue.registerRequest(requestId);
3410
4763
  }
3411
4764
  /**
3412
4765
  * @internal Send an unsubscribe message to server.
4766
+ * Includes query params and tag so server can identify which specific subscription to remove.
3413
4767
  */
3414
- async sendUnsubscribeMessage(path) {
4768
+ async sendUnsubscribeMessage(path, queryParams, tag) {
3415
4769
  const requestId = this.messageQueue.nextRequestId();
3416
4770
  const message = {
3417
4771
  o: "us",
3418
4772
  p: normalizePath(path) || "/",
3419
- r: requestId
4773
+ r: requestId,
4774
+ ...queryParams,
4775
+ ...tag !== void 0 ? { tag } : {}
3420
4776
  };
3421
4777
  this.send(message);
3422
4778
  await this.messageQueue.registerRequest(requestId);
@@ -3424,10 +4780,11 @@ var LarkDatabase = class {
3424
4780
  /**
3425
4781
  * @internal Create a DataSnapshot from event data.
3426
4782
  */
3427
- createSnapshot(path, value, volatile, serverTimestamp) {
4783
+ createSnapshot(path, value, volatile, serverTimestamp, orderedKeys) {
3428
4784
  return new DataSnapshot(value, path, this, {
3429
4785
  volatile,
3430
- serverTimestamp: serverTimestamp ?? null
4786
+ serverTimestamp: serverTimestamp ?? null,
4787
+ orderedKeys: orderedKeys ?? null
3431
4788
  });
3432
4789
  }
3433
4790
  // ============================================
@@ -3436,8 +4793,11 @@ var LarkDatabase = class {
3436
4793
  /**
3437
4794
  * @internal Subscribe to events at a path.
3438
4795
  */
3439
- _subscribe(path, eventType, callback, queryParams) {
3440
- return this.subscriptionManager.subscribe(path, eventType, callback, queryParams);
4796
+ _subscribe(path, eventType, callback, queryParams, queryIdentifier) {
4797
+ if (this.isInfoPath(path)) {
4798
+ return this.subscribeToInfo(path, callback);
4799
+ }
4800
+ return this.subscriptionManager.subscribe(path, eventType, callback, queryParams, queryIdentifier);
3441
4801
  }
3442
4802
  /**
3443
4803
  * @internal Unsubscribe from a specific event type at a path.
@@ -3452,6 +4812,11 @@ var LarkDatabase = class {
3452
4812
  this.subscriptionManager.unsubscribeAll(path);
3453
4813
  }
3454
4814
  };
4815
+ /**
4816
+ * Server values that are resolved by the server when a write is committed.
4817
+ * Alias for the exported ServerValue object.
4818
+ */
4819
+ LarkDatabase.ServerValue = ServerValue;
3455
4820
  export {
3456
4821
  DataSnapshot,
3457
4822
  DatabaseReference,
@@ -3459,6 +4824,8 @@ export {
3459
4824
  LarkError,
3460
4825
  OnDisconnect,
3461
4826
  PendingWriteManager,
4827
+ ServerValue,
4828
+ ThenableReference,
3462
4829
  generatePushId,
3463
4830
  isVolatilePath
3464
4831
  };