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.
- package/dist/app/NexaApp.d.ts +6 -1
- package/dist/app/NexaApp.js +42 -2
- package/dist/firestore/writes.js +16 -17
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/persistence/LocalStore.d.ts +1 -0
- package/dist/persistence/LocalStore.js +43 -0
- package/dist/storage/Storage.d.ts +6 -6
- package/dist/storage/Storage.js +42 -15
- package/dist/storage/StorageReference.d.ts +35 -18
- package/dist/storage/StorageReference.js +125 -20
- package/dist/storage/types.d.ts +20 -1
- 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/package.json +3 -2
package/dist/app/NexaApp.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/app/NexaApp.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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;
|
package/dist/firestore/writes.js
CHANGED
|
@@ -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; } });
|
|
@@ -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
|
|
11
|
+
ref(path?: string): IStorageReference;
|
|
12
12
|
refFromURL(url: string): IStorageReference;
|
|
13
|
-
createUploadTask(path: string, file:
|
|
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:
|
|
25
|
-
list(path: string, options?:
|
|
24
|
+
updateMetadata(path: string, metadata: SettableMetadata): Promise<StorageMetadata>;
|
|
25
|
+
list(path: string, options?: ListOptions): Promise<ListResult>;
|
|
26
26
|
}
|
|
27
|
-
export declare const getStorage: (app
|
|
27
|
+
export declare const getStorage: (app?: any) => Storage;
|
|
28
28
|
export declare const connectStorageEmulator: (storage: Storage, host: string, port: number) => void;
|
package/dist/storage/Storage.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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:
|
|
17
|
+
updateMetadata(metadata: SettableMetadata): Promise<StorageMetadata>;
|
|
12
18
|
delete(): Promise<{
|
|
13
19
|
success: boolean;
|
|
14
20
|
message?: string;
|
|
15
21
|
}>;
|
|
16
|
-
listAll(): Promise<
|
|
17
|
-
list(options?:
|
|
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
|
-
|
|
31
|
+
fullPath: string;
|
|
32
|
+
name: string;
|
|
33
|
+
bucket: string;
|
|
34
|
+
storage: Storage;
|
|
26
35
|
constructor(path: string, storage: Storage);
|
|
27
|
-
|
|
28
|
-
|
|
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:
|
|
42
|
+
updateMetadata(metadata: SettableMetadata): Promise<StorageMetadata>;
|
|
32
43
|
delete(): Promise<{
|
|
33
44
|
success: boolean;
|
|
34
45
|
message?: string;
|
|
35
46
|
}>;
|
|
36
|
-
listAll(): Promise<
|
|
37
|
-
list(options?:
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
return this.storage
|
|
14
|
+
get root() {
|
|
15
|
+
return new StorageReferenceImpl('', this.storage);
|
|
11
16
|
}
|
|
12
|
-
|
|
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 (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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;
|
|
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
|
-
|
|
57
|
-
|
|
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,
|
|
61
|
-
const task = storageRef.put(
|
|
62
|
-
|
|
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
|
-
|
|
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;
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexabase-console",
|
|
3
|
-
"version": "2.0.
|
|
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"
|