nexabase-console 2.1.3 → 2.1.5

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.
@@ -36,6 +36,7 @@ export declare class NexaApp {
36
36
  _activeQueryTimestamps: Map<string, number>;
37
37
  constructor(config: NexaConfig);
38
38
  get _firestoreCache(): Record<string, any>;
39
+ private _setupVisibilityResync;
39
40
  private _setupCrossTabSync;
40
41
  _broadcastLocalMutation(path: string, action: string, data?: any): void;
41
42
  getPendingWritesCount(): number;
@@ -92,7 +93,7 @@ export declare class NexaApp {
92
93
  _ensureFirestoreWebSocket(): void;
93
94
  _closeFirestoreSSEIfIdle(): void;
94
95
  _closeFirestoreWebSocketIfIdle(): void;
95
- _notifySnapshotCallbacks(path: string, actionType: string, payload: any): void;
96
+ _notifySnapshotCallbacks(path: string, actionType: string, payload?: any): void;
96
97
  signInWithEmailAndPassword(email: string, password: string): Promise<AuthSession>;
97
98
  createUserWithEmailAndPassword(email: string, password: string, name?: string): Promise<AuthSession>;
98
99
  sendOtp(email: string): Promise<{
@@ -46,23 +46,51 @@ class NexaApp {
46
46
  this.mutationQueue = new MutationQueue_1.MutationQueue(this.projectId, this.indexedDB, this.client);
47
47
  this.syncEngine = new SyncEngine_1.SyncEngine(this.projectId, this.mutationQueue, this.sseClient);
48
48
  // Realtime & Storage
49
- const realtimeConn = new RealtimeConnection_1.RealtimeConnection(this.sseClient);
49
+ const realtimeConn = new RealtimeConnection_1.RealtimeConnection(this.sseClient, this.wsClient);
50
50
  this.realtimeDatabase = new RealtimeDatabase_1.RealtimeDatabase(this.projectId, this.client, realtimeConn);
51
51
  this.storage = new Storage_1.Storage(this.projectId, this.client);
52
52
  this._setupCrossTabSync();
53
+ this._setupVisibilityResync();
53
54
  }
54
55
  get _firestoreCache() {
55
56
  return this.localStore.getMemoryCache();
56
57
  }
58
+ _setupVisibilityResync() {
59
+ if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
60
+ document.addEventListener('visibilitychange', () => {
61
+ if (document.visibilityState === 'visible') {
62
+ // Re-ensure SSE and WS when coming back from background
63
+ if (this._firestoreListening) {
64
+ if (this.wsClient && typeof this.wsClient.ensureConnected === 'function') {
65
+ this.wsClient.ensureConnected();
66
+ }
67
+ if (this.sseClient && typeof this.sseClient.ensureConnected === 'function') {
68
+ this.sseClient.ensureConnected();
69
+ }
70
+ // Notify server that we need latest changes by simulating subscribe again for each path?
71
+ // wsClient handles this on resync event.
72
+ }
73
+ // Optionally sync offline queue
74
+ this._syncOfflineQueue().catch(console.error);
75
+ }
76
+ });
77
+ }
78
+ }
57
79
  _setupCrossTabSync() {
58
80
  this.syncEngine.listenCrossTab((path, action, data) => {
81
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
59
82
  if (action === 'delete') {
60
- this.localStore.delete(path);
83
+ this.localStore.delete(cleanPath);
61
84
  }
62
85
  else if (data) {
63
- this.localStore.set(path, data);
86
+ this.localStore.set(cleanPath, data);
87
+ }
88
+ this._notifySnapshotCallbacks(cleanPath, action === 'delete' ? 'document_deleted' : 'document_written', data);
89
+ const parts = cleanPath.split('/');
90
+ const colPath = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
91
+ if (colPath) {
92
+ this._notifySnapshotCallbacks(colPath, action === 'delete' ? 'document_deleted' : 'document_written', data);
64
93
  }
65
- this._notifySnapshotCallbacks(path, action === 'delete' ? 'document_deleted' : 'document_written', data);
66
94
  });
67
95
  }
68
96
  _broadcastLocalMutation(path, action, data) {
@@ -366,34 +394,44 @@ class NexaApp {
366
394
  const targetDocPath = docPath || rawPath;
367
395
  const isDelete = type === 'document_deleted' || action === 'delete' || type === 'delete';
368
396
  if (targetDocPath && typeof targetDocPath === 'string') {
397
+ const cleanDocPath = targetDocPath.replace(/^\/+|\/+$/g, '');
369
398
  if (isDelete) {
370
- this._deleteCache(targetDocPath);
399
+ this._deleteCache(cleanDocPath);
371
400
  }
372
- else if (data) {
373
- this._setCache(targetDocPath, data);
401
+ else if (data !== undefined && data !== null) {
402
+ this._setCache(cleanDocPath, data);
374
403
  }
375
- const parts = targetDocPath.split('/');
404
+ const parts = cleanDocPath.split('/');
376
405
  const colPath = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
377
- this._notifySnapshotCallbacks(targetDocPath, isDelete ? 'document_deleted' : 'document_written', data);
406
+ this._notifySnapshotCallbacks(cleanDocPath, isDelete ? 'document_deleted' : 'document_written', data);
378
407
  if (colPath) {
379
408
  this._notifySnapshotCallbacks(colPath, isDelete ? 'document_deleted' : 'document_written', data);
380
409
  }
381
410
  }
382
411
  else if (collectionPath && typeof collectionPath === 'string') {
383
- this._notifySnapshotCallbacks(collectionPath, 'server_update', data);
412
+ const cleanColPath = collectionPath.replace(/^\/+|\/+$/g, '');
413
+ this._notifySnapshotCallbacks(cleanColPath, 'server_update', data);
384
414
  }
385
415
  }
386
416
  _ensureFirestoreSSE() {
387
417
  if (this._firestoreListening)
388
418
  return;
389
419
  this._firestoreListening = true;
420
+ // Listen to WebSocketClient
390
421
  const unsub1 = this.wsClient.addListener('data_changed', (payload) => {
391
422
  this._handleServerFirestoreChange(payload);
392
423
  });
393
424
  const unsub2 = this.wsClient.addListener('firestore_changed', (payload) => {
394
425
  this._handleServerFirestoreChange(payload);
395
426
  });
396
- this._firestoreUnsubscribes.push(unsub1, unsub2);
427
+ // Also listen to SSEClient
428
+ const unsub3 = this.sseClient.addListener('data_changed', (payload) => {
429
+ this._handleServerFirestoreChange(payload);
430
+ });
431
+ const unsub4 = this.sseClient.addListener('firestore_changed', (payload) => {
432
+ this._handleServerFirestoreChange(payload);
433
+ });
434
+ this._firestoreUnsubscribes.push(unsub1, unsub2, unsub3, unsub4);
397
435
  }
398
436
  _ensureFirestoreWebSocket() {
399
437
  this._ensureFirestoreSSE();
@@ -411,14 +449,28 @@ class NexaApp {
411
449
  _notifySnapshotCallbacks(path, actionType, payload) {
412
450
  if (!path || typeof path !== 'string')
413
451
  return;
414
- Object.keys(this._snapshotCallbacks).forEach((cbKey) => {
415
- const parts = cbKey.split('_');
416
- const targetPath = parts.slice(2).join('_');
417
- if (cbKey.startsWith('document_') && targetPath === path) {
418
- this._snapshotCallbacks[cbKey](payload);
452
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
453
+ Object.entries(this._snapshotCallbacks).forEach(([cbKey, callback]) => {
454
+ let type = '';
455
+ let targetPath = '';
456
+ if (cbKey.includes('::')) {
457
+ const parts = cbKey.split('::');
458
+ type = parts[0];
459
+ targetPath = parts.slice(2).join('::');
460
+ }
461
+ else {
462
+ const firstUnderscore = cbKey.indexOf('_');
463
+ const lastUnderscore = cbKey.lastIndexOf('_');
464
+ type = cbKey.substring(0, firstUnderscore);
465
+ targetPath = cbKey.substring(lastUnderscore + 1);
466
+ }
467
+ const cleanTargetPath = (targetPath || '').replace(/^\/+|\/+$/g, '');
468
+ if (type === 'document' && cleanTargetPath === cleanPath) {
469
+ callback(payload);
419
470
  }
420
- else if ((cbKey.startsWith('collection_') || cbKey.startsWith('query_')) && (path.startsWith(targetPath + '/') || path === targetPath)) {
421
- this._snapshotCallbacks[cbKey](payload);
471
+ else if ((type === 'collection' || type === 'query') &&
472
+ (cleanPath === cleanTargetPath || cleanPath.startsWith(cleanTargetPath + '/'))) {
473
+ callback(payload);
422
474
  }
423
475
  });
424
476
  }
@@ -79,9 +79,9 @@ const onSnapshot = (ref, callback) => {
79
79
  }
80
80
  const db = ref.db;
81
81
  let active = true;
82
- const pathKey = ref.path;
82
+ const pathKey = ref.path.replace(/^\/+|\/+$/g, '');
83
83
  const listenerId = `${Date.now()}_${Math.random().toString(36).slice(2)}_${++listenerSequence}`;
84
- const keyToClean = `${ref.type}_${listenerId}_${pathKey}`;
84
+ const keyToClean = `${ref.type}::${listenerId}::${pathKey}`;
85
85
  let isInitial = true;
86
86
  let lastData = undefined;
87
87
  let lastDocsForChanges = [];
@@ -115,13 +115,11 @@ const onSnapshot = (ref, callback) => {
115
115
  };
116
116
  db._snapshotCallbacks[keyToClean] = triggerCallback;
117
117
  triggerCallback();
118
- // Unified transport coordination: Prefer WebSocket if available, otherwise SSE
118
+ // Unified transport coordination: Always ensure listening on change events & subscribe path
119
+ db._ensureFirestoreWebSocket();
119
120
  if (db.wsClient && typeof db.wsClient.subscribe === 'function') {
120
121
  db.wsClient.subscribe(ref.path);
121
122
  }
122
- else {
123
- db._ensureFirestoreSSE();
124
- }
125
123
  (async () => {
126
124
  if (db._initPromise)
127
125
  await db._initPromise;
@@ -48,14 +48,20 @@ const normalizeError = (error) => {
48
48
  };
49
49
  };
50
50
  const safelyNotifyMutation = (db, path, action, data, metadata) => {
51
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
51
52
  try {
52
- db._broadcastLocalMutation(path, action, data, metadata);
53
+ db._broadcastLocalMutation(cleanPath, action, data, metadata);
53
54
  }
54
55
  catch (e) {
55
56
  console.error('[NexaBase] Listener error:', e);
56
57
  }
57
58
  try {
58
- db._notifySnapshotCallbacks(path, action === 'delete' ? 'document_deleted' : 'document_written', data, metadata);
59
+ db._notifySnapshotCallbacks(cleanPath, action === 'delete' ? 'document_deleted' : 'document_written', data, metadata);
60
+ const parts = cleanPath.split('/');
61
+ const colPath = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
62
+ if (colPath) {
63
+ db._notifySnapshotCallbacks(colPath, action === 'delete' ? 'document_deleted' : 'document_written', data, metadata);
64
+ }
59
65
  }
60
66
  catch (e) {
61
67
  console.error('[NexaBase] Listener error:', e);
@@ -1,7 +1,8 @@
1
- import { SSEClient } from '../transport/SSEClient';
1
+ import { SSEClient, WebSocketClient } from '../transport/SSEClient';
2
2
  export declare class RealtimeConnection {
3
3
  private sseClient;
4
- constructor(sseClient: SSEClient);
4
+ private wsClient?;
5
+ constructor(sseClient: SSEClient, wsClient?: WebSocketClient);
5
6
  subscribeToPath(path: string, callback: (value: any) => void): () => void;
6
7
  close(): void;
7
8
  }
@@ -2,15 +2,40 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RealtimeConnection = void 0;
4
4
  class RealtimeConnection {
5
- constructor(sseClient) {
5
+ constructor(sseClient, wsClient) {
6
6
  this.sseClient = sseClient;
7
+ this.wsClient = wsClient;
7
8
  }
8
9
  subscribeToPath(path, callback) {
9
- return this.sseClient.addListener('data_changed', (payload) => {
10
- if (payload && payload.path === path) {
11
- callback(payload.value);
10
+ const cleanPath = (path || '').replace(/^\/+|\/+$/g, '');
11
+ if (this.wsClient && typeof this.wsClient.subscribe === 'function') {
12
+ this.wsClient.subscribe(cleanPath);
13
+ }
14
+ const unsubWS = this.wsClient
15
+ ? this.wsClient.addListener('data_changed', (payload) => {
16
+ if (payload) {
17
+ const p = (payload.path || payload.docPath || '').replace(/^\/+|\/+$/g, '');
18
+ if (p === cleanPath) {
19
+ callback(payload.value !== undefined ? payload.value : payload.data);
20
+ }
21
+ }
22
+ })
23
+ : () => { };
24
+ const unsubSSE = this.sseClient.addListener('data_changed', (payload) => {
25
+ if (payload) {
26
+ const p = (payload.path || payload.docPath || '').replace(/^\/+|\/+$/g, '');
27
+ if (p === cleanPath) {
28
+ callback(payload.value !== undefined ? payload.value : payload.data);
29
+ }
12
30
  }
13
31
  });
32
+ return () => {
33
+ unsubWS();
34
+ unsubSSE();
35
+ if (this.wsClient && typeof this.wsClient.unsubscribe === 'function') {
36
+ this.wsClient.unsubscribe(cleanPath);
37
+ }
38
+ };
14
39
  }
15
40
  close() {
16
41
  this.sseClient.close();
@@ -1882,6 +1882,7 @@ class WebSocketClient {
1882
1882
  return;
1883
1883
  }
1884
1884
  if (document.visibilityState === 'visible') {
1885
+ this.dispatch('resync', { reason: 'visibility_change' });
1885
1886
  if (!this.isLeader && this._coordinationMode === 'shared') {
1886
1887
  this.postBroadcast('follower_heartbeat', {
1887
1888
  globalListenersCount: this.getGlobalListenersCount(),
@@ -1893,6 +1894,9 @@ class WebSocketClient {
1893
1894
  });
1894
1895
  this.postBroadcast('follower_sync_request');
1895
1896
  }
1897
+ if (this.isLeader && this._socket && !this._socket.connected) {
1898
+ this._socket.connect();
1899
+ }
1896
1900
  if (this.hasLocalNeeds() && this._state === 'idle') {
1897
1901
  this.ensureConnected();
1898
1902
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexabase-console",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
4
  "description": "SDK Client resmi untuk NexaBase: Platform Sinkronisasi NoSQL, Realtime, File Storage, & Autentikasi Offline-First.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",