@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.d.mts +354 -58
- package/dist/index.d.ts +354 -58
- package/dist/index.js +1720 -351
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1718 -351
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
/**
|
|
865
|
-
this.
|
|
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
|
|
869
|
-
this.
|
|
870
|
-
/**
|
|
871
|
-
this.
|
|
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
|
-
|
|
890
|
-
|
|
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
|
|
969
|
-
*
|
|
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
|
-
|
|
972
|
-
this.
|
|
985
|
+
setServerCache(value) {
|
|
986
|
+
this._serverCache = this.deepClone(value);
|
|
973
987
|
this._hasReceivedInitialSnapshot = true;
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
-
*
|
|
1008
|
+
* Alias for getDisplayCache() - this is what callbacks receive.
|
|
982
1009
|
*/
|
|
983
1010
|
getCache() {
|
|
984
|
-
return this.
|
|
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 (
|
|
994
|
-
return { value:
|
|
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.
|
|
1001
|
-
const extractedValue = getValueAtPath(
|
|
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
|
-
*
|
|
1303
|
+
* Called when server events (put/patch) arrive.
|
|
1011
1304
|
*/
|
|
1012
|
-
|
|
1305
|
+
updateServerCacheChild(relativePath, value) {
|
|
1013
1306
|
if (relativePath === "/") {
|
|
1014
|
-
this.
|
|
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
|
-
|
|
1020
|
-
|
|
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.
|
|
1315
|
+
const cache = this._serverCache;
|
|
1024
1316
|
if (segments.length === 1) {
|
|
1025
1317
|
if (value === null) {
|
|
1026
|
-
delete cache[
|
|
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
|
-
|
|
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
|
|
1045
|
-
if (!
|
|
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
|
-
*
|
|
1336
|
+
* Remove a child from the SERVER cache.
|
|
1059
1337
|
*/
|
|
1060
|
-
|
|
1061
|
-
|
|
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
|
|
1395
|
+
return this._pendingWriteData.filter((w) => w.requestId !== "").map((w) => w.requestId);
|
|
1154
1396
|
}
|
|
1155
1397
|
/**
|
|
1156
|
-
* Add a pending write
|
|
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
|
-
|
|
1159
|
-
this.
|
|
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
|
-
|
|
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
|
|
1417
|
+
* Clear all pending writes.
|
|
1169
1418
|
*/
|
|
1170
1419
|
clearPendingWrites() {
|
|
1171
|
-
this.
|
|
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.
|
|
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
|
-
*
|
|
1432
|
+
* Get the pending write data (for debugging/testing).
|
|
1196
1433
|
*/
|
|
1197
|
-
|
|
1198
|
-
this.
|
|
1199
|
-
this.clearPendingWrites();
|
|
1434
|
+
getPendingWriteData() {
|
|
1435
|
+
return [...this._pendingWriteData];
|
|
1200
1436
|
}
|
|
1201
1437
|
/**
|
|
1202
|
-
*
|
|
1438
|
+
* Check if a specific write is pending.
|
|
1203
1439
|
*/
|
|
1204
|
-
|
|
1205
|
-
this.
|
|
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
|
-
|
|
1215
|
-
this.
|
|
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.
|
|
1460
|
+
this._serverCache = null;
|
|
1225
1461
|
this._orderedChildren = [];
|
|
1226
1462
|
this._hasReceivedInitialSnapshot = false;
|
|
1227
|
-
this.
|
|
1228
|
-
this.
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
1326
|
-
this.
|
|
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
|
|
1337
|
-
if (!
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
this.views.
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
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
|
|
1357
|
-
if (!
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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
|
|
1389
|
-
if (!view) return;
|
|
1718
|
+
const tag = message.tag;
|
|
1390
1719
|
if (value === void 0) return;
|
|
1391
|
-
if (
|
|
1392
|
-
|
|
1393
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
1439
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1581
|
-
return view
|
|
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
|
|
1592
|
-
|
|
1593
|
-
|
|
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
|
|
1599
|
-
|
|
1600
|
-
|
|
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
|
|
1605
|
-
|
|
1606
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
2097
|
+
view.removeServerCacheChild(relativePath);
|
|
1722
2098
|
} else {
|
|
1723
|
-
view.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
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
|
-
|
|
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
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
2294
|
-
*
|
|
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
|
-
|
|
3178
|
+
limitToLast: limit
|
|
2350
3179
|
});
|
|
2351
3180
|
}
|
|
2352
3181
|
/**
|
|
2353
|
-
*
|
|
3182
|
+
* Start at a specific value/key.
|
|
3183
|
+
* If no value is provided, starts at the beginning (null priority).
|
|
2354
3184
|
*/
|
|
2355
|
-
|
|
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
|
-
|
|
3201
|
+
startAt: { value, key }
|
|
2359
3202
|
});
|
|
2360
3203
|
}
|
|
2361
3204
|
/**
|
|
2362
|
-
*
|
|
3205
|
+
* Start after a specific value/key (exclusive).
|
|
3206
|
+
* Like startAt() but excludes the starting point.
|
|
2363
3207
|
*/
|
|
2364
|
-
|
|
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
|
-
|
|
3224
|
+
startAfter: { value, key }
|
|
2368
3225
|
});
|
|
2369
3226
|
}
|
|
2370
3227
|
/**
|
|
2371
|
-
*
|
|
3228
|
+
* End at a specific value/key.
|
|
3229
|
+
* If no value is provided, ends at the end (null priority).
|
|
2372
3230
|
*/
|
|
2373
|
-
|
|
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
|
-
|
|
3247
|
+
endAt: { value, key }
|
|
2377
3248
|
});
|
|
2378
3249
|
}
|
|
2379
3250
|
/**
|
|
2380
|
-
* End
|
|
3251
|
+
* End before a specific value/key (exclusive).
|
|
3252
|
+
* Like endAt() but excludes the ending point.
|
|
2381
3253
|
*/
|
|
2382
|
-
|
|
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
|
-
|
|
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
|
|
2489
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
3241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|