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.
- package/dist/app/NexaApp.d.ts +2 -1
- package/dist/app/NexaApp.js +70 -18
- package/dist/firestore/Snapshot.js +4 -6
- package/dist/firestore/writes.js +8 -2
- package/dist/realtime/RealtimeConnection.d.ts +3 -2
- package/dist/realtime/RealtimeConnection.js +29 -4
- package/dist/transport/WebSocketClient.js +4 -0
- package/package.json +1 -1
package/dist/app/NexaApp.d.ts
CHANGED
|
@@ -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
|
|
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<{
|
package/dist/app/NexaApp.js
CHANGED
|
@@ -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(
|
|
83
|
+
this.localStore.delete(cleanPath);
|
|
61
84
|
}
|
|
62
85
|
else if (data) {
|
|
63
|
-
this.localStore.set(
|
|
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(
|
|
399
|
+
this._deleteCache(cleanDocPath);
|
|
371
400
|
}
|
|
372
|
-
else if (data) {
|
|
373
|
-
this._setCache(
|
|
401
|
+
else if (data !== undefined && data !== null) {
|
|
402
|
+
this._setCache(cleanDocPath, data);
|
|
374
403
|
}
|
|
375
|
-
const parts =
|
|
404
|
+
const parts = cleanDocPath.split('/');
|
|
376
405
|
const colPath = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
|
|
377
|
-
this._notifySnapshotCallbacks(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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 ((
|
|
421
|
-
|
|
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}
|
|
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:
|
|
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;
|
package/dist/firestore/writes.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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