edmaxlabs-core 1.2.4 → 1.3.5
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 +36 -53
- package/dist/index.cjs +1591 -852
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +273 -95
- package/dist/index.d.ts +273 -95
- package/dist/index.mjs +1590 -852
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
ArraySnapshot: () => ArraySnapshot,
|
|
24
|
+
Authentication: () => Authentication,
|
|
24
25
|
Credentials: () => Credentials,
|
|
25
26
|
DocumentSnapshot: () => DocumentSnapshot,
|
|
26
27
|
StorageSnapshot: () => StorageSnapshot,
|
|
@@ -110,112 +111,18 @@ var HttpsRequest = class {
|
|
|
110
111
|
}
|
|
111
112
|
};
|
|
112
113
|
|
|
113
|
-
// src/utils/waiter.ts
|
|
114
|
-
var listeners = /* @__PURE__ */ new Map();
|
|
115
|
-
var waiters = /* @__PURE__ */ new Map();
|
|
116
|
-
function emitValue(key, value) {
|
|
117
|
-
const keyListeners = listeners.get(key) || [];
|
|
118
|
-
keyListeners.forEach((listener) => listener(value));
|
|
119
|
-
const keyWaiters = waiters.get(key) || [];
|
|
120
|
-
keyWaiters.forEach((resolve) => resolve(value));
|
|
121
|
-
waiters.delete(key);
|
|
122
|
-
}
|
|
123
|
-
function onValue(key, callback) {
|
|
124
|
-
const current = listeners.get(key) || [];
|
|
125
|
-
current.push(callback);
|
|
126
|
-
listeners.set(key, current);
|
|
127
|
-
return () => {
|
|
128
|
-
const next = (listeners.get(key) || []).filter((cb) => cb !== callback);
|
|
129
|
-
listeners.set(key, next);
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
114
|
// src/authentication/Authentication.ts
|
|
134
|
-
var
|
|
115
|
+
var _Authentication = class _Authentication {
|
|
135
116
|
constructor() {
|
|
136
|
-
this.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (!credentials) {
|
|
146
|
-
ldb.removeItem("eauth");
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
ldb.setItem("eauth", JSON.stringify(credentials));
|
|
150
|
-
emitValue("credentials", credentials);
|
|
151
|
-
};
|
|
152
|
-
this.currentUser = () => {
|
|
153
|
-
const ldb = localStorage;
|
|
154
|
-
const data = ldb.getItem("eauth");
|
|
155
|
-
if (data === void 0 || data === null) {
|
|
156
|
-
return void 0;
|
|
157
|
-
}
|
|
158
|
-
const result = JSON.parse(data ?? "{}");
|
|
159
|
-
return Credentials.fromMap(result);
|
|
160
|
-
};
|
|
161
|
-
this.authState = ({
|
|
162
|
-
onChange,
|
|
163
|
-
onSignOut,
|
|
164
|
-
onDeleted
|
|
165
|
-
}) => {
|
|
166
|
-
let userDocUnsubscribe;
|
|
167
|
-
const attachUserListener = (creds) => {
|
|
168
|
-
if (userDocUnsubscribe) {
|
|
169
|
-
userDocUnsubscribe();
|
|
170
|
-
userDocUnsubscribe = void 0;
|
|
171
|
-
}
|
|
172
|
-
if (!creds) {
|
|
173
|
-
this.eUser = void 0;
|
|
174
|
-
onSignOut?.();
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const userRef = this.app?.database.collection("users").doc(creds.uid);
|
|
178
|
-
if (!userRef)
|
|
179
|
-
return;
|
|
180
|
-
userDocUnsubscribe = userRef.onSnapshot(
|
|
181
|
-
(snapshot, change) => {
|
|
182
|
-
if (change === "insert" || change === "update") {
|
|
183
|
-
if (snapshot.data.logged === "false" || snapshot.data.logged === false) {
|
|
184
|
-
this.eUser = void 0;
|
|
185
|
-
this.saveCredentials(null);
|
|
186
|
-
onSignOut?.();
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
const result = Credentials.fromMap(snapshot.toMap());
|
|
190
|
-
this.eUser = result;
|
|
191
|
-
this.saveCredentials(result);
|
|
192
|
-
onChange(result);
|
|
193
|
-
}
|
|
194
|
-
if (change === "delete") {
|
|
195
|
-
this.eUser = void 0;
|
|
196
|
-
this.saveCredentials(null);
|
|
197
|
-
onSignOut?.();
|
|
198
|
-
onDeleted?.();
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
);
|
|
202
|
-
};
|
|
203
|
-
const current = this.currentUser();
|
|
204
|
-
if (current) {
|
|
205
|
-
this.eUser = current;
|
|
206
|
-
onChange(current);
|
|
207
|
-
attachUserListener(current);
|
|
208
|
-
}
|
|
209
|
-
const unsubscribeCredentials = onValue(
|
|
210
|
-
"credentials",
|
|
211
|
-
(creds) => {
|
|
212
|
-
attachUserListener(creds);
|
|
213
|
-
}
|
|
214
|
-
);
|
|
215
|
-
return () => {
|
|
216
|
-
unsubscribeCredentials?.();
|
|
217
|
-
userDocUnsubscribe?.();
|
|
218
|
-
};
|
|
117
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
118
|
+
this.eventWaiters = /* @__PURE__ */ new Map();
|
|
119
|
+
this.unsubscribers = /* @__PURE__ */ new Set();
|
|
120
|
+
this.isSameCredentials = (a, b) => {
|
|
121
|
+
if (!a && !b)
|
|
122
|
+
return true;
|
|
123
|
+
if (!a || !b)
|
|
124
|
+
return false;
|
|
125
|
+
return JSON.stringify(a.toMap()) === JSON.stringify(b.toMap());
|
|
219
126
|
};
|
|
220
127
|
this.createUserWithEmailAndPassword = async ({
|
|
221
128
|
email,
|
|
@@ -223,8 +130,8 @@ var Authentication = class {
|
|
|
223
130
|
}) => {
|
|
224
131
|
this.eUser = void 0;
|
|
225
132
|
this.saveCredentials(null);
|
|
226
|
-
const
|
|
227
|
-
const data = await
|
|
133
|
+
const app = this.app?.getDatabase;
|
|
134
|
+
const data = await app?.collection("users").query.where({
|
|
228
135
|
key: "email",
|
|
229
136
|
op: "===",
|
|
230
137
|
value: email
|
|
@@ -232,10 +139,10 @@ var Authentication = class {
|
|
|
232
139
|
if (data.length > 0) {
|
|
233
140
|
throw new Error("Email Already In Use.");
|
|
234
141
|
}
|
|
235
|
-
const userRef = await
|
|
142
|
+
const userRef = await app?.collection("users").add({
|
|
236
143
|
email,
|
|
237
144
|
password,
|
|
238
|
-
token: this.client?.
|
|
145
|
+
token: this.client?.getConfig().token,
|
|
239
146
|
logged: true,
|
|
240
147
|
lastLogged: Date.now()
|
|
241
148
|
});
|
|
@@ -250,8 +157,8 @@ var Authentication = class {
|
|
|
250
157
|
email,
|
|
251
158
|
password
|
|
252
159
|
}) => {
|
|
253
|
-
const
|
|
254
|
-
const data = await
|
|
160
|
+
const app = this.app?.getDatabase;
|
|
161
|
+
const data = await app?.collection("users").query.where({
|
|
255
162
|
key: "email",
|
|
256
163
|
op: "===",
|
|
257
164
|
value: email
|
|
@@ -267,35 +174,33 @@ var Authentication = class {
|
|
|
267
174
|
throw new Error("User Not Found.");
|
|
268
175
|
}
|
|
269
176
|
const _data = data[0];
|
|
270
|
-
|
|
271
|
-
this.eUser = Credentials.fromMap(_data);
|
|
272
|
-
this.saveCredentials(this.eUser);
|
|
273
|
-
await db?.collection("users").doc(data[0].id).update({
|
|
177
|
+
await app?.collection("users").doc(data[0].id).update({
|
|
274
178
|
logged: true,
|
|
275
179
|
lastLogged: Date.now()
|
|
276
180
|
});
|
|
277
|
-
|
|
181
|
+
this.eUser = Credentials.fromMap(_data);
|
|
182
|
+
this.saveCredentials(this.eUser);
|
|
278
183
|
return Credentials.fromMap(data[0].toMap());
|
|
279
184
|
};
|
|
280
185
|
this.deleteUser = async () => {
|
|
281
186
|
if (!this.eUser) {
|
|
282
187
|
throw new Error("No User Signed in");
|
|
283
188
|
}
|
|
284
|
-
const
|
|
285
|
-
const userRef = await
|
|
286
|
-
if (userRef
|
|
189
|
+
const app = this.app?.getDatabase;
|
|
190
|
+
const userRef = await app?.collection("users").doc(this.eUser.uid).delete();
|
|
191
|
+
if (userRef) {
|
|
287
192
|
throw new Error("Something went wrong try Again.");
|
|
288
193
|
}
|
|
289
194
|
this.eUser = void 0;
|
|
290
195
|
this.saveCredentials(null);
|
|
291
196
|
};
|
|
292
197
|
this.signOut = async () => {
|
|
293
|
-
const
|
|
198
|
+
const app = this.app?.getDatabase;
|
|
294
199
|
const luid = this.currentUser();
|
|
295
200
|
this.eUser = void 0;
|
|
296
201
|
this.saveCredentials(null);
|
|
297
202
|
if (luid)
|
|
298
|
-
await
|
|
203
|
+
await app?.collection("users").doc(luid?.uid).update({
|
|
299
204
|
logged: false,
|
|
300
205
|
lastLogged: Date.now()
|
|
301
206
|
});
|
|
@@ -304,9 +209,9 @@ var Authentication = class {
|
|
|
304
209
|
this.rules = async (path, context) => {
|
|
305
210
|
const res = await new HttpsRequest({
|
|
306
211
|
method: "POST" /* POST */,
|
|
307
|
-
endpoint: this.client.getBaseUrl + "/auth/rules/verify",
|
|
212
|
+
endpoint: this.client.getBaseUrl() + "/auth/rules/verify",
|
|
308
213
|
headers: {
|
|
309
|
-
authorization: this.client.
|
|
214
|
+
authorization: this.client.getConfig().token
|
|
310
215
|
},
|
|
311
216
|
body: {
|
|
312
217
|
path,
|
|
@@ -315,15 +220,148 @@ var Authentication = class {
|
|
|
315
220
|
}).sendRequest();
|
|
316
221
|
return res;
|
|
317
222
|
};
|
|
223
|
+
if (_Authentication.instance)
|
|
224
|
+
return _Authentication.instance;
|
|
318
225
|
this.client = EdmaxLabs.instance;
|
|
319
226
|
this.app = new EdmaxLabs({
|
|
320
227
|
token: "auth",
|
|
321
|
-
project: this.client.
|
|
228
|
+
project: this.client.getConfig().project
|
|
322
229
|
});
|
|
323
|
-
this
|
|
230
|
+
_Authentication.instance = this;
|
|
231
|
+
this.restoreSession();
|
|
232
|
+
}
|
|
233
|
+
restoreSession() {
|
|
234
|
+
const saved = this.currentUser();
|
|
235
|
+
if (saved) {
|
|
236
|
+
this.eUser = saved;
|
|
237
|
+
this.emitValue("creds", saved);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Better singleton
|
|
241
|
+
static getInstance() {
|
|
242
|
+
if (!_Authentication.instance) {
|
|
243
|
+
_Authentication.instance = new _Authentication();
|
|
244
|
+
}
|
|
245
|
+
return _Authentication.instance;
|
|
246
|
+
}
|
|
247
|
+
emitValue(key, value) {
|
|
248
|
+
const listeners = this.eventListeners.get(key) || [];
|
|
249
|
+
listeners.forEach((l) => l(value));
|
|
250
|
+
const waiters = this.eventWaiters.get(key) || [];
|
|
251
|
+
waiters.forEach((resolve) => resolve(value));
|
|
252
|
+
this.eventWaiters.delete(key);
|
|
253
|
+
}
|
|
254
|
+
onValue(key, callback) {
|
|
255
|
+
const listeners = this.eventListeners.get(key) || [];
|
|
256
|
+
listeners.push(callback);
|
|
257
|
+
this.eventListeners.set(key, listeners);
|
|
258
|
+
return () => {
|
|
259
|
+
const filtered = listeners.filter((cb) => cb !== callback);
|
|
260
|
+
this.eventListeners.set(key, filtered);
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
currentUser() {
|
|
264
|
+
const data = localStorage.getItem("eauth");
|
|
265
|
+
if (!data)
|
|
266
|
+
return null;
|
|
267
|
+
try {
|
|
268
|
+
return Credentials.fromMap(JSON.parse(data));
|
|
269
|
+
} catch {
|
|
270
|
+
localStorage.removeItem("eauth");
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
saveCredentials(credentials) {
|
|
275
|
+
if (!credentials) {
|
|
276
|
+
localStorage.removeItem("eauth");
|
|
277
|
+
this.eUser = null;
|
|
278
|
+
this.emitValue("creds", null);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
localStorage.setItem("eauth", JSON.stringify(credentials.toMap()));
|
|
282
|
+
this.eUser = credentials;
|
|
283
|
+
this.emitValue("creds", credentials);
|
|
284
|
+
}
|
|
285
|
+
// Main auth state listener - should be called ONCE at app root
|
|
286
|
+
authState({
|
|
287
|
+
onChange,
|
|
288
|
+
onSignOut,
|
|
289
|
+
onDeleted
|
|
290
|
+
}) {
|
|
291
|
+
let userDocUnsubscribe;
|
|
292
|
+
const handleCredsChange = (creds) => {
|
|
293
|
+
if (userDocUnsubscribe) {
|
|
294
|
+
userDocUnsubscribe();
|
|
295
|
+
userDocUnsubscribe = void 0;
|
|
296
|
+
}
|
|
297
|
+
if (!creds) {
|
|
298
|
+
this.eUser = null;
|
|
299
|
+
onSignOut?.();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const userRef = this.app?.getDatabase.collection("users").doc(creds.uid);
|
|
303
|
+
if (!userRef)
|
|
304
|
+
return;
|
|
305
|
+
userDocUnsubscribe = userRef.onSnapshot(
|
|
306
|
+
(snapshot, change) => {
|
|
307
|
+
if (change === "delete") {
|
|
308
|
+
this.saveCredentials(null);
|
|
309
|
+
onSignOut?.();
|
|
310
|
+
onDeleted?.();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (change === "insert" || change === "update") {
|
|
314
|
+
if (!snapshot)
|
|
315
|
+
return;
|
|
316
|
+
if (snapshot.data.logged === false || snapshot.data.logged === "false") {
|
|
317
|
+
this.saveCredentials(null);
|
|
318
|
+
onSignOut?.();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const newCreds = Credentials.fromMap(snapshot.toMap());
|
|
322
|
+
if (!this.isSameCredentials(this.eUser, newCreds)) {
|
|
323
|
+
this.eUser = newCreds;
|
|
324
|
+
this.saveCredentials(newCreds);
|
|
325
|
+
onChange(newCreds);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
};
|
|
331
|
+
const initialUser = this.currentUser();
|
|
332
|
+
if (initialUser) {
|
|
333
|
+
this.eUser = initialUser;
|
|
334
|
+
onChange(initialUser);
|
|
335
|
+
}
|
|
336
|
+
handleCredsChange(initialUser);
|
|
337
|
+
const credsUnsub = this.onValue("creds", handleCredsChange);
|
|
338
|
+
return () => {
|
|
339
|
+
credsUnsub();
|
|
340
|
+
userDocUnsubscribe?.();
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
_Authentication.instance = null;
|
|
345
|
+
var Authentication = _Authentication;
|
|
346
|
+
|
|
347
|
+
// src/database/DocumentSnapshot.ts
|
|
348
|
+
var DocumentSnapshot = class _DocumentSnapshot {
|
|
349
|
+
constructor(id, doc) {
|
|
350
|
+
this.id = id;
|
|
351
|
+
this.data = doc;
|
|
352
|
+
}
|
|
353
|
+
static fromMap(map) {
|
|
354
|
+
const id = map.id ?? map._id ?? "";
|
|
355
|
+
const document2 = map.document ?? map.documents ?? map.data ?? map;
|
|
356
|
+
return new _DocumentSnapshot(id, document2);
|
|
357
|
+
}
|
|
358
|
+
toMap() {
|
|
359
|
+
return {
|
|
360
|
+
id: this.id,
|
|
361
|
+
data: this.data
|
|
362
|
+
};
|
|
324
363
|
}
|
|
325
364
|
};
|
|
326
|
-
Authentication.instance = null;
|
|
327
365
|
|
|
328
366
|
// src/database/Timestamp.ts
|
|
329
367
|
var Timestamp = class _Timestamp {
|
|
@@ -471,8 +509,8 @@ var ArraySnapshot = class _ArraySnapshot {
|
|
|
471
509
|
static fromMap(map) {
|
|
472
510
|
const index = map.index ?? map.index ?? -1;
|
|
473
511
|
const id = map.id ?? map._id ?? "";
|
|
474
|
-
const
|
|
475
|
-
return new _ArraySnapshot(id, index,
|
|
512
|
+
const document2 = map.data ?? map.document ?? map;
|
|
513
|
+
return new _ArraySnapshot(id, index, document2);
|
|
476
514
|
}
|
|
477
515
|
toMap() {
|
|
478
516
|
return {
|
|
@@ -484,87 +522,126 @@ var ArraySnapshot = class _ArraySnapshot {
|
|
|
484
522
|
};
|
|
485
523
|
|
|
486
524
|
// src/database/Array.ts
|
|
487
|
-
var
|
|
488
|
-
constructor(
|
|
489
|
-
this.
|
|
525
|
+
var Array2 = class {
|
|
526
|
+
constructor(app, collection, key, id) {
|
|
527
|
+
this.app = app;
|
|
490
528
|
this.collection = collection;
|
|
491
529
|
this.key = key;
|
|
492
530
|
this.docID = id;
|
|
531
|
+
this.persistence = app.offline().persistence;
|
|
532
|
+
this.syncEngine = app.offline().syncEngine;
|
|
533
|
+
this.localStore = app.offline().localStore;
|
|
493
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* Get current array elements (offline-first: prefers local cache)
|
|
537
|
+
*/
|
|
494
538
|
async show() {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
},
|
|
501
|
-
body: {
|
|
502
|
-
collection: this.collection,
|
|
503
|
-
document: this.docID,
|
|
504
|
-
array: this.key
|
|
539
|
+
if (this.persistence) {
|
|
540
|
+
const doc = await this.persistence.getDoc(this.collection, this.docID);
|
|
541
|
+
if (doc?.exists && !doc.deleted) {
|
|
542
|
+
const arrayData = doc.data[this.key] || [];
|
|
543
|
+
return arrayData.map((item) => ArraySnapshot.fromMap(item));
|
|
505
544
|
}
|
|
506
|
-
}
|
|
507
|
-
if (
|
|
508
|
-
|
|
509
|
-
if (d === void 0)
|
|
510
|
-
return [];
|
|
511
|
-
return ArraySnapshot.fromMap(d);
|
|
545
|
+
}
|
|
546
|
+
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
547
|
+
this.refreshFromRemote().catch(() => {
|
|
512
548
|
});
|
|
513
|
-
}
|
|
549
|
+
}
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
async refreshFromRemote() {
|
|
553
|
+
try {
|
|
554
|
+
const res = await new HttpsRequest({
|
|
555
|
+
method: "POST" /* POST */,
|
|
556
|
+
endpoint: `${this.app.getBaseUrl()}/app/array/show`,
|
|
557
|
+
headers: { authorization: this.app.getConfig().token },
|
|
558
|
+
body: {
|
|
559
|
+
collection: this.collection,
|
|
560
|
+
document: this.docID,
|
|
561
|
+
array: this.key
|
|
562
|
+
}
|
|
563
|
+
}).sendRequest();
|
|
564
|
+
if (!res?.success || !globalThis.Array.isArray(res.documents)) {
|
|
565
|
+
return [];
|
|
566
|
+
}
|
|
567
|
+
const snapshots = res.documents.filter((d) => d != null).map((d) => ArraySnapshot.fromMap(d));
|
|
568
|
+
if (this.persistence) {
|
|
569
|
+
const currentDoc = await this.persistence.getDoc(this.collection, this.docID);
|
|
570
|
+
if (currentDoc) {
|
|
571
|
+
await this.persistence.upsertDoc({
|
|
572
|
+
...currentDoc,
|
|
573
|
+
data: { ...currentDoc.data, [this.key]: res.documents },
|
|
574
|
+
pending: 0,
|
|
575
|
+
status: "synced",
|
|
576
|
+
lastSyncedAt: Date.now()
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return snapshots;
|
|
581
|
+
} catch {
|
|
514
582
|
return [];
|
|
515
583
|
}
|
|
516
584
|
}
|
|
517
585
|
async push(data, id) {
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
586
|
+
const payload = {
|
|
587
|
+
data,
|
|
588
|
+
id: id || this.persistence?.createLocalId?.() || void 0,
|
|
589
|
+
dt: Timestamp.now()
|
|
590
|
+
};
|
|
591
|
+
if (this.persistence) {
|
|
592
|
+
await this.persistence.enqueueMutation({
|
|
593
|
+
mutationId: this.persistence.createMutationId(),
|
|
525
594
|
collection: this.collection,
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
595
|
+
documentId: this.docID,
|
|
596
|
+
type: "array_push",
|
|
597
|
+
// custom type
|
|
598
|
+
payload: { arrayKey: this.key, ...payload }
|
|
599
|
+
});
|
|
600
|
+
this.syncEngine?.flush().catch(console.error);
|
|
601
|
+
const doc = await this.persistence.getDoc(this.collection, this.docID);
|
|
602
|
+
if (doc) {
|
|
603
|
+
const updatedArray = [...doc.data[this.key] || [], payload];
|
|
604
|
+
const snap = ArraySnapshot.fromMap(payload);
|
|
605
|
+
this.localStore?.emitDocument(
|
|
606
|
+
this.collection,
|
|
607
|
+
this.docID,
|
|
608
|
+
DocumentSnapshot.fromMap({ ...doc.data, [this.key]: updatedArray }),
|
|
609
|
+
"local_update"
|
|
610
|
+
);
|
|
531
611
|
}
|
|
532
|
-
|
|
533
|
-
if (res.success) {
|
|
534
|
-
return ArraySnapshot.fromMap(res.document);
|
|
535
|
-
} else {
|
|
536
|
-
return null;
|
|
612
|
+
return ArraySnapshot.fromMap(payload);
|
|
537
613
|
}
|
|
538
|
-
}
|
|
539
|
-
async update(position, data, id) {
|
|
540
614
|
const res = await new HttpsRequest({
|
|
541
615
|
method: "POST" /* POST */,
|
|
542
|
-
endpoint: this.
|
|
543
|
-
headers: {
|
|
544
|
-
authorization: this.db.getAuth.token
|
|
545
|
-
},
|
|
616
|
+
endpoint: `${this.app.getBaseUrl()}/app/array/push`,
|
|
617
|
+
headers: { authorization: this.app.getConfig().token },
|
|
546
618
|
body: {
|
|
547
619
|
collection: this.collection,
|
|
548
620
|
document: this.docID,
|
|
549
621
|
array: this.key,
|
|
550
|
-
|
|
551
|
-
data,
|
|
552
|
-
id
|
|
622
|
+
...payload
|
|
553
623
|
}
|
|
554
624
|
}).sendRequest();
|
|
555
|
-
|
|
556
|
-
return ArraySnapshot.fromMap(res.document);
|
|
557
|
-
} else {
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
625
|
+
return res?.success ? ArraySnapshot.fromMap(res.document) : null;
|
|
560
626
|
}
|
|
561
|
-
|
|
627
|
+
// Similar pattern for update, insert, remove, get...
|
|
628
|
+
// (I'll show one more as example; apply the same logic to the rest)
|
|
629
|
+
async update(position, data, id) {
|
|
630
|
+
if (this.persistence) {
|
|
631
|
+
await this.persistence.enqueueMutation({
|
|
632
|
+
mutationId: this.persistence.createMutationId(),
|
|
633
|
+
collection: this.collection,
|
|
634
|
+
documentId: this.docID,
|
|
635
|
+
type: "array_update",
|
|
636
|
+
payload: { arrayKey: this.key, position, data, id }
|
|
637
|
+
});
|
|
638
|
+
this.syncEngine?.flush().catch(console.error);
|
|
639
|
+
return ArraySnapshot.fromMap({ ...data, id });
|
|
640
|
+
}
|
|
562
641
|
const res = await new HttpsRequest({
|
|
563
642
|
method: "POST" /* POST */,
|
|
564
|
-
endpoint: this.
|
|
565
|
-
headers: {
|
|
566
|
-
authorization: this.db.getAuth.token
|
|
567
|
-
},
|
|
643
|
+
endpoint: `${this.app.getBaseUrl()}/app/array/update`,
|
|
644
|
+
headers: { authorization: this.app.getConfig().token },
|
|
568
645
|
body: {
|
|
569
646
|
collection: this.collection,
|
|
570
647
|
document: this.docID,
|
|
@@ -574,172 +651,825 @@ var Array = class {
|
|
|
574
651
|
id
|
|
575
652
|
}
|
|
576
653
|
}).sendRequest();
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
654
|
+
return res?.success ? ArraySnapshot.fromMap(res.document) : null;
|
|
655
|
+
}
|
|
656
|
+
// TODO: Implement insert(), get(), remove() using the exact same offline pattern as push/update
|
|
657
|
+
async insert(position, data, id) {
|
|
658
|
+
return null;
|
|
582
659
|
}
|
|
583
660
|
async get(position) {
|
|
584
|
-
|
|
585
|
-
method: "POST" /* POST */,
|
|
586
|
-
endpoint: this.db.getBaseUrl + "/db/array/read",
|
|
587
|
-
headers: {
|
|
588
|
-
authorization: this.db.getAuth.token
|
|
589
|
-
},
|
|
590
|
-
body: {
|
|
591
|
-
collection: this.collection,
|
|
592
|
-
document: this.docID,
|
|
593
|
-
array: this.key,
|
|
594
|
-
position
|
|
595
|
-
}
|
|
596
|
-
}).sendRequest();
|
|
597
|
-
if (res.success) {
|
|
598
|
-
return ArraySnapshot.fromMap(res.document);
|
|
599
|
-
} else {
|
|
600
|
-
return null;
|
|
601
|
-
}
|
|
661
|
+
return null;
|
|
602
662
|
}
|
|
603
663
|
async remove(position) {
|
|
604
|
-
|
|
605
|
-
method: "POST" /* POST */,
|
|
606
|
-
endpoint: this.db.getBaseUrl + "/db/array/remove",
|
|
607
|
-
headers: {
|
|
608
|
-
authorization: this.db.getAuth.token
|
|
609
|
-
},
|
|
610
|
-
body: {
|
|
611
|
-
collection: this.collection,
|
|
612
|
-
document: this.docID,
|
|
613
|
-
array: this.key,
|
|
614
|
-
position
|
|
615
|
-
}
|
|
616
|
-
}).sendRequest();
|
|
617
|
-
if (res.success) {
|
|
618
|
-
return ArraySnapshot.fromMap(res.document);
|
|
619
|
-
} else {
|
|
620
|
-
return null;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
// src/database/DocumentSnapshot.ts
|
|
626
|
-
var DocumentSnapshot = class _DocumentSnapshot {
|
|
627
|
-
constructor(id, doc) {
|
|
628
|
-
this.id = id;
|
|
629
|
-
this.data = doc;
|
|
630
|
-
}
|
|
631
|
-
static fromMap(map) {
|
|
632
|
-
const id = map.id ?? map._id ?? "";
|
|
633
|
-
const document = map.document ?? map.documents ?? map.data ?? map;
|
|
634
|
-
return new _DocumentSnapshot(id, document);
|
|
635
|
-
}
|
|
636
|
-
toMap() {
|
|
637
|
-
return {
|
|
638
|
-
id: this.id,
|
|
639
|
-
data: this.data
|
|
640
|
-
};
|
|
664
|
+
return null;
|
|
641
665
|
}
|
|
642
666
|
};
|
|
643
667
|
|
|
644
668
|
// src/database/DocumentRef.ts
|
|
645
669
|
var DocumentRef = class {
|
|
646
|
-
constructor(
|
|
647
|
-
this.
|
|
670
|
+
constructor(app, collection, id) {
|
|
671
|
+
this.app = app;
|
|
648
672
|
this.collection = collection;
|
|
649
673
|
this.id = id;
|
|
674
|
+
this.persistence = app.offline().persistence;
|
|
675
|
+
this.syncEngine = app.offline().syncEngine;
|
|
676
|
+
this.localStore = app.offline().localStore;
|
|
650
677
|
}
|
|
651
678
|
async get() {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
679
|
+
if (this.persistence) {
|
|
680
|
+
const localSnap = await this.persistence.getDocSnapshot(this.collection, this.id);
|
|
681
|
+
if (localSnap)
|
|
682
|
+
return localSnap;
|
|
683
|
+
}
|
|
684
|
+
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
685
|
+
this.refreshFromRemote().catch(() => {
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
async refreshFromRemote() {
|
|
691
|
+
try {
|
|
692
|
+
const res = await new HttpsRequest({
|
|
693
|
+
method: "POST" /* POST */,
|
|
694
|
+
endpoint: `${this.app.getBaseUrl()}/app/read`,
|
|
695
|
+
headers: { authorization: this.app.getConfig().token },
|
|
696
|
+
body: {
|
|
697
|
+
collection: this.collection,
|
|
698
|
+
document: this.id
|
|
699
|
+
}
|
|
700
|
+
}).sendRequest();
|
|
701
|
+
if (!res?.success || !res.document)
|
|
665
702
|
return null;
|
|
666
|
-
|
|
667
|
-
|
|
703
|
+
const doc = {
|
|
704
|
+
...res.document,
|
|
705
|
+
id: res.document.id ?? res.document._id ?? this.id
|
|
706
|
+
};
|
|
707
|
+
delete doc._id;
|
|
708
|
+
if (this.persistence) {
|
|
709
|
+
await this.persistence.applyRemoteDoc(this.collection, doc);
|
|
710
|
+
}
|
|
711
|
+
const snap = DocumentSnapshot.fromMap(doc);
|
|
712
|
+
this.localStore?.emitDocument(this.collection, this.id, snap, "remote_update");
|
|
713
|
+
return snap;
|
|
714
|
+
} catch {
|
|
668
715
|
return null;
|
|
669
716
|
}
|
|
670
717
|
}
|
|
671
718
|
async set(data) {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
authorization: this.
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
document: this.id,
|
|
681
|
-
data
|
|
682
|
-
}
|
|
683
|
-
}).sendRequest();
|
|
684
|
-
if (res.success) {
|
|
685
|
-
if (res.document === void 0)
|
|
686
|
-
return null;
|
|
687
|
-
return DocumentSnapshot.fromMap(res.document);
|
|
688
|
-
} else {
|
|
689
|
-
return null;
|
|
719
|
+
if (!this.persistence) {
|
|
720
|
+
const res = await new HttpsRequest({
|
|
721
|
+
method: "POST" /* POST */,
|
|
722
|
+
endpoint: `${this.app.getBaseUrl()}/app/create`,
|
|
723
|
+
headers: { authorization: this.app.getConfig().token },
|
|
724
|
+
body: { collection: this.collection, data: { ...data, id: this.id } }
|
|
725
|
+
}).sendRequest();
|
|
726
|
+
return res?.success ? DocumentSnapshot.fromMap(data) : null;
|
|
690
727
|
}
|
|
728
|
+
const updated = await this.persistence.upsertDoc({
|
|
729
|
+
collection: this.collection,
|
|
730
|
+
id: this.id,
|
|
731
|
+
data: { ...data, id: this.id },
|
|
732
|
+
exists: true,
|
|
733
|
+
deleted: false,
|
|
734
|
+
pending: 1,
|
|
735
|
+
localOnly: false,
|
|
736
|
+
status: "pending"
|
|
737
|
+
});
|
|
738
|
+
await this.persistence.enqueueMutation({
|
|
739
|
+
mutationId: this.persistence.createMutationId(),
|
|
740
|
+
collection: this.collection,
|
|
741
|
+
documentId: this.id,
|
|
742
|
+
type: "update",
|
|
743
|
+
// or "create" if you want to distinguish truly new docs
|
|
744
|
+
payload: data
|
|
745
|
+
});
|
|
746
|
+
const snap = DocumentSnapshot.fromMap(updated.data);
|
|
747
|
+
this.localStore?.emitDocument(this.collection, this.id, snap, "local_update");
|
|
748
|
+
const currentCollection = await this.persistence.getCollectionSnapshots(this.collection);
|
|
749
|
+
this.localStore?.emitCollection(this.collection, currentCollection, "local_update", this.id);
|
|
750
|
+
this.syncEngine?.flush().catch(console.error);
|
|
751
|
+
return snap;
|
|
691
752
|
}
|
|
692
753
|
async update(data) {
|
|
693
|
-
if (this.
|
|
754
|
+
if (!this.persistence)
|
|
694
755
|
return null;
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
endpoint: this.db.getBaseUrl + "/db/update",
|
|
698
|
-
headers: {
|
|
699
|
-
authorization: this.db.getAuth.token
|
|
700
|
-
},
|
|
701
|
-
body: {
|
|
702
|
-
collection: this.collection,
|
|
703
|
-
document: this.id,
|
|
704
|
-
data
|
|
705
|
-
}
|
|
706
|
-
}).sendRequest();
|
|
707
|
-
if (res.success) {
|
|
708
|
-
if (res.document === void 0)
|
|
709
|
-
return null;
|
|
710
|
-
return DocumentSnapshot.fromMap(res.document);
|
|
711
|
-
} else {
|
|
756
|
+
const old = await this.persistence.getDoc(this.collection, this.id);
|
|
757
|
+
if (!old || old.deleted)
|
|
712
758
|
return null;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
759
|
+
const mergedData = { ...old.data, ...data, id: this.id };
|
|
760
|
+
const updated = await this.persistence.upsertDoc({
|
|
761
|
+
collection: this.collection,
|
|
762
|
+
id: this.id,
|
|
763
|
+
data: mergedData,
|
|
764
|
+
exists: true,
|
|
765
|
+
deleted: false,
|
|
766
|
+
pending: 1,
|
|
767
|
+
localOnly: old.localOnly,
|
|
768
|
+
status: "pending",
|
|
769
|
+
revision: old.revision,
|
|
770
|
+
lastSyncedAt: old.lastSyncedAt
|
|
771
|
+
});
|
|
772
|
+
await this.persistence.enqueueMutation({
|
|
773
|
+
mutationId: this.persistence.createMutationId(),
|
|
774
|
+
collection: this.collection,
|
|
775
|
+
documentId: this.id,
|
|
776
|
+
type: "update",
|
|
777
|
+
payload: data,
|
|
778
|
+
baseRevision: old.revision
|
|
779
|
+
});
|
|
780
|
+
const snap = DocumentSnapshot.fromMap(updated.data);
|
|
781
|
+
this.localStore?.emitDocument(this.collection, this.id, snap, "local_update");
|
|
782
|
+
const currentCollection = await this.persistence.getCollectionSnapshots(this.collection);
|
|
783
|
+
this.localStore?.emitCollection(this.collection, currentCollection, "local_update", this.id);
|
|
784
|
+
this.syncEngine?.flush().catch(console.error);
|
|
785
|
+
return snap;
|
|
786
|
+
}
|
|
787
|
+
async delete() {
|
|
788
|
+
if (!this.persistence)
|
|
789
|
+
return false;
|
|
790
|
+
await this.persistence.markDeleted(this.collection, this.id, 1);
|
|
791
|
+
await this.persistence.enqueueMutation({
|
|
792
|
+
mutationId: this.persistence.createMutationId(),
|
|
793
|
+
collection: this.collection,
|
|
794
|
+
documentId: this.id,
|
|
795
|
+
type: "delete",
|
|
796
|
+
payload: null
|
|
797
|
+
});
|
|
798
|
+
this.localStore?.emitDocument(this.collection, this.id, null, "local_delete");
|
|
799
|
+
const currentCollection = await this.persistence.getCollectionSnapshots(this.collection);
|
|
800
|
+
this.localStore?.emitCollection(this.collection, currentCollection, "local_delete", this.id);
|
|
801
|
+
this.syncEngine?.flush().catch(console.error);
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
array(key) {
|
|
805
|
+
return new Array2(this.app, this.collection, key, this.id);
|
|
806
|
+
}
|
|
807
|
+
onSnapshot(callback) {
|
|
808
|
+
this.app.offline().realtimeBridge?.watchDocument(this.collection, this.id);
|
|
809
|
+
return this.localStore?.subscribeToDocument(
|
|
810
|
+
this.collection,
|
|
811
|
+
this.id,
|
|
812
|
+
callback
|
|
813
|
+
) ?? (() => {
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// src/database/Query.ts
|
|
819
|
+
var Query = class {
|
|
820
|
+
constructor(app, collection) {
|
|
821
|
+
this.filter = [];
|
|
822
|
+
this.app = app;
|
|
823
|
+
this.collection = collection;
|
|
824
|
+
}
|
|
825
|
+
where(expression) {
|
|
826
|
+
this.filter.push(expression);
|
|
827
|
+
return this;
|
|
828
|
+
}
|
|
829
|
+
buildFilter() {
|
|
830
|
+
const mongoFilter = {};
|
|
831
|
+
this.filter.forEach(({ key, op, value }) => {
|
|
832
|
+
switch (op) {
|
|
833
|
+
case "==":
|
|
834
|
+
case "===":
|
|
835
|
+
mongoFilter[key] = { $eq: value };
|
|
836
|
+
break;
|
|
837
|
+
case "!=":
|
|
838
|
+
mongoFilter[key] = { $ne: value };
|
|
839
|
+
break;
|
|
840
|
+
case "<":
|
|
841
|
+
mongoFilter[key] = { $lt: value };
|
|
842
|
+
break;
|
|
843
|
+
case "<=":
|
|
844
|
+
mongoFilter[key] = { $lte: value };
|
|
845
|
+
break;
|
|
846
|
+
case ">":
|
|
847
|
+
mongoFilter[key] = { $gt: value };
|
|
848
|
+
break;
|
|
849
|
+
case ">=":
|
|
850
|
+
mongoFilter[key] = { $gte: value };
|
|
851
|
+
break;
|
|
852
|
+
case "in":
|
|
853
|
+
mongoFilter[key] = { $in: value };
|
|
854
|
+
break;
|
|
855
|
+
case "nin":
|
|
856
|
+
mongoFilter[key] = { $nin: value };
|
|
857
|
+
break;
|
|
858
|
+
case "contains":
|
|
859
|
+
mongoFilter[key] = { $regex: value, $options: "i" };
|
|
860
|
+
break;
|
|
861
|
+
default:
|
|
862
|
+
throw new Error(`Unknown operator: ${op}`);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
return mongoFilter;
|
|
866
|
+
}
|
|
867
|
+
async get() {
|
|
868
|
+
const persistence = this.app.offline().persistence;
|
|
869
|
+
if (persistence) {
|
|
870
|
+
const local = await persistence.getCollectionSnapshots(this.collection);
|
|
871
|
+
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
872
|
+
this.refreshFromRemote().catch(() => {
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
return local;
|
|
876
|
+
}
|
|
877
|
+
return this.refreshFromRemote();
|
|
878
|
+
}
|
|
879
|
+
async refreshFromRemote() {
|
|
880
|
+
try {
|
|
881
|
+
const genfilter = this.buildFilter();
|
|
882
|
+
const res = await new HttpsRequest({
|
|
883
|
+
method: "POST" /* POST */,
|
|
884
|
+
endpoint: `${this.app.getBaseUrl()}/app/read`,
|
|
885
|
+
headers: { authorization: this.app.getConfig().token },
|
|
886
|
+
body: {
|
|
887
|
+
collection: this.collection,
|
|
888
|
+
filter: genfilter
|
|
889
|
+
}
|
|
890
|
+
}).sendRequest();
|
|
891
|
+
if (!res?.success || !Array.isArray(res.documents)) {
|
|
892
|
+
return [];
|
|
893
|
+
}
|
|
894
|
+
return res.documents.map((d) => {
|
|
895
|
+
delete d.token;
|
|
896
|
+
d.id = d.id ?? d._id;
|
|
897
|
+
delete d._id;
|
|
898
|
+
return DocumentSnapshot.fromMap(d);
|
|
899
|
+
});
|
|
900
|
+
} catch {
|
|
901
|
+
return [];
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
// Advanced: query-wide update (keep for now, but document it's server-side only)
|
|
905
|
+
async update(data) {
|
|
906
|
+
const genfilter = this.buildFilter();
|
|
907
|
+
const res = await new HttpsRequest({
|
|
717
908
|
method: "POST" /* POST */,
|
|
718
|
-
endpoint: this.
|
|
719
|
-
headers: {
|
|
720
|
-
authorization: this.db.getAuth.token
|
|
721
|
-
},
|
|
909
|
+
endpoint: `${this.app.getBaseUrl()}/app/update`,
|
|
910
|
+
headers: { authorization: this.app.getConfig().token },
|
|
722
911
|
body: {
|
|
723
912
|
collection: this.collection,
|
|
724
|
-
|
|
913
|
+
filter: genfilter,
|
|
914
|
+
data
|
|
725
915
|
}
|
|
726
916
|
}).sendRequest();
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
917
|
+
return res;
|
|
918
|
+
}
|
|
919
|
+
onSnapshot(callback) {
|
|
920
|
+
const genfilter = this.buildFilter();
|
|
921
|
+
this.app.rtdb().subscribeToCollectionRaw?.(
|
|
922
|
+
this.collection,
|
|
923
|
+
(payload, change) => {
|
|
924
|
+
},
|
|
925
|
+
genfilter
|
|
926
|
+
);
|
|
927
|
+
return this.app.offline().localStore?.subscribeToCollection(
|
|
928
|
+
this.collection,
|
|
929
|
+
callback
|
|
930
|
+
) ?? (() => {
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
// src/database/CollectionRef.ts
|
|
936
|
+
var CollectionRef = class {
|
|
937
|
+
constructor(app, collection) {
|
|
938
|
+
this.app = app;
|
|
939
|
+
this.collection = collection;
|
|
940
|
+
this.persistence = app.offline().persistence;
|
|
941
|
+
this.syncEngine = app.offline().syncEngine;
|
|
942
|
+
this.localStore = app.offline().localStore;
|
|
943
|
+
}
|
|
944
|
+
doc(id) {
|
|
945
|
+
return new DocumentRef(this.app, this.collection, id);
|
|
946
|
+
}
|
|
947
|
+
get query() {
|
|
948
|
+
return new Query(this.app, this.collection);
|
|
949
|
+
}
|
|
950
|
+
async get() {
|
|
951
|
+
if (!this.persistence)
|
|
952
|
+
return [];
|
|
953
|
+
const local = await this.persistence.getCollectionSnapshots(this.collection);
|
|
954
|
+
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
955
|
+
this.refreshFromRemote().catch(() => {
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
return local;
|
|
959
|
+
}
|
|
960
|
+
async refreshFromRemote() {
|
|
961
|
+
try {
|
|
962
|
+
const res = await new HttpsRequest({
|
|
963
|
+
method: "POST" /* POST */,
|
|
964
|
+
endpoint: `${this.app.getBaseUrl()}/app/read`,
|
|
965
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
966
|
+
body: {
|
|
967
|
+
collection: this.collection,
|
|
968
|
+
filter: {}
|
|
969
|
+
}
|
|
970
|
+
}).sendRequest();
|
|
971
|
+
if (!res?.success || !Array.isArray(res.documents)) {
|
|
972
|
+
return [];
|
|
973
|
+
}
|
|
974
|
+
for (const raw of res.documents) {
|
|
975
|
+
const doc = { ...raw };
|
|
976
|
+
doc.id = doc.id ?? doc._id;
|
|
977
|
+
delete doc._id;
|
|
978
|
+
delete doc.token;
|
|
979
|
+
if (this.persistence) {
|
|
980
|
+
await this.persistence.applyRemoteDoc(this.collection, doc);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (this.persistence) {
|
|
984
|
+
const updatedCollection = await this.persistence.getCollectionSnapshots(this.collection);
|
|
985
|
+
this.localStore?.emitCollection(this.collection, updatedCollection, "remote_update");
|
|
986
|
+
return updatedCollection;
|
|
987
|
+
}
|
|
988
|
+
return [];
|
|
989
|
+
} catch {
|
|
990
|
+
return [];
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async add(data) {
|
|
994
|
+
if (!this.persistence)
|
|
995
|
+
return null;
|
|
996
|
+
const localId = this.persistence.createLocalId();
|
|
997
|
+
const docRecord = await this.persistence.upsertDoc({
|
|
998
|
+
collection: this.collection,
|
|
999
|
+
id: localId,
|
|
1000
|
+
data: { ...data, id: localId },
|
|
1001
|
+
exists: true,
|
|
1002
|
+
deleted: false,
|
|
1003
|
+
pending: 1,
|
|
1004
|
+
localOnly: true,
|
|
1005
|
+
status: "pending"
|
|
1006
|
+
});
|
|
1007
|
+
await this.persistence.enqueueMutation({
|
|
1008
|
+
mutationId: this.persistence.createMutationId(),
|
|
1009
|
+
collection: this.collection,
|
|
1010
|
+
documentId: localId,
|
|
1011
|
+
type: "create",
|
|
1012
|
+
payload: docRecord.data
|
|
1013
|
+
});
|
|
1014
|
+
const snap = DocumentSnapshot.fromMap(docRecord.data);
|
|
1015
|
+
this.localStore?.emitDocument(this.collection, localId, snap, "local_create");
|
|
1016
|
+
const currentCollection = await this.persistence.getCollectionSnapshots(this.collection);
|
|
1017
|
+
this.localStore?.emitCollection(this.collection, currentCollection, "local_create", localId);
|
|
1018
|
+
this.syncEngine?.flush().catch(console.error);
|
|
1019
|
+
return snap;
|
|
1020
|
+
}
|
|
1021
|
+
onSnapshot(callback) {
|
|
1022
|
+
this.app.offline().realtimeBridge?.watchCollection(this.collection);
|
|
1023
|
+
return this.localStore?.subscribeToCollection(this.collection, callback) ?? (() => {
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// src/database/Batch.ts
|
|
1029
|
+
var Batch = class {
|
|
1030
|
+
constructor(app) {
|
|
1031
|
+
this.ops = [];
|
|
1032
|
+
this.app = app;
|
|
1033
|
+
}
|
|
1034
|
+
set(docRef, data) {
|
|
1035
|
+
this.ops.push({
|
|
1036
|
+
op: "set",
|
|
1037
|
+
collection: docRef.collection,
|
|
1038
|
+
id: docRef.id,
|
|
1039
|
+
data
|
|
1040
|
+
});
|
|
1041
|
+
return this;
|
|
1042
|
+
}
|
|
1043
|
+
update(docRef, data) {
|
|
1044
|
+
this.ops.push({
|
|
1045
|
+
op: "update",
|
|
1046
|
+
collection: docRef.collection,
|
|
1047
|
+
id: docRef.id,
|
|
1048
|
+
data
|
|
1049
|
+
});
|
|
1050
|
+
return this;
|
|
1051
|
+
}
|
|
1052
|
+
delete(docRef) {
|
|
1053
|
+
this.ops.push({
|
|
1054
|
+
op: "delete",
|
|
1055
|
+
collection: docRef.collection,
|
|
1056
|
+
id: docRef.id
|
|
1057
|
+
});
|
|
1058
|
+
return this;
|
|
1059
|
+
}
|
|
1060
|
+
async commit() {
|
|
1061
|
+
const persistence = this.app.offline().persistence;
|
|
1062
|
+
const localStore = this.app.offline().localStore;
|
|
1063
|
+
const syncEngine = this.app.offline().syncEngine;
|
|
1064
|
+
if (persistence && localStore) {
|
|
1065
|
+
const results = [];
|
|
1066
|
+
for (const op of this.ops) {
|
|
1067
|
+
try {
|
|
1068
|
+
if (op.op === "set" || op.op === "update") {
|
|
1069
|
+
const docRef = new DocumentRef(this.app, op.collection, op.id);
|
|
1070
|
+
const snap = await docRef.set(op.data);
|
|
1071
|
+
if (snap)
|
|
1072
|
+
results.push(snap);
|
|
1073
|
+
} else if (op.op === "delete") {
|
|
1074
|
+
const docRef = new DocumentRef(this.app, op.collection, op.id);
|
|
1075
|
+
await docRef.delete();
|
|
1076
|
+
}
|
|
1077
|
+
} catch (e) {
|
|
1078
|
+
console.error(`[EdmaxLabs Batch] Failed optimistic ${op.op}`, e);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
syncEngine?.flush().catch(console.error);
|
|
1082
|
+
return { success: true, results };
|
|
1083
|
+
}
|
|
1084
|
+
const res = await new HttpsRequest({
|
|
1085
|
+
method: "POST" /* POST */,
|
|
1086
|
+
endpoint: `${this.app.getBaseUrl}/app/batch`,
|
|
1087
|
+
headers: {
|
|
1088
|
+
authorization: this.app.getConfig().token
|
|
1089
|
+
},
|
|
1090
|
+
body: { ops: this.ops }
|
|
1091
|
+
}).sendRequest();
|
|
1092
|
+
if (res?.error) {
|
|
1093
|
+
throw new Error(res.error.message || "Batch commit failed");
|
|
1094
|
+
}
|
|
1095
|
+
return res;
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
// src/database/Database.ts
|
|
1100
|
+
var Database = class {
|
|
1101
|
+
constructor(app) {
|
|
1102
|
+
this.app = app;
|
|
1103
|
+
}
|
|
1104
|
+
collection(name) {
|
|
1105
|
+
return new CollectionRef(this.app, name);
|
|
1106
|
+
}
|
|
1107
|
+
doc(path) {
|
|
1108
|
+
if (path.includes("/")) {
|
|
1109
|
+
const [col, id] = path.split("/");
|
|
1110
|
+
return new DocumentRef(this.app, col, id);
|
|
1111
|
+
}
|
|
1112
|
+
throw new Error("doc(path) expects format 'collection/documentId'");
|
|
1113
|
+
}
|
|
1114
|
+
batch() {
|
|
1115
|
+
return new Batch(this.app);
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Server-side transaction (unchanged, but improved error handling)
|
|
1119
|
+
* This remains online-only for now — offline transactions are complex and can be added later as opt-in.
|
|
1120
|
+
*/
|
|
1121
|
+
async runTransaction(transactionFn) {
|
|
1122
|
+
const tx = {
|
|
1123
|
+
ops: [],
|
|
1124
|
+
async get(docRef) {
|
|
1125
|
+
return docRef.get();
|
|
1126
|
+
},
|
|
1127
|
+
set(docRef, data) {
|
|
1128
|
+
tx.ops.push({ op: "set", collection: docRef.collection, id: docRef.id, data });
|
|
1129
|
+
},
|
|
1130
|
+
update(docRef, data) {
|
|
1131
|
+
tx.ops.push({ op: "update", collection: docRef.collection, id: docRef.id, data });
|
|
1132
|
+
},
|
|
1133
|
+
delete(docRef) {
|
|
1134
|
+
tx.ops.push({ op: "delete", collection: docRef.collection, id: docRef.id });
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
await transactionFn(tx);
|
|
1138
|
+
const res = await new HttpsRequest({
|
|
1139
|
+
method: "POST" /* POST */,
|
|
1140
|
+
endpoint: `${this.app.getBaseUrl}/app/transaction`,
|
|
1141
|
+
headers: {
|
|
1142
|
+
authorization: this.app.getConfig().token
|
|
1143
|
+
},
|
|
1144
|
+
body: { ops: tx.ops }
|
|
1145
|
+
}).sendRequest();
|
|
1146
|
+
if (res?.error) {
|
|
1147
|
+
throw new Error(res.error.message || "Transaction failed");
|
|
1148
|
+
}
|
|
1149
|
+
return res;
|
|
1150
|
+
}
|
|
1151
|
+
// ===================== OFFLINE HELPERS (Internal) =====================
|
|
1152
|
+
/**
|
|
1153
|
+
* Returns true if persistence is enabled and we are currently offline
|
|
1154
|
+
*/
|
|
1155
|
+
get shouldUseOffline() {
|
|
1156
|
+
return !!(this.app.offline().persistence && typeof navigator !== "undefined" && !navigator.onLine);
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Returns the SyncEngine if persistence is enabled
|
|
1160
|
+
*/
|
|
1161
|
+
get syncEngine() {
|
|
1162
|
+
return this.app.offline().syncEngine;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Returns the RealtimeBridge if persistence is enabled
|
|
1166
|
+
*/
|
|
1167
|
+
get realtimeBridge() {
|
|
1168
|
+
return this.app.offline().realtimeBridge;
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
// src/database/Realtime.ts
|
|
1173
|
+
var import_socket = require("socket.io-client");
|
|
1174
|
+
|
|
1175
|
+
// src/utils/uuid.ts
|
|
1176
|
+
function generateUUID() {
|
|
1177
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1178
|
+
return crypto.randomUUID();
|
|
1179
|
+
}
|
|
1180
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
1181
|
+
const r = Math.random() * 16 | 0;
|
|
1182
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
1183
|
+
return v.toString(16);
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/database/Realtime.ts
|
|
1188
|
+
var Realtime = class {
|
|
1189
|
+
constructor(app) {
|
|
1190
|
+
this.socket = null;
|
|
1191
|
+
this.events = ["error", "insert", "update", "delete"];
|
|
1192
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
1193
|
+
this.app = app;
|
|
1194
|
+
}
|
|
1195
|
+
connect() {
|
|
1196
|
+
if (this.socket)
|
|
1197
|
+
return this.socket;
|
|
1198
|
+
if (!this.app.getConfig().token) {
|
|
1199
|
+
throw new Error("Auth token is required for realtime connection");
|
|
1200
|
+
}
|
|
1201
|
+
this.socket = (0, import_socket.io)(this.app.getSocketUrl(), {
|
|
1202
|
+
transports: ["websocket"],
|
|
1203
|
+
auth: { token: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
1204
|
+
autoConnect: true,
|
|
1205
|
+
reconnection: true,
|
|
1206
|
+
reconnectionAttempts: Infinity,
|
|
1207
|
+
reconnectionDelay: 1e3,
|
|
1208
|
+
reconnectionDelayMax: 5e3
|
|
1209
|
+
});
|
|
1210
|
+
this.setupSocketListeners();
|
|
1211
|
+
return this.socket;
|
|
1212
|
+
}
|
|
1213
|
+
setupSocketListeners() {
|
|
1214
|
+
if (!this.socket)
|
|
1215
|
+
return;
|
|
1216
|
+
this.socket.on("connect", () => {
|
|
1217
|
+
console.log("[EdmaxLabs Realtime] Connected");
|
|
1218
|
+
this.resubscribeAll();
|
|
1219
|
+
this.app.offline().syncEngine?.flush().catch(console.error);
|
|
1220
|
+
this.app.offline().realtimeBridge?.onReconnect?.().catch(console.error);
|
|
1221
|
+
});
|
|
1222
|
+
this.socket.on("disconnect", (reason) => {
|
|
1223
|
+
console.log(`[EdmaxLabs Realtime] Disconnected: ${reason}`);
|
|
1224
|
+
});
|
|
1225
|
+
this.socket.on("error", (err) => {
|
|
1226
|
+
console.error("[EdmaxLabs Realtime] Socket error:", err);
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
// ===================== PUBLIC HELPERS =====================
|
|
1230
|
+
on(event, cb) {
|
|
1231
|
+
this.connect().on(event, cb);
|
|
1232
|
+
}
|
|
1233
|
+
off(event, cb) {
|
|
1234
|
+
if (cb)
|
|
1235
|
+
this.socket?.off(event, cb);
|
|
1236
|
+
else
|
|
1237
|
+
this.socket?.removeAllListeners(event);
|
|
1238
|
+
}
|
|
1239
|
+
emit(event, data) {
|
|
1240
|
+
this.connect().emit(event, data);
|
|
1241
|
+
}
|
|
1242
|
+
// ===================== INTERNAL RAW SUBSCRIPTIONS =====================
|
|
1243
|
+
/**
|
|
1244
|
+
* Low-level collection subscription (used by RealtimeBridge)
|
|
1245
|
+
*/
|
|
1246
|
+
subscribeToCollectionRaw(collection, callback, filter = {}) {
|
|
1247
|
+
this.connect();
|
|
1248
|
+
const lid = `col_${collection}_${generateUUID()}`;
|
|
1249
|
+
const handlers = [];
|
|
1250
|
+
this.socket.emit("subscribe", { collection, filter, lid });
|
|
1251
|
+
this.events.forEach((event) => {
|
|
1252
|
+
const channel = `${lid}-${event}`;
|
|
1253
|
+
const fn = (payload) => {
|
|
1254
|
+
const normalized = this.normalizePayload(payload);
|
|
1255
|
+
if (!normalized)
|
|
1256
|
+
return;
|
|
1257
|
+
callback(normalized.data, normalized.change ?? event);
|
|
1258
|
+
};
|
|
1259
|
+
this.socket.on(channel, fn);
|
|
1260
|
+
handlers.push({ event: channel, fn });
|
|
1261
|
+
});
|
|
1262
|
+
this.registerSubscription(lid, collection, filter, handlers);
|
|
1263
|
+
return () => this.cleanupSubscription(lid);
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Low-level document subscription (used by RealtimeBridge)
|
|
1267
|
+
*/
|
|
1268
|
+
subscribeToDocumentRaw(collection, id, callback) {
|
|
1269
|
+
this.connect();
|
|
1270
|
+
const lid = `doc_${collection}_${id}_${generateUUID()}`;
|
|
1271
|
+
const filter = { _id: id };
|
|
1272
|
+
const handlers = [];
|
|
1273
|
+
this.socket.emit("subscribe", { collection, filter, lid });
|
|
1274
|
+
this.events.forEach((event) => {
|
|
1275
|
+
const channel = `${lid}-${event}`;
|
|
1276
|
+
const fn = (payload) => {
|
|
1277
|
+
const normalized = this.normalizePayload(payload);
|
|
1278
|
+
if (!normalized) {
|
|
1279
|
+
callback({ id }, "delete");
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
callback(normalized.data, normalized.change ?? event);
|
|
1283
|
+
};
|
|
1284
|
+
this.socket.on(channel, fn);
|
|
1285
|
+
handlers.push({ event: channel, fn });
|
|
1286
|
+
});
|
|
1287
|
+
this.registerSubscription(lid, collection, filter, handlers);
|
|
1288
|
+
return () => this.cleanupSubscription(lid);
|
|
1289
|
+
}
|
|
1290
|
+
// ===================== PRIVATE HELPERS =====================
|
|
1291
|
+
normalizePayload(payload) {
|
|
1292
|
+
const raw = payload?.document ?? payload?.data ?? payload;
|
|
1293
|
+
if (!raw)
|
|
732
1294
|
return null;
|
|
1295
|
+
const doc = { ...raw };
|
|
1296
|
+
doc.id = doc.id ?? doc._id;
|
|
1297
|
+
delete doc._id;
|
|
1298
|
+
return {
|
|
1299
|
+
change: payload?.change ?? raw?.change ?? null,
|
|
1300
|
+
data: doc
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
registerSubscription(lid, collection, filter, handlers) {
|
|
1304
|
+
this.subscriptions.set(lid, { lid, collection, filter, handlers });
|
|
1305
|
+
}
|
|
1306
|
+
resubscribeAll() {
|
|
1307
|
+
if (!this.socket)
|
|
1308
|
+
return;
|
|
1309
|
+
for (const sub of this.subscriptions.values()) {
|
|
1310
|
+
this.socket.emit("subscribe", {
|
|
1311
|
+
collection: sub.collection,
|
|
1312
|
+
filter: sub.filter,
|
|
1313
|
+
lid: sub.lid
|
|
1314
|
+
});
|
|
733
1315
|
}
|
|
734
1316
|
}
|
|
735
|
-
|
|
736
|
-
|
|
1317
|
+
cleanupSubscription(lid) {
|
|
1318
|
+
const sub = this.subscriptions.get(lid);
|
|
1319
|
+
if (!sub || !this.socket)
|
|
1320
|
+
return;
|
|
1321
|
+
for (const handler of sub.handlers) {
|
|
1322
|
+
this.socket.off(handler.event, handler.fn);
|
|
1323
|
+
}
|
|
1324
|
+
this.socket.emit("unsubscribe", { lid });
|
|
1325
|
+
this.subscriptions.delete(lid);
|
|
1326
|
+
}
|
|
1327
|
+
// ===================== LIFECYCLE =====================
|
|
1328
|
+
disconnect() {
|
|
1329
|
+
this.socket?.disconnect();
|
|
1330
|
+
this.socket = null;
|
|
1331
|
+
this.subscriptions.clear();
|
|
1332
|
+
}
|
|
1333
|
+
dispose() {
|
|
1334
|
+
this.disconnect();
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
// src/functions/Functions.ts
|
|
1339
|
+
var Functions = class {
|
|
1340
|
+
constructor(app) {
|
|
1341
|
+
this.app = app;
|
|
1342
|
+
}
|
|
1343
|
+
async call(functionName, data) {
|
|
1344
|
+
try {
|
|
1345
|
+
const res = await new HttpsRequest({
|
|
1346
|
+
method: "POST" /* POST */,
|
|
1347
|
+
endpoint: this.app.getBaseUrl + "/functions/call/" + functionName,
|
|
1348
|
+
headers: {
|
|
1349
|
+
authorization: this.app.getConfig().token
|
|
1350
|
+
},
|
|
1351
|
+
body: data
|
|
1352
|
+
}).sendRequest();
|
|
1353
|
+
return res;
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
throw {
|
|
1356
|
+
message: err.message || "Function call failed",
|
|
1357
|
+
code: err.code || "UNKNOWN_ERROR"
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
// src/hosting/Hosting.ts
|
|
1364
|
+
var Hosting = class {
|
|
1365
|
+
constructor(app) {
|
|
1366
|
+
this.app = app;
|
|
1367
|
+
}
|
|
1368
|
+
async createSubdomain(name) {
|
|
1369
|
+
try {
|
|
1370
|
+
const res = await new HttpsRequest({
|
|
1371
|
+
method: "POST" /* POST */,
|
|
1372
|
+
endpoint: this.app.getBaseUrl + "/hosting/register",
|
|
1373
|
+
headers: {
|
|
1374
|
+
authorization: this.app.getConfig().token
|
|
1375
|
+
},
|
|
1376
|
+
body: {
|
|
1377
|
+
hostname: name,
|
|
1378
|
+
project: this.app.getConfig().project
|
|
1379
|
+
}
|
|
1380
|
+
}).sendRequest();
|
|
1381
|
+
return res;
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
throw {
|
|
1384
|
+
message: err.message || "Function call failed",
|
|
1385
|
+
code: err.code || "UNKNOWN_ERROR"
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
|
|
1391
|
+
// src/persistence/LocalStore.ts
|
|
1392
|
+
var LocalStore = class {
|
|
1393
|
+
constructor() {
|
|
1394
|
+
this.documentListeners = /* @__PURE__ */ new Map();
|
|
1395
|
+
this.collectionListeners = /* @__PURE__ */ new Map();
|
|
1396
|
+
}
|
|
1397
|
+
docKey(collection, id) {
|
|
1398
|
+
return `${collection}:${id}`;
|
|
1399
|
+
}
|
|
1400
|
+
// ===================== DOCUMENT LISTENERS =====================
|
|
1401
|
+
subscribeToDocument(collection, id, callback) {
|
|
1402
|
+
const key = this.docKey(collection, id);
|
|
1403
|
+
if (!this.documentListeners.has(key)) {
|
|
1404
|
+
this.documentListeners.set(key, /* @__PURE__ */ new Set());
|
|
1405
|
+
}
|
|
1406
|
+
const listeners = this.documentListeners.get(key);
|
|
1407
|
+
listeners.add(callback);
|
|
1408
|
+
return () => {
|
|
1409
|
+
listeners.delete(callback);
|
|
1410
|
+
if (listeners.size === 0) {
|
|
1411
|
+
this.documentListeners.delete(key);
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
// ===================== COLLECTION LISTENERS =====================
|
|
1416
|
+
subscribeToCollection(collection, callback) {
|
|
1417
|
+
if (!this.collectionListeners.has(collection)) {
|
|
1418
|
+
this.collectionListeners.set(collection, /* @__PURE__ */ new Set());
|
|
1419
|
+
}
|
|
1420
|
+
const listeners = this.collectionListeners.get(collection);
|
|
1421
|
+
listeners.add(callback);
|
|
1422
|
+
return () => {
|
|
1423
|
+
listeners.delete(callback);
|
|
1424
|
+
if (listeners.size === 0) {
|
|
1425
|
+
this.collectionListeners.delete(collection);
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
737
1428
|
}
|
|
1429
|
+
// ===================== EMITTERS =====================
|
|
738
1430
|
/**
|
|
739
|
-
*
|
|
1431
|
+
* Notify all listeners for a specific document
|
|
740
1432
|
*/
|
|
741
|
-
|
|
742
|
-
|
|
1433
|
+
emitDocument(collection, id, snapshot, change) {
|
|
1434
|
+
const key = this.docKey(collection, id);
|
|
1435
|
+
this.documentListeners.get(key)?.forEach((cb) => {
|
|
1436
|
+
try {
|
|
1437
|
+
cb(snapshot, change);
|
|
1438
|
+
} catch (err) {
|
|
1439
|
+
console.error(`[EdmaxLabs] Error in document listener for ${key}:`, err);
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Notify all listeners for a collection.
|
|
1445
|
+
* This is the most important fix — collection listeners now receive the full list.
|
|
1446
|
+
*/
|
|
1447
|
+
emitCollection(collection, snapshots, change, changedDocId) {
|
|
1448
|
+
this.collectionListeners.get(collection)?.forEach((cb) => {
|
|
1449
|
+
try {
|
|
1450
|
+
cb(snapshots, change, changedDocId);
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
console.error(`[EdmaxLabs] Error in collection listener for ${collection}:`, err);
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
// ===================== UTILITY =====================
|
|
1457
|
+
/**
|
|
1458
|
+
* Clear all listeners (useful for testing or when persistence is disabled)
|
|
1459
|
+
*/
|
|
1460
|
+
clearAllListeners() {
|
|
1461
|
+
this.documentListeners.clear();
|
|
1462
|
+
this.collectionListeners.clear();
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Get current listener count (for debugging / dev tools)
|
|
1466
|
+
*/
|
|
1467
|
+
get listenerCount() {
|
|
1468
|
+
let docCount = 0;
|
|
1469
|
+
this.documentListeners.forEach((set) => docCount += set.size);
|
|
1470
|
+
let collCount = 0;
|
|
1471
|
+
this.collectionListeners.forEach((set) => collCount += set.size);
|
|
1472
|
+
return { documents: docCount, collections: collCount };
|
|
743
1473
|
}
|
|
744
1474
|
};
|
|
745
1475
|
|
|
@@ -951,542 +1681,521 @@ replaceTraps((oldTraps) => ({
|
|
|
951
1681
|
}));
|
|
952
1682
|
|
|
953
1683
|
// src/persistence/Persistence.ts
|
|
954
|
-
var
|
|
955
|
-
constructor(
|
|
956
|
-
this.
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
if (!
|
|
960
|
-
const
|
|
961
|
-
|
|
1684
|
+
var Persistence = class {
|
|
1685
|
+
constructor() {
|
|
1686
|
+
this.dbPromise = openDB("edmaxlabs_offline", 1, {
|
|
1687
|
+
upgrade(app, oldVersion) {
|
|
1688
|
+
if (oldVersion < 1) {
|
|
1689
|
+
if (!app.objectStoreNames.contains("docs")) {
|
|
1690
|
+
const docs = app.createObjectStore("docs", { keyPath: "key" });
|
|
1691
|
+
docs.createIndex("by-collection", "collection");
|
|
1692
|
+
docs.createIndex("by-id", "id");
|
|
1693
|
+
docs.createIndex("by-updatedAt", "updatedAt");
|
|
1694
|
+
docs.createIndex("by-pending", "pending");
|
|
1695
|
+
}
|
|
1696
|
+
if (!app.objectStoreNames.contains("mutations")) {
|
|
1697
|
+
const mutations = app.createObjectStore("mutations", {
|
|
1698
|
+
keyPath: "mutationId"
|
|
962
1699
|
});
|
|
963
|
-
|
|
964
|
-
|
|
1700
|
+
mutations.createIndex("by-status", "status");
|
|
1701
|
+
mutations.createIndex("by-collection", "collection");
|
|
1702
|
+
mutations.createIndex("by-documentId", "documentId");
|
|
1703
|
+
mutations.createIndex("by-createdAt", "createdAt");
|
|
965
1704
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
return dbPromise;
|
|
969
|
-
};
|
|
970
|
-
this.db = db;
|
|
971
|
-
}
|
|
972
|
-
async syncPendingWrite(id, collection = "default") {
|
|
973
|
-
const idb = await this.getIDB("edmaxlabs");
|
|
974
|
-
const tx = idb.transaction(collection, "readwrite");
|
|
975
|
-
const store = tx.store;
|
|
976
|
-
const oid = IDBKeyRange.only(id);
|
|
977
|
-
const pendingPackets = await store.index("id").getAll(oid);
|
|
978
|
-
for (const packet of pendingPackets) {
|
|
979
|
-
try {
|
|
980
|
-
const res = await new HttpsRequest({
|
|
981
|
-
method: "POST" /* POST */,
|
|
982
|
-
endpoint: `${this.db?.getBaseUrl}/db/${packet.action}`.replace(
|
|
983
|
-
"$",
|
|
984
|
-
"/"
|
|
985
|
-
),
|
|
986
|
-
body: {
|
|
987
|
-
collection: packet.collection,
|
|
988
|
-
data: packet
|
|
1705
|
+
if (!app.objectStoreNames.contains("meta")) {
|
|
1706
|
+
app.createObjectStore("meta");
|
|
989
1707
|
}
|
|
990
|
-
}).sendRequest();
|
|
991
|
-
if (res?.success) {
|
|
992
|
-
packet.status = "success";
|
|
993
|
-
packet.timestamp = Timestamp.now();
|
|
994
|
-
await store.put(packet);
|
|
995
|
-
return true;
|
|
996
1708
|
}
|
|
997
|
-
} catch {
|
|
998
|
-
return false;
|
|
999
1709
|
}
|
|
1000
|
-
}
|
|
1001
|
-
await tx.done;
|
|
1710
|
+
});
|
|
1002
1711
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
const tx = idb.transaction(collection, "readwrite");
|
|
1006
|
-
const store = tx.store;
|
|
1007
|
-
const pendingPackets = await store.index("status").getAll(IDBKeyRange.only("pending"));
|
|
1008
|
-
for (const packet of pendingPackets) {
|
|
1009
|
-
try {
|
|
1010
|
-
const res = await new HttpsRequest({
|
|
1011
|
-
method: "POST" /* POST */,
|
|
1012
|
-
endpoint: `${this.db?.getBaseUrl}/db/${packet.action}`.replace(
|
|
1013
|
-
"$",
|
|
1014
|
-
"/"
|
|
1015
|
-
),
|
|
1016
|
-
body: {
|
|
1017
|
-
collection: packet.collection,
|
|
1018
|
-
data: packet
|
|
1019
|
-
}
|
|
1020
|
-
}).sendRequest();
|
|
1021
|
-
if (res?.success) {
|
|
1022
|
-
packet.status = "success";
|
|
1023
|
-
packet.timestamp = Timestamp.now();
|
|
1024
|
-
await store.put(packet);
|
|
1025
|
-
}
|
|
1026
|
-
} catch {
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
await tx.done;
|
|
1712
|
+
docKey(collection, id) {
|
|
1713
|
+
return `${collection}:${id}`;
|
|
1030
1714
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
// src/database/Query.ts
|
|
1034
|
-
var Query = class {
|
|
1035
|
-
constructor(db, collection) {
|
|
1036
|
-
this.filter = [];
|
|
1037
|
-
this.db = db;
|
|
1038
|
-
this.collection = collection;
|
|
1715
|
+
now() {
|
|
1716
|
+
return Date.now();
|
|
1039
1717
|
}
|
|
1040
|
-
|
|
1041
|
-
this.
|
|
1042
|
-
return this;
|
|
1718
|
+
async getDb() {
|
|
1719
|
+
return this.dbPromise;
|
|
1043
1720
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
case "==":
|
|
1049
|
-
case "===":
|
|
1050
|
-
mongoFilter[key] = { $eq: value };
|
|
1051
|
-
break;
|
|
1052
|
-
case "!=":
|
|
1053
|
-
mongoFilter[key] = { $ne: value };
|
|
1054
|
-
break;
|
|
1055
|
-
case "<":
|
|
1056
|
-
mongoFilter[key] = { $lt: value };
|
|
1057
|
-
break;
|
|
1058
|
-
case "<=":
|
|
1059
|
-
mongoFilter[key] = { $lte: value };
|
|
1060
|
-
break;
|
|
1061
|
-
case ">":
|
|
1062
|
-
mongoFilter[key] = { $gt: value };
|
|
1063
|
-
break;
|
|
1064
|
-
case ">=":
|
|
1065
|
-
mongoFilter[key] = { $gte: value };
|
|
1066
|
-
break;
|
|
1067
|
-
case "in":
|
|
1068
|
-
mongoFilter[key] = { $in: value };
|
|
1069
|
-
break;
|
|
1070
|
-
case "nin":
|
|
1071
|
-
mongoFilter[key] = { $nin: value };
|
|
1072
|
-
break;
|
|
1073
|
-
case "contains":
|
|
1074
|
-
mongoFilter[key] = { $regex: value };
|
|
1075
|
-
break;
|
|
1076
|
-
default:
|
|
1077
|
-
throw new Error(`Unknown operator: ${op}`);
|
|
1078
|
-
}
|
|
1079
|
-
});
|
|
1080
|
-
return mongoFilter;
|
|
1721
|
+
// ==================== DOCS ====================
|
|
1722
|
+
async getDoc(collection, id) {
|
|
1723
|
+
const app = await this.getDb();
|
|
1724
|
+
return await app.get("docs", this.docKey(collection, id)) ?? null;
|
|
1081
1725
|
}
|
|
1082
|
-
async
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1726
|
+
async getDocSnapshot(collection, id) {
|
|
1727
|
+
const doc = await this.getDoc(collection, id);
|
|
1728
|
+
if (!doc || !doc.exists || doc.deleted)
|
|
1729
|
+
return null;
|
|
1730
|
+
return DocumentSnapshot.fromMap(doc.data);
|
|
1731
|
+
}
|
|
1732
|
+
async getCollection(collection) {
|
|
1733
|
+
const app = await this.getDb();
|
|
1734
|
+
const all = await app.getAllFromIndex("docs", "by-collection", collection);
|
|
1735
|
+
return all.filter((d) => d.exists && !d.deleted);
|
|
1736
|
+
}
|
|
1737
|
+
async getCollectionSnapshots(collection) {
|
|
1738
|
+
const docs = await this.getCollection(collection);
|
|
1739
|
+
return docs.map((d) => DocumentSnapshot.fromMap(d.data));
|
|
1740
|
+
}
|
|
1741
|
+
async upsertDoc(input) {
|
|
1742
|
+
const app = await this.getDb();
|
|
1743
|
+
const record = {
|
|
1744
|
+
...input,
|
|
1745
|
+
key: this.docKey(input.collection, input.id),
|
|
1746
|
+
updatedAt: input.updatedAt ?? this.now()
|
|
1747
|
+
};
|
|
1748
|
+
await app.put("docs", record);
|
|
1749
|
+
return record;
|
|
1750
|
+
}
|
|
1751
|
+
async markDeleted(collection, id, pending = 1) {
|
|
1752
|
+
const old = await this.getDoc(collection, id);
|
|
1753
|
+
const record = {
|
|
1754
|
+
key: this.docKey(collection, id),
|
|
1755
|
+
collection,
|
|
1756
|
+
id,
|
|
1757
|
+
data: old?.data ?? { id },
|
|
1758
|
+
exists: false,
|
|
1759
|
+
deleted: true,
|
|
1760
|
+
pending,
|
|
1761
|
+
localOnly: false,
|
|
1762
|
+
status: pending ? "pending" : "synced",
|
|
1763
|
+
updatedAt: this.now(),
|
|
1764
|
+
lastSyncedAt: old?.lastSyncedAt,
|
|
1765
|
+
revision: old?.revision
|
|
1766
|
+
};
|
|
1767
|
+
const app = await this.getDb();
|
|
1768
|
+
await app.put("docs", record);
|
|
1769
|
+
return record;
|
|
1770
|
+
}
|
|
1771
|
+
// ==================== MUTATIONS ====================
|
|
1772
|
+
async enqueueMutation(mutation) {
|
|
1773
|
+
const app = await this.getDb();
|
|
1774
|
+
const record = {
|
|
1775
|
+
...mutation,
|
|
1776
|
+
createdAt: this.now(),
|
|
1777
|
+
updatedAt: this.now(),
|
|
1778
|
+
retryCount: 0,
|
|
1779
|
+
status: "pending"
|
|
1780
|
+
};
|
|
1781
|
+
await app.put("mutations", record);
|
|
1782
|
+
return record;
|
|
1783
|
+
}
|
|
1784
|
+
async getPendingMutations() {
|
|
1785
|
+
const app = await this.getDb();
|
|
1786
|
+
const [pending, failed] = await Promise.all([
|
|
1787
|
+
app.getAllFromIndex("mutations", "by-status", "pending"),
|
|
1788
|
+
app.getAllFromIndex("mutations", "by-status", "failed")
|
|
1789
|
+
]);
|
|
1790
|
+
return [...pending, ...failed].sort((a, b) => a.createdAt - b.createdAt);
|
|
1791
|
+
}
|
|
1792
|
+
async setMutationStatus(mutationId, status, error) {
|
|
1793
|
+
const app = await this.getDb();
|
|
1794
|
+
const old = await app.get("mutations", mutationId);
|
|
1795
|
+
if (!old)
|
|
1796
|
+
return null;
|
|
1797
|
+
const next = {
|
|
1798
|
+
...old,
|
|
1799
|
+
status,
|
|
1800
|
+
updatedAt: this.now(),
|
|
1801
|
+
retryCount: status === "failed" ? (old.retryCount || 0) + 1 : old.retryCount,
|
|
1802
|
+
error
|
|
1803
|
+
};
|
|
1804
|
+
await app.put("mutations", next);
|
|
1805
|
+
return next;
|
|
1806
|
+
}
|
|
1807
|
+
async removeMutation(mutationId) {
|
|
1808
|
+
const app = await this.getDb();
|
|
1809
|
+
await app.delete("mutations", mutationId);
|
|
1810
|
+
}
|
|
1811
|
+
// ==================== ADVANCED OPERATIONS ====================
|
|
1812
|
+
async replaceDocId(collection, oldId, newId) {
|
|
1813
|
+
const app = await this.getDb();
|
|
1814
|
+
const oldKey = this.docKey(collection, oldId);
|
|
1815
|
+
const oldDoc = await app.get("docs", oldKey);
|
|
1816
|
+
if (!oldDoc)
|
|
1817
|
+
return null;
|
|
1818
|
+
const next = {
|
|
1819
|
+
...oldDoc,
|
|
1820
|
+
id: newId,
|
|
1821
|
+
key: this.docKey(collection, newId),
|
|
1822
|
+
data: { ...oldDoc.data, id: newId },
|
|
1823
|
+
localOnly: false,
|
|
1824
|
+
pending: 0,
|
|
1825
|
+
status: "synced",
|
|
1826
|
+
updatedAt: this.now(),
|
|
1827
|
+
lastSyncedAt: this.now()
|
|
1828
|
+
};
|
|
1829
|
+
const tx = app.transaction(["docs", "mutations"], "readwrite");
|
|
1830
|
+
const docsStore = tx.objectStore("docs");
|
|
1831
|
+
const mutationsStore = tx.objectStore("mutations");
|
|
1832
|
+
await docsStore.put(next);
|
|
1833
|
+
await docsStore.delete(oldKey);
|
|
1834
|
+
const allMutations = await mutationsStore.getAll();
|
|
1835
|
+
for (const mut of allMutations) {
|
|
1836
|
+
if (mut.collection === collection && mut.documentId === oldId) {
|
|
1837
|
+
await mutationsStore.put({
|
|
1838
|
+
...mut,
|
|
1839
|
+
documentId: newId,
|
|
1840
|
+
payload: mut.payload ? { ...mut.payload, id: newId } : null,
|
|
1841
|
+
updatedAt: this.now()
|
|
1842
|
+
});
|
|
1093
1843
|
}
|
|
1094
|
-
}).sendRequest();
|
|
1095
|
-
if (res.success) {
|
|
1096
|
-
return res.documents.map((d) => {
|
|
1097
|
-
delete d.token;
|
|
1098
|
-
d.id = d._id;
|
|
1099
|
-
delete d._id;
|
|
1100
|
-
return DocumentSnapshot.fromMap(d);
|
|
1101
|
-
});
|
|
1102
|
-
} else {
|
|
1103
|
-
return [];
|
|
1104
1844
|
}
|
|
1845
|
+
await tx.done;
|
|
1846
|
+
return next;
|
|
1105
1847
|
}
|
|
1106
|
-
async
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
},
|
|
1114
|
-
body: {
|
|
1115
|
-
collection: this.collection,
|
|
1116
|
-
filter: genfilter
|
|
1117
|
-
}
|
|
1118
|
-
}).sendRequest();
|
|
1119
|
-
if (res.success) {
|
|
1120
|
-
return res.documents.map((d) => {
|
|
1121
|
-
delete d.token;
|
|
1122
|
-
d.id = d._id;
|
|
1123
|
-
delete d._id;
|
|
1124
|
-
return DocumentSnapshot.fromMap(d);
|
|
1125
|
-
});
|
|
1126
|
-
} else {
|
|
1127
|
-
return [];
|
|
1848
|
+
async applyRemoteDoc(collection, data, revision) {
|
|
1849
|
+
const id = data.id ?? data._id;
|
|
1850
|
+
if (!id)
|
|
1851
|
+
return null;
|
|
1852
|
+
const local = await this.getDoc(collection, id);
|
|
1853
|
+
if (local?.pending && local.pending > 0) {
|
|
1854
|
+
return local;
|
|
1128
1855
|
}
|
|
1856
|
+
return this.upsertDoc({
|
|
1857
|
+
collection,
|
|
1858
|
+
id,
|
|
1859
|
+
data: { ...data, id },
|
|
1860
|
+
exists: true,
|
|
1861
|
+
deleted: false,
|
|
1862
|
+
pending: 0,
|
|
1863
|
+
localOnly: false,
|
|
1864
|
+
status: "synced",
|
|
1865
|
+
revision,
|
|
1866
|
+
lastSyncedAt: this.now()
|
|
1867
|
+
});
|
|
1129
1868
|
}
|
|
1130
|
-
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1869
|
+
async applyRemoteDelete(collection, id) {
|
|
1870
|
+
const local = await this.getDoc(collection, id);
|
|
1871
|
+
if (local?.pending && local.pending > 0) {
|
|
1872
|
+
return local;
|
|
1873
|
+
}
|
|
1874
|
+
return this.markDeleted(collection, id, 0);
|
|
1875
|
+
}
|
|
1876
|
+
// ==================== UTILITIES ====================
|
|
1877
|
+
createLocalId() {
|
|
1878
|
+
return `local_${crypto.randomUUID()}`;
|
|
1879
|
+
}
|
|
1880
|
+
createMutationId() {
|
|
1881
|
+
return `mut_${crypto.randomUUID()}`;
|
|
1882
|
+
}
|
|
1883
|
+
// Future-proof maintenance methods (optional for now)
|
|
1884
|
+
async clearAll() {
|
|
1885
|
+
const app = await this.getDb();
|
|
1886
|
+
const tx = app.transaction(["docs", "mutations", "meta"], "readwrite");
|
|
1887
|
+
await Promise.all([
|
|
1888
|
+
tx.objectStore("docs").clear(),
|
|
1889
|
+
tx.objectStore("mutations").clear(),
|
|
1890
|
+
tx.objectStore("meta").clear()
|
|
1891
|
+
]);
|
|
1892
|
+
await tx.done;
|
|
1893
|
+
}
|
|
1894
|
+
async getStorageUsage() {
|
|
1895
|
+
return 0;
|
|
1137
1896
|
}
|
|
1138
1897
|
};
|
|
1139
1898
|
|
|
1140
|
-
// src/
|
|
1141
|
-
var
|
|
1142
|
-
constructor(
|
|
1143
|
-
this.
|
|
1144
|
-
this.
|
|
1145
|
-
this.
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
headers: {
|
|
1158
|
-
authorization: this.db.getAuth.token
|
|
1159
|
-
},
|
|
1160
|
-
body: {
|
|
1161
|
-
collection: this.collection,
|
|
1162
|
-
filter: {}
|
|
1163
|
-
}
|
|
1164
|
-
}).sendRequest();
|
|
1165
|
-
if (res.success) {
|
|
1166
|
-
return res.documents.map((d) => {
|
|
1167
|
-
delete d.token;
|
|
1168
|
-
d.id = d._id;
|
|
1169
|
-
delete d._id;
|
|
1170
|
-
if (d === void 0)
|
|
1171
|
-
return [];
|
|
1172
|
-
return DocumentSnapshot.fromMap(d);
|
|
1173
|
-
});
|
|
1174
|
-
} else {
|
|
1175
|
-
return [];
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
async add(data) {
|
|
1179
|
-
const localId = Timestamp.now().toString();
|
|
1180
|
-
const payload = {
|
|
1181
|
-
id: localId,
|
|
1182
|
-
data,
|
|
1183
|
-
collection: this.collection,
|
|
1184
|
-
action: "create",
|
|
1185
|
-
status: "pending",
|
|
1186
|
-
timestamp: Timestamp.now()
|
|
1187
|
-
};
|
|
1188
|
-
if (this.db.getPersistence) {
|
|
1899
|
+
// src/persistence/RealtimeBridge.ts
|
|
1900
|
+
var RealtimeBridge = class {
|
|
1901
|
+
constructor(app, persistence, store) {
|
|
1902
|
+
this.app = app;
|
|
1903
|
+
this.persistence = persistence;
|
|
1904
|
+
this.store = store;
|
|
1905
|
+
this.collectionUnsubs = /* @__PURE__ */ new Map();
|
|
1906
|
+
this.documentUnsubs = /* @__PURE__ */ new Map();
|
|
1907
|
+
}
|
|
1908
|
+
docKey(collection, id) {
|
|
1909
|
+
return `${collection}:${id}`;
|
|
1910
|
+
}
|
|
1911
|
+
// ===================== PUBLIC API =====================
|
|
1912
|
+
watchCollection(collection, filter = {}) {
|
|
1913
|
+
const key = `${collection}:${JSON.stringify(filter)}`;
|
|
1914
|
+
if (this.collectionUnsubs.has(key)) {
|
|
1915
|
+
return this.collectionUnsubs.get(key);
|
|
1189
1916
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1917
|
+
this.emitCurrentCollection(collection);
|
|
1918
|
+
const unsub = this.app.rtdb().subscribeToCollectionRaw(
|
|
1919
|
+
collection,
|
|
1920
|
+
async (payload, change) => {
|
|
1921
|
+
if (change === "delete") {
|
|
1922
|
+
const id = payload?.id || payload?._id;
|
|
1923
|
+
if (id)
|
|
1924
|
+
await this.handleRemoteDelete(collection, id);
|
|
1925
|
+
} else {
|
|
1926
|
+
await this.handleRemoteCreateOrUpdate(collection, payload);
|
|
1200
1927
|
}
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1928
|
+
},
|
|
1929
|
+
filter
|
|
1930
|
+
);
|
|
1931
|
+
const fullUnsub = () => {
|
|
1932
|
+
unsub();
|
|
1933
|
+
this.collectionUnsubs.delete(key);
|
|
1934
|
+
};
|
|
1935
|
+
this.collectionUnsubs.set(key, fullUnsub);
|
|
1936
|
+
return fullUnsub;
|
|
1937
|
+
}
|
|
1938
|
+
watchDocument(collection, id) {
|
|
1939
|
+
const key = this.docKey(collection, id);
|
|
1940
|
+
if (this.documentUnsubs.has(key)) {
|
|
1941
|
+
return this.documentUnsubs.get(key);
|
|
1942
|
+
}
|
|
1943
|
+
this.emitCurrentDocument(collection, id);
|
|
1944
|
+
const unsub = this.app.rtdb().subscribeToDocumentRaw(
|
|
1945
|
+
collection,
|
|
1946
|
+
id,
|
|
1947
|
+
async (payload, change) => {
|
|
1948
|
+
if (change === "delete") {
|
|
1949
|
+
await this.handleRemoteDelete(collection, id);
|
|
1950
|
+
} else {
|
|
1951
|
+
await this.handleRemoteCreateOrUpdate(collection, payload);
|
|
1206
1952
|
}
|
|
1207
|
-
return null;
|
|
1208
|
-
}
|
|
1209
|
-
payload.status = "success";
|
|
1210
|
-
payload.id = res.id;
|
|
1211
|
-
if (this.db.getPersistence) {
|
|
1212
1953
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1954
|
+
);
|
|
1955
|
+
const fullUnsub = () => {
|
|
1956
|
+
unsub();
|
|
1957
|
+
this.documentUnsubs.delete(key);
|
|
1958
|
+
};
|
|
1959
|
+
this.documentUnsubs.set(key, fullUnsub);
|
|
1960
|
+
return fullUnsub;
|
|
1961
|
+
}
|
|
1962
|
+
// ===================== INTERNAL HANDLERS =====================
|
|
1963
|
+
async emitCurrentDocument(collection, id) {
|
|
1964
|
+
const localDoc = await this.persistence.getDoc(collection, id);
|
|
1965
|
+
const snapshot = localDoc && localDoc.exists && !localDoc.deleted ? DocumentSnapshot.fromMap(localDoc.data) : null;
|
|
1966
|
+
this.store.emitDocument(collection, id, snapshot, "init");
|
|
1967
|
+
}
|
|
1968
|
+
async emitCurrentCollection(collection) {
|
|
1969
|
+
const localDocs = await this.persistence.getCollectionSnapshots(collection);
|
|
1970
|
+
this.store.emitCollection(collection, localDocs, "init");
|
|
1971
|
+
}
|
|
1972
|
+
async handleRemoteCreateOrUpdate(collection, raw) {
|
|
1973
|
+
const id = raw.id ?? raw._id;
|
|
1974
|
+
if (!id)
|
|
1975
|
+
return;
|
|
1976
|
+
const saved = await this.persistence.applyRemoteDoc(collection, raw);
|
|
1977
|
+
if (!saved)
|
|
1978
|
+
return;
|
|
1979
|
+
const snap = DocumentSnapshot.fromMap(saved.data);
|
|
1980
|
+
this.store.emitDocument(collection, id, snap, "remote_update");
|
|
1981
|
+
const currentCollection = await this.persistence.getCollectionSnapshots(collection);
|
|
1982
|
+
this.store.emitCollection(collection, currentCollection, "remote_update", id);
|
|
1983
|
+
}
|
|
1984
|
+
async handleRemoteDelete(collection, id) {
|
|
1985
|
+
await this.persistence.applyRemoteDelete(collection, id);
|
|
1986
|
+
this.store.emitDocument(collection, id, null, "remote_delete");
|
|
1987
|
+
const currentCollection = await this.persistence.getCollectionSnapshots(collection);
|
|
1988
|
+
this.store.emitCollection(collection, currentCollection, "remote_delete", id);
|
|
1989
|
+
}
|
|
1990
|
+
// ===================== LIFECYCLE =====================
|
|
1991
|
+
/**
|
|
1992
|
+
* Call this when Socket.IO reconnects or when going online
|
|
1993
|
+
* Replays pending mutations + refreshes all active subscriptions from cache
|
|
1994
|
+
*/
|
|
1995
|
+
async onReconnect() {
|
|
1996
|
+
for (const [key] of this.collectionUnsubs) {
|
|
1997
|
+
const collection = key.split(":")[0];
|
|
1998
|
+
await this.emitCurrentCollection(collection);
|
|
1999
|
+
}
|
|
2000
|
+
for (const [key] of this.documentUnsubs) {
|
|
2001
|
+
const [collection, id] = key.split(":");
|
|
2002
|
+
await this.emitCurrentDocument(collection, id);
|
|
1219
2003
|
}
|
|
1220
2004
|
}
|
|
1221
2005
|
/**
|
|
1222
|
-
*
|
|
2006
|
+
* Clean up all subscriptions (call from EdmaxLabs destructor if needed)
|
|
1223
2007
|
*/
|
|
1224
|
-
|
|
1225
|
-
|
|
2008
|
+
dispose() {
|
|
2009
|
+
this.collectionUnsubs.forEach((unsub) => unsub());
|
|
2010
|
+
this.documentUnsubs.forEach((unsub) => unsub());
|
|
2011
|
+
this.collectionUnsubs.clear();
|
|
2012
|
+
this.documentUnsubs.clear();
|
|
1226
2013
|
}
|
|
1227
2014
|
};
|
|
1228
2015
|
|
|
1229
|
-
// src/
|
|
1230
|
-
var
|
|
1231
|
-
constructor(app) {
|
|
2016
|
+
// src/persistence/SyncEngine.ts
|
|
2017
|
+
var SyncEngine = class {
|
|
2018
|
+
constructor(app, persistence, store, realtimeBridge) {
|
|
2019
|
+
this.realtimeBridge = null;
|
|
2020
|
+
this.syncing = false;
|
|
2021
|
+
this.retryTimeout = null;
|
|
2022
|
+
this.MAX_RETRIES = 5;
|
|
2023
|
+
this.BASE_RETRY_DELAY = 2e3;
|
|
1232
2024
|
this.app = app;
|
|
1233
|
-
this.
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
2025
|
+
this.persistence = persistence;
|
|
2026
|
+
this.store = store;
|
|
2027
|
+
this.realtimeBridge = realtimeBridge || null;
|
|
2028
|
+
if (typeof window !== "undefined") {
|
|
2029
|
+
window.addEventListener("online", () => this.onNetworkOnline());
|
|
2030
|
+
document.addEventListener("visibilitychange", () => {
|
|
2031
|
+
if (document.visibilityState === "visible")
|
|
2032
|
+
this.onNetworkOnline();
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
1243
2035
|
}
|
|
1244
|
-
|
|
1245
|
-
this.
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
});
|
|
1251
|
-
return this;
|
|
2036
|
+
onNetworkOnline() {
|
|
2037
|
+
if (this.retryTimeout) {
|
|
2038
|
+
clearTimeout(this.retryTimeout);
|
|
2039
|
+
this.retryTimeout = null;
|
|
2040
|
+
}
|
|
2041
|
+
this.flush().catch(console.error);
|
|
1252
2042
|
}
|
|
1253
|
-
|
|
1254
|
-
this.
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
2043
|
+
async flush() {
|
|
2044
|
+
if (this.syncing)
|
|
2045
|
+
return;
|
|
2046
|
+
if (typeof navigator !== "undefined" && !navigator.onLine)
|
|
2047
|
+
return;
|
|
2048
|
+
this.syncing = true;
|
|
2049
|
+
try {
|
|
2050
|
+
const pending = await this.persistence.getPendingMutations();
|
|
2051
|
+
for (const mutation of pending) {
|
|
2052
|
+
if (mutation.retryCount >= this.MAX_RETRIES) {
|
|
2053
|
+
await this.persistence.setMutationStatus(
|
|
2054
|
+
mutation.mutationId,
|
|
2055
|
+
"failed",
|
|
2056
|
+
"Max retries exceeded"
|
|
2057
|
+
);
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
await this.persistence.setMutationStatus(mutation.mutationId, "syncing");
|
|
2061
|
+
try {
|
|
2062
|
+
let success = false;
|
|
2063
|
+
switch (mutation.type) {
|
|
2064
|
+
case "create":
|
|
2065
|
+
success = await this.syncCreate(mutation);
|
|
2066
|
+
break;
|
|
2067
|
+
case "update":
|
|
2068
|
+
success = await this.syncUpdate(mutation);
|
|
2069
|
+
break;
|
|
2070
|
+
case "delete":
|
|
2071
|
+
success = await this.syncDelete(mutation);
|
|
2072
|
+
break;
|
|
2073
|
+
case "array_push":
|
|
2074
|
+
case "array_update":
|
|
2075
|
+
case "array_insert":
|
|
2076
|
+
case "array_remove":
|
|
2077
|
+
default:
|
|
2078
|
+
console.warn(`[EdmaxLabs] Unknown mutation type: ${mutation.type}`);
|
|
2079
|
+
}
|
|
2080
|
+
if (success) {
|
|
2081
|
+
await this.persistence.removeMutation(mutation.mutationId);
|
|
2082
|
+
} else {
|
|
2083
|
+
throw new Error("Sync operation failed");
|
|
2084
|
+
}
|
|
2085
|
+
} catch (error) {
|
|
2086
|
+
const nextRetryCount = (mutation.retryCount || 0) + 1;
|
|
2087
|
+
await this.persistence.setMutationStatus(
|
|
2088
|
+
mutation.mutationId,
|
|
2089
|
+
"failed",
|
|
2090
|
+
error?.message ?? "Unknown sync error"
|
|
2091
|
+
);
|
|
2092
|
+
if (nextRetryCount < this.MAX_RETRIES) {
|
|
2093
|
+
this.scheduleRetry();
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
if (this.realtimeBridge) {
|
|
2098
|
+
await this.realtimeBridge.onReconnect();
|
|
2099
|
+
}
|
|
2100
|
+
} finally {
|
|
2101
|
+
this.syncing = false;
|
|
2102
|
+
}
|
|
1260
2103
|
}
|
|
1261
|
-
async
|
|
2104
|
+
async syncCreate(mutation) {
|
|
1262
2105
|
const res = await new HttpsRequest({
|
|
1263
2106
|
method: "POST" /* POST */,
|
|
1264
|
-
endpoint: this.app.getBaseUrl
|
|
1265
|
-
headers: {
|
|
1266
|
-
authorization: this.app.getAuth.token
|
|
1267
|
-
},
|
|
2107
|
+
endpoint: `${this.app.getBaseUrl()}/app/create`,
|
|
2108
|
+
headers: { authorization: this.app.getConfig().token },
|
|
1268
2109
|
body: {
|
|
1269
|
-
|
|
2110
|
+
collection: mutation.collection,
|
|
2111
|
+
data: mutation.payload
|
|
1270
2112
|
}
|
|
1271
2113
|
}).sendRequest();
|
|
1272
|
-
if (res?.
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
if (path.includes("/")) {
|
|
1288
|
-
const [col, id] = path.split("/");
|
|
1289
|
-
return new DocumentRef(this.app, col, id);
|
|
2114
|
+
if (!res?.success || !res.id)
|
|
2115
|
+
return false;
|
|
2116
|
+
const oldId = mutation.documentId;
|
|
2117
|
+
const newId = String(res.id);
|
|
2118
|
+
const replaced = await this.persistence.replaceDocId(
|
|
2119
|
+
mutation.collection,
|
|
2120
|
+
oldId,
|
|
2121
|
+
newId
|
|
2122
|
+
);
|
|
2123
|
+
if (replaced) {
|
|
2124
|
+
const snap = DocumentSnapshot.fromMap(replaced.data);
|
|
2125
|
+
this.store.emitDocument(mutation.collection, oldId, snap, "sync");
|
|
2126
|
+
this.store.emitDocument(mutation.collection, newId, snap, "sync");
|
|
2127
|
+
const currentCollection = await this.persistence.getCollectionSnapshots(mutation.collection);
|
|
2128
|
+
this.store.emitCollection(mutation.collection, currentCollection, "sync", newId);
|
|
1290
2129
|
}
|
|
1291
|
-
|
|
1292
|
-
}
|
|
1293
|
-
batch() {
|
|
1294
|
-
return new Batch(this.app);
|
|
2130
|
+
return true;
|
|
1295
2131
|
}
|
|
1296
|
-
async
|
|
1297
|
-
const tx = {
|
|
1298
|
-
ops: [],
|
|
1299
|
-
async get(docRef) {
|
|
1300
|
-
return docRef.get();
|
|
1301
|
-
},
|
|
1302
|
-
set(docRef, data) {
|
|
1303
|
-
tx.ops.push({
|
|
1304
|
-
op: "set",
|
|
1305
|
-
collection: docRef.collection,
|
|
1306
|
-
id: docRef.id,
|
|
1307
|
-
data
|
|
1308
|
-
});
|
|
1309
|
-
},
|
|
1310
|
-
update(docRef, data) {
|
|
1311
|
-
tx.ops.push({
|
|
1312
|
-
op: "update",
|
|
1313
|
-
collection: docRef.collection,
|
|
1314
|
-
id: docRef.id,
|
|
1315
|
-
data
|
|
1316
|
-
});
|
|
1317
|
-
},
|
|
1318
|
-
delete(docRef) {
|
|
1319
|
-
tx.ops.push({
|
|
1320
|
-
op: "delete",
|
|
1321
|
-
collection: docRef.collection,
|
|
1322
|
-
id: docRef.id
|
|
1323
|
-
});
|
|
1324
|
-
}
|
|
1325
|
-
};
|
|
1326
|
-
await transactionFn(tx);
|
|
2132
|
+
async syncUpdate(mutation) {
|
|
1327
2133
|
const res = await new HttpsRequest({
|
|
1328
2134
|
method: "POST" /* POST */,
|
|
1329
|
-
endpoint: this.app.getBaseUrl
|
|
1330
|
-
headers: {
|
|
1331
|
-
authorization: this.app.getAuth.token
|
|
1332
|
-
},
|
|
2135
|
+
endpoint: `${this.app.getBaseUrl()}/app/update`,
|
|
2136
|
+
headers: { authorization: this.app.getConfig().token },
|
|
1333
2137
|
body: {
|
|
1334
|
-
|
|
2138
|
+
collection: mutation.collection,
|
|
2139
|
+
document: mutation.documentId,
|
|
2140
|
+
data: mutation.payload
|
|
1335
2141
|
}
|
|
1336
2142
|
}).sendRequest();
|
|
1337
|
-
if (res?.
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
// src/utils/uuid.ts
|
|
1347
|
-
function generateUUID() {
|
|
1348
|
-
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1349
|
-
return crypto.randomUUID();
|
|
1350
|
-
}
|
|
1351
|
-
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
1352
|
-
const r = Math.random() * 16 | 0;
|
|
1353
|
-
const v = c === "x" ? r : r & 3 | 8;
|
|
1354
|
-
return v.toString(16);
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
// src/database/Realtime.ts
|
|
1359
|
-
var Realtime = class {
|
|
1360
|
-
constructor(db) {
|
|
1361
|
-
this.db = db;
|
|
1362
|
-
this.socket = null;
|
|
1363
|
-
this.events = ["error", "insert", "update", "delete"];
|
|
1364
|
-
}
|
|
1365
|
-
connect() {
|
|
1366
|
-
if (!this.db.getAuth?.token)
|
|
1367
|
-
throw new Error("Auth token missing");
|
|
1368
|
-
this.socket = (0, import_socket.io)(this.db.getSocketUrl, {
|
|
1369
|
-
transports: ["websocket"],
|
|
1370
|
-
auth: { token: this.db.getAuth.token }
|
|
1371
|
-
});
|
|
1372
|
-
return this.socket;
|
|
1373
|
-
}
|
|
1374
|
-
on(event, cb) {
|
|
1375
|
-
this.socket?.on(event, cb);
|
|
1376
|
-
}
|
|
1377
|
-
off(event, cb) {
|
|
1378
|
-
if (cb)
|
|
1379
|
-
this.socket?.off(event, cb);
|
|
1380
|
-
else
|
|
1381
|
-
this.socket?.removeAllListeners(event);
|
|
1382
|
-
}
|
|
1383
|
-
emit(event, data) {
|
|
1384
|
-
this.socket?.emit(event, data);
|
|
1385
|
-
}
|
|
1386
|
-
// ------------------------------------------------------------
|
|
1387
|
-
// 🔥 ADDITIONS BELOW — NO BREAKING CHANGES
|
|
1388
|
-
// ------------------------------------------------------------
|
|
1389
|
-
/**
|
|
1390
|
-
* Realtime listener for entire collection
|
|
1391
|
-
*/
|
|
1392
|
-
subscribeToCollection(collection, callback, filter) {
|
|
1393
|
-
if (!this.socket)
|
|
1394
|
-
this.connect();
|
|
1395
|
-
if (!filter) {
|
|
1396
|
-
filter = {};
|
|
1397
|
-
}
|
|
1398
|
-
const lid = `${generateUUID()}`;
|
|
1399
|
-
this.socket.emit("subscribe", { collection, filter, lid });
|
|
1400
|
-
const handler = (payload) => {
|
|
1401
|
-
const snapshot = DocumentSnapshot.fromMap(payload);
|
|
1402
|
-
callback(snapshot, payload.change);
|
|
1403
|
-
};
|
|
1404
|
-
this.events.forEach((event) => {
|
|
1405
|
-
this.socket.on(`${lid}-${event}`, handler);
|
|
1406
|
-
});
|
|
1407
|
-
return () => {
|
|
1408
|
-
this.socket.emit("unsubscribe", { lid });
|
|
1409
|
-
this.socket.off(lid, handler);
|
|
1410
|
-
};
|
|
1411
|
-
}
|
|
1412
|
-
/**
|
|
1413
|
-
* Realtime listener for a single document
|
|
1414
|
-
*/
|
|
1415
|
-
subscribeToDocument(collection, id, callback) {
|
|
1416
|
-
if (!this.socket)
|
|
1417
|
-
this.connect();
|
|
1418
|
-
const lid = id;
|
|
1419
|
-
this.socket.emit("subscribe", {
|
|
1420
|
-
collection,
|
|
1421
|
-
filter: {
|
|
1422
|
-
_id: lid
|
|
2143
|
+
if (!res?.success)
|
|
2144
|
+
return false;
|
|
2145
|
+
const local = await this.persistence.upsertDoc({
|
|
2146
|
+
collection: mutation.collection,
|
|
2147
|
+
id: mutation.documentId,
|
|
2148
|
+
data: {
|
|
2149
|
+
...res.document ?? mutation.payload ?? {},
|
|
2150
|
+
id: mutation.documentId
|
|
1423
2151
|
},
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
const listener = `${lid}-${event}`;
|
|
1432
|
-
this.socket.on(listener, handler);
|
|
2152
|
+
exists: true,
|
|
2153
|
+
deleted: false,
|
|
2154
|
+
pending: 0,
|
|
2155
|
+
localOnly: false,
|
|
2156
|
+
status: "synced",
|
|
2157
|
+
lastSyncedAt: this.persistence["now"]?.() ?? Date.now()
|
|
2158
|
+
// fallback
|
|
1433
2159
|
});
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
};
|
|
1440
|
-
|
|
1441
|
-
// src/functions/Functions.ts
|
|
1442
|
-
var Functions = class {
|
|
1443
|
-
constructor(app) {
|
|
1444
|
-
this.app = app;
|
|
1445
|
-
}
|
|
1446
|
-
async call(functionName, data) {
|
|
1447
|
-
try {
|
|
1448
|
-
const res = await new HttpsRequest({
|
|
1449
|
-
method: "POST" /* POST */,
|
|
1450
|
-
endpoint: this.app.getBaseUrl + "/functions/call/" + functionName,
|
|
1451
|
-
headers: {
|
|
1452
|
-
authorization: this.app.getAuth.token
|
|
1453
|
-
},
|
|
1454
|
-
body: data
|
|
1455
|
-
}).sendRequest();
|
|
1456
|
-
return res;
|
|
1457
|
-
} catch (err) {
|
|
1458
|
-
throw {
|
|
1459
|
-
message: err.message || "Function call failed",
|
|
1460
|
-
code: err.code || "UNKNOWN_ERROR"
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
2160
|
+
const snap = DocumentSnapshot.fromMap(local.data);
|
|
2161
|
+
this.store.emitDocument(mutation.collection, mutation.documentId, snap, "sync");
|
|
2162
|
+
const currentCollection = await this.persistence.getCollectionSnapshots(mutation.collection);
|
|
2163
|
+
this.store.emitCollection(mutation.collection, currentCollection, "sync", mutation.documentId);
|
|
2164
|
+
return true;
|
|
1463
2165
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
2166
|
+
async syncDelete(mutation) {
|
|
2167
|
+
const res = await new HttpsRequest({
|
|
2168
|
+
method: "POST" /* POST */,
|
|
2169
|
+
endpoint: `${this.app.getBaseUrl()}/app/delete`,
|
|
2170
|
+
headers: { authorization: this.app.getConfig().token },
|
|
2171
|
+
body: {
|
|
2172
|
+
collection: mutation.collection,
|
|
2173
|
+
document: mutation.documentId
|
|
2174
|
+
}
|
|
2175
|
+
}).sendRequest();
|
|
2176
|
+
if (!res?.success)
|
|
2177
|
+
return false;
|
|
2178
|
+
await this.persistence.markDeleted(mutation.collection, mutation.documentId, 0);
|
|
2179
|
+
this.store.emitDocument(mutation.collection, mutation.documentId, null, "sync");
|
|
2180
|
+
const currentCollection = await this.persistence.getCollectionSnapshots(mutation.collection);
|
|
2181
|
+
this.store.emitCollection(mutation.collection, currentCollection, "sync", mutation.documentId);
|
|
2182
|
+
return true;
|
|
1470
2183
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
throw {
|
|
1487
|
-
message: err.message || "Function call failed",
|
|
1488
|
-
code: err.code || "UNKNOWN_ERROR"
|
|
1489
|
-
};
|
|
2184
|
+
scheduleRetry() {
|
|
2185
|
+
if (this.retryTimeout)
|
|
2186
|
+
clearTimeout(this.retryTimeout);
|
|
2187
|
+
const delay = this.BASE_RETRY_DELAY * Math.pow(1.5, Math.random() * 0.5);
|
|
2188
|
+
this.retryTimeout = setTimeout(() => {
|
|
2189
|
+
this.flush().catch(console.error);
|
|
2190
|
+
}, delay);
|
|
2191
|
+
}
|
|
2192
|
+
// Public method users can call manually if needed
|
|
2193
|
+
async forceSync() {
|
|
2194
|
+
return this.flush();
|
|
2195
|
+
}
|
|
2196
|
+
dispose() {
|
|
2197
|
+
if (this.retryTimeout) {
|
|
2198
|
+
clearTimeout(this.retryTimeout);
|
|
1490
2199
|
}
|
|
1491
2200
|
}
|
|
1492
2201
|
};
|
|
@@ -1499,8 +2208,8 @@ var StorageSnapshot = class _StorageSnapshot {
|
|
|
1499
2208
|
}
|
|
1500
2209
|
static fromMap(map) {
|
|
1501
2210
|
const id = map.id ?? map._id ?? "";
|
|
1502
|
-
const
|
|
1503
|
-
return new _StorageSnapshot(id,
|
|
2211
|
+
const document2 = map.file ?? map.data ?? map;
|
|
2212
|
+
return new _StorageSnapshot(id, document2);
|
|
1504
2213
|
}
|
|
1505
2214
|
toMap() {
|
|
1506
2215
|
return {
|
|
@@ -1512,15 +2221,15 @@ var StorageSnapshot = class _StorageSnapshot {
|
|
|
1512
2221
|
|
|
1513
2222
|
// src/storage/StorageRef.ts
|
|
1514
2223
|
var StorageRef = class {
|
|
1515
|
-
constructor(
|
|
1516
|
-
this.
|
|
2224
|
+
constructor(app) {
|
|
2225
|
+
this.app = app;
|
|
1517
2226
|
}
|
|
1518
2227
|
async get(id) {
|
|
1519
2228
|
const res = await new HttpsRequest({
|
|
1520
2229
|
method: "GET" /* GET */,
|
|
1521
|
-
endpoint: this.
|
|
2230
|
+
endpoint: this.app.getBaseUrl() + `/storage/${id}`,
|
|
1522
2231
|
headers: {
|
|
1523
|
-
//authorization: this.
|
|
2232
|
+
//authorization: this.app.getConfig.token,
|
|
1524
2233
|
}
|
|
1525
2234
|
}).sendRequest();
|
|
1526
2235
|
if (res.success) {
|
|
@@ -1535,9 +2244,9 @@ var StorageRef = class {
|
|
|
1535
2244
|
async getMeta(id) {
|
|
1536
2245
|
const res = await new HttpsRequest({
|
|
1537
2246
|
method: "GET" /* GET */,
|
|
1538
|
-
endpoint: this.
|
|
2247
|
+
endpoint: this.app.getBaseUrl() + `/storage/${id}/info`,
|
|
1539
2248
|
headers: {
|
|
1540
|
-
//authorization: this.
|
|
2249
|
+
//authorization: this.app.getConfig.token,
|
|
1541
2250
|
}
|
|
1542
2251
|
}).sendRequest();
|
|
1543
2252
|
if (res.success) {
|
|
@@ -1556,9 +2265,9 @@ var StorageRef = class {
|
|
|
1556
2265
|
console.log("SRCF");
|
|
1557
2266
|
const res = await new HttpsRequest({
|
|
1558
2267
|
method: "POST" /* POST */,
|
|
1559
|
-
endpoint: this.
|
|
2268
|
+
endpoint: this.app.getBaseUrl() + "/storage/file/upload",
|
|
1560
2269
|
headers: {
|
|
1561
|
-
authorization: this.
|
|
2270
|
+
authorization: this.app.getConfig().token
|
|
1562
2271
|
},
|
|
1563
2272
|
file: srcFile,
|
|
1564
2273
|
isMultipart: true
|
|
@@ -1575,9 +2284,9 @@ var StorageRef = class {
|
|
|
1575
2284
|
async delete(id) {
|
|
1576
2285
|
const res = await new HttpsRequest({
|
|
1577
2286
|
method: "POST" /* POST */,
|
|
1578
|
-
endpoint: this.
|
|
2287
|
+
endpoint: this.app.getBaseUrl() + "/storage/file/delete",
|
|
1579
2288
|
headers: {
|
|
1580
|
-
authorization: this.
|
|
2289
|
+
authorization: this.app.getConfig().token
|
|
1581
2290
|
},
|
|
1582
2291
|
body: {
|
|
1583
2292
|
file_id: id
|
|
@@ -1603,66 +2312,95 @@ var socketURI = "https://api.edmaxlabs.com";
|
|
|
1603
2312
|
|
|
1604
2313
|
// src/core/EdmaxLabs.ts
|
|
1605
2314
|
var _EdmaxLabs = class _EdmaxLabs {
|
|
1606
|
-
constructor(
|
|
1607
|
-
|
|
2315
|
+
constructor(config) {
|
|
2316
|
+
// Offline layer (only initialized when persistence: true)
|
|
2317
|
+
this.persistence = null;
|
|
2318
|
+
this.localStore = null;
|
|
2319
|
+
this.syncEngine = null;
|
|
2320
|
+
this.realtimeBridge = null;
|
|
2321
|
+
const { token, project, persistence = false } = config;
|
|
2322
|
+
if (!token || !project) {
|
|
2323
|
+
throw new Error("EdmaxLabs: 'token' and 'project' are required in config");
|
|
2324
|
+
}
|
|
1608
2325
|
this.baseUrl = serverURI;
|
|
1609
2326
|
this.socketUrl = socketURI;
|
|
1610
|
-
this.
|
|
2327
|
+
this._config = config;
|
|
1611
2328
|
this._db = new Database(this);
|
|
2329
|
+
this._realtime = new Realtime(this);
|
|
1612
2330
|
this._hosting = new Hosting(this);
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
2331
|
+
if (persistence) {
|
|
2332
|
+
this.persistence = new Persistence();
|
|
2333
|
+
this.localStore = new LocalStore();
|
|
2334
|
+
this.realtimeBridge = new RealtimeBridge(this, this.persistence, this.localStore);
|
|
2335
|
+
this.syncEngine = new SyncEngine(
|
|
2336
|
+
this,
|
|
2337
|
+
this.persistence,
|
|
2338
|
+
this.localStore,
|
|
2339
|
+
this.realtimeBridge
|
|
2340
|
+
);
|
|
1619
2341
|
}
|
|
1620
2342
|
}
|
|
2343
|
+
// ===================== PUBLIC API =====================
|
|
2344
|
+
/** Recommended for most React apps (creates fresh instance) */
|
|
2345
|
+
static create(config) {
|
|
2346
|
+
return new _EdmaxLabs(config);
|
|
2347
|
+
}
|
|
2348
|
+
/** Firebase-style singleton (kept for backward compatibility) */
|
|
1621
2349
|
static initialize(config) {
|
|
1622
2350
|
if (!_EdmaxLabs.instance) {
|
|
1623
2351
|
_EdmaxLabs.instance = new _EdmaxLabs(config);
|
|
1624
2352
|
}
|
|
1625
2353
|
return _EdmaxLabs.instance;
|
|
1626
2354
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
}
|
|
1630
|
-
get getAuth() {
|
|
1631
|
-
return this.auth;
|
|
1632
|
-
}
|
|
1633
|
-
get getBaseUrl() {
|
|
1634
|
-
return this.baseUrl;
|
|
1635
|
-
}
|
|
1636
|
-
get getSocketUrl() {
|
|
1637
|
-
return this.socketUrl;
|
|
1638
|
-
}
|
|
1639
|
-
get database() {
|
|
2355
|
+
// ===================== CLEAN GETTERS =====================
|
|
2356
|
+
get getDatabase() {
|
|
1640
2357
|
return this._db;
|
|
1641
2358
|
}
|
|
1642
|
-
get
|
|
1643
|
-
return new Storage(this);
|
|
1644
|
-
}
|
|
1645
|
-
get hosting() {
|
|
1646
|
-
return this._hosting;
|
|
1647
|
-
}
|
|
1648
|
-
get functions() {
|
|
2359
|
+
get getFunctions() {
|
|
1649
2360
|
return new Functions(this);
|
|
1650
2361
|
}
|
|
1651
|
-
get
|
|
1652
|
-
return this
|
|
1653
|
-
}
|
|
1654
|
-
sync(id, collection) {
|
|
1655
|
-
if (!this.getPersistence) {
|
|
1656
|
-
throw new Error("Persistence is Off Can't Sync.");
|
|
1657
|
-
}
|
|
1658
|
-
return this.persistence_worker.syncPendingWrite(id, collection);
|
|
2362
|
+
get getStorage() {
|
|
2363
|
+
return new Storage(this);
|
|
1659
2364
|
}
|
|
1660
|
-
get
|
|
2365
|
+
get getAuthentication() {
|
|
1661
2366
|
if (!Authentication.instance) {
|
|
1662
2367
|
Authentication.instance = new Authentication();
|
|
1663
2368
|
}
|
|
1664
2369
|
return Authentication.instance;
|
|
1665
2370
|
}
|
|
2371
|
+
/** New clean offline namespace - highly recommended */
|
|
2372
|
+
offline() {
|
|
2373
|
+
return {
|
|
2374
|
+
persistence: this.persistence,
|
|
2375
|
+
localStore: this.localStore,
|
|
2376
|
+
syncEngine: this.syncEngine,
|
|
2377
|
+
realtimeBridge: this.realtimeBridge,
|
|
2378
|
+
enabled: !!this.persistence
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
// Internal access (for internal classes only)
|
|
2382
|
+
getConfig() {
|
|
2383
|
+
return { ...this._config };
|
|
2384
|
+
}
|
|
2385
|
+
getBaseUrl() {
|
|
2386
|
+
return this.baseUrl;
|
|
2387
|
+
}
|
|
2388
|
+
getSocketUrl() {
|
|
2389
|
+
return this.socketUrl;
|
|
2390
|
+
}
|
|
2391
|
+
rtdb() {
|
|
2392
|
+
return this._realtime;
|
|
2393
|
+
}
|
|
2394
|
+
// ===================== LIFECYCLE =====================
|
|
2395
|
+
/** Call this when your app unmounts or user logs out */
|
|
2396
|
+
dispose() {
|
|
2397
|
+
this._realtime.dispose?.();
|
|
2398
|
+
this.syncEngine?.dispose?.();
|
|
2399
|
+
this.realtimeBridge?.dispose?.();
|
|
2400
|
+
if (_EdmaxLabs.instance === this) {
|
|
2401
|
+
_EdmaxLabs.instance = null;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
1666
2404
|
};
|
|
1667
2405
|
_EdmaxLabs.instance = null;
|
|
1668
2406
|
var EdmaxLabs = _EdmaxLabs;
|
|
@@ -1672,6 +2410,7 @@ var src_default = EdmaxLabs;
|
|
|
1672
2410
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1673
2411
|
0 && (module.exports = {
|
|
1674
2412
|
ArraySnapshot,
|
|
2413
|
+
Authentication,
|
|
1675
2414
|
Credentials,
|
|
1676
2415
|
DocumentSnapshot,
|
|
1677
2416
|
StorageSnapshot,
|