nexabase-console 2.1.2 → 2.1.3
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/app/NexaApp.d.ts +3 -0
- package/dist/app/NexaApp.js +38 -11
- package/dist/firestore/CollectionReference.d.ts +2 -2
- package/dist/firestore/CollectionReference.js +41 -2
- package/dist/firestore/DocumentReference.d.ts +1 -1
- package/dist/firestore/DocumentReference.js +41 -11
- package/dist/firestore/FieldPath.d.ts +2 -1
- package/dist/firestore/FieldPath.js +31 -4
- package/dist/firestore/FieldValue.d.ts +8 -4
- package/dist/firestore/FieldValue.js +59 -7
- package/dist/firestore/Firestore.js +16 -4
- package/dist/firestore/GeoPoint.d.ts +1 -0
- package/dist/firestore/GeoPoint.js +13 -6
- package/dist/firestore/Query.d.ts +5 -0
- package/dist/firestore/Query.js +190 -56
- package/dist/firestore/QueryUtils.d.ts +19 -2
- package/dist/firestore/QueryUtils.js +296 -58
- package/dist/firestore/Snapshot.js +62 -17
- package/dist/firestore/SnapshotManager.js +27 -12
- package/dist/firestore/Timestamp.d.ts +1 -0
- package/dist/firestore/Timestamp.js +28 -2
- package/dist/firestore/batch.js +89 -36
- package/dist/firestore/pathValidation.d.ts +17 -0
- package/dist/firestore/pathValidation.js +61 -0
- package/dist/firestore/queryConstraints.d.ts +4 -4
- package/dist/firestore/queryConstraints.js +69 -8
- package/dist/firestore/transaction.js +72 -18
- package/dist/firestore/writes.d.ts +1 -1
- package/dist/firestore/writes.js +32 -14
- package/dist/transport/WebSocketClient.d.ts +341 -9
- package/dist/transport/WebSocketClient.js +1895 -131
- package/dist/transport/WebSocketClient.test.d.ts +1 -0
- package/dist/transport/WebSocketClient.test.js +454 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,92 +1,330 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isDirectChildDocument = isDirectChildDocument;
|
|
4
|
-
exports.
|
|
4
|
+
exports.getNestedValue = getNestedValue;
|
|
5
5
|
exports.cloneNexaData = cloneNexaData;
|
|
6
|
+
exports.reviveNexaData = reviveNexaData;
|
|
6
7
|
exports.compareNexaValues = compareNexaValues;
|
|
8
|
+
exports.deepEqual = deepEqual;
|
|
7
9
|
exports.serializeConstraint = serializeConstraint;
|
|
10
|
+
const FieldPath_1 = require("./FieldPath");
|
|
8
11
|
const Timestamp_1 = require("./Timestamp");
|
|
12
|
+
const GeoPoint_1 = require("./GeoPoint");
|
|
13
|
+
const FieldValue_1 = require("./FieldValue");
|
|
14
|
+
const NexaError_1 = require("../errors/NexaError");
|
|
9
15
|
function isDirectChildDocument(collectionPath, documentPath) {
|
|
10
16
|
if (!documentPath.startsWith(`${collectionPath}/`))
|
|
11
17
|
return false;
|
|
12
18
|
return documentPath.split('/').length === collectionPath.split('/').length + 1;
|
|
13
19
|
}
|
|
14
|
-
function
|
|
20
|
+
function getNestedValue(obj, fieldPath) {
|
|
21
|
+
if (obj === null || obj === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
let segments;
|
|
24
|
+
if (fieldPath instanceof FieldPath_1.FieldPath) {
|
|
25
|
+
segments = fieldPath.segments;
|
|
26
|
+
}
|
|
27
|
+
else if (typeof fieldPath === 'string') {
|
|
28
|
+
// If exact key exists directly on object, prefer it
|
|
29
|
+
if (Object.prototype.hasOwnProperty.call(obj, fieldPath)) {
|
|
30
|
+
return obj[fieldPath];
|
|
31
|
+
}
|
|
32
|
+
segments = fieldPath.split('.');
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
let curr = obj;
|
|
38
|
+
for (const seg of segments) {
|
|
39
|
+
if (curr === null || curr === undefined || typeof curr !== 'object') {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
if (seg === '__proto__' || seg === 'constructor' || seg === 'prototype') {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
curr = curr[seg];
|
|
46
|
+
}
|
|
47
|
+
return curr;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Type-aware codec that preserves supported Firestore primitives/classes
|
|
51
|
+
* while rejecting cycles, unsupported prototypes, and prototype pollution.
|
|
52
|
+
*/
|
|
53
|
+
function cloneNexaData(data, seen = new WeakSet()) {
|
|
15
54
|
if (data === null || data === undefined)
|
|
16
55
|
return data;
|
|
17
|
-
|
|
56
|
+
const type = typeof data;
|
|
57
|
+
if (type === 'boolean' || type === 'number' || type === 'string') {
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
if (type === 'bigint' || type === 'symbol' || type === 'function') {
|
|
61
|
+
throw new NexaError_1.NexaError('invalid-argument', `Cannot serialize unsupported data type: ${type}`);
|
|
62
|
+
}
|
|
63
|
+
if (data instanceof Timestamp_1.Timestamp) {
|
|
18
64
|
return new Timestamp_1.Timestamp(data.seconds, data.nanoseconds);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
65
|
+
}
|
|
66
|
+
if (data instanceof GeoPoint_1.GeoPoint) {
|
|
67
|
+
return new GeoPoint_1.GeoPoint(data.latitude, data.longitude);
|
|
68
|
+
}
|
|
69
|
+
if (data instanceof FieldPath_1.FieldPath) {
|
|
70
|
+
return data;
|
|
71
|
+
}
|
|
72
|
+
if (FieldValue_1.FieldValue.isFieldValue(data)) {
|
|
73
|
+
return data;
|
|
74
|
+
}
|
|
75
|
+
if (data instanceof Date) {
|
|
76
|
+
if (isNaN(data.getTime())) {
|
|
77
|
+
throw new NexaError_1.NexaError('invalid-argument', 'Invalid Date object cannot be cloned.');
|
|
28
78
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
79
|
+
return new Date(data.getTime());
|
|
80
|
+
}
|
|
81
|
+
if (data?.type === 'document' && typeof data.path === 'string') {
|
|
82
|
+
return { type: 'document', path: data.path, db: data.db };
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(data)) {
|
|
85
|
+
if (seen.has(data)) {
|
|
86
|
+
throw new NexaError_1.NexaError('invalid-argument', 'Circular reference detected in array.');
|
|
87
|
+
}
|
|
88
|
+
seen.add(data);
|
|
89
|
+
const clonedArr = data.map((item) => cloneNexaData(item, seen));
|
|
90
|
+
seen.delete(data);
|
|
91
|
+
return clonedArr;
|
|
92
|
+
}
|
|
93
|
+
if (type === 'object') {
|
|
94
|
+
if (seen.has(data)) {
|
|
95
|
+
throw new NexaError_1.NexaError('invalid-argument', 'Circular reference detected in object.');
|
|
96
|
+
}
|
|
97
|
+
seen.add(data);
|
|
98
|
+
// Validate prototype: allow only plain objects or Object.create(null)
|
|
99
|
+
const proto = Object.getPrototypeOf(data);
|
|
100
|
+
if (proto !== null && proto !== Object.prototype) {
|
|
101
|
+
throw new NexaError_1.NexaError('invalid-argument', 'Custom class instances or unsupported prototypes cannot be stored.');
|
|
102
|
+
}
|
|
103
|
+
const clonedObj = {};
|
|
104
|
+
for (const key of Object.keys(data)) {
|
|
105
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
106
|
+
continue;
|
|
33
107
|
}
|
|
108
|
+
clonedObj[key] = cloneNexaData(data[key], seen);
|
|
34
109
|
}
|
|
35
|
-
|
|
110
|
+
seen.delete(data);
|
|
111
|
+
return clonedObj;
|
|
36
112
|
}
|
|
37
|
-
|
|
113
|
+
throw new NexaError_1.NexaError('invalid-argument', `Unsupported data type: ${type}`);
|
|
38
114
|
}
|
|
39
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Revives serialized JSON data back into rich Nexa/Firestore types.
|
|
117
|
+
* Never infers Timestamp from an unbranded ordinary object.
|
|
118
|
+
*/
|
|
119
|
+
function reviveNexaData(data, seen = new WeakSet()) {
|
|
40
120
|
if (data === null || data === undefined)
|
|
41
121
|
return data;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
122
|
+
const type = typeof data;
|
|
123
|
+
if (type === 'boolean' || type === 'number' || type === 'string') {
|
|
124
|
+
return data;
|
|
125
|
+
}
|
|
126
|
+
if (data instanceof Timestamp_1.Timestamp || data instanceof GeoPoint_1.GeoPoint || data instanceof FieldPath_1.FieldPath || FieldValue_1.FieldValue.isFieldValue(data)) {
|
|
127
|
+
return data;
|
|
128
|
+
}
|
|
129
|
+
if (data instanceof Date) {
|
|
130
|
+
return new Date(data.getTime());
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(data)) {
|
|
133
|
+
if (seen.has(data)) {
|
|
134
|
+
throw new NexaError_1.NexaError('invalid-argument', 'Circular reference detected in array.');
|
|
135
|
+
}
|
|
136
|
+
seen.add(data);
|
|
137
|
+
const revivedArr = data.map((item) => reviveNexaData(item, seen));
|
|
138
|
+
seen.delete(data);
|
|
139
|
+
return revivedArr;
|
|
140
|
+
}
|
|
141
|
+
if (type === 'object') {
|
|
142
|
+
if (seen.has(data)) {
|
|
143
|
+
throw new NexaError_1.NexaError('invalid-argument', 'Circular reference detected in object.');
|
|
144
|
+
}
|
|
145
|
+
seen.add(data);
|
|
146
|
+
// Explicit type markers (e.g. from backend serializer)
|
|
147
|
+
if (data.__nexa_type__ === 'timestamp' && typeof data.seconds === 'number' && typeof data.nanoseconds === 'number') {
|
|
148
|
+
seen.delete(data);
|
|
149
|
+
return new Timestamp_1.Timestamp(data.seconds, data.nanoseconds);
|
|
150
|
+
}
|
|
151
|
+
if (data.__nexa_type__ === 'geopoint' && typeof data.latitude === 'number' && typeof data.longitude === 'number') {
|
|
152
|
+
seen.delete(data);
|
|
153
|
+
return new GeoPoint_1.GeoPoint(data.latitude, data.longitude);
|
|
154
|
+
}
|
|
155
|
+
const proto = Object.getPrototypeOf(data);
|
|
156
|
+
if (proto !== null && proto !== Object.prototype) {
|
|
157
|
+
throw new NexaError_1.NexaError('invalid-argument', 'Custom class instances or unsupported prototypes cannot be revived.');
|
|
158
|
+
}
|
|
159
|
+
const revivedObj = {};
|
|
160
|
+
for (const key of Object.keys(data)) {
|
|
161
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
162
|
+
continue;
|
|
54
163
|
}
|
|
164
|
+
revivedObj[key] = reviveNexaData(data[key], seen);
|
|
55
165
|
}
|
|
56
|
-
|
|
166
|
+
seen.delete(data);
|
|
167
|
+
return revivedObj;
|
|
57
168
|
}
|
|
58
169
|
return data;
|
|
59
170
|
}
|
|
171
|
+
function getTypeOrder(v) {
|
|
172
|
+
if (v === null || v === undefined)
|
|
173
|
+
return 0;
|
|
174
|
+
if (typeof v === 'boolean')
|
|
175
|
+
return 1;
|
|
176
|
+
if (typeof v === 'number')
|
|
177
|
+
return 2;
|
|
178
|
+
if (v instanceof Timestamp_1.Timestamp || v instanceof Date)
|
|
179
|
+
return 3;
|
|
180
|
+
if (typeof v === 'string')
|
|
181
|
+
return 4;
|
|
182
|
+
if (v instanceof Uint8Array)
|
|
183
|
+
return 5;
|
|
184
|
+
if (v?.type === 'document' && typeof v.path === 'string')
|
|
185
|
+
return 6;
|
|
186
|
+
if (v instanceof GeoPoint_1.GeoPoint)
|
|
187
|
+
return 7;
|
|
188
|
+
if (Array.isArray(v))
|
|
189
|
+
return 8;
|
|
190
|
+
if (typeof v === 'object')
|
|
191
|
+
return 9;
|
|
192
|
+
return 10;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Deterministic Firestore-compatible value comparison.
|
|
196
|
+
*/
|
|
60
197
|
function compareNexaValues(a, b) {
|
|
61
198
|
if (a === b)
|
|
62
199
|
return 0;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
return 1;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
200
|
+
const orderA = getTypeOrder(a);
|
|
201
|
+
const orderB = getTypeOrder(b);
|
|
202
|
+
if (orderA !== orderB) {
|
|
203
|
+
return orderA < orderB ? -1 : 1;
|
|
204
|
+
}
|
|
205
|
+
switch (orderA) {
|
|
206
|
+
case 0: // null / undefined
|
|
207
|
+
return 0;
|
|
208
|
+
case 1: // boolean
|
|
209
|
+
return a === b ? 0 : a ? 1 : -1;
|
|
210
|
+
case 2: // number
|
|
211
|
+
if (Number.isNaN(a) && Number.isNaN(b))
|
|
212
|
+
return 0;
|
|
213
|
+
if (Number.isNaN(a))
|
|
214
|
+
return -1;
|
|
215
|
+
if (Number.isNaN(b))
|
|
216
|
+
return 1;
|
|
217
|
+
return a === b ? 0 : a < b ? -1 : 1;
|
|
218
|
+
case 3: { // Timestamp / Date
|
|
219
|
+
const tsA = a instanceof Timestamp_1.Timestamp ? a : Timestamp_1.Timestamp.fromDate(a);
|
|
220
|
+
const tsB = b instanceof Timestamp_1.Timestamp ? b : Timestamp_1.Timestamp.fromDate(b);
|
|
221
|
+
if (tsA.seconds !== tsB.seconds)
|
|
222
|
+
return tsA.seconds < tsB.seconds ? -1 : 1;
|
|
223
|
+
if (tsA.nanoseconds !== tsB.nanoseconds)
|
|
224
|
+
return tsA.nanoseconds < tsB.nanoseconds ? -1 : 1;
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
case 4: // string
|
|
228
|
+
return a === b ? 0 : a < b ? -1 : 1;
|
|
229
|
+
case 5: { // Uint8Array
|
|
230
|
+
const minLen = Math.min(a.length, b.length);
|
|
231
|
+
for (let i = 0; i < minLen; i++) {
|
|
232
|
+
if (a[i] !== b[i])
|
|
233
|
+
return a[i] < b[i] ? -1 : 1;
|
|
234
|
+
}
|
|
235
|
+
return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
|
|
236
|
+
}
|
|
237
|
+
case 6: // DocumentReference
|
|
238
|
+
return a.path === b.path ? 0 : a.path < b.path ? -1 : 1;
|
|
239
|
+
case 7: // GeoPoint
|
|
240
|
+
if (a.latitude !== b.latitude)
|
|
241
|
+
return a.latitude < b.latitude ? -1 : 1;
|
|
242
|
+
if (a.longitude !== b.longitude)
|
|
243
|
+
return a.longitude < b.longitude ? -1 : 1;
|
|
244
|
+
return 0;
|
|
245
|
+
case 8: { // Array
|
|
246
|
+
const minLen = Math.min(a.length, b.length);
|
|
247
|
+
for (let i = 0; i < minLen; i++) {
|
|
248
|
+
const cmp = compareNexaValues(a[i], b[i]);
|
|
249
|
+
if (cmp !== 0)
|
|
250
|
+
return cmp;
|
|
251
|
+
}
|
|
252
|
+
return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
|
|
253
|
+
}
|
|
254
|
+
case 9: { // Object
|
|
255
|
+
const keysA = Object.keys(a).filter((k) => k !== '__proto__' && k !== 'constructor' && k !== 'prototype').sort();
|
|
256
|
+
const keysB = Object.keys(b).filter((k) => k !== '__proto__' && k !== 'constructor' && k !== 'prototype').sort();
|
|
257
|
+
const minLen = Math.min(keysA.length, keysB.length);
|
|
258
|
+
for (let i = 0; i < minLen; i++) {
|
|
259
|
+
const keyCmp = keysA[i].localeCompare(keysB[i]);
|
|
260
|
+
if (keyCmp !== 0)
|
|
261
|
+
return keyCmp;
|
|
262
|
+
const valCmp = compareNexaValues(a[keysA[i]], b[keysB[i]]);
|
|
263
|
+
if (valCmp !== 0)
|
|
264
|
+
return valCmp;
|
|
265
|
+
}
|
|
266
|
+
return keysA.length === keysB.length ? 0 : keysA.length < keysB.length ? -1 : 1;
|
|
267
|
+
}
|
|
268
|
+
default:
|
|
73
269
|
return 0;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (a
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Deterministic deep equality check.
|
|
274
|
+
*/
|
|
275
|
+
function deepEqual(a, b, seen = new WeakSet()) {
|
|
276
|
+
if (a === b)
|
|
277
|
+
return true;
|
|
278
|
+
if (a === null || b === null || a === undefined || b === undefined)
|
|
279
|
+
return false;
|
|
280
|
+
if (typeof a !== typeof b)
|
|
281
|
+
return false;
|
|
282
|
+
if (typeof a !== 'object') {
|
|
283
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
284
|
+
return Number.isNaN(a) && Number.isNaN(b);
|
|
285
|
+
}
|
|
286
|
+
return a === b;
|
|
287
|
+
}
|
|
288
|
+
if (a instanceof Timestamp_1.Timestamp && b instanceof Timestamp_1.Timestamp) {
|
|
289
|
+
return a.isEqual(b);
|
|
290
|
+
}
|
|
291
|
+
if (a instanceof GeoPoint_1.GeoPoint && b instanceof GeoPoint_1.GeoPoint) {
|
|
292
|
+
return a.isEqual(b);
|
|
293
|
+
}
|
|
294
|
+
if (a instanceof FieldPath_1.FieldPath && b instanceof FieldPath_1.FieldPath) {
|
|
295
|
+
return a.isEqual(b);
|
|
296
|
+
}
|
|
297
|
+
if (FieldValue_1.FieldValue.isFieldValue(a) && FieldValue_1.FieldValue.isFieldValue(b)) {
|
|
298
|
+
return a.isEqual(b);
|
|
299
|
+
}
|
|
300
|
+
if (a instanceof Date && b instanceof Date) {
|
|
301
|
+
return a.getTime() === b.getTime();
|
|
302
|
+
}
|
|
303
|
+
if (a?.type === 'document' && b?.type === 'document') {
|
|
304
|
+
return a.path === b.path;
|
|
305
|
+
}
|
|
306
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
307
|
+
return false;
|
|
308
|
+
if (Array.isArray(a)) {
|
|
309
|
+
if (a.length !== b.length)
|
|
310
|
+
return false;
|
|
311
|
+
for (let i = 0; i < a.length; i++) {
|
|
312
|
+
if (!deepEqual(a[i], b[i], seen))
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
const keysA = Object.keys(a).filter((k) => k !== '__proto__' && k !== 'constructor' && k !== 'prototype');
|
|
318
|
+
const keysB = Object.keys(b).filter((k) => k !== '__proto__' && k !== 'constructor' && k !== 'prototype');
|
|
319
|
+
if (keysA.length !== keysB.length)
|
|
320
|
+
return false;
|
|
321
|
+
for (const k of keysA) {
|
|
322
|
+
if (!Object.prototype.hasOwnProperty.call(b, k))
|
|
323
|
+
return false;
|
|
324
|
+
if (!deepEqual(a[k], b[k], seen))
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
return true;
|
|
90
328
|
}
|
|
91
329
|
function serializeConstraint(c) {
|
|
92
330
|
const res = { type: c.type };
|
|
@@ -5,26 +5,39 @@ const SnapshotManager_1 = require("./SnapshotManager");
|
|
|
5
5
|
Object.defineProperty(exports, "SnapshotManager", { enumerable: true, get: function () { return SnapshotManager_1.SnapshotManager; } });
|
|
6
6
|
const Query_1 = require("./Query");
|
|
7
7
|
const NexaError_1 = require("../errors/NexaError");
|
|
8
|
-
const
|
|
8
|
+
const QueryUtils_1 = require("./QueryUtils");
|
|
9
|
+
const pathValidation_1 = require("./pathValidation");
|
|
10
|
+
let listenerSequence = 0;
|
|
9
11
|
const getCachedDoc = (docRef) => {
|
|
12
|
+
if (!docRef || !docRef.path) {
|
|
13
|
+
throw new NexaError_1.NexaError('invalid-argument', 'getCachedDoc requires a valid DocumentReference.');
|
|
14
|
+
}
|
|
15
|
+
(0, pathValidation_1.validateDocumentPath)(docRef.path);
|
|
10
16
|
const db = docRef.db;
|
|
11
|
-
const data = (db._firestoreCache[docRef.path]
|
|
17
|
+
const data = (db._firestoreCache[docRef.path] ?? null);
|
|
12
18
|
return SnapshotManager_1.SnapshotManager.createDocumentSnapshot(docRef, data, true, db.hasPendingWrites(docRef.path));
|
|
13
19
|
};
|
|
14
20
|
exports.getCachedDoc = getCachedDoc;
|
|
15
21
|
const getDoc = async (docRef) => {
|
|
22
|
+
if (!docRef || !docRef.path) {
|
|
23
|
+
throw new NexaError_1.NexaError('invalid-argument', 'getDoc requires a valid DocumentReference.');
|
|
24
|
+
}
|
|
25
|
+
(0, pathValidation_1.validateDocumentPath)(docRef.path);
|
|
16
26
|
const db = docRef.db;
|
|
17
27
|
if (db._initPromise)
|
|
18
28
|
await db._initPromise;
|
|
19
29
|
if (db.cacheStrategy === 'cache-first') {
|
|
20
30
|
const cached = (0, exports.getCachedDoc)(docRef);
|
|
21
31
|
if (cached && cached.exists()) {
|
|
32
|
+
// background refresh
|
|
22
33
|
db.client
|
|
23
34
|
.get(`/api/firestore/${db.projectId}/document?docPath=${encodeURIComponent(docRef.path)}`)
|
|
24
35
|
.then(async (res) => {
|
|
25
|
-
const data = res.data
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
const data = res.data?.data;
|
|
37
|
+
if (!db.hasPendingWrites(docRef.path)) {
|
|
38
|
+
await db._setCache(docRef.path, data);
|
|
39
|
+
db._notifySnapshotCallbacks(docRef.path, 'document_written', data);
|
|
40
|
+
}
|
|
28
41
|
})
|
|
29
42
|
.catch(() => { });
|
|
30
43
|
return cached;
|
|
@@ -32,23 +45,42 @@ const getDoc = async (docRef) => {
|
|
|
32
45
|
}
|
|
33
46
|
try {
|
|
34
47
|
const res = await db.client.get(`/api/firestore/${db.projectId}/document?docPath=${encodeURIComponent(docRef.path)}`);
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
const serverData = res.data?.data;
|
|
49
|
+
if (!db.hasPendingWrites(docRef.path)) {
|
|
50
|
+
await db._setCache(docRef.path, serverData);
|
|
51
|
+
}
|
|
52
|
+
const effectiveData = db._firestoreCache[docRef.path] ?? serverData;
|
|
53
|
+
return SnapshotManager_1.SnapshotManager.createDocumentSnapshot(docRef, effectiveData, false, db.hasPendingWrites(docRef.path));
|
|
38
54
|
}
|
|
39
55
|
catch (err) {
|
|
40
|
-
|
|
56
|
+
const status = err?.response?.status;
|
|
57
|
+
// Genuine 404: Document does not exist on server
|
|
58
|
+
if (status === 404) {
|
|
59
|
+
if (!db.hasPendingWrites(docRef.path)) {
|
|
60
|
+
await db._deleteCache(docRef.path);
|
|
61
|
+
}
|
|
62
|
+
return SnapshotManager_1.SnapshotManager.createDocumentSnapshot(docRef, db._firestoreCache[docRef.path] ?? null, false, db.hasPendingWrites(docRef.path));
|
|
63
|
+
}
|
|
64
|
+
// Permission or authentication error: throw immediately
|
|
65
|
+
if (status === 401 || status === 403) {
|
|
41
66
|
throw (0, NexaError_1.toNexaError)(err);
|
|
42
67
|
}
|
|
68
|
+
// Network / 5xx error: fallback to local cache
|
|
43
69
|
return (0, exports.getCachedDoc)(docRef);
|
|
44
70
|
}
|
|
45
71
|
};
|
|
46
72
|
exports.getDoc = getDoc;
|
|
47
73
|
const onSnapshot = (ref, callback) => {
|
|
74
|
+
if (!ref || !ref.path) {
|
|
75
|
+
throw new NexaError_1.NexaError('invalid-argument', 'onSnapshot requires a valid Reference or Query.');
|
|
76
|
+
}
|
|
77
|
+
if (typeof callback !== 'function') {
|
|
78
|
+
throw new NexaError_1.NexaError('invalid-argument', 'onSnapshot callback must be a function.');
|
|
79
|
+
}
|
|
48
80
|
const db = ref.db;
|
|
49
81
|
let active = true;
|
|
50
82
|
const pathKey = ref.path;
|
|
51
|
-
const listenerId = Math.random().toString(36).
|
|
83
|
+
const listenerId = `${Date.now()}_${Math.random().toString(36).slice(2)}_${++listenerSequence}`;
|
|
52
84
|
const keyToClean = `${ref.type}_${listenerId}_${pathKey}`;
|
|
53
85
|
let isInitial = true;
|
|
54
86
|
let lastData = undefined;
|
|
@@ -60,7 +92,7 @@ const onSnapshot = (ref, callback) => {
|
|
|
60
92
|
if (ref.type === 'collection' || ref.type === 'query') {
|
|
61
93
|
const cached = (0, Query_1.getCachedDocs)(ref);
|
|
62
94
|
const newData = cached.docs.map((d) => ({ id: d.id, data: d.data() }));
|
|
63
|
-
if (!(0,
|
|
95
|
+
if (!(0, QueryUtils_1.deepEqual)(newData, lastData)) {
|
|
64
96
|
const currentDocsForChanges = cached.docs.map((d) => ({ id: d.id, data: d.data(), doc: d }));
|
|
65
97
|
const snapshotWithChanges = SnapshotManager_1.SnapshotManager.createQuerySnapshot(cached.docs, true, db.hasPendingWrites(ref.path), lastDocsForChanges);
|
|
66
98
|
lastDocsForChanges = currentDocsForChanges;
|
|
@@ -71,17 +103,25 @@ const onSnapshot = (ref, callback) => {
|
|
|
71
103
|
else {
|
|
72
104
|
const cached = (0, exports.getCachedDoc)(ref);
|
|
73
105
|
const newData = cached.exists() ? { id: cached.id, data: cached.data() } : null;
|
|
74
|
-
if (!(0,
|
|
106
|
+
if (!(0, QueryUtils_1.deepEqual)(newData, lastData)) {
|
|
75
107
|
callback(cached);
|
|
76
108
|
lastData = newData;
|
|
77
109
|
}
|
|
78
110
|
}
|
|
79
111
|
}
|
|
80
|
-
catch (e) {
|
|
112
|
+
catch (e) {
|
|
113
|
+
console.error('[NexaBase] onSnapshot trigger error:', e);
|
|
114
|
+
}
|
|
81
115
|
};
|
|
82
116
|
db._snapshotCallbacks[keyToClean] = triggerCallback;
|
|
83
117
|
triggerCallback();
|
|
84
|
-
|
|
118
|
+
// Unified transport coordination: Prefer WebSocket if available, otherwise SSE
|
|
119
|
+
if (db.wsClient && typeof db.wsClient.subscribe === 'function') {
|
|
120
|
+
db.wsClient.subscribe(ref.path);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
db._ensureFirestoreSSE();
|
|
124
|
+
}
|
|
85
125
|
(async () => {
|
|
86
126
|
if (db._initPromise)
|
|
87
127
|
await db._initPromise;
|
|
@@ -93,7 +133,7 @@ const onSnapshot = (ref, callback) => {
|
|
|
93
133
|
if (ref.type === 'collection' || ref.type === 'query') {
|
|
94
134
|
const docs = await (0, Query_1.getDocs)(ref);
|
|
95
135
|
const newData = docs.docs.map((d) => ({ id: d.id, data: d.data() }));
|
|
96
|
-
if (!(0,
|
|
136
|
+
if (!(0, QueryUtils_1.deepEqual)(newData, lastData) && active) {
|
|
97
137
|
const currentDocsForChanges = docs.docs.map((d) => ({ id: d.id, data: d.data(), doc: d }));
|
|
98
138
|
const snapshotWithChanges = SnapshotManager_1.SnapshotManager.createQuerySnapshot(docs.docs, false, db.hasPendingWrites(ref.path), lastDocsForChanges);
|
|
99
139
|
lastDocsForChanges = currentDocsForChanges;
|
|
@@ -104,19 +144,24 @@ const onSnapshot = (ref, callback) => {
|
|
|
104
144
|
else {
|
|
105
145
|
const docData = await (0, exports.getDoc)(ref);
|
|
106
146
|
const newData = docData.exists() ? { id: docData.id, data: docData.data() } : null;
|
|
107
|
-
if (!(0,
|
|
147
|
+
if (!(0, QueryUtils_1.deepEqual)(newData, lastData) && active) {
|
|
108
148
|
callback(docData);
|
|
109
149
|
lastData = newData;
|
|
110
150
|
}
|
|
111
151
|
}
|
|
112
152
|
}
|
|
113
|
-
catch (e) {
|
|
153
|
+
catch (e) {
|
|
154
|
+
// Suppress initial fetch network errors if offline
|
|
155
|
+
}
|
|
114
156
|
}
|
|
115
157
|
})();
|
|
116
158
|
return () => {
|
|
117
159
|
active = false;
|
|
118
160
|
if (keyToClean) {
|
|
119
161
|
delete db._snapshotCallbacks[keyToClean];
|
|
162
|
+
if (db.wsClient && typeof db.wsClient.unsubscribe === 'function') {
|
|
163
|
+
db.wsClient.unsubscribe(ref.path);
|
|
164
|
+
}
|
|
120
165
|
if (Object.keys(db._snapshotCallbacks).length === 0) {
|
|
121
166
|
db._closeFirestoreSSEIfIdle();
|
|
122
167
|
}
|
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SnapshotManager = void 0;
|
|
4
|
+
const FieldPath_1 = require("./FieldPath");
|
|
5
|
+
const QueryUtils_1 = require("./QueryUtils");
|
|
4
6
|
const helpers_1 = require("../utils/helpers");
|
|
5
7
|
class SnapshotManager {
|
|
6
8
|
static createDocumentSnapshot(docRef, data, fromCache, hasPendingWrites) {
|
|
7
9
|
const segments = docRef.path.split('/');
|
|
8
10
|
const docId = segments[segments.length - 1];
|
|
9
|
-
|
|
11
|
+
const internalData = data !== null && data !== undefined ? (0, QueryUtils_1.cloneNexaData)(data) : null;
|
|
12
|
+
const snapshot = {
|
|
10
13
|
id: docId,
|
|
11
14
|
ref: docRef,
|
|
12
|
-
metadata: { fromCache, hasPendingWrites },
|
|
13
|
-
exists: () =>
|
|
14
|
-
data: () =>
|
|
15
|
-
get: (fieldPath) =>
|
|
15
|
+
metadata: Object.freeze({ fromCache, hasPendingWrites }),
|
|
16
|
+
exists: () => internalData !== null && internalData !== undefined,
|
|
17
|
+
data: () => (internalData !== null && internalData !== undefined ? (0, QueryUtils_1.cloneNexaData)(internalData) : undefined),
|
|
18
|
+
get: (fieldPath) => {
|
|
19
|
+
if (fieldPath === '__name__' || (fieldPath instanceof FieldPath_1.FieldPath && fieldPath.segments[0] === '__name__')) {
|
|
20
|
+
return docId;
|
|
21
|
+
}
|
|
22
|
+
if (internalData === null || internalData === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
const val = (0, QueryUtils_1.getNestedValue)(internalData, fieldPath);
|
|
25
|
+
return (0, QueryUtils_1.cloneNexaData)(val);
|
|
26
|
+
}
|
|
16
27
|
};
|
|
28
|
+
return Object.freeze(snapshot);
|
|
17
29
|
}
|
|
18
30
|
static createQuerySnapshot(docs, fromCache, hasPendingWrites, previousDocs) {
|
|
31
|
+
const frozenDocs = Object.freeze([...docs]);
|
|
19
32
|
const currentDocsForChanges = docs.map((d) => ({
|
|
20
33
|
id: d.id,
|
|
21
34
|
data: d.data(),
|
|
@@ -24,14 +37,16 @@ class SnapshotManager {
|
|
|
24
37
|
const changes = previousDocs
|
|
25
38
|
? (0, helpers_1.calculateDocChanges)(previousDocs, currentDocsForChanges)
|
|
26
39
|
: docs.map((d, i) => ({ type: 'added', doc: d, oldIndex: -1, newIndex: i }));
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
const frozenChanges = Object.freeze([...changes]);
|
|
41
|
+
const snapshot = {
|
|
42
|
+
docs: frozenDocs,
|
|
43
|
+
forEach: (cb) => frozenDocs.forEach(cb),
|
|
44
|
+
empty: frozenDocs.length === 0,
|
|
45
|
+
size: frozenDocs.length,
|
|
46
|
+
metadata: Object.freeze({ fromCache, hasPendingWrites }),
|
|
47
|
+
docChanges: () => frozenChanges
|
|
34
48
|
};
|
|
49
|
+
return Object.freeze(snapshot);
|
|
35
50
|
}
|
|
36
51
|
}
|
|
37
52
|
exports.SnapshotManager = SnapshotManager;
|