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