nexabase-console 2.0.6 → 2.0.8

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.
@@ -1,7 +1,7 @@
1
1
  import { AxiosInstance } from 'axios';
2
2
  import { NexaConfig, DatabaseReference, StorageReference, UploadOptions, OfflineJob } from '../types/index';
3
3
  import { HttpClient } from '../transport/HttpClient';
4
- import { SSEClient } from '../transport/SSEClient';
4
+ import { SSEClient, WebSocketClient } from '../transport/SSEClient';
5
5
  import { Auth } from '../auth/Auth';
6
6
  import { User as AuthUser, AuthSession } from '../auth/authTypes';
7
7
  import { IndexedDB } from '../persistence/IndexedDB';
@@ -16,8 +16,10 @@ export declare class NexaApp {
16
16
  endpoint: string;
17
17
  enablePersistence: boolean;
18
18
  cacheStrategy: 'network-first' | 'cache-first';
19
+ _queryCacheMetadata: Record<string, boolean>;
19
20
  httpClient: HttpClient;
20
21
  client: AxiosInstance;
22
+ wsClient: WebSocketClient;
21
23
  sseClient: SSEClient;
22
24
  authService: Auth;
23
25
  indexedDB: IndexedDB;
@@ -35,15 +37,55 @@ export declare class NexaApp {
35
37
  private _setupCrossTabSync;
36
38
  _broadcastLocalMutation(path: string, action: string, data?: any): void;
37
39
  hasPendingWrites(path: string): boolean;
38
- markInflight(path: string): void;
39
- unmarkInflight(path: string): void;
40
+ markInflight(path: string, mutationId: string): void;
41
+ unmarkInflight(path: string, mutationId: string): void;
40
42
  _initFirestoreState(): Promise<void>;
41
43
  _setCache(path: string, data: any): Promise<void>;
42
44
  _deleteCache(path: string): Promise<void>;
43
45
  _addOfflineJob(job: OfflineJob): Promise<void>;
46
+ _calculateLocalView(path: string): {
47
+ data: any | null;
48
+ hasPendingWrites: boolean;
49
+ };
50
+ _commitAtomicMutation(path: string, action: 'set' | 'patch' | 'delete', job: OfflineJob): Promise<{
51
+ data: any | null;
52
+ hasPendingWrites: boolean;
53
+ }>;
54
+ _commitMutationAck(options: {
55
+ mutationId: string;
56
+ path: string;
57
+ action: 'set' | 'patch' | 'delete';
58
+ serverDocument?: any;
59
+ updateTime?: string;
60
+ }): Promise<{
61
+ data: any | null;
62
+ hasPendingWrites: boolean;
63
+ }>;
64
+ _rejectMutationAtomically(options: {
65
+ mutationId: string;
66
+ path: string;
67
+ error: any;
68
+ }): Promise<{
69
+ data: any | null;
70
+ hasPendingWrites: boolean;
71
+ }>;
72
+ _commitAtomicQueryResults(options: {
73
+ queryKey: string;
74
+ collectionPath: string;
75
+ serverDocs: {
76
+ id: string;
77
+ fields: any;
78
+ }[];
79
+ isComplete: boolean;
80
+ constraints?: any[];
81
+ readTime?: string;
82
+ revision?: string;
83
+ }): Promise<void>;
44
84
  _syncOfflineQueue(): Promise<void>;
45
85
  _ensureFirestoreSSE(): void;
86
+ _ensureFirestoreWebSocket(): void;
46
87
  _closeFirestoreSSEIfIdle(): void;
88
+ _closeFirestoreWebSocketIfIdle(): void;
47
89
  _notifySnapshotCallbacks(path: string, actionType: string, payload: any): void;
48
90
  signInWithEmailAndPassword(email: string, password: string): Promise<AuthSession>;
49
91
  createUserWithEmailAndPassword(email: string, password: string, name?: string): Promise<AuthSession>;
@@ -61,4 +103,6 @@ export declare class NexaApp {
61
103
  path: string;
62
104
  }>;
63
105
  getDownloadURL(path: string): Promise<string>;
106
+ isConnected(): boolean;
107
+ onConnectionStateChanged(callback: (isConnected: boolean) => void): () => void;
64
108
  }
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NexaApp = void 0;
4
+ const helpers_1 = require("../utils/helpers");
5
+ const QueryUtils_1 = require("../firestore/QueryUtils");
4
6
  const HttpClient_1 = require("../transport/HttpClient");
5
7
  const SSEClient_1 = require("../transport/SSEClient");
6
8
  const Auth_1 = require("../auth/Auth");
@@ -14,6 +16,7 @@ const Storage_1 = require("../storage/Storage");
14
16
  class NexaApp {
15
17
  constructor(config) {
16
18
  this.token = null;
19
+ this._queryCacheMetadata = {};
17
20
  // Internal Firestore caches/callbacks
18
21
  this._snapshotCallbacks = {};
19
22
  this._firestoreListening = false;
@@ -31,6 +34,7 @@ class NexaApp {
31
34
  // Transport
32
35
  this.httpClient = new HttpClient_1.HttpClient(this.endpoint, () => (this.authService ? this.authService.getToken() : this.token), this.projectId, this.token);
33
36
  this.client = this.httpClient.getAxiosInstance();
37
+ this.wsClient = new SSEClient_1.WebSocketClient(this.projectId, this.endpoint);
34
38
  this.sseClient = new SSEClient_1.SSEClient(this.projectId, this.endpoint);
35
39
  // Auth & Persistence
36
40
  this.authService = new Auth_1.Auth(this.projectId, this.client, this.token);
@@ -65,11 +69,11 @@ class NexaApp {
65
69
  hasPendingWrites(path) {
66
70
  return this.mutationQueue.hasPendingWrites(path);
67
71
  }
68
- markInflight(path) {
69
- this.mutationQueue.markInflight(path);
72
+ markInflight(path, mutationId) {
73
+ this.mutationQueue.markInflight(path, mutationId);
70
74
  }
71
- unmarkInflight(path) {
72
- this.mutationQueue.unmarkInflight(path);
75
+ unmarkInflight(path, mutationId) {
76
+ this.mutationQueue.unmarkInflight(path, mutationId);
73
77
  }
74
78
  async _initFirestoreState() {
75
79
  if (!this.enablePersistence)
@@ -77,6 +81,7 @@ class NexaApp {
77
81
  try {
78
82
  await this.indexedDB.init();
79
83
  await this.localStore.loadAll();
84
+ await this.localStore.garbageCollect(1000); // 1000 docs limit
80
85
  await this.mutationQueue.init();
81
86
  if (typeof window !== 'undefined') {
82
87
  window.addEventListener('online', () => this._syncOfflineQueue());
@@ -96,6 +101,226 @@ class NexaApp {
96
101
  async _addOfflineJob(job) {
97
102
  await this.mutationQueue.addJob(job);
98
103
  }
104
+ _calculateLocalView(path) {
105
+ let base = this.localStore.getServerBaseCache()[path];
106
+ base = base !== undefined && base !== null ? (0, QueryUtils_1.cloneNexaData)(base) : base;
107
+ const mQueue = this.mutationQueue.inMemoryQueue || [];
108
+ let hasPendingWrites = false;
109
+ for (const job of mQueue) {
110
+ if (job.path !== path)
111
+ continue;
112
+ hasPendingWrites = true;
113
+ if (job.type === 'delete') {
114
+ base = null;
115
+ }
116
+ else if (job.type === 'set' && !job.merge) {
117
+ base = (0, helpers_1.applyDataTransforms)({}, job.data);
118
+ }
119
+ else if (base !== null && base !== undefined) {
120
+ base = (0, helpers_1.applyDataTransforms)(base, job.data);
121
+ }
122
+ else if (job.type === 'set' || job.type === 'patch' || job.type === 'update') {
123
+ base = (0, helpers_1.applyDataTransforms)(base || {}, job.data);
124
+ }
125
+ }
126
+ return { data: base === undefined ? null : base, hasPendingWrites };
127
+ }
128
+ async _commitAtomicMutation(path, action, job) {
129
+ const db = this.indexedDB.getRawDB();
130
+ this.mutationQueue.inMemoryQueue.push(job);
131
+ const view = this._calculateLocalView(path);
132
+ const rebasedData = view.data;
133
+ if (rebasedData === null) {
134
+ delete this.localStore.getMemoryCache()[path];
135
+ }
136
+ else {
137
+ this.localStore.getMemoryCache()[path] = rebasedData;
138
+ }
139
+ if (!db)
140
+ return view;
141
+ return new Promise((resolve, reject) => {
142
+ const tx = db.transaction(['offline_cache', 'offline_queue'], 'readwrite');
143
+ const cacheStore = tx.objectStore('offline_cache');
144
+ const queueStore = tx.objectStore('offline_queue');
145
+ if (rebasedData === null) {
146
+ const sb = this.localStore.getServerBaseCache()[path];
147
+ if (sb !== undefined) {
148
+ cacheStore.put({ path, data: null, serverBase: sb, timestamp: Date.now() });
149
+ }
150
+ else {
151
+ cacheStore.delete(path);
152
+ }
153
+ }
154
+ else {
155
+ const sb = this.localStore.getServerBaseCache()[path];
156
+ cacheStore.put({ path, data: rebasedData, serverBase: sb !== undefined ? sb : null, timestamp: Date.now() });
157
+ }
158
+ queueStore.put(job);
159
+ tx.oncomplete = () => resolve(view);
160
+ tx.onerror = () => reject(tx.error);
161
+ });
162
+ }
163
+ async _commitMutationAck(options) {
164
+ const { mutationId, path, serverDocument } = options;
165
+ const db = this.indexedDB.getRawDB();
166
+ const queue = this.mutationQueue.inMemoryQueue;
167
+ const jobIndex = queue.findIndex((j) => j.id === mutationId);
168
+ if (jobIndex !== -1)
169
+ queue.splice(jobIndex, 1);
170
+ if (options.action === 'delete') {
171
+ this.localStore.getServerBaseCache()[path] = null;
172
+ }
173
+ else if (serverDocument && serverDocument.fields) {
174
+ this.localStore.getServerBaseCache()[path] = serverDocument.fields;
175
+ }
176
+ const view = this._calculateLocalView(path);
177
+ const rebasedData = view.data;
178
+ if (rebasedData === null) {
179
+ delete this.localStore.getMemoryCache()[path];
180
+ }
181
+ else {
182
+ this.localStore.getMemoryCache()[path] = rebasedData;
183
+ }
184
+ if (!db)
185
+ return view;
186
+ return new Promise((resolve, reject) => {
187
+ const tx = db.transaction(['offline_cache', 'offline_queue'], 'readwrite');
188
+ const cacheStore = tx.objectStore('offline_cache');
189
+ const queueStore = tx.objectStore('offline_queue');
190
+ queueStore.delete(mutationId);
191
+ if (rebasedData === null) {
192
+ if (options.action === 'delete') {
193
+ cacheStore.put({ path, data: null, serverBase: null, timestamp: Date.now() });
194
+ }
195
+ else {
196
+ const sb = this.localStore.getServerBaseCache()[path];
197
+ cacheStore.put({ path, data: null, serverBase: sb !== undefined ? sb : null, timestamp: Date.now() });
198
+ }
199
+ }
200
+ else {
201
+ const sb = this.localStore.getServerBaseCache()[path];
202
+ cacheStore.put({ path, data: rebasedData, serverBase: sb !== undefined ? sb : null, timestamp: Date.now() });
203
+ }
204
+ tx.oncomplete = () => resolve(view);
205
+ tx.onerror = () => reject(tx.error);
206
+ });
207
+ }
208
+ async _rejectMutationAtomically(options) {
209
+ const { mutationId, path } = options;
210
+ const db = this.indexedDB.getRawDB();
211
+ const queue = this.mutationQueue.inMemoryQueue;
212
+ const jobIndex = queue.findIndex((j) => j.id === mutationId);
213
+ if (jobIndex !== -1)
214
+ queue.splice(jobIndex, 1);
215
+ const view = this._calculateLocalView(path);
216
+ const rebasedData = view.data;
217
+ if (rebasedData === null) {
218
+ delete this.localStore.getMemoryCache()[path];
219
+ }
220
+ else {
221
+ this.localStore.getMemoryCache()[path] = rebasedData;
222
+ }
223
+ if (!db)
224
+ return view;
225
+ return new Promise((resolve, reject) => {
226
+ const tx = db.transaction(['offline_cache', 'offline_queue'], 'readwrite');
227
+ const cacheStore = tx.objectStore('offline_cache');
228
+ const queueStore = tx.objectStore('offline_queue');
229
+ queueStore.delete(mutationId);
230
+ if (rebasedData === null) {
231
+ const sb = this.localStore.getServerBaseCache()[path];
232
+ if (sb === undefined)
233
+ cacheStore.delete(path);
234
+ else
235
+ cacheStore.put({ path, data: null, serverBase: sb, timestamp: Date.now() });
236
+ }
237
+ else {
238
+ const sb = this.localStore.getServerBaseCache()[path];
239
+ cacheStore.put({ path, data: rebasedData, serverBase: sb !== undefined ? sb : null, timestamp: Date.now() });
240
+ }
241
+ tx.oncomplete = () => resolve(view);
242
+ tx.onerror = () => reject(tx.error);
243
+ });
244
+ }
245
+ async _commitAtomicQueryResults(options) {
246
+ const { collectionPath, serverDocs, isComplete } = options;
247
+ const db = this.indexedDB.getRawDB();
248
+ const pendingMutations = new Map();
249
+ // Get jobs array directly from mutation queue memory
250
+ const mQueue = this.mutationQueue.inMemoryQueue || [];
251
+ for (const job of mQueue) {
252
+ if (!pendingMutations.has(job.path))
253
+ pendingMutations.set(job.path, []);
254
+ pendingMutations.get(job.path).push(job);
255
+ }
256
+ const finalUpdates = new Map();
257
+ const serverIds = new Set();
258
+ for (const doc of serverDocs) {
259
+ const docPath = `${collectionPath}/${doc.id}`;
260
+ serverIds.add(docPath);
261
+ let data = doc.fields;
262
+ const jobs = pendingMutations.get(docPath);
263
+ if (jobs) {
264
+ for (const job of jobs) {
265
+ if (job.type === 'delete')
266
+ data = null;
267
+ else if (job.type === 'set' && !job.merge)
268
+ data = job.data;
269
+ else if (data !== null) {
270
+ data = { ...data, ...job.data };
271
+ }
272
+ else if (job.type === 'set') {
273
+ data = job.data;
274
+ }
275
+ }
276
+ }
277
+ finalUpdates.set(docPath, data);
278
+ }
279
+ const deletedPaths = [];
280
+ if (isComplete) {
281
+ for (const cachePath of Object.keys(this.localStore.getMemoryCache())) {
282
+ if (cachePath.startsWith(collectionPath + '/') && cachePath.split('/').length === collectionPath.split('/').length + 1) {
283
+ if (!serverIds.has(cachePath)) {
284
+ const jobs = pendingMutations.get(cachePath);
285
+ const hasActivePending = jobs && jobs.length > 0 && jobs[jobs.length - 1].type !== 'delete';
286
+ if (!hasActivePending) {
287
+ deletedPaths.push(cachePath);
288
+ }
289
+ }
290
+ }
291
+ }
292
+ }
293
+ for (const path of Array.from(finalUpdates.keys())) {
294
+ const data = finalUpdates.get(path);
295
+ if (data === null)
296
+ delete this.localStore.getMemoryCache()[path];
297
+ else
298
+ this.localStore.getMemoryCache()[path] = data;
299
+ }
300
+ for (const path of deletedPaths) {
301
+ delete this.localStore.getMemoryCache()[path];
302
+ }
303
+ if (!db)
304
+ return;
305
+ return new Promise((resolve, reject) => {
306
+ const tx = db.transaction(['offline_cache'], 'readwrite');
307
+ const cacheStore = tx.objectStore('offline_cache');
308
+ for (const path of Array.from(finalUpdates.keys())) {
309
+ const data = finalUpdates.get(path);
310
+ if (data !== null) {
311
+ cacheStore.put({ path, data, timestamp: Date.now() });
312
+ }
313
+ else {
314
+ cacheStore.delete(path);
315
+ }
316
+ }
317
+ for (const path of deletedPaths) {
318
+ cacheStore.delete(path);
319
+ }
320
+ tx.oncomplete = () => resolve();
321
+ tx.onerror = () => reject(tx.error);
322
+ });
323
+ }
99
324
  async _syncOfflineQueue() {
100
325
  await this.syncEngine.syncOfflineMutations();
101
326
  }
@@ -103,7 +328,7 @@ class NexaApp {
103
328
  if (this._firestoreListening)
104
329
  return;
105
330
  this._firestoreListening = true;
106
- const unsub1 = this.sseClient.addListener('data_changed', (payload) => {
331
+ const unsub1 = this.wsClient.addListener('data_changed', (payload) => {
107
332
  if (payload && payload.path) {
108
333
  if (payload.action === 'delete') {
109
334
  this._deleteCache(payload.path);
@@ -114,11 +339,14 @@ class NexaApp {
114
339
  this._notifySnapshotCallbacks(payload.path, payload.action === 'delete' ? 'document_deleted' : 'document_written', payload.data);
115
340
  }
116
341
  });
117
- const unsub2 = this.sseClient.addListener('firestore_changed', (payload) => {
342
+ const unsub2 = this.wsClient.addListener('firestore_changed', (payload) => {
118
343
  this._notifySnapshotCallbacks(payload.collectionPath || payload.docPath, 'server_update', payload.data);
119
344
  });
120
345
  this._firestoreUnsubscribes.push(unsub1, unsub2);
121
346
  }
347
+ _ensureFirestoreWebSocket() {
348
+ this._ensureFirestoreSSE();
349
+ }
122
350
  _closeFirestoreSSEIfIdle() {
123
351
  if (Object.keys(this._snapshotCallbacks).length === 0) {
124
352
  this._firestoreUnsubscribes.forEach((unsub) => unsub());
@@ -126,6 +354,9 @@ class NexaApp {
126
354
  this._firestoreListening = false;
127
355
  }
128
356
  }
357
+ _closeFirestoreWebSocketIfIdle() {
358
+ this._closeFirestoreSSEIfIdle();
359
+ }
129
360
  _notifySnapshotCallbacks(path, actionType, payload) {
130
361
  Object.keys(this._snapshotCallbacks).forEach((cbKey) => {
131
362
  const parts = cbKey.split('_');
@@ -180,5 +411,37 @@ class NexaApp {
180
411
  async getDownloadURL(path) {
181
412
  return this.storage.getDownloadURL(path);
182
413
  }
414
+ isConnected() {
415
+ return this.wsClient.isConnected;
416
+ }
417
+ onConnectionStateChanged(callback) {
418
+ let lastState = this.isConnected();
419
+ callback(lastState);
420
+ // Make sure socket is created and connected if it isn't already
421
+ this.wsClient.ensureConnected();
422
+ const unsubOpen = this.wsClient.addListener('open', () => {
423
+ if (!lastState) {
424
+ lastState = true;
425
+ callback(true);
426
+ }
427
+ });
428
+ const unsubClose = this.wsClient.addListener('close', () => {
429
+ if (lastState) {
430
+ lastState = false;
431
+ callback(false);
432
+ }
433
+ });
434
+ const unsubError = this.wsClient.addListener('error', () => {
435
+ if (lastState) {
436
+ lastState = false;
437
+ callback(false);
438
+ }
439
+ });
440
+ return () => {
441
+ unsubOpen();
442
+ unsubClose();
443
+ unsubError();
444
+ };
445
+ }
183
446
  }
184
447
  exports.NexaApp = NexaApp;
@@ -1,7 +1,7 @@
1
1
  import { FieldPath } from './FieldPath';
2
2
  import { CollectionReference, Query, QueryConstraint, QuerySnapshot, AggregateField, AggregateSpec, AggregateSpecData, AggregateQuerySnapshot } from '../types/index';
3
3
  export declare const query: <T = any>(queryObject: Query<T> | CollectionReference<T>, ...queryConstraints: QueryConstraint[]) => Query<T>;
4
- export declare const getCachedDocs: <T = any>(queryOrCollection: Query<T> | CollectionReference<T>) => QuerySnapshot<T>;
4
+ export declare const getCachedDocs: <T = any>(queryOrCollection: Query<T> | CollectionReference<T>, fromCache?: boolean) => QuerySnapshot<T>;
5
5
  export declare const getDocs: <T = any>(queryOrCollection: Query<T> | CollectionReference<T>) => Promise<QuerySnapshot<T>>;
6
6
  export declare const count: () => AggregateField<number>;
7
7
  export declare const sum: (field: string | FieldPath) => AggregateField<number>;