nexabase-console 1.1.4 → 2.0.0

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.
Files changed (72) hide show
  1. package/README.md +33 -8
  2. package/dist/app/NexaApp.d.ts +63 -0
  3. package/dist/app/NexaApp.js +184 -0
  4. package/dist/app/initializeApp.d.ts +8 -0
  5. package/dist/app/initializeApp.js +27 -0
  6. package/dist/auth/Auth.d.ts +21 -0
  7. package/dist/auth/Auth.js +73 -0
  8. package/dist/auth/authTypes.d.ts +1 -0
  9. package/dist/auth/authTypes.js +2 -0
  10. package/dist/auth/persistence.d.ts +9 -0
  11. package/dist/auth/persistence.js +34 -0
  12. package/dist/errors/NexaError.d.ts +7 -0
  13. package/dist/errors/NexaError.js +61 -0
  14. package/dist/firestore/CollectionReference.d.ts +3 -0
  15. package/dist/firestore/CollectionReference.js +7 -0
  16. package/dist/firestore/DocumentReference.d.ts +3 -0
  17. package/dist/firestore/DocumentReference.js +22 -0
  18. package/dist/firestore/FieldPath.d.ts +7 -0
  19. package/dist/firestore/FieldPath.js +23 -0
  20. package/dist/firestore/FieldValue.d.ts +11 -0
  21. package/dist/firestore/FieldValue.js +21 -0
  22. package/dist/firestore/Firestore.d.ts +8 -0
  23. package/dist/firestore/Firestore.js +24 -0
  24. package/dist/firestore/GeoPoint.d.ts +10 -0
  25. package/dist/firestore/GeoPoint.js +22 -0
  26. package/dist/firestore/Query.d.ts +4 -0
  27. package/dist/firestore/Query.js +172 -0
  28. package/dist/firestore/Snapshot.d.ts +6 -0
  29. package/dist/firestore/Snapshot.js +126 -0
  30. package/dist/firestore/SnapshotManager.d.ts +9 -0
  31. package/dist/firestore/SnapshotManager.js +37 -0
  32. package/dist/firestore/Timestamp.d.ts +16 -0
  33. package/dist/firestore/Timestamp.js +36 -0
  34. package/dist/firestore/batch.d.ts +3 -0
  35. package/dist/firestore/batch.js +79 -0
  36. package/dist/firestore/queryConstraints.d.ts +9 -0
  37. package/dist/firestore/queryConstraints.js +31 -0
  38. package/dist/firestore/transaction.d.ts +5 -0
  39. package/dist/firestore/transaction.js +71 -0
  40. package/dist/firestore/writes.d.ts +15 -0
  41. package/dist/firestore/writes.js +146 -0
  42. package/dist/index.d.ts +34 -206
  43. package/dist/index.js +56 -1022
  44. package/dist/persistence/IndexedDB.d.ts +8 -0
  45. package/dist/persistence/IndexedDB.js +37 -0
  46. package/dist/persistence/LocalStore.d.ts +12 -0
  47. package/dist/persistence/LocalStore.js +78 -0
  48. package/dist/persistence/MutationQueue.d.ts +22 -0
  49. package/dist/persistence/MutationQueue.js +145 -0
  50. package/dist/realtime/RealtimeConnection.d.ts +7 -0
  51. package/dist/realtime/RealtimeConnection.js +19 -0
  52. package/dist/realtime/RealtimeDatabase.d.ts +10 -0
  53. package/dist/realtime/RealtimeDatabase.js +62 -0
  54. package/dist/storage/Storage.d.ts +12 -0
  55. package/dist/storage/Storage.js +41 -0
  56. package/dist/storage/StorageReference.d.ts +13 -0
  57. package/dist/storage/StorageReference.js +20 -0
  58. package/dist/storage/UploadTask.d.ts +10 -0
  59. package/dist/storage/UploadTask.js +22 -0
  60. package/dist/sync/ConflictResolver.d.ts +3 -0
  61. package/dist/sync/ConflictResolver.js +24 -0
  62. package/dist/sync/SyncEngine.d.ts +13 -0
  63. package/dist/sync/SyncEngine.js +47 -0
  64. package/dist/transport/HttpClient.d.ts +8 -0
  65. package/dist/transport/HttpClient.js +42 -0
  66. package/dist/transport/SSEClient.d.ts +15 -0
  67. package/dist/transport/SSEClient.js +96 -0
  68. package/dist/types/index.d.ts +150 -0
  69. package/dist/types/index.js +2 -0
  70. package/dist/utils/helpers.d.ts +7 -0
  71. package/dist/utils/helpers.js +124 -0
  72. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,1025 +1,59 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
4
15
  };
5
16
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.runTransaction = exports.writeBatch = exports.enableOfflinePersistence = exports.enableIndexedDbPersistence = exports.onSnapshot = exports.deleteDoc = exports.updateDoc = exports.patchDoc = exports.setDoc = exports.getDoc = exports.getDocs = exports.getCachedDoc = exports.getCachedDocs = exports.limit = exports.orderBy = exports.where = exports.query = exports.doc = exports.collection = exports.getFirestore = exports.initializeApp = exports.NexaBase = exports.NexaApp = void 0;
7
- exports.generateIdempotencyKey = generateIdempotencyKey;
8
- const axios_1 = __importDefault(require("axios"));
9
- // -----------------------------------------------------------------
10
- // Idempotency Utility
11
- // -----------------------------------------------------------------
12
- function generateIdempotencyKey() {
13
- if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
14
- return crypto.randomUUID();
15
- }
16
- return `nexa_idemp_${Date.now()}_${Math.random().toString(36).substring(2, 9)}_${Math.floor(Math.random() * 1000000)}`;
17
- }
18
- // -----------------------------------------------------------------
19
- // Main NexaApp SDK Class
20
- // -----------------------------------------------------------------
21
- class NexaApp {
22
- constructor(config) {
23
- this.token = null;
24
- this.sse = null;
25
- // Internal Firestore and Realtime caches/states
26
- this._firestoreCache = {};
27
- this._snapshotCallbacks = {};
28
- this._sseCallbacks = { data_changed: [], firestore_changed: [] };
29
- this._offlineQueueInitialized = false;
30
- this._isSyncing = false;
31
- this._idb = null;
32
- this._firestoreListening = false;
33
- this._initPromise = null;
34
- // Real-time Engine Utilities (Cross-Tab & Cross-Device)
35
- this._broadcastChannel = null;
36
- this._reconnectAttempts = 0;
37
- this._reconnectTimer = null;
38
- this.projectId = config.projectId;
39
- this.token = config.apiKey || null;
40
- this.enablePersistence = config.enablePersistence !== false;
41
- this.cacheStrategy = config.cacheStrategy || 'cache-first';
42
- if (typeof window !== 'undefined' && window.localStorage) {
43
- const savedToken = localStorage.getItem(`nexa_token_${this.projectId}`);
44
- if (savedToken) {
45
- this.token = savedToken;
46
- }
47
- }
48
- // Auto-detect endpoint
49
- let defaultEndpoint = 'https://db.nexabase.id';
50
- if (typeof window !== 'undefined' && window.location) {
51
- defaultEndpoint = window.location.origin;
52
- }
53
- this.endpoint = config.endpoint || defaultEndpoint;
54
- // Do not set default Content-Type globally to let Axios auto-detect FormData
55
- this.client = axios_1.default.create({
56
- baseURL: this.endpoint
57
- });
58
- // Request interceptor to attach bearer/x-api-key token and boundary fix
59
- this.client.interceptors.request.use((req) => {
60
- if (this.token) {
61
- // Send both Authorization Bearer and x-api-key for full server compatibility
62
- req.headers.Authorization = `Bearer ${this.token}`;
63
- req.headers['x-api-key'] = this.token;
64
- }
65
- // If data is FormData, remove Content-Type so Axios/Browser can format multipart boundary dynamically
66
- if (typeof FormData !== 'undefined' && req.data instanceof FormData) {
67
- if (req.headers) {
68
- delete req.headers['Content-Type'];
69
- delete req.headers['content-type'];
70
- }
71
- }
72
- else {
73
- // Otherwise default to application/json for non-FormData payload requests
74
- if (req.data && req.headers && !req.headers['Content-Type'] && !req.headers['content-type']) {
75
- req.headers['Content-Type'] = 'application/json';
76
- }
77
- }
78
- return req;
79
- });
80
- // Initialize Cross-Tab Sync via BroadcastChannel
81
- this._initCrossTabSync();
82
- }
83
- // Cross-Tab Engine via BroadcastChannel API
84
- _initCrossTabSync() {
85
- if (typeof window !== 'undefined' && 'BroadcastChannel' in window && !this._broadcastChannel) {
86
- try {
87
- this._broadcastChannel = new BroadcastChannel(`nexabase_sync_${this.projectId}`);
88
- this._broadcastChannel.onmessage = (event) => {
89
- const { type, docPath, data, action } = event.data || {};
90
- if (type === 'cross_tab_mutation' && docPath) {
91
- if (action === 'set' || action === 'update' || action === 'patch') {
92
- if (action === 'patch' || action === 'update') {
93
- const existing = this._firestoreCache[docPath] || {};
94
- this._firestoreCache[docPath] = { ...existing, ...data };
95
- }
96
- else {
97
- this._firestoreCache[docPath] = data;
98
- }
99
- if (this.enablePersistence && this._idb) {
100
- this._setCache(docPath, this._firestoreCache[docPath]);
101
- }
102
- this._notifySnapshotCallbacks(docPath, 'document_written', this._firestoreCache[docPath]);
103
- }
104
- else if (action === 'delete') {
105
- delete this._firestoreCache[docPath];
106
- if (this.enablePersistence && this._idb) {
107
- this._deleteCache(docPath);
108
- }
109
- this._notifySnapshotCallbacks(docPath, 'document_deleted', null);
110
- }
111
- else if (action === 'bulk_update') {
112
- this._notifySnapshotCallbacks('*', 'bulk_update', null);
113
- }
114
- }
115
- };
116
- }
117
- catch (e) {
118
- // BroadcastChannel fallback silently if disabled by security context
119
- }
120
- }
121
- }
122
- _broadcastLocalMutation(docPath, action, data) {
123
- if (this._broadcastChannel) {
124
- try {
125
- this._broadcastChannel.postMessage({
126
- type: 'cross_tab_mutation',
127
- docPath,
128
- action,
129
- data,
130
- timestamp: Date.now()
131
- });
132
- }
133
- catch (e) { }
134
- }
135
- }
136
- // Cross-Device Realtime Connection via SSE with Auto-Reconnect & Heartbeat
137
- connectRealtime() {
138
- if (this.sse)
139
- return this.sse;
140
- const url = `${this.endpoint}/api/firestore/${this.projectId}/listen`;
141
- const listenUrl = this.token ? `${url}?token=${this.token}` : url;
142
- try {
143
- this.sse = new EventSource(listenUrl);
144
- this.sse.onmessage = async (event) => {
145
- try {
146
- const payload = JSON.parse(event.data);
147
- if (payload.type === 'connected') {
148
- this._reconnectAttempts = 0; // Reset backoff on stable connection
149
- return;
150
- }
151
- if (payload.type && (payload.type.includes('document_') || payload.type === 'bulk_update')) {
152
- this._sseCallbacks.firestore_changed.forEach((cb) => cb(payload));
153
- }
154
- else {
155
- this._sseCallbacks.data_changed.forEach((cb) => cb(payload));
156
- }
157
- }
158
- catch (e) {
159
- // Ignore JSON parse errors on ping
160
- }
161
- };
162
- this.sse.onerror = () => {
163
- if (this.sse) {
164
- this.sse.close();
165
- this.sse = null;
166
- }
167
- if (this._reconnectTimer)
168
- clearTimeout(this._reconnectTimer);
169
- // Exponential backoff reconnect: 1s, 2s, 4s, ... max 30s
170
- this._reconnectAttempts++;
171
- const backoffMs = Math.min(1000 * Math.pow(2, this._reconnectAttempts), 30000);
172
- this._reconnectTimer = setTimeout(() => {
173
- this.connectRealtime();
174
- }, backoffMs);
175
- };
176
- }
177
- catch (e) {
178
- // Return dummy EventSource if SSE fails in environment
179
- }
180
- return this.sse;
181
- }
182
- _notifySnapshotCallbacks(docPath, eventType, data) {
183
- Object.keys(this._snapshotCallbacks).forEach((key) => {
184
- const cb = this._snapshotCallbacks[key];
185
- if (docPath === '*') {
186
- cb({ type: eventType });
187
- return;
188
- }
189
- const parts = key.split('_');
190
- const pathKey = parts.slice(2).join('_');
191
- if (pathKey === docPath || docPath.startsWith(pathKey + '/')) {
192
- cb({ type: eventType, docPath, data });
193
- }
194
- });
195
- }
196
- // -----------------------------------------------------------------
197
- // Realtime Key-Value Database API
198
- // -----------------------------------------------------------------
199
- database() {
200
- return {
201
- ref: (path = '') => {
202
- const cleanPath = (path || '').replace(/^\/+|\/+$/g, '');
203
- return {
204
- path: cleanPath,
205
- get: async () => {
206
- const response = await this.client.get(`/api/db/${this.projectId}/${cleanPath}`);
207
- return response.data;
208
- },
209
- set: async (data) => {
210
- const payload = data instanceof Map ? Object.fromEntries(data) : data;
211
- const response = await this.client.put(`/api/db/${this.projectId}/${cleanPath}`, payload);
212
- return response.data;
213
- },
214
- update: async (data) => {
215
- const payload = data instanceof Map ? Object.fromEntries(data) : data;
216
- const response = await this.client.patch(`/api/db/${this.projectId}/${cleanPath}`, payload);
217
- return response.data;
218
- },
219
- delete: async () => {
220
- const response = await this.client.put(`/api/db/${this.projectId}/${cleanPath}`, null);
221
- return response.data;
222
- },
223
- remove: async () => {
224
- const response = await this.client.put(`/api/db/${this.projectId}/${cleanPath}`, null);
225
- return response.data;
226
- },
227
- onDataChanged: (callback) => {
228
- this.connectRealtime();
229
- const handler = (payload) => {
230
- const payloadPath = (payload.path || '').replace(/^\/+|\/+$/g, '');
231
- if (payloadPath === cleanPath ||
232
- payloadPath.startsWith(cleanPath + '/') ||
233
- cleanPath === '' ||
234
- cleanPath.startsWith(payloadPath + '/')) {
235
- callback(payload.data);
236
- }
237
- };
238
- this._sseCallbacks.data_changed.push(handler);
239
- return () => {
240
- this._sseCallbacks.data_changed = this._sseCallbacks.data_changed.filter((cb) => cb !== handler);
241
- };
242
- }
243
- };
244
- }
245
- };
246
- }
247
- // -----------------------------------------------------------------
248
- // User Authentication API
249
- // -----------------------------------------------------------------
250
- auth() {
251
- return {
252
- signInWithEmailAndPassword: async (email, password) => {
253
- const res = await this.client.post(`/api/project/${this.projectId}/auth/login`, { email, password });
254
- if (res.data.token) {
255
- this.token = res.data.token;
256
- if (typeof window !== 'undefined' && window.localStorage) {
257
- localStorage.setItem(`nexa_token_${this.projectId}`, res.data.token);
258
- localStorage.setItem(`nexa_user_${this.projectId}`, JSON.stringify(res.data.user));
259
- }
260
- }
261
- return res.data;
262
- },
263
- createUserWithEmailAndPassword: async (email, password, name) => {
264
- const res = await this.client.post(`/api/project/${this.projectId}/users`, { email, password, name });
265
- return res.data;
266
- },
267
- sendOtp: async (email) => {
268
- const res = await this.client.post(`/api/project/${this.projectId}/auth/otp/send`, { email });
269
- return res.data;
270
- },
271
- signInWithOtp: async (email, otp, name) => {
272
- const res = await this.client.post(`/api/project/${this.projectId}/auth/otp/login`, { email, otp, name });
273
- if (res.data.token) {
274
- this.token = res.data.token;
275
- if (typeof window !== 'undefined' && window.localStorage) {
276
- localStorage.setItem(`nexa_token_${this.projectId}`, res.data.token);
277
- localStorage.setItem(`nexa_user_${this.projectId}`, JSON.stringify(res.data.user));
278
- }
279
- }
280
- return res.data;
281
- },
282
- signOut: () => {
283
- this.token = null;
284
- if (typeof window !== 'undefined' && window.localStorage) {
285
- localStorage.removeItem(`nexa_token_${this.projectId}`);
286
- localStorage.removeItem(`nexa_user_${this.projectId}`);
287
- }
288
- },
289
- getCurrentUser: () => {
290
- if (typeof window !== 'undefined' && window.localStorage) {
291
- const userStr = localStorage.getItem(`nexa_user_${this.projectId}`);
292
- return userStr ? JSON.parse(userStr) : null;
293
- }
294
- return null;
295
- }
296
- };
297
- }
298
- // -----------------------------------------------------------------
299
- // Blob File Storage API
300
- // -----------------------------------------------------------------
301
- storage() {
302
- return {
303
- ref: (path = '') => {
304
- const name = path.substring(path.lastIndexOf('/') + 1) || path;
305
- return {
306
- name,
307
- fullPath: path,
308
- put: async (file, options, onProgress) => {
309
- let actualOptions = {};
310
- let actualOnProgress = onProgress;
311
- if (typeof options === 'function') {
312
- actualOnProgress = options;
313
- }
314
- else if (options && typeof options === 'object') {
315
- actualOptions = options;
316
- }
317
- const formData = new FormData();
318
- formData.append('file', file);
319
- if (actualOptions.contextType) {
320
- formData.append('contextType', actualOptions.contextType);
321
- }
322
- if (actualOptions.contextId) {
323
- formData.append('contextId', actualOptions.contextId);
324
- }
325
- if (actualOptions.fileId) {
326
- formData.append('fileId', actualOptions.fileId);
327
- }
328
- const response = await this.client.post(`/api/project/${this.projectId}/storage/upload`, formData, {
329
- onUploadProgress: (progressEvent) => {
330
- if (actualOnProgress && progressEvent.total) {
331
- const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
332
- actualOnProgress(percent);
333
- }
334
- }
335
- });
336
- return {
337
- url: response.data.url,
338
- success: true,
339
- fileId: response.data.fileId,
340
- proxyViewUrl: response.data.proxyViewUrl,
341
- proxyDownloadUrl: response.data.proxyDownloadUrl,
342
- metadata: response.data.metadata
343
- };
344
- },
345
- delete: async () => {
346
- const response = await this.client.delete(`/api/project/${this.projectId}/storage/${encodeURIComponent(name)}`);
347
- return response.data;
348
- }
349
- };
350
- },
351
- refFromURL: (url) => {
352
- const cleanUrl = url.split('?')[0];
353
- const filename = decodeURIComponent(cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1));
354
- return this.storage().ref(filename);
355
- }
356
- };
357
- }
358
- // -----------------------------------------------------------------
359
- // Offline Synchronization & Local IndexedDB Cache Engine
360
- // -----------------------------------------------------------------
361
- async _initFirestoreState() {
362
- if (!this.enablePersistence)
363
- return; // Respect enablePersistence ON / OFF option
364
- if (this._offlineQueueInitialized)
365
- return;
366
- this._offlineQueueInitialized = true;
367
- this._isSyncing = false;
368
- if (typeof window === 'undefined' || !window.indexedDB) {
369
- return;
370
- }
371
- const dbName = `nexa_idb_${this.projectId}`;
372
- try {
373
- this._idb = await new Promise((resolve, reject) => {
374
- const req = indexedDB.open(dbName, 1);
375
- req.onupgradeneeded = (e) => {
376
- const db = e.target.result;
377
- if (!db.objectStoreNames.contains('offline_queue')) {
378
- db.createObjectStore('offline_queue', { keyPath: 'id' });
379
- }
380
- if (!db.objectStoreNames.contains('offline_cache')) {
381
- db.createObjectStore('offline_cache', { keyPath: 'path' });
382
- }
383
- };
384
- req.onsuccess = () => resolve(req.result);
385
- req.onerror = () => reject(req.error);
386
- });
387
- await this._loadCache();
388
- if (typeof window !== 'undefined') {
389
- window.addEventListener('online', () => this._syncOfflineQueue());
390
- setTimeout(() => this._syncOfflineQueue(), 1000);
391
- }
392
- }
393
- catch (err) {
394
- console.warn('[NexaBase SDK] IndexedDB persistence unavailable, falling back to memory cache.');
395
- }
396
- }
397
- async _getOfflineQueue() {
398
- if (!this._idb || !this.enablePersistence)
399
- return [];
400
- return new Promise((resolve, reject) => {
401
- const tx = this._idb.transaction('offline_queue', 'readonly');
402
- const store = tx.objectStore('offline_queue');
403
- const req = store.getAll();
404
- req.onsuccess = () => resolve(req.result);
405
- req.onerror = () => reject(req.error);
406
- });
407
- }
408
- async _saveOfflineQueue(queue) {
409
- if (!this._idb || !this.enablePersistence)
410
- return;
411
- return new Promise((resolve, reject) => {
412
- const tx = this._idb.transaction('offline_queue', 'readwrite');
413
- const store = tx.objectStore('offline_queue');
414
- store.clear();
415
- queue.forEach((item) => store.put(item));
416
- tx.oncomplete = () => resolve();
417
- tx.onerror = () => reject(tx.error);
418
- });
419
- }
420
- async _addOfflineJob(job) {
421
- if (!this._idb || !this.enablePersistence)
422
- return;
423
- return new Promise((resolve, reject) => {
424
- const tx = this._idb.transaction('offline_queue', 'readwrite');
425
- const store = tx.objectStore('offline_queue');
426
- store.put(job);
427
- tx.oncomplete = () => {
428
- resolve();
429
- if (typeof navigator !== 'undefined' && navigator.onLine) {
430
- this._syncOfflineQueue();
431
- }
432
- };
433
- tx.onerror = () => reject(tx.error);
434
- });
435
- }
436
- async _setCache(path, data) {
437
- this._firestoreCache[path] = data;
438
- if (!this._idb || !this.enablePersistence)
439
- return;
440
- return new Promise((resolve, reject) => {
441
- const tx = this._idb.transaction('offline_cache', 'readwrite');
442
- const store = tx.objectStore('offline_cache');
443
- store.put({ path, data });
444
- tx.oncomplete = () => resolve();
445
- tx.onerror = () => reject(tx.error);
446
- });
447
- }
448
- async _deleteCache(path) {
449
- delete this._firestoreCache[path];
450
- if (!this._idb || !this.enablePersistence)
451
- return;
452
- return new Promise((resolve, reject) => {
453
- const tx = this._idb.transaction('offline_cache', 'readwrite');
454
- const store = tx.objectStore('offline_cache');
455
- store.delete(path);
456
- tx.oncomplete = () => resolve();
457
- tx.onerror = () => reject(tx.error);
458
- });
459
- }
460
- async _loadCache() {
461
- if (!this._idb || !this.enablePersistence)
462
- return;
463
- return new Promise((resolve, reject) => {
464
- const tx = this._idb.transaction('offline_cache', 'readonly');
465
- const store = tx.objectStore('offline_cache');
466
- const req = store.getAll();
467
- req.onsuccess = () => {
468
- req.result.forEach((item) => {
469
- this._firestoreCache[item.path] = item.data;
470
- });
471
- resolve();
472
- };
473
- req.onerror = () => reject(req.error);
474
- });
475
- }
476
- // Outbox Pattern Offline Queue Flush with Idempotency Key
477
- async _syncOfflineQueue() {
478
- if (this._isSyncing || !this.enablePersistence)
479
- return;
480
- this._isSyncing = true;
481
- const queue = await this._getOfflineQueue();
482
- if (queue.length === 0) {
483
- this._isSyncing = false;
484
- return;
485
- }
486
- for (const job of [...queue]) {
487
- try {
488
- const headers = job.idempotencyKey ? { 'X-Idempotency-Key': job.idempotencyKey } : {};
489
- if (job.type === 'set') {
490
- await this.client.post(`/api/firestore/${this.projectId}/document`, { docPath: job.path, data: job.data, idempotencyKey: job.idempotencyKey }, { headers });
491
- }
492
- else if (job.type === 'update' || job.type === 'patch') {
493
- await this.client.patch(`/api/firestore/${this.projectId}/document`, { docPath: job.path, data: job.data, idempotencyKey: job.idempotencyKey }, { headers });
494
- }
495
- else if (job.type === 'delete') {
496
- await this.client.delete(`/api/firestore/${this.projectId}/document?docPath=${encodeURIComponent(job.path)}`, { headers });
497
- }
498
- const currentQueue = await this._getOfflineQueue();
499
- await this._saveOfflineQueue(currentQueue.filter((q) => q.id !== job.id));
500
- }
501
- catch (err) {
502
- break; // Stop syncing on network exception
503
- }
504
- }
505
- this._isSyncing = false;
506
- this._notifySnapshotCallbacks('*', 'bulk_update', null);
507
- }
508
- _ensureFirestoreSSE() {
509
- this.connectRealtime();
510
- if (this._firestoreListening)
511
- return;
512
- this._firestoreListening = true;
513
- this._sseCallbacks.firestore_changed.push(async (change) => {
514
- if (change.type === 'bulk_update') {
515
- this._notifySnapshotCallbacks('*', 'bulk_update', null);
516
- return;
517
- }
518
- if (change.type === 'document_written') {
519
- await this._setCache(change.docPath, change.data);
520
- }
521
- else if (change.type === 'document_deleted') {
522
- await this._deleteCache(change.docPath);
523
- }
524
- this._notifySnapshotCallbacks(change.docPath, change.type, change.data);
525
- });
526
- }
527
- }
528
- exports.NexaApp = NexaApp;
529
- // -----------------------------------------------------------------
530
- // Static NexaBase Initialization Helper
531
- // -----------------------------------------------------------------
532
- exports.NexaBase = {
533
- init: (config) => {
534
- const app = new NexaApp(config);
535
- if (app.enablePersistence) {
536
- app._initPromise = app._initFirestoreState();
537
- }
538
- return app;
539
- },
540
- initializeApp: (config) => {
541
- return exports.NexaBase.init(config);
542
- }
543
- };
544
- // -----------------------------------------------------------------
545
- // Modular Firestore-like APIs (Firebase Modular pattern)
546
- // -----------------------------------------------------------------
547
- const initializeApp = (config) => {
548
- return exports.NexaBase.init(config);
549
- };
550
- exports.initializeApp = initializeApp;
551
- const getFirestore = (app) => {
552
- if (app.enablePersistence) {
553
- app._initPromise = app._initFirestoreState();
554
- }
555
- return app;
556
- };
557
- exports.getFirestore = getFirestore;
558
- const collection = (db, collectionPath) => {
559
- return { type: 'collection', path: collectionPath, db };
560
- };
561
- exports.collection = collection;
562
- const doc = (dbOrCollection, ...pathSegments) => {
563
- let docRef;
564
- if ('type' in dbOrCollection && dbOrCollection.type === 'collection') {
565
- docRef = { type: 'document', path: `${dbOrCollection.path}/${pathSegments.join('/')}`, db: dbOrCollection.db };
566
- }
567
- else {
568
- docRef = { type: 'document', path: pathSegments.join('/'), db: dbOrCollection };
569
- }
570
- docRef.patch = (data) => (0, exports.patchDoc)(docRef, data);
571
- return docRef;
572
- };
573
- exports.doc = doc;
574
- const query = (queryObject, ...queryConstraints) => {
575
- const q = {
576
- type: 'query',
577
- path: queryObject.path,
578
- db: queryObject.db,
579
- constraints: queryObject.type === 'query' ? [...(queryObject.constraints || [])] : []
580
- };
581
- q.constraints = [...(q.constraints || []), ...queryConstraints];
582
- return q;
583
- };
584
- exports.query = query;
585
- const where = (fieldPath, opStr, value) => {
586
- return { type: 'where', fieldPath, opStr, value };
587
- };
588
- exports.where = where;
589
- const orderBy = (fieldPath, directionStr = 'asc') => {
590
- return { type: 'orderBy', fieldPath, directionStr };
591
- };
592
- exports.orderBy = orderBy;
593
- const limit = (limitValue) => {
594
- return { type: 'limit', limitValue };
595
- };
596
- exports.limit = limit;
597
- // Helper to retrieve docs from local cache
598
- const getCachedDocs = (queryOrCollection) => {
599
- const db = queryOrCollection.db;
600
- const collectionPath = queryOrCollection.path;
601
- const constraints = 'constraints' in queryOrCollection ? queryOrCollection.constraints || [] : [];
602
- const results = [];
603
- Object.keys(db._firestoreCache).forEach((p) => {
604
- if (p.startsWith(collectionPath + '/')) {
605
- const parts = p.split('/');
606
- const docId = parts[parts.length - 1];
607
- const docRef = (0, exports.doc)(db, `${collectionPath}/${docId}`);
608
- results.push({
609
- id: docId,
610
- ref: docRef,
611
- exists: () => true,
612
- data: () => db._firestoreCache[p]
613
- });
614
- }
615
- });
616
- let filtered = results;
617
- for (const c of constraints) {
618
- if (c.type === 'where' && c.fieldPath && c.opStr) {
619
- filtered = filtered.filter((docItem) => {
620
- const val = docItem.data()[c.fieldPath];
621
- if (c.opStr === '==')
622
- return val === c.value;
623
- if (c.opStr === '>')
624
- return val > c.value;
625
- if (c.opStr === '<')
626
- return val < c.value;
627
- if (c.opStr === '>=')
628
- return val >= c.value;
629
- if (c.opStr === '<=')
630
- return val <= c.value;
631
- if (c.opStr === '!=')
632
- return val !== c.value;
633
- return true;
634
- });
635
- }
636
- }
637
- return {
638
- docs: filtered,
639
- forEach: (cb) => filtered.forEach(cb),
640
- empty: filtered.length === 0,
641
- size: filtered.length,
642
- docChanges: () => filtered.map((d) => ({ type: 'added', doc: d }))
643
- };
644
- };
645
- exports.getCachedDocs = getCachedDocs;
646
- const getCachedDoc = (docRef) => {
647
- const db = docRef.db;
648
- const segments = docRef.path.split('/');
649
- const docId = segments[segments.length - 1];
650
- const data = db._firestoreCache[docRef.path];
651
- return {
652
- id: docId,
653
- ref: docRef,
654
- exists: () => !!data,
655
- data: () => (data || null)
656
- };
657
- };
658
- exports.getCachedDoc = getCachedDoc;
659
- const getDocs = async (queryOrCollection) => {
660
- const db = queryOrCollection.db;
661
- if (db._initPromise)
662
- await db._initPromise;
663
- const collectionPath = queryOrCollection.path;
664
- const constraints = 'constraints' in queryOrCollection ? queryOrCollection.constraints || [] : [];
665
- try {
666
- const res = await db.client.post(`/api/firestore/${db.projectId}/query`, {
667
- collectionPath,
668
- constraints
669
- });
670
- const docs = res.data.documents || [];
671
- for (const d of docs) {
672
- await db._setCache(`${collectionPath}/${d.id}`, d.fields);
673
- }
674
- const docsArray = docs.map((d) => {
675
- const docRef = (0, exports.doc)(db, `${collectionPath}/${d.id}`);
676
- return {
677
- id: d.id,
678
- ref: docRef,
679
- exists: () => true,
680
- data: () => d.fields
681
- };
682
- });
683
- return {
684
- docs: docsArray,
685
- forEach: (cb) => docsArray.forEach(cb),
686
- empty: docsArray.length === 0,
687
- size: docsArray.length,
688
- docChanges: () => docsArray.map((d) => ({ type: 'added', doc: d }))
689
- };
690
- }
691
- catch (err) {
692
- if (err && err.response) {
693
- throw err;
694
- }
695
- return (0, exports.getCachedDocs)(queryOrCollection);
696
- }
697
- };
698
- exports.getDocs = getDocs;
699
- const getDoc = async (docRef) => {
700
- const db = docRef.db;
701
- if (db._initPromise)
702
- await db._initPromise;
703
- const segments = docRef.path.split('/');
704
- const docId = segments[segments.length - 1];
705
- try {
706
- const res = await db.client.get(`/api/firestore/${db.projectId}/document?docPath=${encodeURIComponent(docRef.path)}`);
707
- const data = res.data.data;
708
- await db._setCache(docRef.path, data);
709
- return {
710
- id: docId,
711
- ref: docRef,
712
- exists: () => !!data,
713
- data: () => data
714
- };
715
- }
716
- catch (err) {
717
- if (err && err.response) {
718
- throw err;
719
- }
720
- return (0, exports.getCachedDoc)(docRef);
721
- }
722
- };
723
- exports.getDoc = getDoc;
724
- // -----------------------------------------------------------------
725
- // Atomic Document Mutation APIs with Idempotency & Partial Patching
726
- // -----------------------------------------------------------------
727
- const setDoc = async (docRef, data, options) => {
728
- const db = docRef.db;
729
- if (db._initPromise)
730
- await db._initPromise;
731
- await db._setCache(docRef.path, data);
732
- // Broadcast cross-tab and notify snapshot listeners
733
- db._broadcastLocalMutation(docRef.path, 'set', data);
734
- db._notifySnapshotCallbacks(docRef.path, 'document_written', data);
735
- const key = options?.idempotencyKey || generateIdempotencyKey();
736
- const jobId = Math.random().toString(36).substring(2, 9);
737
- const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
738
- if (!isOnline) {
739
- await db._addOfflineJob({ id: jobId, type: 'set', path: docRef.path, data, idempotencyKey: key });
740
- return { success: true, message: 'Offline: Tersimpan di cache lokal' };
741
- }
742
- else {
743
- try {
744
- return (await db.client.post(`/api/firestore/${db.projectId}/document`, { docPath: docRef.path, data, idempotencyKey: key }, { headers: { 'X-Idempotency-Key': key } })).data;
745
- }
746
- catch (error) {
747
- await db._addOfflineJob({ id: jobId, type: 'set', path: docRef.path, data, idempotencyKey: key });
748
- return { success: true, message: 'Koneksi terganggu: Tersimpan di antrean offline' };
749
- }
750
- }
751
- };
752
- exports.setDoc = setDoc;
753
- // Partial Update (UpdateDoc) & Atomic Patch (PatchDoc)
754
- const patchDoc = async (docRef, data, options) => {
755
- const db = docRef.db;
756
- if (db._initPromise)
757
- await db._initPromise;
758
- const existingData = db._firestoreCache[docRef.path] || {};
759
- const updatedData = { ...existingData, ...data };
760
- await db._setCache(docRef.path, updatedData);
761
- db._broadcastLocalMutation(docRef.path, 'patch', data);
762
- db._notifySnapshotCallbacks(docRef.path, 'document_written', updatedData);
763
- const key = options?.idempotencyKey || generateIdempotencyKey();
764
- const jobId = Math.random().toString(36).substring(2, 9);
765
- const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
766
- if (!isOnline) {
767
- await db._addOfflineJob({ id: jobId, type: 'patch', path: docRef.path, data, idempotencyKey: key });
768
- return { success: true, message: 'Offline: Tersimpan di cache lokal' };
769
- }
770
- else {
771
- try {
772
- return (await db.client.patch(`/api/firestore/${db.projectId}/document`, { docPath: docRef.path, data, idempotencyKey: key }, { headers: { 'X-Idempotency-Key': key } })).data;
773
- }
774
- catch (error) {
775
- await db._addOfflineJob({ id: jobId, type: 'patch', path: docRef.path, data, idempotencyKey: key });
776
- return { success: true, message: 'Koneksi terganggu: Tersimpan di antrean offline' };
777
- }
778
- }
779
- };
780
- exports.patchDoc = patchDoc;
781
- const updateDoc = async (docRef, data, options) => {
782
- return (0, exports.patchDoc)(docRef, data, options);
783
- };
784
- exports.updateDoc = updateDoc;
785
- const deleteDoc = async (docRef, options) => {
786
- const db = docRef.db;
787
- if (db._initPromise)
788
- await db._initPromise;
789
- await db._deleteCache(docRef.path);
790
- db._broadcastLocalMutation(docRef.path, 'delete');
791
- db._notifySnapshotCallbacks(docRef.path, 'document_deleted', null);
792
- const key = options?.idempotencyKey || generateIdempotencyKey();
793
- const jobId = Math.random().toString(36).substring(2, 9);
794
- const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
795
- if (!isOnline) {
796
- await db._addOfflineJob({ id: jobId, type: 'delete', path: docRef.path, idempotencyKey: key });
797
- return { success: true };
798
- }
799
- else {
800
- try {
801
- return (await db.client.delete(`/api/firestore/${db.projectId}/document?docPath=${encodeURIComponent(docRef.path)}`, { headers: { 'X-Idempotency-Key': key } })).data;
802
- }
803
- catch (error) {
804
- await db._addOfflineJob({ id: jobId, type: 'delete', path: docRef.path, idempotencyKey: key });
805
- return { success: true };
806
- }
807
- }
808
- };
809
- exports.deleteDoc = deleteDoc;
810
- const onSnapshot = (ref, callback) => {
811
- const db = ref.db;
812
- const initSnapshot = async () => {
813
- if (db._initPromise)
814
- await db._initPromise;
815
- db._ensureFirestoreSSE();
816
- const pathKey = ref.path;
817
- // Fast cache emit
818
- if (ref.type === 'collection' || ref.type === 'query') {
819
- const cached = (0, exports.getCachedDocs)(ref);
820
- if (cached && cached.docs.length > 0) {
821
- callback(cached);
822
- }
823
- }
824
- else {
825
- const cached = (0, exports.getCachedDoc)(ref);
826
- if (cached && cached.exists()) {
827
- callback(cached);
828
- }
829
- }
830
- let debounceTimer = null;
831
- const triggerCallback = () => {
832
- if (debounceTimer)
833
- clearTimeout(debounceTimer);
834
- debounceTimer = setTimeout(async () => {
835
- if (ref.type === 'collection' || ref.type === 'query') {
836
- callback((0, exports.getCachedDocs)(ref));
837
- try {
838
- const docs = await (0, exports.getDocs)(ref);
839
- callback(docs);
840
- }
841
- catch (e) { }
842
- }
843
- else {
844
- callback((0, exports.getCachedDoc)(ref));
845
- try {
846
- const docData = await (0, exports.getDoc)(ref);
847
- callback(docData);
848
- }
849
- catch (e) { }
850
- }
851
- }, 50); // Debounce to batch rapid updates
852
- };
853
- triggerCallback();
854
- const listenerId = Math.random().toString(36).substring(2, 9);
855
- db._snapshotCallbacks[`${ref.type}_${listenerId}_${pathKey}`] = triggerCallback;
856
- return () => {
857
- delete db._snapshotCallbacks[`${ref.type}_${listenerId}_${pathKey}`];
858
- };
859
- };
860
- let unsubscribe = () => { };
861
- initSnapshot().then((unsub) => {
862
- unsubscribe = unsub;
863
- });
864
- return () => unsubscribe();
865
- };
866
- exports.onSnapshot = onSnapshot;
867
- const enableIndexedDbPersistence = async (db) => {
868
- db.enablePersistence = true;
869
- if (db._initPromise)
870
- await db._initPromise;
871
- };
872
- exports.enableIndexedDbPersistence = enableIndexedDbPersistence;
873
- const enableOfflinePersistence = async (db) => {
874
- db.enablePersistence = true;
875
- if (db._initPromise)
876
- await db._initPromise;
877
- };
878
- exports.enableOfflinePersistence = enableOfflinePersistence;
879
- const writeBatch = (db) => {
880
- const operations = [];
881
- const batchObj = {
882
- set: (docRef, data) => {
883
- operations.push({ type: 'set', path: docRef.path, data });
884
- return batchObj;
885
- },
886
- update: (docRef, data) => {
887
- operations.push({ type: 'update', path: docRef.path, data });
888
- return batchObj;
889
- },
890
- patch: (docRef, data) => {
891
- operations.push({ type: 'update', path: docRef.path, data });
892
- return batchObj;
893
- },
894
- delete: (docRef) => {
895
- operations.push({ type: 'delete', path: docRef.path });
896
- return batchObj;
897
- },
898
- commit: async () => {
899
- if (db._initPromise)
900
- await db._initPromise;
901
- if (operations.length === 0)
902
- return;
903
- const idempotencyKey = generateIdempotencyKey();
904
- const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
905
- // Update local cache optimistically
906
- for (const op of operations) {
907
- if (op.type === 'set' || op.type === 'update') {
908
- const newData = { ...db._firestoreCache[op.path], ...op.data };
909
- await db._setCache(op.path, newData);
910
- db._broadcastLocalMutation(op.path, op.type === 'set' ? 'set' : 'patch', op.data);
911
- db._notifySnapshotCallbacks(op.path, 'document_written', db._firestoreCache[op.path]);
912
- }
913
- else if (op.type === 'delete') {
914
- await db._deleteCache(op.path);
915
- db._broadcastLocalMutation(op.path, 'delete');
916
- db._notifySnapshotCallbacks(op.path, 'document_deleted', null);
917
- }
918
- }
919
- if (!isOnline) {
920
- for (const op of operations) {
921
- await db._addOfflineJob({
922
- id: Math.random().toString(36).substring(2, 9),
923
- type: op.type,
924
- path: op.path,
925
- data: op.data,
926
- idempotencyKey
927
- });
928
- }
929
- return { success: true, message: 'Offline: Batch operations queued in IndexedDB' };
930
- }
931
- else {
932
- try {
933
- return (await db.client.post(`/api/firestore/${db.projectId}/batch`, { operations, idempotencyKey }, { headers: { 'X-Idempotency-Key': idempotencyKey } })).data;
934
- }
935
- catch (error) {
936
- for (const op of operations) {
937
- await db._addOfflineJob({
938
- id: Math.random().toString(36).substring(2, 9),
939
- type: op.type,
940
- path: op.path,
941
- data: op.data,
942
- idempotencyKey
943
- });
944
- }
945
- return { success: true, message: 'Koneksi terganggu: Tersimpan di antrean offline' };
946
- }
947
- }
948
- }
949
- };
950
- return batchObj;
951
- };
952
- exports.writeBatch = writeBatch;
953
- const runTransaction = async (db, updateFunction, options) => {
954
- if (db._initPromise)
955
- await db._initPromise;
956
- const maxAttempts = options?.maxAttempts ?? 5;
957
- let attempt = 0;
958
- while (true) {
959
- attempt++;
960
- const reads = [];
961
- const operations = [];
962
- const idempotencyKey = generateIdempotencyKey();
963
- const transaction = {
964
- get: async (docRef) => {
965
- const segments = docRef.path.split('/');
966
- const docId = segments[segments.length - 1];
967
- const res = await db.client.get(`/api/firestore/${db.projectId}/documentWithMetadata?docPath=${encodeURIComponent(docRef.path)}`);
968
- const { data, updatedAt } = res.data;
969
- reads.push({ path: docRef.path, readUpdatedAt: updatedAt });
970
- return {
971
- id: docId,
972
- ref: docRef,
973
- exists: () => !!data,
974
- data: () => data
975
- };
976
- },
977
- set: (docRef, data) => {
978
- operations.push({ type: 'set', path: docRef.path, data });
979
- return transaction;
980
- },
981
- update: (docRef, data) => {
982
- operations.push({ type: 'update', path: docRef.path, data });
983
- return transaction;
984
- },
985
- patch: (docRef, data) => {
986
- operations.push({ type: 'update', path: docRef.path, data });
987
- return transaction;
988
- },
989
- delete: (docRef) => {
990
- operations.push({ type: 'delete', path: docRef.path });
991
- return transaction;
992
- }
993
- };
994
- try {
995
- const result = await updateFunction(transaction);
996
- if (operations.length > 0 || reads.length > 0) {
997
- await db.client.post(`/api/firestore/${db.projectId}/transaction`, { reads, operations, idempotencyKey }, { headers: { 'X-Idempotency-Key': idempotencyKey } });
998
- }
999
- for (const op of operations) {
1000
- if (op.type === 'set' || op.type === 'update') {
1001
- const newData = { ...db._firestoreCache[op.path], ...op.data };
1002
- await db._setCache(op.path, newData);
1003
- db._broadcastLocalMutation(op.path, op.type === 'set' ? 'set' : 'patch', op.data);
1004
- db._notifySnapshotCallbacks(op.path, 'document_written', db._firestoreCache[op.path]);
1005
- }
1006
- else if (op.type === 'delete') {
1007
- await db._deleteCache(op.path);
1008
- db._broadcastLocalMutation(op.path, 'delete');
1009
- db._notifySnapshotCallbacks(op.path, 'document_deleted', null);
1010
- }
1011
- }
1012
- return result;
1013
- }
1014
- catch (err) {
1015
- const isConflict = err && err.response && err.response.status === 409;
1016
- if (isConflict && attempt < maxAttempts) {
1017
- console.warn(`[NexaBase Transaction] Retrying due to concurrency conflict (Attempt ${attempt}/${maxAttempts})`);
1018
- await new Promise((resolve) => setTimeout(resolve, Math.random() * 100 * attempt));
1019
- continue;
1020
- }
1021
- throw err;
1022
- }
1023
- }
1024
- };
1025
- exports.runTransaction = runTransaction;
17
+ // App
18
+ __exportStar(require("./app/initializeApp"), exports);
19
+ __exportStar(require("./app/NexaApp"), exports);
20
+ // Auth
21
+ __exportStar(require("./auth/Auth"), exports);
22
+ __exportStar(require("./auth/authTypes"), exports);
23
+ __exportStar(require("./auth/persistence"), exports);
24
+ // Firestore
25
+ __exportStar(require("./firestore/Firestore"), exports);
26
+ __exportStar(require("./firestore/DocumentReference"), exports);
27
+ __exportStar(require("./firestore/CollectionReference"), exports);
28
+ __exportStar(require("./firestore/Query"), exports);
29
+ __exportStar(require("./firestore/Snapshot"), exports);
30
+ __exportStar(require("./firestore/queryConstraints"), exports);
31
+ __exportStar(require("./firestore/writes"), exports);
32
+ __exportStar(require("./firestore/batch"), exports);
33
+ __exportStar(require("./firestore/transaction"), exports);
34
+ __exportStar(require("./firestore/Timestamp"), exports);
35
+ __exportStar(require("./firestore/GeoPoint"), exports);
36
+ __exportStar(require("./firestore/FieldPath"), exports);
37
+ __exportStar(require("./firestore/FieldValue"), exports);
38
+ __exportStar(require("./firestore/SnapshotManager"), exports);
39
+ // Realtime
40
+ __exportStar(require("./realtime/RealtimeDatabase"), exports);
41
+ __exportStar(require("./realtime/RealtimeConnection"), exports);
42
+ // Storage
43
+ __exportStar(require("./storage/Storage"), exports);
44
+ __exportStar(require("./storage/StorageReference"), exports);
45
+ __exportStar(require("./storage/UploadTask"), exports);
46
+ // Persistence
47
+ __exportStar(require("./persistence/IndexedDB"), exports);
48
+ __exportStar(require("./persistence/LocalStore"), exports);
49
+ __exportStar(require("./persistence/MutationQueue"), exports);
50
+ // Sync
51
+ __exportStar(require("./sync/SyncEngine"), exports);
52
+ __exportStar(require("./sync/ConflictResolver"), exports);
53
+ // Transport
54
+ __exportStar(require("./transport/HttpClient"), exports);
55
+ __exportStar(require("./transport/SSEClient"), exports);
56
+ // Errors & Types
57
+ __exportStar(require("./errors/NexaError"), exports);
58
+ __exportStar(require("./types/index"), exports);
59
+ __exportStar(require("./utils/helpers"), exports);