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