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.
- package/dist/app/NexaApp.d.ts +47 -3
- package/dist/app/NexaApp.js +269 -6
- package/dist/firestore/Query.d.ts +1 -1
- package/dist/firestore/Query.js +176 -125
- package/dist/firestore/QueryUtils.d.ts +4 -0
- package/dist/firestore/QueryUtils.js +85 -0
- package/dist/firestore/writes.d.ts +17 -4
- package/dist/firestore/writes.js +177 -123
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/persistence/LocalStore.d.ts +5 -0
- package/dist/persistence/LocalStore.js +57 -1
- package/dist/persistence/MutationQueue.d.ts +3 -2
- package/dist/persistence/MutationQueue.js +32 -7
- package/dist/transport/SSEClient.d.ts +6 -5
- package/dist/transport/SSEClient.js +19 -76
- package/dist/transport/WebSocketClient.d.ts +24 -0
- package/dist/transport/WebSocketClient.js +245 -0
- package/dist/types/index.d.ts +8 -1
- package/package.json +3 -2
|
@@ -1,95 +1,38 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SSEManager = exports.SSEClient = void 0;
|
|
3
|
+
exports.WebSocketClient = exports.SSEManager = exports.SSEClient = void 0;
|
|
4
|
+
const WebSocketClient_1 = require("./WebSocketClient");
|
|
5
|
+
Object.defineProperty(exports, "WebSocketClient", { enumerable: true, get: function () { return WebSocketClient_1.WebSocketClient; } });
|
|
4
6
|
class SSEClient {
|
|
5
7
|
constructor(projectId, sseUrl) {
|
|
6
|
-
this.
|
|
7
|
-
this.isListening = false;
|
|
8
|
-
this.listeners = new Map();
|
|
9
|
-
this.projectId = projectId;
|
|
10
|
-
this.sseUrl = sseUrl;
|
|
8
|
+
this.wsClient = new WebSocketClient_1.WebSocketClient(projectId, sseUrl);
|
|
11
9
|
}
|
|
12
10
|
addListener(event, callback) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (callbacks) {
|
|
21
|
-
callbacks.delete(callback);
|
|
22
|
-
if (callbacks.size === 0) {
|
|
23
|
-
this.listeners.delete(event);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
this.checkCloseConnection();
|
|
27
|
-
};
|
|
11
|
+
return this.wsClient.addListener(event, callback);
|
|
12
|
+
}
|
|
13
|
+
subscribe(path) {
|
|
14
|
+
this.wsClient.subscribe(path);
|
|
15
|
+
}
|
|
16
|
+
unsubscribe(path) {
|
|
17
|
+
this.wsClient.unsubscribe(path);
|
|
28
18
|
}
|
|
29
19
|
ensureConnected() {
|
|
30
|
-
|
|
31
|
-
return;
|
|
32
|
-
this.isListening = true;
|
|
33
|
-
this.sse = new EventSource(`${this.sseUrl}/api/firestore/${this.projectId}/sse`);
|
|
34
|
-
this.sse.onopen = () => {
|
|
35
|
-
this.dispatch('open', null);
|
|
36
|
-
};
|
|
37
|
-
this.sse.onmessage = (event) => {
|
|
38
|
-
try {
|
|
39
|
-
const payload = JSON.parse(event.data);
|
|
40
|
-
this.dispatch(payload.event || 'message', payload.data || payload);
|
|
41
|
-
this.dispatch('all', payload);
|
|
42
|
-
}
|
|
43
|
-
catch (e) {
|
|
44
|
-
console.error('[SSEClient] Error parsing SSE payload:', e);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
this.sse.onerror = (err) => {
|
|
48
|
-
this.dispatch('error', err);
|
|
49
|
-
if (this.sse) {
|
|
50
|
-
this.sse.close();
|
|
51
|
-
this.sse = null;
|
|
52
|
-
}
|
|
53
|
-
this.isListening = false;
|
|
54
|
-
if (this.hasListeners()) {
|
|
55
|
-
setTimeout(() => this.ensureConnected(), 3000);
|
|
56
|
-
}
|
|
57
|
-
};
|
|
20
|
+
this.wsClient.ensureConnected();
|
|
58
21
|
}
|
|
59
22
|
checkCloseConnection() {
|
|
60
|
-
|
|
61
|
-
this.sse.close();
|
|
62
|
-
this.sse = null;
|
|
63
|
-
this.isListening = false;
|
|
64
|
-
}
|
|
23
|
+
this.wsClient.checkCloseConnection();
|
|
65
24
|
}
|
|
66
25
|
hasListeners() {
|
|
67
|
-
|
|
68
|
-
this.listeners.forEach((set) => {
|
|
69
|
-
count += set.size;
|
|
70
|
-
});
|
|
71
|
-
return count > 0;
|
|
26
|
+
return this.wsClient.hasListeners();
|
|
72
27
|
}
|
|
73
28
|
dispatch(event, data) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
cb(data);
|
|
79
|
-
}
|
|
80
|
-
catch (e) {
|
|
81
|
-
console.error(`[SSEClient] Error in listener for event ${event}:`, e);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
}
|
|
29
|
+
this.wsClient.dispatch(event, data);
|
|
30
|
+
}
|
|
31
|
+
send(event, data) {
|
|
32
|
+
return this.wsClient.send(event, data);
|
|
85
33
|
}
|
|
86
34
|
close() {
|
|
87
|
-
|
|
88
|
-
this.sse.close();
|
|
89
|
-
this.sse = null;
|
|
90
|
-
}
|
|
91
|
-
this.isListening = false;
|
|
92
|
-
this.listeners.clear();
|
|
35
|
+
this.wsClient.close();
|
|
93
36
|
}
|
|
94
37
|
}
|
|
95
38
|
exports.SSEClient = SSEClient;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare class WebSocketClient {
|
|
2
|
+
private projectId;
|
|
3
|
+
private serverUrl;
|
|
4
|
+
private socket;
|
|
5
|
+
isConnected: boolean;
|
|
6
|
+
private listeners;
|
|
7
|
+
private subscribedPaths;
|
|
8
|
+
private broadcastChannel;
|
|
9
|
+
private isLeader;
|
|
10
|
+
private hasInitialized;
|
|
11
|
+
constructor(projectId: string, serverUrl: string);
|
|
12
|
+
private handleBroadcastMessage;
|
|
13
|
+
addListener(event: string, callback: (data: any) => void): () => void;
|
|
14
|
+
subscribe(path: string): void;
|
|
15
|
+
unsubscribe(path: string): void;
|
|
16
|
+
private broadcastToFollowers;
|
|
17
|
+
ensureConnected(): void;
|
|
18
|
+
private startSocketConnection;
|
|
19
|
+
send(event: string, data: any): Promise<any>;
|
|
20
|
+
checkCloseConnection(): void;
|
|
21
|
+
hasListeners(): boolean;
|
|
22
|
+
dispatch(event: string, data: any): void;
|
|
23
|
+
close(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebSocketClient = void 0;
|
|
4
|
+
const socket_io_client_1 = require("socket.io-client");
|
|
5
|
+
class WebSocketClient {
|
|
6
|
+
constructor(projectId, serverUrl) {
|
|
7
|
+
this.socket = null;
|
|
8
|
+
this.isConnected = false;
|
|
9
|
+
this.listeners = new Map();
|
|
10
|
+
this.subscribedPaths = new Set();
|
|
11
|
+
// Cross-tab sync properties
|
|
12
|
+
this.broadcastChannel = null;
|
|
13
|
+
this.isLeader = false;
|
|
14
|
+
this.hasInitialized = false;
|
|
15
|
+
this.projectId = projectId;
|
|
16
|
+
this.serverUrl = serverUrl;
|
|
17
|
+
if (typeof window !== 'undefined' && 'BroadcastChannel' in window) {
|
|
18
|
+
this.broadcastChannel = new BroadcastChannel(`nexabase_ws_${projectId}`);
|
|
19
|
+
this.broadcastChannel.onmessage = this.handleBroadcastMessage.bind(this);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
handleBroadcastMessage(event) {
|
|
23
|
+
const { type, payload } = event.data;
|
|
24
|
+
if (this.isLeader) {
|
|
25
|
+
if (type === 'follower_subscribe') {
|
|
26
|
+
this.socket?.emit('subscribe', this.projectId, payload.path);
|
|
27
|
+
}
|
|
28
|
+
else if (type === 'follower_unsubscribe') {
|
|
29
|
+
this.socket?.emit('unsubscribe', this.projectId, payload.path);
|
|
30
|
+
}
|
|
31
|
+
else if (type === 'follower_sync_request') {
|
|
32
|
+
this.broadcastChannel?.postMessage({ type: 'leader_state', payload: { isConnected: this.isConnected } });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
if (type === 'leader_event') {
|
|
37
|
+
const { event: socketEvent, data } = payload;
|
|
38
|
+
if (socketEvent === 'connect') {
|
|
39
|
+
this.isConnected = true;
|
|
40
|
+
this.dispatch('open', data);
|
|
41
|
+
}
|
|
42
|
+
else if (socketEvent === 'disconnect') {
|
|
43
|
+
this.isConnected = false;
|
|
44
|
+
this.dispatch('close', data);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
this.dispatch(socketEvent, data);
|
|
48
|
+
if (socketEvent === 'data_changed' || socketEvent === 'firestore_changed') {
|
|
49
|
+
this.dispatch('message', data);
|
|
50
|
+
this.dispatch('all', data);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (type === 'leader_state') {
|
|
55
|
+
this.isConnected = payload.isConnected;
|
|
56
|
+
if (this.isConnected) {
|
|
57
|
+
this.dispatch('open', {});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
addListener(event, callback) {
|
|
63
|
+
if (!this.listeners.has(event)) {
|
|
64
|
+
this.listeners.set(event, new Set());
|
|
65
|
+
}
|
|
66
|
+
this.listeners.get(event).add(callback);
|
|
67
|
+
this.ensureConnected();
|
|
68
|
+
return () => {
|
|
69
|
+
const callbacks = this.listeners.get(event);
|
|
70
|
+
if (callbacks) {
|
|
71
|
+
callbacks.delete(callback);
|
|
72
|
+
if (callbacks.size === 0) {
|
|
73
|
+
this.listeners.delete(event);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
this.checkCloseConnection();
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
subscribe(path) {
|
|
80
|
+
const cleanPath = (path || '').replace(/^\/+|\/+$/g, '');
|
|
81
|
+
this.subscribedPaths.add(cleanPath);
|
|
82
|
+
if (this.isLeader && this.socket && this.isConnected) {
|
|
83
|
+
this.socket.emit('subscribe', this.projectId, cleanPath);
|
|
84
|
+
}
|
|
85
|
+
else if (!this.isLeader && this.broadcastChannel) {
|
|
86
|
+
this.broadcastChannel.postMessage({ type: 'follower_subscribe', payload: { path: cleanPath } });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
unsubscribe(path) {
|
|
90
|
+
const cleanPath = (path || '').replace(/^\/+|\/+$/g, '');
|
|
91
|
+
this.subscribedPaths.delete(cleanPath);
|
|
92
|
+
if (this.isLeader && this.socket && this.isConnected) {
|
|
93
|
+
this.socket.emit('unsubscribe', this.projectId, cleanPath);
|
|
94
|
+
}
|
|
95
|
+
else if (!this.isLeader && this.broadcastChannel) {
|
|
96
|
+
this.broadcastChannel.postMessage({ type: 'follower_unsubscribe', payload: { path: cleanPath } });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
broadcastToFollowers(event, data) {
|
|
100
|
+
if (this.isLeader && this.broadcastChannel) {
|
|
101
|
+
this.broadcastChannel.postMessage({ type: 'leader_event', payload: { event, data } });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
ensureConnected() {
|
|
105
|
+
if (this.hasInitialized || typeof window === 'undefined')
|
|
106
|
+
return;
|
|
107
|
+
this.hasInitialized = true;
|
|
108
|
+
if (navigator.locks) {
|
|
109
|
+
navigator.locks.request(`nexabase_ws_leader_${this.projectId}`, { mode: 'exclusive', ifAvailable: false }, async (lock) => {
|
|
110
|
+
// This promise won't resolve until we explicitly return from it (which we only do on close)
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
this.isLeader = true;
|
|
113
|
+
this.startSocketConnection();
|
|
114
|
+
// If we are closed intentionally, resolve the lock
|
|
115
|
+
const originalClose = this.close.bind(this);
|
|
116
|
+
this.close = () => {
|
|
117
|
+
originalClose();
|
|
118
|
+
resolve();
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}).catch(() => {
|
|
122
|
+
// We are a follower
|
|
123
|
+
this.isLeader = false;
|
|
124
|
+
this.broadcastChannel?.postMessage({ type: 'follower_sync_request' });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Fallback if no Web Locks API
|
|
129
|
+
this.isLeader = true;
|
|
130
|
+
this.startSocketConnection();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
startSocketConnection() {
|
|
134
|
+
this.socket = (0, socket_io_client_1.io)(this.serverUrl, {
|
|
135
|
+
transports: ['websocket', 'polling'],
|
|
136
|
+
autoConnect: true,
|
|
137
|
+
reconnection: true,
|
|
138
|
+
reconnectionDelay: 1000,
|
|
139
|
+
});
|
|
140
|
+
this.socket.on('connect', () => {
|
|
141
|
+
this.isConnected = true;
|
|
142
|
+
this.dispatch('open', { socketId: this.socket?.id });
|
|
143
|
+
this.broadcastToFollowers('connect', { socketId: this.socket?.id });
|
|
144
|
+
// Always join project room
|
|
145
|
+
this.socket?.emit('subscribe', this.projectId, '');
|
|
146
|
+
// Resubscribe to all paths
|
|
147
|
+
this.subscribedPaths.forEach((path) => {
|
|
148
|
+
this.socket?.emit('subscribe', this.projectId, path);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
this.socket.on('data_changed', (payload) => {
|
|
152
|
+
this.dispatch('data_changed', payload);
|
|
153
|
+
this.dispatch('message', payload);
|
|
154
|
+
this.dispatch('all', payload);
|
|
155
|
+
this.broadcastToFollowers('data_changed', payload);
|
|
156
|
+
});
|
|
157
|
+
this.socket.on('firestore_changed', (payload) => {
|
|
158
|
+
this.dispatch('firestore_changed', payload);
|
|
159
|
+
this.dispatch('message', payload);
|
|
160
|
+
this.dispatch('all', payload);
|
|
161
|
+
this.broadcastToFollowers('firestore_changed', payload);
|
|
162
|
+
});
|
|
163
|
+
this.socket.on('activity_logged', (payload) => {
|
|
164
|
+
this.dispatch('activity_logged', payload);
|
|
165
|
+
this.broadcastToFollowers('activity_logged', payload);
|
|
166
|
+
});
|
|
167
|
+
this.socket.on('stats_update', (payload) => {
|
|
168
|
+
this.dispatch('stats_update', payload);
|
|
169
|
+
this.broadcastToFollowers('stats_update', payload);
|
|
170
|
+
});
|
|
171
|
+
this.socket.on('connect_error', (err) => {
|
|
172
|
+
this.dispatch('error', err);
|
|
173
|
+
this.broadcastToFollowers('connect_error', err);
|
|
174
|
+
});
|
|
175
|
+
this.socket.on('disconnect', (reason) => {
|
|
176
|
+
this.isConnected = false;
|
|
177
|
+
this.dispatch('close', { reason });
|
|
178
|
+
this.broadcastToFollowers('disconnect', { reason });
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
send(event, data) {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
this.ensureConnected();
|
|
184
|
+
if (!this.socket && this.isLeader) {
|
|
185
|
+
return reject(new Error('WebSocket client unavailable'));
|
|
186
|
+
}
|
|
187
|
+
if (!this.isLeader) {
|
|
188
|
+
// Followers can't send via websocket directly in this design without more plumbing.
|
|
189
|
+
// We fallback to standard HTTP API for writes anyway.
|
|
190
|
+
return reject(new Error('Cannot send WebSocket message from follower tab'));
|
|
191
|
+
}
|
|
192
|
+
this.socket.emit(event, data, (response) => {
|
|
193
|
+
if (response && response.error) {
|
|
194
|
+
reject(new Error(response.error));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
resolve(response);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
checkCloseConnection() {
|
|
203
|
+
if (!this.hasListeners() && this.subscribedPaths.size === 0 && this.socket) {
|
|
204
|
+
this.socket.disconnect();
|
|
205
|
+
this.socket = null;
|
|
206
|
+
this.isConnected = false;
|
|
207
|
+
this.hasInitialized = false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
hasListeners() {
|
|
211
|
+
let count = 0;
|
|
212
|
+
this.listeners.forEach((set) => {
|
|
213
|
+
count += set.size;
|
|
214
|
+
});
|
|
215
|
+
return count > 0;
|
|
216
|
+
}
|
|
217
|
+
dispatch(event, data) {
|
|
218
|
+
const callbacks = this.listeners.get(event);
|
|
219
|
+
if (callbacks) {
|
|
220
|
+
callbacks.forEach((cb) => {
|
|
221
|
+
try {
|
|
222
|
+
cb(data);
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
console.error(`[WebSocketClient] Error in listener for event ${event}:`, e);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
close() {
|
|
231
|
+
if (this.socket) {
|
|
232
|
+
this.socket.disconnect();
|
|
233
|
+
this.socket = null;
|
|
234
|
+
}
|
|
235
|
+
this.isConnected = false;
|
|
236
|
+
this.hasInitialized = false;
|
|
237
|
+
this.listeners.clear();
|
|
238
|
+
this.subscribedPaths.clear();
|
|
239
|
+
if (this.broadcastChannel) {
|
|
240
|
+
this.broadcastChannel.close();
|
|
241
|
+
this.broadcastChannel = null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
exports.WebSocketClient = WebSocketClient;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -140,14 +140,21 @@ export interface WriteBatch {
|
|
|
140
140
|
}
|
|
141
141
|
export interface OfflineJob {
|
|
142
142
|
id: string;
|
|
143
|
-
type: 'set' | '
|
|
143
|
+
type: 'set' | 'patch' | 'delete';
|
|
144
144
|
path: string;
|
|
145
145
|
data?: any;
|
|
146
146
|
idempotencyKey?: string;
|
|
147
|
+
precondition?: {
|
|
148
|
+
exists?: boolean;
|
|
149
|
+
updateTime?: string;
|
|
150
|
+
};
|
|
147
151
|
timestamp?: number;
|
|
148
152
|
merge?: boolean;
|
|
149
153
|
state?: 'pending' | 'acknowledged' | 'rejected';
|
|
150
154
|
retryCount?: number;
|
|
155
|
+
leaseOwner?: string;
|
|
156
|
+
leaseExpiresAt?: number;
|
|
157
|
+
sequenceNumber?: number;
|
|
151
158
|
lastError?: string;
|
|
152
159
|
}
|
|
153
160
|
export interface Transaction {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexabase-console",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
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",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"author": "NexaBase Team",
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"axios": "^1.7.9"
|
|
29
|
+
"axios": "^1.7.9",
|
|
30
|
+
"socket.io-client": "^4.8.3"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"typescript": "^5.0.0"
|