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.
- package/README.md +33 -8
- package/dist/app/NexaApp.d.ts +63 -0
- package/dist/app/NexaApp.js +184 -0
- package/dist/app/initializeApp.d.ts +8 -0
- package/dist/app/initializeApp.js +27 -0
- package/dist/auth/Auth.d.ts +21 -0
- package/dist/auth/Auth.js +73 -0
- package/dist/auth/authTypes.d.ts +1 -0
- package/dist/auth/authTypes.js +2 -0
- package/dist/auth/persistence.d.ts +9 -0
- package/dist/auth/persistence.js +34 -0
- package/dist/errors/NexaError.d.ts +7 -0
- package/dist/errors/NexaError.js +61 -0
- package/dist/firestore/CollectionReference.d.ts +3 -0
- package/dist/firestore/CollectionReference.js +7 -0
- package/dist/firestore/DocumentReference.d.ts +3 -0
- package/dist/firestore/DocumentReference.js +22 -0
- package/dist/firestore/FieldPath.d.ts +7 -0
- package/dist/firestore/FieldPath.js +23 -0
- package/dist/firestore/FieldValue.d.ts +11 -0
- package/dist/firestore/FieldValue.js +21 -0
- package/dist/firestore/Firestore.d.ts +8 -0
- package/dist/firestore/Firestore.js +24 -0
- package/dist/firestore/GeoPoint.d.ts +10 -0
- package/dist/firestore/GeoPoint.js +22 -0
- package/dist/firestore/Query.d.ts +4 -0
- package/dist/firestore/Query.js +172 -0
- package/dist/firestore/Snapshot.d.ts +6 -0
- package/dist/firestore/Snapshot.js +126 -0
- package/dist/firestore/SnapshotManager.d.ts +9 -0
- package/dist/firestore/SnapshotManager.js +37 -0
- package/dist/firestore/Timestamp.d.ts +16 -0
- package/dist/firestore/Timestamp.js +36 -0
- package/dist/firestore/batch.d.ts +3 -0
- package/dist/firestore/batch.js +79 -0
- package/dist/firestore/queryConstraints.d.ts +9 -0
- package/dist/firestore/queryConstraints.js +31 -0
- package/dist/firestore/transaction.d.ts +5 -0
- package/dist/firestore/transaction.js +71 -0
- package/dist/firestore/writes.d.ts +15 -0
- package/dist/firestore/writes.js +146 -0
- package/dist/index.d.ts +34 -206
- package/dist/index.js +56 -1022
- package/dist/persistence/IndexedDB.d.ts +8 -0
- package/dist/persistence/IndexedDB.js +37 -0
- package/dist/persistence/LocalStore.d.ts +12 -0
- package/dist/persistence/LocalStore.js +78 -0
- package/dist/persistence/MutationQueue.d.ts +22 -0
- package/dist/persistence/MutationQueue.js +145 -0
- package/dist/realtime/RealtimeConnection.d.ts +7 -0
- package/dist/realtime/RealtimeConnection.js +19 -0
- package/dist/realtime/RealtimeDatabase.d.ts +10 -0
- package/dist/realtime/RealtimeDatabase.js +62 -0
- package/dist/storage/Storage.d.ts +12 -0
- package/dist/storage/Storage.js +41 -0
- package/dist/storage/StorageReference.d.ts +13 -0
- package/dist/storage/StorageReference.js +20 -0
- package/dist/storage/UploadTask.d.ts +10 -0
- package/dist/storage/UploadTask.js +22 -0
- package/dist/sync/ConflictResolver.d.ts +3 -0
- package/dist/sync/ConflictResolver.js +24 -0
- package/dist/sync/SyncEngine.d.ts +13 -0
- package/dist/sync/SyncEngine.js +47 -0
- package/dist/transport/HttpClient.d.ts +8 -0
- package/dist/transport/HttpClient.js +42 -0
- package/dist/transport/SSEClient.d.ts +15 -0
- package/dist/transport/SSEClient.js +96 -0
- package/dist/types/index.d.ts +150 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/helpers.d.ts +7 -0
- package/dist/utils/helpers.js +124 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,1025 +1,59 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
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
|
-
|
|
7
|
-
exports
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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);
|