nexabase-console 2.0.5 → 2.0.7

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';
@@ -18,6 +18,7 @@ export declare class NexaApp {
18
18
  cacheStrategy: 'network-first' | 'cache-first';
19
19
  httpClient: HttpClient;
20
20
  client: AxiosInstance;
21
+ wsClient: WebSocketClient;
21
22
  sseClient: SSEClient;
22
23
  authService: Auth;
23
24
  indexedDB: IndexedDB;
@@ -43,7 +44,9 @@ export declare class NexaApp {
43
44
  _addOfflineJob(job: OfflineJob): Promise<void>;
44
45
  _syncOfflineQueue(): Promise<void>;
45
46
  _ensureFirestoreSSE(): void;
47
+ _ensureFirestoreWebSocket(): void;
46
48
  _closeFirestoreSSEIfIdle(): void;
49
+ _closeFirestoreWebSocketIfIdle(): void;
47
50
  _notifySnapshotCallbacks(path: string, actionType: string, payload: any): void;
48
51
  signInWithEmailAndPassword(email: string, password: string): Promise<AuthSession>;
49
52
  createUserWithEmailAndPassword(email: string, password: string, name?: string): Promise<AuthSession>;
@@ -61,4 +64,6 @@ export declare class NexaApp {
61
64
  path: string;
62
65
  }>;
63
66
  getDownloadURL(path: string): Promise<string>;
67
+ isConnected(): boolean;
68
+ onConnectionStateChanged(callback: (isConnected: boolean) => void): () => void;
64
69
  }
@@ -31,6 +31,7 @@ class NexaApp {
31
31
  // Transport
32
32
  this.httpClient = new HttpClient_1.HttpClient(this.endpoint, () => (this.authService ? this.authService.getToken() : this.token), this.projectId, this.token);
33
33
  this.client = this.httpClient.getAxiosInstance();
34
+ this.wsClient = new SSEClient_1.WebSocketClient(this.projectId, this.endpoint);
34
35
  this.sseClient = new SSEClient_1.SSEClient(this.projectId, this.endpoint);
35
36
  // Auth & Persistence
36
37
  this.authService = new Auth_1.Auth(this.projectId, this.client, this.token);
@@ -77,6 +78,7 @@ class NexaApp {
77
78
  try {
78
79
  await this.indexedDB.init();
79
80
  await this.localStore.loadAll();
81
+ await this.localStore.garbageCollect(1000); // 1000 docs limit
80
82
  await this.mutationQueue.init();
81
83
  if (typeof window !== 'undefined') {
82
84
  window.addEventListener('online', () => this._syncOfflineQueue());
@@ -103,7 +105,7 @@ class NexaApp {
103
105
  if (this._firestoreListening)
104
106
  return;
105
107
  this._firestoreListening = true;
106
- const unsub1 = this.sseClient.addListener('data_changed', (payload) => {
108
+ const unsub1 = this.wsClient.addListener('data_changed', (payload) => {
107
109
  if (payload && payload.path) {
108
110
  if (payload.action === 'delete') {
109
111
  this._deleteCache(payload.path);
@@ -114,11 +116,14 @@ class NexaApp {
114
116
  this._notifySnapshotCallbacks(payload.path, payload.action === 'delete' ? 'document_deleted' : 'document_written', payload.data);
115
117
  }
116
118
  });
117
- const unsub2 = this.sseClient.addListener('firestore_changed', (payload) => {
119
+ const unsub2 = this.wsClient.addListener('firestore_changed', (payload) => {
118
120
  this._notifySnapshotCallbacks(payload.collectionPath || payload.docPath, 'server_update', payload.data);
119
121
  });
120
122
  this._firestoreUnsubscribes.push(unsub1, unsub2);
121
123
  }
124
+ _ensureFirestoreWebSocket() {
125
+ this._ensureFirestoreSSE();
126
+ }
122
127
  _closeFirestoreSSEIfIdle() {
123
128
  if (Object.keys(this._snapshotCallbacks).length === 0) {
124
129
  this._firestoreUnsubscribes.forEach((unsub) => unsub());
@@ -126,6 +131,9 @@ class NexaApp {
126
131
  this._firestoreListening = false;
127
132
  }
128
133
  }
134
+ _closeFirestoreWebSocketIfIdle() {
135
+ this._closeFirestoreSSEIfIdle();
136
+ }
129
137
  _notifySnapshotCallbacks(path, actionType, payload) {
130
138
  Object.keys(this._snapshotCallbacks).forEach((cbKey) => {
131
139
  const parts = cbKey.split('_');
@@ -180,5 +188,37 @@ class NexaApp {
180
188
  async getDownloadURL(path) {
181
189
  return this.storage.getDownloadURL(path);
182
190
  }
191
+ isConnected() {
192
+ return this.wsClient.isConnected;
193
+ }
194
+ onConnectionStateChanged(callback) {
195
+ let lastState = this.isConnected();
196
+ callback(lastState);
197
+ // Make sure socket is created and connected if it isn't already
198
+ this.wsClient.ensureConnected();
199
+ const unsubOpen = this.wsClient.addListener('open', () => {
200
+ if (!lastState) {
201
+ lastState = true;
202
+ callback(true);
203
+ }
204
+ });
205
+ const unsubClose = this.wsClient.addListener('close', () => {
206
+ if (lastState) {
207
+ lastState = false;
208
+ callback(false);
209
+ }
210
+ });
211
+ const unsubError = this.wsClient.addListener('error', () => {
212
+ if (lastState) {
213
+ lastState = false;
214
+ callback(false);
215
+ }
216
+ });
217
+ return () => {
218
+ unsubOpen();
219
+ unsubClose();
220
+ unsubError();
221
+ };
222
+ }
183
223
  }
184
224
  exports.NexaApp = NexaApp;
@@ -11,26 +11,21 @@ const setDoc = async (docRef, data, options) => {
11
11
  await db._initPromise;
12
12
  const existingData = options?.merge ? db._firestoreCache[docRef.path] || {} : {};
13
13
  const finalData = (0, helpers_1.applyDataTransforms)(existingData, data);
14
- await db._setCache(docRef.path, finalData);
15
- db._broadcastLocalMutation(docRef.path, 'set', finalData);
16
- db._notifySnapshotCallbacks(docRef.path, 'document_written', finalData);
17
14
  const key = options?.idempotencyKey || (0, helpers_1.generateIdempotencyKey)();
18
15
  const jobId = Math.random().toString(36).substring(2, 9);
19
16
  const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
17
+ db.markInflight(docRef.path);
18
+ await db._setCache(docRef.path, finalData);
19
+ db._broadcastLocalMutation(docRef.path, 'set', finalData);
20
+ db._notifySnapshotCallbacks(docRef.path, 'document_written', finalData);
20
21
  if (!isOnline) {
22
+ db.unmarkInflight(docRef.path);
21
23
  await db._addOfflineJob({
22
- id: jobId,
23
- type: 'set',
24
- path: docRef.path,
25
- data,
26
- idempotencyKey: key,
27
- merge: options?.merge,
28
- state: 'pending'
24
+ id: jobId, type: 'set', path: docRef.path, data, idempotencyKey: key, merge: options?.merge, state: 'pending'
29
25
  });
30
26
  return { success: true, message: 'Offline: Tersimpan di cache lokal' };
31
27
  }
32
28
  else {
33
- db.markInflight(docRef.path);
34
29
  try {
35
30
  const res = (await db.client.post(`/api/firestore/${db.projectId}/document`, { docPath: docRef.path, data, idempotencyKey: key, merge: options?.merge }, { headers: { 'X-Idempotency-Key': key } })).data;
36
31
  return res;
@@ -62,13 +57,15 @@ const patchDoc = async (docRef, data, options) => {
62
57
  await db._initPromise;
63
58
  const existingData = db._firestoreCache[docRef.path] || {};
64
59
  const finalData = (0, helpers_1.applyDataTransforms)(existingData, data);
65
- await db._setCache(docRef.path, finalData);
66
- db._broadcastLocalMutation(docRef.path, 'patch', finalData);
67
- db._notifySnapshotCallbacks(docRef.path, 'document_written', finalData);
68
60
  const key = options?.idempotencyKey || (0, helpers_1.generateIdempotencyKey)();
69
61
  const jobId = Math.random().toString(36).substring(2, 9);
70
62
  const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
63
+ db.markInflight(docRef.path);
64
+ await db._setCache(docRef.path, finalData);
65
+ db._broadcastLocalMutation(docRef.path, 'patch', finalData);
66
+ db._notifySnapshotCallbacks(docRef.path, 'document_written', finalData);
71
67
  if (!isOnline) {
68
+ db.unmarkInflight(docRef.path);
72
69
  await db._addOfflineJob({
73
70
  id: jobId,
74
71
  type: 'patch',
@@ -113,13 +110,15 @@ const deleteDoc = async (docRef, options) => {
113
110
  const db = docRef.db;
114
111
  if (db._initPromise)
115
112
  await db._initPromise;
116
- await db._deleteCache(docRef.path);
117
- db._broadcastLocalMutation(docRef.path, 'delete');
118
- db._notifySnapshotCallbacks(docRef.path, 'document_deleted', null);
119
113
  const key = options?.idempotencyKey || (0, helpers_1.generateIdempotencyKey)();
120
114
  const jobId = Math.random().toString(36).substring(2, 9);
121
115
  const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
116
+ db.markInflight(docRef.path);
117
+ await db._deleteCache(docRef.path);
118
+ db._broadcastLocalMutation(docRef.path, 'delete');
119
+ db._notifySnapshotCallbacks(docRef.path, 'document_deleted', null);
122
120
  if (!isOnline) {
121
+ db.unmarkInflight(docRef.path);
123
122
  await db._addOfflineJob({
124
123
  id: jobId,
125
124
  type: 'delete',
package/dist/index.d.ts CHANGED
@@ -30,6 +30,10 @@ export * from './sync/SyncEngine';
30
30
  export * from './sync/ConflictResolver';
31
31
  export * from './transport/HttpClient';
32
32
  export * from './transport/SSEClient';
33
+ export * from './transport/WebSocketClient';
33
34
  export * from './errors/NexaError';
34
35
  export * from './types/index';
35
36
  export * from './utils/helpers';
37
+ export * from './storage/types';
38
+ export * from './storage/NexaStorageError';
39
+ export { connectStorageEmulator } from './storage/Storage';
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.connectStorageEmulator = void 0;
17
18
  // App
18
19
  __exportStar(require("./app/initializeApp"), exports);
19
20
  __exportStar(require("./app/NexaApp"), exports);
@@ -54,7 +55,12 @@ __exportStar(require("./sync/ConflictResolver"), exports);
54
55
  // Transport
55
56
  __exportStar(require("./transport/HttpClient"), exports);
56
57
  __exportStar(require("./transport/SSEClient"), exports);
58
+ __exportStar(require("./transport/WebSocketClient"), exports);
57
59
  // Errors & Types
58
60
  __exportStar(require("./errors/NexaError"), exports);
59
61
  __exportStar(require("./types/index"), exports);
60
62
  __exportStar(require("./utils/helpers"), exports);
63
+ __exportStar(require("./storage/types"), exports);
64
+ __exportStar(require("./storage/NexaStorageError"), exports);
65
+ var Storage_1 = require("./storage/Storage");
66
+ Object.defineProperty(exports, "connectStorageEmulator", { enumerable: true, get: function () { return Storage_1.connectStorageEmulator; } });
@@ -9,4 +9,5 @@ export declare class LocalStore {
9
9
  get(path: string): Promise<any>;
10
10
  delete(path: string): Promise<void>;
11
11
  loadAll(): Promise<Record<string, any>>;
12
+ garbageCollect(maxItems?: number): Promise<void>;
12
13
  }
@@ -74,5 +74,48 @@ class LocalStore {
74
74
  req.onerror = () => reject(req.error);
75
75
  });
76
76
  }
77
+ // LRU Cache Garbage Collection (Max size 5000 items)
78
+ async garbageCollect(maxItems = 5000) {
79
+ const db = this.indexedDB.getRawDB();
80
+ if (!db)
81
+ return;
82
+ return new Promise((resolve, reject) => {
83
+ const tx = db.transaction('offline_cache', 'readwrite');
84
+ const store = tx.objectStore('offline_cache');
85
+ const req = store.getAll();
86
+ req.onsuccess = () => {
87
+ const items = req.result;
88
+ if (items.length > maxItems) {
89
+ // Sort by timestamp (oldest first)
90
+ items.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
91
+ const itemsToRemove = items.slice(0, items.length - maxItems);
92
+ if (itemsToRemove.length === 0) {
93
+ return resolve();
94
+ }
95
+ let removedCount = 0;
96
+ itemsToRemove.forEach((item) => {
97
+ const delReq = store.delete(item.path);
98
+ delReq.onsuccess = () => {
99
+ delete this.inMemoryCache[item.path];
100
+ removedCount++;
101
+ if (removedCount === itemsToRemove.length) {
102
+ resolve();
103
+ }
104
+ };
105
+ delReq.onerror = () => {
106
+ removedCount++;
107
+ if (removedCount === itemsToRemove.length) {
108
+ resolve();
109
+ }
110
+ };
111
+ });
112
+ }
113
+ else {
114
+ resolve();
115
+ }
116
+ };
117
+ req.onerror = () => reject(req.error);
118
+ });
119
+ }
77
120
  }
78
121
  exports.LocalStore = LocalStore;
@@ -1,5 +1,5 @@
1
1
  import { AxiosInstance } from 'axios';
2
- import { UploadMetadata, StorageMetadata } from './types';
2
+ import { UploadMetadata, StorageMetadata, SettableMetadata, ListOptions, ListResult } from './types';
3
3
  import { UploadOptions } from '../types/index';
4
4
  import { UploadTask } from './UploadTask';
5
5
  import { IStorageReference } from './StorageReference';
@@ -8,9 +8,9 @@ export declare class Storage {
8
8
  private client;
9
9
  emulatorUrl?: string;
10
10
  constructor(projectId: string, client: AxiosInstance);
11
- ref(path: string): IStorageReference;
11
+ ref(path?: string): IStorageReference;
12
12
  refFromURL(url: string): IStorageReference;
13
- createUploadTask(path: string, file: File | Blob, metadata?: UploadMetadata): UploadTask;
13
+ createUploadTask(path: string, file: Blob, metadata?: UploadMetadata): UploadTask;
14
14
  uploadFile(path: string, file: File | Blob, options?: UploadOptions): Promise<{
15
15
  url: string;
16
16
  path: string;
@@ -21,8 +21,8 @@ export declare class Storage {
21
21
  message?: string;
22
22
  }>;
23
23
  getMetadata(path: string): Promise<StorageMetadata>;
24
- updateMetadata(path: string, metadata: Partial<StorageMetadata>): Promise<StorageMetadata>;
25
- list(path: string, options?: any): Promise<any>;
24
+ updateMetadata(path: string, metadata: SettableMetadata): Promise<StorageMetadata>;
25
+ list(path: string, options?: ListOptions): Promise<ListResult>;
26
26
  }
27
- export declare const getStorage: (app: any) => Storage;
27
+ export declare const getStorage: (app?: any) => Storage;
28
28
  export declare const connectStorageEmulator: (storage: Storage, host: string, port: number) => void;
@@ -9,15 +9,16 @@ class Storage {
9
9
  this.projectId = projectId;
10
10
  this.client = client;
11
11
  }
12
- ref(path) {
12
+ ref(path = '') {
13
13
  return new StorageReference_1.StorageReferenceImpl(path, this);
14
14
  }
15
15
  refFromURL(url) {
16
- const match = url.match(/storage\/([^?]*)/);
16
+ // Handle both NexaBase and standard URL paths
17
+ const match = url.match(/[?&]path=([^&]+)/) || url.match(/storage\/file\/([^?]+)/) || url.match(/storage\/([^?]+)/);
17
18
  if (match && match[1]) {
18
19
  return this.ref(decodeURIComponent(match[1]));
19
20
  }
20
- throw new NexaStorageError_1.NexaStorageError('storage/invalid-path', 'Invalid storage URL');
21
+ throw new NexaStorageError_1.NexaStorageError('storage/invalid-path', 'Invalid storage URL format');
21
22
  }
22
23
  createUploadTask(path, file, metadata) {
23
24
  const task = new UploadTask_1.UploadTask(this.projectId, this.client, path, file, metadata);
@@ -35,11 +36,24 @@ class Storage {
35
36
  async getDownloadURL(path) {
36
37
  try {
37
38
  const res = await this.client.get(`/api/project/${this.projectId}/storage/url?path=${encodeURIComponent(path)}`);
38
- return res.data.url;
39
+ let url = res.data.downloadUrl || res.data.url;
40
+ // If the backend returns a relative URL, build full absolute URL using client baseURL or window.location.origin
41
+ if (url && !url.startsWith('http://') && !url.startsWith('https://')) {
42
+ let baseUrl = this.client.defaults.baseURL || '';
43
+ if (!baseUrl && typeof window !== 'undefined' && window.location) {
44
+ baseUrl = window.location.origin;
45
+ }
46
+ if (baseUrl) {
47
+ const cleanBase = baseUrl.replace(/\/+$/, '');
48
+ const cleanPath = url.startsWith('/') ? url : `/${url}`;
49
+ url = `${cleanBase}${cleanPath}`;
50
+ }
51
+ }
52
+ return url;
39
53
  }
40
54
  catch (err) {
41
55
  if (err.response) {
42
- throw new NexaStorageError_1.NexaStorageError(err.response.data?.error?.code || 'storage/server-error', err.response.data?.error?.message || err.message);
56
+ throw new NexaStorageError_1.NexaStorageError(err.response.data?.error?.code || 'storage/server-error', err.response.data?.error?.message || err.message, err.response.status);
43
57
  }
44
58
  throw new NexaStorageError_1.NexaStorageError('storage/network-error', err.message);
45
59
  }
@@ -51,7 +65,7 @@ class Storage {
51
65
  }
52
66
  catch (err) {
53
67
  if (err.response) {
54
- throw new NexaStorageError_1.NexaStorageError(err.response.data?.error?.code || 'storage/server-error', err.response.data?.error?.message || err.message);
68
+ throw new NexaStorageError_1.NexaStorageError(err.response.data?.error?.code || 'storage/server-error', err.response.data?.error?.message || err.message, err.response.status);
55
69
  }
56
70
  throw new NexaStorageError_1.NexaStorageError('storage/network-error', err.message);
57
71
  }
@@ -62,8 +76,9 @@ class Storage {
62
76
  return res.data;
63
77
  }
64
78
  catch (err) {
65
- if (err.response)
66
- throw new NexaStorageError_1.NexaStorageError(err.response.data?.error?.code || 'storage/server-error', err.response.data?.error?.message || err.message);
79
+ if (err.response) {
80
+ throw new NexaStorageError_1.NexaStorageError(err.response.data?.error?.code || 'storage/server-error', err.response.data?.error?.message || err.message, err.response.status);
81
+ }
67
82
  throw new NexaStorageError_1.NexaStorageError('storage/network-error', err.message);
68
83
  }
69
84
  }
@@ -73,19 +88,26 @@ class Storage {
73
88
  return res.data;
74
89
  }
75
90
  catch (err) {
76
- if (err.response)
77
- throw new NexaStorageError_1.NexaStorageError(err.response.data?.error?.code || 'storage/server-error', err.response.data?.error?.message || err.message);
91
+ if (err.response) {
92
+ throw new NexaStorageError_1.NexaStorageError(err.response.data?.error?.code || 'storage/server-error', err.response.data?.error?.message || err.message, err.response.status);
93
+ }
78
94
  throw new NexaStorageError_1.NexaStorageError('storage/network-error', err.message);
79
95
  }
80
96
  }
81
97
  async list(path, options) {
82
98
  try {
83
- const res = await this.client.get(`/api/project/${this.projectId}/storage/list?path=${encodeURIComponent(path)}`);
99
+ const params = new URLSearchParams();
100
+ if (path)
101
+ params.append('path', path);
102
+ if (options?.maxResults)
103
+ params.append('maxResults', options.maxResults.toString());
104
+ const res = await this.client.get(`/api/project/${this.projectId}/storage/list?${params.toString()}`);
84
105
  return res.data;
85
106
  }
86
107
  catch (err) {
87
- if (err.response)
88
- throw new NexaStorageError_1.NexaStorageError(err.response.data?.error?.code || 'storage/server-error', err.response.data?.error?.message || err.message);
108
+ if (err.response) {
109
+ throw new NexaStorageError_1.NexaStorageError(err.response.data?.error?.code || 'storage/server-error', err.response.data?.error?.message || err.message, err.response.status);
110
+ }
89
111
  throw new NexaStorageError_1.NexaStorageError('storage/network-error', err.message);
90
112
  }
91
113
  }
@@ -95,7 +117,13 @@ const getStorage = (app) => {
95
117
  if (app && typeof app.storage === 'function') {
96
118
  return app.storage();
97
119
  }
98
- throw new Error('Invalid NexaApp instance provided to getStorage()');
120
+ if (app && app.storageService) {
121
+ return app.storageService;
122
+ }
123
+ if (app instanceof Storage) {
124
+ return app;
125
+ }
126
+ throw new Error('NexaApp instance is required for getStorage(app)');
99
127
  };
100
128
  exports.getStorage = getStorage;
101
129
  const connectStorageEmulator = (storage, host, port) => {
@@ -103,7 +131,6 @@ const connectStorageEmulator = (storage, host, port) => {
103
131
  throw new Error('Emulator already connected.');
104
132
  }
105
133
  storage.emulatorUrl = `http://${host}:${port}`;
106
- // Modify the axios client inside storage to point to emulator
107
134
  const client = storage.client;
108
135
  if (client) {
109
136
  client.defaults.baseURL = storage.emulatorUrl;
@@ -1,20 +1,26 @@
1
1
  import { Storage } from './Storage';
2
2
  import { UploadTask } from './UploadTask';
3
- import { UploadMetadata, StorageMetadata } from './types';
3
+ import { UploadMetadata, StorageMetadata, SettableMetadata, StringFormat, UploadResult, ListOptions, ListResult } from './types';
4
4
  import { UploadOptions } from '../types/index';
5
5
  export interface IStorageReference {
6
+ name: string;
7
+ bucket: string;
8
+ fullPath: string;
6
9
  path: string;
7
- put(file: File | Blob, metadata?: UploadMetadata): UploadTask;
8
- putString(data: string, format?: string, metadata?: UploadMetadata): UploadTask;
10
+ root: IStorageReference;
11
+ parent: IStorageReference | null;
12
+ storage: Storage;
13
+ put(file: Blob | Uint8Array | ArrayBuffer, metadata?: UploadMetadata): UploadTask;
14
+ putString(data: string, format?: StringFormat, metadata?: UploadMetadata): UploadTask;
9
15
  getDownloadURL(): Promise<string>;
10
16
  getMetadata(): Promise<StorageMetadata>;
11
- updateMetadata(metadata: Partial<StorageMetadata>): Promise<StorageMetadata>;
17
+ updateMetadata(metadata: SettableMetadata): Promise<StorageMetadata>;
12
18
  delete(): Promise<{
13
19
  success: boolean;
14
20
  message?: string;
15
21
  }>;
16
- listAll(): Promise<any>;
17
- list(options?: any): Promise<any>;
22
+ listAll(): Promise<ListResult>;
23
+ list(options?: ListOptions): Promise<ListResult>;
18
24
  upload?: (file: File | Blob, options?: UploadOptions) => Promise<{
19
25
  url: string;
20
26
  path: string;
@@ -22,28 +28,39 @@ export interface IStorageReference {
22
28
  }
23
29
  export declare class StorageReferenceImpl implements IStorageReference {
24
30
  path: string;
25
- private storage;
31
+ fullPath: string;
32
+ name: string;
33
+ bucket: string;
34
+ storage: Storage;
26
35
  constructor(path: string, storage: Storage);
27
- put(file: File | Blob, metadata?: UploadMetadata): UploadTask;
28
- putString(data: string, format?: string, metadata?: UploadMetadata): UploadTask;
36
+ get root(): IStorageReference;
37
+ get parent(): IStorageReference | null;
38
+ put(data: Blob | Uint8Array | ArrayBuffer, metadata?: UploadMetadata): UploadTask;
39
+ putString(data: string, format?: StringFormat, metadata?: UploadMetadata): UploadTask;
29
40
  getDownloadURL(): Promise<string>;
30
41
  getMetadata(): Promise<StorageMetadata>;
31
- updateMetadata(metadata: Partial<StorageMetadata>): Promise<StorageMetadata>;
42
+ updateMetadata(metadata: SettableMetadata): Promise<StorageMetadata>;
32
43
  delete(): Promise<{
33
44
  success: boolean;
34
45
  message?: string;
35
46
  }>;
36
- listAll(): Promise<any>;
37
- list(options?: any): Promise<any>;
47
+ listAll(): Promise<ListResult>;
48
+ list(options?: ListOptions): Promise<ListResult>;
38
49
  upload(file: File | Blob, options?: UploadOptions): Promise<{
39
50
  url: string;
40
51
  path: string;
41
52
  }>;
42
53
  }
43
- export declare const ref: (storage: Storage, path: string) => IStorageReference;
44
- export declare const uploadBytes: (storageRef: IStorageReference, file: File | Blob, metadata?: UploadMetadata) => Promise<any>;
54
+ /**
55
+ * 1:1 Modular Firebase Storage API functions
56
+ */
57
+ export declare const ref: (storageOrRef: Storage | IStorageReference, path?: string) => IStorageReference;
58
+ export declare const uploadBytes: (storageRef: IStorageReference, data: Blob | Uint8Array | ArrayBuffer, metadata?: UploadMetadata) => Promise<UploadResult>;
59
+ export declare const uploadBytesResumable: (storageRef: IStorageReference, data: Blob | Uint8Array | ArrayBuffer, metadata?: UploadMetadata) => UploadTask;
60
+ export declare const uploadString: (storageRef: IStorageReference, value: string, format?: StringFormat, metadata?: UploadMetadata) => Promise<UploadResult>;
45
61
  export declare const getDownloadURL: (storageRef: IStorageReference) => Promise<string>;
46
- export declare const deleteObject: (storageRef: IStorageReference) => Promise<{
47
- success: boolean;
48
- message?: string;
49
- }>;
62
+ export declare const getMetadata: (storageRef: IStorageReference) => Promise<StorageMetadata>;
63
+ export declare const updateMetadata: (storageRef: IStorageReference, metadata: SettableMetadata) => Promise<StorageMetadata>;
64
+ export declare const deleteObject: (storageRef: IStorageReference) => Promise<void>;
65
+ export declare const listAll: (storageRef: IStorageReference) => Promise<ListResult>;
66
+ export declare const list: (storageRef: IStorageReference, options?: ListOptions) => Promise<ListResult>;
@@ -1,28 +1,81 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.deleteObject = exports.getDownloadURL = exports.uploadBytes = exports.ref = exports.StorageReferenceImpl = void 0;
3
+ exports.list = exports.listAll = exports.deleteObject = exports.updateMetadata = exports.getMetadata = exports.getDownloadURL = exports.uploadString = exports.uploadBytesResumable = exports.uploadBytes = exports.ref = exports.StorageReferenceImpl = void 0;
4
+ const NexaStorageError_1 = require("./NexaStorageError");
4
5
  class StorageReferenceImpl {
5
6
  constructor(path, storage) {
6
- this.path = path;
7
+ // Normalize path by stripping leading slashes
8
+ this.path = (path || '').replace(/^\/+/, '');
9
+ this.fullPath = this.path;
10
+ this.name = this.path.split('/').filter(Boolean).pop() || '';
11
+ this.bucket = storage.projectId || '';
7
12
  this.storage = storage;
8
13
  }
9
- put(file, metadata) {
10
- return this.storage.createUploadTask(this.path, file, metadata);
14
+ get root() {
15
+ return new StorageReferenceImpl('', this.storage);
11
16
  }
12
- putString(data, format = 'raw', metadata) {
17
+ get parent() {
18
+ const parts = this.path.split('/').filter(Boolean);
19
+ if (parts.length <= 1)
20
+ return null;
21
+ parts.pop();
22
+ return new StorageReferenceImpl(parts.join('/'), this.storage);
23
+ }
24
+ put(data, metadata) {
13
25
  let blob;
14
- if (format === 'base64') {
15
- const byteCharacters = atob(data);
16
- const byteNumbers = new Array(byteCharacters.length);
17
- for (let i = 0; i < byteCharacters.length; i++) {
18
- byteNumbers[i] = byteCharacters.charCodeAt(i);
19
- }
20
- const byteArray = new Uint8Array(byteNumbers);
21
- blob = new Blob([byteArray]);
26
+ if (data instanceof Blob) {
27
+ blob = data;
28
+ }
29
+ else if (data instanceof Uint8Array || data instanceof ArrayBuffer) {
30
+ blob = new Blob([data], { type: metadata?.contentType || 'application/octet-stream' });
22
31
  }
23
32
  else {
24
33
  blob = new Blob([data]);
25
34
  }
35
+ return this.storage.createUploadTask(this.path, blob, metadata);
36
+ }
37
+ putString(data, format = 'raw', metadata) {
38
+ let blob;
39
+ try {
40
+ if (format === 'base64') {
41
+ const byteCharacters = atob(data);
42
+ const byteNumbers = new Uint8Array(byteCharacters.length);
43
+ for (let i = 0; i < byteCharacters.length; i++) {
44
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
45
+ }
46
+ blob = new Blob([byteNumbers], { type: metadata?.contentType || 'application/octet-stream' });
47
+ }
48
+ else if (format === 'base64url') {
49
+ let base64 = data.replace(/-/g, '+').replace(/_/g, '/');
50
+ while (base64.length % 4) {
51
+ base64 += '=';
52
+ }
53
+ const byteCharacters = atob(base64);
54
+ const byteNumbers = new Uint8Array(byteCharacters.length);
55
+ for (let i = 0; i < byteCharacters.length; i++) {
56
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
57
+ }
58
+ blob = new Blob([byteNumbers], { type: metadata?.contentType || 'application/octet-stream' });
59
+ }
60
+ else if (format === 'data_url') {
61
+ const parts = data.split(',');
62
+ const mimeMatch = parts[0]?.match(/:(.*?);/);
63
+ const detectedMime = mimeMatch ? mimeMatch[1] : 'application/octet-stream';
64
+ const b64Data = parts[1] || '';
65
+ const byteCharacters = atob(b64Data);
66
+ const byteNumbers = new Uint8Array(byteCharacters.length);
67
+ for (let i = 0; i < byteCharacters.length; i++) {
68
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
69
+ }
70
+ blob = new Blob([byteNumbers], { type: metadata?.contentType || detectedMime });
71
+ }
72
+ else {
73
+ blob = new Blob([data], { type: metadata?.contentType || 'text/plain;charset=utf-8' });
74
+ }
75
+ }
76
+ catch (err) {
77
+ throw new NexaStorageError_1.NexaStorageError('storage/invalid-format', `Failed to parse data as ${format}: ${err.message}`);
78
+ }
26
79
  return this.put(blob, metadata);
27
80
  }
28
81
  getDownloadURL() {
@@ -46,27 +99,79 @@ class StorageReferenceImpl {
46
99
  // Legacy for backward compatibility
47
100
  async upload(file, options) {
48
101
  const task = this.put(file, options);
49
- task.options = options; // Inject legacy options
102
+ task.options = options;
50
103
  await task;
51
104
  const url = await this.getDownloadURL();
52
105
  return { url, path: this.path };
53
106
  }
54
107
  }
55
108
  exports.StorageReferenceImpl = StorageReferenceImpl;
56
- const ref = (storage, path) => {
57
- return new StorageReferenceImpl(path, storage);
109
+ /**
110
+ * 1:1 Modular Firebase Storage API functions
111
+ */
112
+ const ref = (storageOrRef, path) => {
113
+ if (!storageOrRef) {
114
+ throw new NexaStorageError_1.NexaStorageError('storage/invalid-path', 'No storage instance or reference provided');
115
+ }
116
+ if ('storage' in storageOrRef && typeof storageOrRef.storage?.ref === 'function') {
117
+ // First argument is a StorageReference, child path is appended
118
+ const parent = storageOrRef;
119
+ const combinedPath = path ? `${parent.path.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}` : parent.path;
120
+ return new StorageReferenceImpl(combinedPath, parent.storage);
121
+ }
122
+ const storage = storageOrRef;
123
+ return new StorageReferenceImpl(path || '', storage);
58
124
  };
59
125
  exports.ref = ref;
60
- const uploadBytes = async (storageRef, file, metadata) => {
61
- const task = storageRef.put(file, metadata);
62
- return await task;
126
+ const uploadBytes = async (storageRef, data, metadata) => {
127
+ const task = storageRef.put(data, metadata);
128
+ const snapshot = await task;
129
+ return {
130
+ bytesTransferred: snapshot.bytesTransferred,
131
+ totalBytes: snapshot.totalBytes,
132
+ state: snapshot.state,
133
+ metadata: snapshot.metadata,
134
+ ref: storageRef
135
+ };
63
136
  };
64
137
  exports.uploadBytes = uploadBytes;
138
+ const uploadBytesResumable = (storageRef, data, metadata) => {
139
+ return storageRef.put(data, metadata);
140
+ };
141
+ exports.uploadBytesResumable = uploadBytesResumable;
142
+ const uploadString = async (storageRef, value, format = 'raw', metadata) => {
143
+ const task = storageRef.putString(value, format, metadata);
144
+ const snapshot = await task;
145
+ return {
146
+ bytesTransferred: snapshot.bytesTransferred,
147
+ totalBytes: snapshot.totalBytes,
148
+ state: snapshot.state,
149
+ metadata: snapshot.metadata,
150
+ ref: storageRef
151
+ };
152
+ };
153
+ exports.uploadString = uploadString;
65
154
  const getDownloadURL = async (storageRef) => {
66
155
  return storageRef.getDownloadURL();
67
156
  };
68
157
  exports.getDownloadURL = getDownloadURL;
158
+ const getMetadata = async (storageRef) => {
159
+ return storageRef.getMetadata();
160
+ };
161
+ exports.getMetadata = getMetadata;
162
+ const updateMetadata = async (storageRef, metadata) => {
163
+ return storageRef.updateMetadata(metadata);
164
+ };
165
+ exports.updateMetadata = updateMetadata;
69
166
  const deleteObject = async (storageRef) => {
70
- return storageRef.delete();
167
+ await storageRef.delete();
71
168
  };
72
169
  exports.deleteObject = deleteObject;
170
+ const listAll = async (storageRef) => {
171
+ return storageRef.listAll();
172
+ };
173
+ exports.listAll = listAll;
174
+ const list = async (storageRef, options) => {
175
+ return storageRef.list(options);
176
+ };
177
+ exports.list = list;
@@ -1,4 +1,4 @@
1
- export type StorageErrorCode = 'storage/unauthenticated' | 'storage/unauthorized' | 'storage/object-not-found' | 'storage/object-already-exists' | 'storage/invalid-path' | 'storage/invalid-file' | 'storage/invalid-metadata' | 'storage/invalid-checksum' | 'storage/quota-exceeded' | 'storage/upload-session-expired' | 'storage/retry-limit-exceeded' | 'storage/canceled' | 'storage/conflict' | 'storage/network-error' | 'storage/server-error';
1
+ export type StorageErrorCode = 'storage/unauthenticated' | 'storage/unauthorized' | 'storage/object-not-found' | 'storage/object-already-exists' | 'storage/invalid-path' | 'storage/invalid-file' | 'storage/invalid-format' | 'storage/invalid-metadata' | 'storage/invalid-checksum' | 'storage/quota-exceeded' | 'storage/upload-session-expired' | 'storage/retry-limit-exceeded' | 'storage/canceled' | 'storage/conflict' | 'storage/network-error' | 'storage/server-error';
2
2
  export interface StorageMetadata {
3
3
  name: string;
4
4
  bucket: string;
@@ -15,6 +15,7 @@ export interface StorageMetadata {
15
15
  contentLanguage?: string;
16
16
  contentType?: string;
17
17
  customMetadata?: Record<string, string>;
18
+ downloadTokens?: string;
18
19
  }
19
20
  export interface UploadMetadata {
20
21
  md5Hash?: string;
@@ -25,6 +26,8 @@ export interface UploadMetadata {
25
26
  contentType?: string;
26
27
  customMetadata?: Record<string, string>;
27
28
  }
29
+ export type SettableMetadata = UploadMetadata;
30
+ export type StringFormat = 'raw' | 'base64' | 'base64url' | 'data_url';
28
31
  export type TaskState = 'running' | 'paused' | 'success' | 'canceled' | 'error';
29
32
  export interface UploadTaskSnapshot {
30
33
  bytesTransferred: number;
@@ -34,3 +37,19 @@ export interface UploadTaskSnapshot {
34
37
  task: any;
35
38
  ref: any;
36
39
  }
40
+ export interface UploadResult {
41
+ bytesTransferred: number;
42
+ totalBytes: number;
43
+ state: TaskState;
44
+ metadata?: StorageMetadata;
45
+ ref: any;
46
+ }
47
+ export interface ListOptions {
48
+ maxResults?: number;
49
+ pageToken?: string;
50
+ }
51
+ export interface ListResult {
52
+ items: any[];
53
+ prefixes: any[];
54
+ nextPageToken?: string;
55
+ }
@@ -1,15 +1,16 @@
1
+ import { WebSocketClient } from './WebSocketClient';
1
2
  export declare class SSEClient {
2
- private projectId;
3
- private sseUrl;
4
- private sse;
5
- private isListening;
6
- private listeners;
3
+ private wsClient;
7
4
  constructor(projectId: string, sseUrl: string);
8
5
  addListener(event: string, callback: (data: any) => void): () => void;
6
+ subscribe(path: string): void;
7
+ unsubscribe(path: string): void;
9
8
  ensureConnected(): void;
10
9
  checkCloseConnection(): void;
11
10
  hasListeners(): boolean;
12
11
  dispatch(event: string, data: any): void;
12
+ send(event: string, data: any): Promise<any>;
13
13
  close(): void;
14
14
  }
15
15
  export { SSEClient as SSEManager };
16
+ export { WebSocketClient };
@@ -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.sse = null;
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
- if (!this.listeners.has(event)) {
14
- this.listeners.set(event, new Set());
15
- }
16
- this.listeners.get(event).add(callback);
17
- this.ensureConnected();
18
- return () => {
19
- const callbacks = this.listeners.get(event);
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
- if (this.isListening || typeof window === 'undefined')
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
- if (!this.hasListeners() && this.sse) {
61
- this.sse.close();
62
- this.sse = null;
63
- this.isListening = false;
64
- }
23
+ this.wsClient.checkCloseConnection();
65
24
  }
66
25
  hasListeners() {
67
- let count = 0;
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
- const callbacks = this.listeners.get(event);
75
- if (callbacks) {
76
- callbacks.forEach((cb) => {
77
- try {
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
- if (this.sse) {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexabase-console",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
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"