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.
Files changed (35) hide show
  1. package/dist/app/NexaApp.d.ts +3 -0
  2. package/dist/app/NexaApp.js +38 -11
  3. package/dist/firestore/CollectionReference.d.ts +2 -2
  4. package/dist/firestore/CollectionReference.js +41 -2
  5. package/dist/firestore/DocumentReference.d.ts +1 -1
  6. package/dist/firestore/DocumentReference.js +41 -11
  7. package/dist/firestore/FieldPath.d.ts +2 -1
  8. package/dist/firestore/FieldPath.js +31 -4
  9. package/dist/firestore/FieldValue.d.ts +8 -4
  10. package/dist/firestore/FieldValue.js +59 -7
  11. package/dist/firestore/Firestore.js +16 -4
  12. package/dist/firestore/GeoPoint.d.ts +1 -0
  13. package/dist/firestore/GeoPoint.js +13 -6
  14. package/dist/firestore/Query.d.ts +5 -0
  15. package/dist/firestore/Query.js +190 -56
  16. package/dist/firestore/QueryUtils.d.ts +19 -2
  17. package/dist/firestore/QueryUtils.js +296 -58
  18. package/dist/firestore/Snapshot.js +62 -17
  19. package/dist/firestore/SnapshotManager.js +27 -12
  20. package/dist/firestore/Timestamp.d.ts +1 -0
  21. package/dist/firestore/Timestamp.js +28 -2
  22. package/dist/firestore/batch.js +89 -36
  23. package/dist/firestore/pathValidation.d.ts +17 -0
  24. package/dist/firestore/pathValidation.js +61 -0
  25. package/dist/firestore/queryConstraints.d.ts +4 -4
  26. package/dist/firestore/queryConstraints.js +69 -8
  27. package/dist/firestore/transaction.js +72 -18
  28. package/dist/firestore/writes.d.ts +1 -1
  29. package/dist/firestore/writes.js +32 -14
  30. package/dist/transport/WebSocketClient.d.ts +341 -9
  31. package/dist/transport/WebSocketClient.js +1895 -131
  32. package/dist/transport/WebSocketClient.test.d.ts +1 -0
  33. package/dist/transport/WebSocketClient.test.js +454 -0
  34. package/dist/types/index.d.ts +1 -0
  35. 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.reviveNexaData = reviveNexaData;
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 reviveNexaData(data) {
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
- if (data instanceof Timestamp_1.Timestamp)
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
- if (Array.isArray(data))
20
- return data.map(reviveNexaData);
21
- if (typeof data === 'object') {
22
- if (typeof data.isEqual === 'function' && typeof data.toMillis === 'function') {
23
- return data;
24
- }
25
- // Revive timestamp
26
- if (typeof data.seconds === 'number' && typeof data.nanoseconds === 'number' && Object.keys(data).length <= 2) {
27
- return new Timestamp_1.Timestamp(data.seconds, data.nanoseconds);
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
- const cloned = {};
30
- for (const k in data) {
31
- if (Object.prototype.hasOwnProperty.call(data, k)) {
32
- cloned[k] = reviveNexaData(data[k]);
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
- return cloned;
110
+ seen.delete(data);
111
+ return clonedObj;
36
112
  }
37
- return data;
113
+ throw new NexaError_1.NexaError('invalid-argument', `Unsupported data type: ${type}`);
38
114
  }
39
- function cloneNexaData(data) {
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
- if (data instanceof Timestamp_1.Timestamp)
43
- return new Timestamp_1.Timestamp(data.seconds, data.nanoseconds);
44
- if (Array.isArray(data))
45
- return data.map(cloneNexaData);
46
- if (typeof data === 'object') {
47
- if (typeof data.isEqual === 'function' && typeof data.toMillis === 'function') {
48
- return data; // Treat as immutable if it's our own object like Timestamp/GeoPoint
49
- }
50
- const cloned = {};
51
- for (const k in data) {
52
- if (Object.prototype.hasOwnProperty.call(data, k)) {
53
- cloned[k] = cloneNexaData(data[k]);
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
- return cloned;
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
- if (a === null && b !== null)
64
- return -1;
65
- if (a !== null && b === null)
66
- return 1;
67
- if (a === undefined && b !== undefined)
68
- return -1;
69
- if (a !== undefined && b === undefined)
70
- return 1;
71
- if (typeof a === 'number' && typeof b === 'number') {
72
- if (Number.isNaN(a) && Number.isNaN(b))
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
- if (Number.isNaN(a))
75
- return -1;
76
- if (Number.isNaN(b))
77
- return 1;
78
- return a < b ? -1 : 1;
79
- }
80
- if (a?.toMillis && b?.toMillis) {
81
- const ma = a.toMillis();
82
- const mb = b.toMillis();
83
- return ma < mb ? -1 : (ma > mb ? 1 : 0);
84
- }
85
- if (typeof a === 'string' && typeof b === 'string')
86
- return a < b ? -1 : 1;
87
- if (typeof a === 'boolean' && typeof b === 'boolean')
88
- return a === b ? 0 : (a ? 1 : -1);
89
- return 0; // Fallback
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 helpers_1 = require("../utils/helpers");
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] || null);
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.data;
26
- await db._setCache(docRef.path, data);
27
- db._notifySnapshotCallbacks(docRef.path, 'document_written', data);
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 data = res.data.data;
36
- await db._setCache(docRef.path, data);
37
- return SnapshotManager_1.SnapshotManager.createDocumentSnapshot(docRef, data, false, db.hasPendingWrites(docRef.path));
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
- if (err && err.response && err.response.status < 500) {
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).substring(2, 9);
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, helpers_1.deepEqual)(newData, lastData)) {
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, helpers_1.deepEqual)(newData, lastData)) {
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
- db._ensureFirestoreSSE();
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, helpers_1.deepEqual)(newData, lastData) && active) {
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, helpers_1.deepEqual)(newData, lastData) && active) {
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
- return {
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: () => data !== null && data !== undefined,
14
- data: () => data,
15
- get: (fieldPath) => (0, helpers_1.getNestedValue)(data, fieldPath, docId)
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
- return {
28
- docs,
29
- forEach: (cb) => docs.forEach(cb),
30
- empty: docs.length === 0,
31
- size: docs.length,
32
- metadata: { fromCache, hasPendingWrites },
33
- docChanges: () => changes
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;
@@ -8,6 +8,7 @@ export declare class Timestamp {
8
8
  toDate(): Date;
9
9
  toMillis(): number;
10
10
  isEqual(other: Timestamp): boolean;
11
+ valueOf(): string;
11
12
  toString(): string;
12
13
  toJSON(): {
13
14
  seconds: number;