edmaxlabs-core 2.5.0 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  // src/authentication/Credentials.ts
2
2
  var Credentials = class _Credentials {
3
- constructor(uid, displayInfo, createdAt, updatedAt, lastLogged) {
3
+ constructor(uid, displayInfo, createdAt, updatedAt, lastLogged, logged) {
4
4
  this.uid = uid;
5
5
  this.displayInfo = displayInfo;
6
6
  this.createdAt = createdAt;
7
7
  this.updatedAt = updatedAt;
8
8
  this.lastLogged = lastLogged;
9
+ this.logged = logged === true;
9
10
  }
10
11
  static fromMap(map) {
11
12
  const uid = map.id ?? map.uid ?? "";
@@ -14,12 +15,14 @@ var Credentials = class _Credentials {
14
15
  const createdAt = data.createdAt;
15
16
  const updatedAt = data.updatedAt;
16
17
  const lastLogged = data.lastLogged;
18
+ const logged = data.logged;
17
19
  const result = new _Credentials(
18
20
  uid,
19
21
  d_info,
20
22
  createdAt,
21
23
  updatedAt,
22
- lastLogged
24
+ lastLogged,
25
+ logged
23
26
  );
24
27
  return result;
25
28
  }
@@ -29,7 +32,8 @@ var Credentials = class _Credentials {
29
32
  displayInfo: this.displayInfo,
30
33
  createdAt: this.createdAt,
31
34
  updatedAt: this.updatedAt,
32
- lastLogged: this.lastLogged
35
+ lastLogged: this.lastLogged,
36
+ logged: this.logged
33
37
  };
34
38
  }
35
39
  };
@@ -97,91 +101,123 @@ var HttpsRequest = class {
97
101
 
98
102
  // src/authentication/Authentication.ts
99
103
  var _Authentication = class _Authentication {
100
- // Re-entrancy guard
101
104
  constructor() {
102
- this.eUser = null;
103
- this.client = null;
104
- this.app = null;
105
- // Dedicated lightweight instance for auth
106
105
  this.eventListeners = /* @__PURE__ */ new Map();
107
- this.isHandlingChange = false;
108
- // ====================== AUTH METHODS ======================
106
+ this.eventWaiters = /* @__PURE__ */ new Map();
107
+ this.unsubscribers = /* @__PURE__ */ new Set();
108
+ this.isSameCredentials = (a, b) => {
109
+ if (!a && !b)
110
+ return true;
111
+ if (!a || !b)
112
+ return false;
113
+ return JSON.stringify(a.toMap()) === JSON.stringify(b.toMap());
114
+ };
109
115
  this.createUserWithEmailAndPassword = async ({
110
116
  email,
111
117
  password
112
118
  }) => {
119
+ this.eUser = void 0;
113
120
  this.saveCredentials(null);
114
- const app = this.app.getDatabase;
115
- const existing = await app.collection("users").query.where({ key: "email", op: "===", value: email }).get();
116
- if (existing.length > 0) {
121
+ const app = this.app?.getDatabase;
122
+ const data = await app?.collection("users").query.where({
123
+ key: "email",
124
+ op: "===",
125
+ value: email
126
+ }).get();
127
+ if (data.length > 0) {
117
128
  throw new Error("Email Already In Use.");
118
129
  }
119
- const userRef = await app.collection("users").add({
130
+ const userRef = await app?.collection("users").add({
120
131
  email,
121
132
  password,
122
- token: this.client.getConfig().token,
123
133
  logged: true,
124
- lastLogged: Date.now()
134
+ lastLogged: Date.now(),
135
+ displayInfo: {
136
+ firstname: email.split("@")[0],
137
+ lastname: "",
138
+ surname: "",
139
+ profile: "./logo.png",
140
+ email,
141
+ phone: ""
142
+ }
125
143
  });
126
- if (!userRef?.id) {
127
- throw new Error("Something went wrong. Please try again.");
144
+ if (!userRef?.id || userRef?.id === "") {
145
+ throw new Error("Something went wrong try Again.");
128
146
  }
129
- const newCreds = Credentials.fromMap(userRef.toMap());
130
- this.saveCredentials(newCreds);
131
- return newCreds;
147
+ this.eUser = Credentials.fromMap(userRef);
148
+ this.saveCredentials(this.eUser);
149
+ return Credentials.fromMap(userRef.toMap());
132
150
  };
133
151
  this.signInWithEmailAndPassword = async ({
134
152
  email,
135
153
  password
136
154
  }) => {
137
- const app = this.app.getDatabase;
138
- const data = await app.collection("users").query.where({ key: "email", op: "===", value: email }).where({ key: "password", op: "===", value: password }).get();
139
- if (!data || data.length === 0) {
155
+ const app = this.app?.getDatabase;
156
+ const data = await app?.collection("users").query.where({
157
+ key: "email",
158
+ op: "===",
159
+ value: email
160
+ }).where({
161
+ key: "password",
162
+ op: "===",
163
+ value: password
164
+ }).get();
165
+ if (data === void 0) {
166
+ throw new Error("Auth Failed.");
167
+ }
168
+ if (data?.length === 0) {
140
169
  throw new Error("User Not Found.");
141
170
  }
142
- const userDoc = data[0];
143
- await app.collection("users").doc(userDoc.id).update({
171
+ const _data = data[0];
172
+ await app?.collection("users").doc(_data.id).update({
144
173
  logged: true,
145
174
  lastLogged: Date.now()
146
175
  });
147
- const signedInUser = Credentials.fromMap(userDoc.toMap());
148
- this.saveCredentials(signedInUser);
149
- return signedInUser;
150
- };
151
- this.signOut = async () => {
152
- const current = this.currentUser();
153
- if (!current)
154
- return;
155
- const app = this.app.getDatabase;
156
- await app.collection("users").doc(current.uid).update({
157
- logged: false,
158
- lastLogged: Date.now()
159
- });
160
- this.saveCredentials(null);
176
+ this.eUser = Credentials.fromMap(_data);
177
+ this.saveCredentials(this.eUser);
178
+ return Credentials.fromMap(_data);
161
179
  };
162
180
  this.deleteUser = async () => {
163
- if (!this.eUser)
181
+ if (!this.eUser) {
164
182
  throw new Error("No User Signed in");
165
- const app = this.app.getDatabase;
166
- await app.collection("users").doc(this.eUser.uid).delete();
183
+ }
184
+ const app = this.app?.getDatabase;
185
+ const userRef = await app?.collection("users").doc(this.eUser.uid).delete();
186
+ if (userRef) {
187
+ throw new Error("Something went wrong try Again.");
188
+ }
189
+ this.eUser = void 0;
190
+ this.saveCredentials(null);
191
+ };
192
+ this.signOut = async () => {
193
+ const app = this.app?.getDatabase;
194
+ const luid = this.currentUser();
195
+ this.eUser = void 0;
167
196
  this.saveCredentials(null);
197
+ if (luid)
198
+ await app?.collection("users").doc(luid?.uid).update({
199
+ logged: false,
200
+ lastLogged: Date.now()
201
+ });
202
+ return;
168
203
  };
169
- // Optional: Rules verification
170
204
  this.rules = async (path, context) => {
171
205
  const res = await new HttpsRequest({
172
206
  method: "POST" /* POST */,
173
- endpoint: `${this.client.getBaseUrl()}/auth/rules/verify`,
207
+ endpoint: this.client.getBaseUrl() + "/auth/rules/verify",
174
208
  headers: {
175
209
  authorization: this.client.getConfig().token,
176
210
  project: this.client.getConfig().project
177
211
  },
178
- body: { path, context }
212
+ body: {
213
+ path,
214
+ context
215
+ }
179
216
  }).sendRequest();
180
217
  return res;
181
218
  };
182
- if (_Authentication.instance) {
219
+ if (_Authentication.instance)
183
220
  return _Authentication.instance;
184
- }
185
221
  this.client = EdmaxLabs.instance;
186
222
  this.app = new EdmaxLabs({
187
223
  token: "auth",
@@ -190,16 +226,26 @@ var _Authentication = class _Authentication {
190
226
  _Authentication.instance = this;
191
227
  this.restoreSession();
192
228
  }
229
+ restoreSession() {
230
+ const saved = this.currentUser();
231
+ if (saved) {
232
+ this.eUser = saved;
233
+ this.emitValue("creds", saved);
234
+ }
235
+ }
236
+ // Better singleton
193
237
  static getInstance() {
194
238
  if (!_Authentication.instance) {
195
239
  _Authentication.instance = new _Authentication();
196
240
  }
197
241
  return _Authentication.instance;
198
242
  }
199
- // ====================== EVENT SYSTEM ======================
200
243
  emitValue(key, value) {
201
244
  const listeners = this.eventListeners.get(key) || [];
202
- [...listeners].forEach((listener) => listener(value));
245
+ listeners.forEach((l) => l(value));
246
+ const waiters = this.eventWaiters.get(key) || [];
247
+ waiters.forEach((resolve) => resolve(value));
248
+ this.eventWaiters.delete(key);
203
249
  }
204
250
  onValue(key, callback) {
205
251
  const listeners = this.eventListeners.get(key) || [];
@@ -210,7 +256,6 @@ var _Authentication = class _Authentication {
210
256
  this.eventListeners.set(key, filtered);
211
257
  };
212
258
  }
213
- // ====================== CREDENTIALS ======================
214
259
  currentUser() {
215
260
  const data = localStorage.getItem("eauth");
216
261
  if (!data)
@@ -233,71 +278,50 @@ var _Authentication = class _Authentication {
233
278
  this.eUser = credentials;
234
279
  this.emitValue("creds", credentials);
235
280
  }
236
- isSameCredentials(a, b) {
237
- if (!a && !b)
238
- return true;
239
- if (!a || !b)
240
- return false;
241
- return a.uid === b.uid && a.displayInfo.email === b.displayInfo.email && a.lastLogged === b.lastLogged;
242
- }
243
- restoreSession() {
244
- const saved = this.currentUser();
245
- if (saved) {
246
- this.eUser = saved;
247
- }
248
- }
249
- // ====================== MAIN AUTH STATE LISTENER ======================
250
- /**
251
- * Main method to listen to authentication state changes.
252
- * This listener stays alive for the entire app lifetime.
253
- */
281
+ // Main auth state listener - should be called ONCE at app root
254
282
  authState({
255
283
  onChange,
256
284
  onSignOut,
257
285
  onDeleted
258
286
  }) {
259
287
  let userDocUnsubscribe;
260
- let credsUnsub;
261
288
  const handleCredsChange = (creds) => {
262
- if (this.isHandlingChange)
263
- return;
264
- this.isHandlingChange = true;
265
- userDocUnsubscribe?.();
266
- userDocUnsubscribe = void 0;
289
+ if (userDocUnsubscribe) {
290
+ userDocUnsubscribe();
291
+ userDocUnsubscribe = void 0;
292
+ }
267
293
  if (!creds) {
268
294
  this.eUser = null;
269
295
  onSignOut?.();
270
- this.isHandlingChange = false;
271
296
  return;
272
297
  }
273
- const userRef = this.app.getDatabase.collection("users").doc(creds.uid);
298
+ const userRef = this.app?.getDatabase.collection("users").doc(creds.uid);
299
+ if (!userRef)
300
+ return;
274
301
  userDocUnsubscribe = userRef.onSnapshot(
275
302
  (snapshot, change) => {
303
+ console.log(snapshot);
276
304
  if (change === "delete") {
277
305
  this.saveCredentials(null);
278
306
  onSignOut?.();
279
307
  onDeleted?.();
280
- this.isHandlingChange = false;
281
308
  return;
282
309
  }
283
310
  if (change === "insert" || change === "update") {
284
- if (!snapshot?.data) {
285
- this.isHandlingChange = false;
311
+ if (!snapshot)
286
312
  return;
287
- }
288
313
  if (snapshot.data.logged === false || snapshot.data.logged === "false") {
289
314
  this.saveCredentials(null);
290
315
  onSignOut?.();
291
- this.isHandlingChange = false;
292
316
  return;
293
317
  }
294
318
  const newCreds = Credentials.fromMap(snapshot.toMap());
295
319
  if (!this.isSameCredentials(this.eUser, newCreds)) {
320
+ this.eUser = newCreds;
296
321
  this.saveCredentials(newCreds);
297
322
  onChange(newCreds);
298
323
  }
299
324
  }
300
- this.isHandlingChange = false;
301
325
  }
302
326
  );
303
327
  };
@@ -306,10 +330,10 @@ var _Authentication = class _Authentication {
306
330
  this.eUser = initialUser;
307
331
  onChange(initialUser);
308
332
  }
309
- credsUnsub = this.onValue("creds", handleCredsChange);
310
333
  handleCredsChange(initialUser);
334
+ const credsUnsub = this.onValue("creds", handleCredsChange);
311
335
  return () => {
312
- credsUnsub?.();
336
+ credsUnsub();
313
337
  userDocUnsubscribe?.();
314
338
  };
315
339
  }
@@ -336,308 +360,6 @@ var DocumentSnapshot = class _DocumentSnapshot {
336
360
  }
337
361
  };
338
362
 
339
- // src/database/Timestamp.ts
340
- var Timestamp = class _Timestamp {
341
- constructor(seconds, nanoseconds = 0) {
342
- this.seconds = seconds;
343
- this.nanoseconds = nanoseconds;
344
- }
345
- static now() {
346
- return _Timestamp.fromMillis(Date.now());
347
- }
348
- static fromDate(date) {
349
- return new _Timestamp(
350
- Math.floor(date.getTime() / 1e3),
351
- date.getTime() % 1e3 * 1e6
352
- );
353
- }
354
- static fromMillis(ms) {
355
- return new _Timestamp(Math.floor(ms / 1e3), ms % 1e3 * 1e6);
356
- }
357
- static fromMongo(dateObj) {
358
- return _Timestamp.fromMillis(dateObj.getTime());
359
- }
360
- static fromJSON(obj) {
361
- return new _Timestamp(obj.seconds, obj.nanoseconds);
362
- }
363
- toDate() {
364
- return new Date(this.toMillis());
365
- }
366
- toMillis() {
367
- return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
368
- }
369
- sinceEpoch() {
370
- return this.seconds;
371
- }
372
- toMongo() {
373
- return new Date(this.toMillis());
374
- }
375
- format(pattern = "dd-MM-yyyy HH:mm:ss") {
376
- const d = this.toDate();
377
- const monthsShort = [
378
- "Jan",
379
- "Feb",
380
- "Mar",
381
- "Apr",
382
- "May",
383
- "Jun",
384
- "Jul",
385
- "Aug",
386
- "Sep",
387
- "Oct",
388
- "Nov",
389
- "Dec"
390
- ];
391
- const monthsLong = [
392
- "January",
393
- "February",
394
- "March",
395
- "April",
396
- "May",
397
- "June",
398
- "July",
399
- "August",
400
- "September",
401
- "October",
402
- "November",
403
- "December"
404
- ];
405
- const hours = d.getHours();
406
- const hours12 = hours % 12 === 0 ? 12 : hours % 12;
407
- const ampm = hours < 12 ? "AM" : "PM";
408
- const map = {
409
- dd: String(d.getDate()).padStart(2, "0"),
410
- MM: String(d.getMonth() + 1).padStart(2, "0"),
411
- yyyy: d.getFullYear(),
412
- HH: String(d.getHours()).padStart(2, "0"),
413
- mm: String(d.getMinutes()).padStart(2, "0"),
414
- ss: String(d.getSeconds()).padStart(2, "0"),
415
- SSS: String(d.getMilliseconds()).padStart(3, "0"),
416
- MMM: monthsShort[d.getMonth()],
417
- MMMM: monthsLong[d.getMonth()],
418
- a: ampm.toLowerCase(),
419
- // am/pm
420
- A: ampm
421
- // AM/PM
422
- };
423
- return pattern.replace(
424
- /MMMM|MMM|dd|MM|yyyy|HH|mm|ss|SSS|a|A/g,
425
- (match) => String(map[match])
426
- );
427
- }
428
- compare(other) {
429
- if (this.seconds !== other.seconds) {
430
- return this.seconds - other.seconds;
431
- }
432
- return this.nanoseconds - other.nanoseconds;
433
- }
434
- relative(fmt = "dd-MM-yyyy HH:mm:ss") {
435
- const now = Date.now();
436
- const diff = now - this.toMillis();
437
- const sec = Math.floor(diff / 1e3);
438
- if (sec < 60)
439
- return `${sec}s ago`;
440
- const min = Math.floor(sec / 60);
441
- if (min < 60)
442
- return `${min}m ago`;
443
- const hrs = Math.floor(min / 60);
444
- if (hrs < 24)
445
- return `${hrs}h ago`;
446
- const days = Math.floor(hrs / 24);
447
- if (days < 3)
448
- return `${days}d ago`;
449
- return this.format(fmt);
450
- }
451
- add({
452
- seconds = 0,
453
- minutes = 0,
454
- hours = 0,
455
- days = 0
456
- }) {
457
- const totalMs = seconds * 1e3 + minutes * 6e4 + hours * 36e5 + days * 864e5;
458
- return _Timestamp.fromMillis(this.toMillis() + totalMs);
459
- }
460
- weekdayName() {
461
- return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][this.toDate().getDay()];
462
- }
463
- toJSON() {
464
- return {
465
- _type: "timestamp",
466
- seconds: this.seconds,
467
- nanoseconds: this.nanoseconds
468
- };
469
- }
470
- toString() {
471
- return this.format("yyyy-MM-dd HH:mm:ss");
472
- }
473
- };
474
-
475
- // src/database/ArraySnapshot.ts
476
- var ArraySnapshot = class _ArraySnapshot {
477
- constructor(id = "", index, data, dt) {
478
- this.data = data;
479
- this.id = id;
480
- this.dt = dt ?? Timestamp.now();
481
- }
482
- static fromMap(map) {
483
- const index = map.index ?? map.index ?? -1;
484
- const id = map.id ?? map._id ?? "";
485
- const document2 = map.data ?? map.document ?? map;
486
- return new _ArraySnapshot(id, index, document2);
487
- }
488
- toMap() {
489
- return {
490
- id: this.id ?? "",
491
- data: this.data,
492
- dt: this.dt
493
- };
494
- }
495
- };
496
-
497
- // src/database/Array.ts
498
- var Array2 = class {
499
- constructor(app, collection, key, id) {
500
- this.app = app;
501
- this.collection = collection;
502
- this.key = key;
503
- this.docID = id;
504
- this.persistence = app.offline().persistence;
505
- this.syncEngine = app.offline().syncEngine;
506
- this.localStore = app.offline().localStore;
507
- }
508
- /**
509
- * Get current array elements (offline-first: prefers local cache)
510
- */
511
- async show() {
512
- if (this.persistence) {
513
- const doc = await this.persistence.getDoc(this.collection, this.docID);
514
- if (doc?.exists && !doc.deleted) {
515
- const arrayData = doc.data[this.key] || [];
516
- return arrayData.map((item) => ArraySnapshot.fromMap(item));
517
- }
518
- }
519
- if (typeof navigator === "undefined" || navigator.onLine) {
520
- this.refreshFromRemote().catch(() => {
521
- });
522
- }
523
- return [];
524
- }
525
- async refreshFromRemote() {
526
- try {
527
- const res = await new HttpsRequest({
528
- method: "POST" /* POST */,
529
- endpoint: `${this.app.getBaseUrl()}/db/array/show`,
530
- headers: { authorization: this.app.getConfig().token, "x-project": this.app.getConfig().project },
531
- body: {
532
- collection: this.collection,
533
- document: this.docID,
534
- array: this.key
535
- }
536
- }).sendRequest();
537
- if (!res?.success || !globalThis.Array.isArray(res.documents)) {
538
- return [];
539
- }
540
- const snapshots = res.documents.filter((d) => d != null).map((d) => ArraySnapshot.fromMap(d));
541
- if (this.persistence) {
542
- const currentDoc = await this.persistence.getDoc(this.collection, this.docID);
543
- if (currentDoc) {
544
- await this.persistence.upsertDoc({
545
- ...currentDoc,
546
- data: { ...currentDoc.data, [this.key]: res.documents },
547
- pending: 0,
548
- status: "synced",
549
- lastSyncedAt: Date.now()
550
- });
551
- }
552
- }
553
- return snapshots;
554
- } catch {
555
- return [];
556
- }
557
- }
558
- async push(data, id) {
559
- const payload = {
560
- data,
561
- id: id || this.persistence?.createLocalId?.() || void 0,
562
- dt: Timestamp.now()
563
- };
564
- if (this.persistence) {
565
- await this.persistence.enqueueMutation({
566
- mutationId: this.persistence.createMutationId(),
567
- collection: this.collection,
568
- documentId: this.docID,
569
- type: "array_push",
570
- // custom type
571
- payload: { arrayKey: this.key, ...payload }
572
- });
573
- this.syncEngine?.flush().catch(console.error);
574
- const doc = await this.persistence.getDoc(this.collection, this.docID);
575
- if (doc) {
576
- const updatedArray = [...doc.data[this.key] || [], payload];
577
- const snap = ArraySnapshot.fromMap(payload);
578
- this.localStore?.emitDocument(
579
- this.collection,
580
- this.docID,
581
- DocumentSnapshot.fromMap({ ...doc.data, [this.key]: updatedArray }),
582
- "update"
583
- );
584
- }
585
- return ArraySnapshot.fromMap(payload);
586
- }
587
- const res = await new HttpsRequest({
588
- method: "POST" /* POST */,
589
- endpoint: `${this.app.getBaseUrl()}/db/array/push`,
590
- headers: { authorization: this.app.getConfig().token, "x-project": this.app.getConfig().project },
591
- body: {
592
- collection: this.collection,
593
- document: this.docID,
594
- array: this.key,
595
- ...payload
596
- }
597
- }).sendRequest();
598
- return res?.success ? ArraySnapshot.fromMap(res.document) : null;
599
- }
600
- // Similar pattern for update, insert, remove, get...
601
- // (I'll show one more as example; apply the same logic to the rest)
602
- async update(position, data, id) {
603
- if (this.persistence) {
604
- await this.persistence.enqueueMutation({
605
- mutationId: this.persistence.createMutationId(),
606
- collection: this.collection,
607
- documentId: this.docID,
608
- type: "array_update",
609
- payload: { arrayKey: this.key, position, data, id }
610
- });
611
- this.syncEngine?.flush().catch(console.error);
612
- return ArraySnapshot.fromMap({ ...data, id });
613
- }
614
- const res = await new HttpsRequest({
615
- method: "POST" /* POST */,
616
- endpoint: `${this.app.getBaseUrl()}/db/array/update`,
617
- headers: { authorization: this.app.getConfig().token, "x-project": this.app.getConfig().project },
618
- body: {
619
- collection: this.collection,
620
- document: this.docID,
621
- array: this.key,
622
- position,
623
- data,
624
- id
625
- }
626
- }).sendRequest();
627
- return res?.success ? ArraySnapshot.fromMap(res.document) : null;
628
- }
629
- // TODO: Implement insert(), get(), remove() using the exact same offline pattern as push/update
630
- async insert(position, data, id) {
631
- return null;
632
- }
633
- async get(position) {
634
- return null;
635
- }
636
- async remove(position) {
637
- return null;
638
- }
639
- };
640
-
641
363
  // src/utils/documentNomalizer.ts
642
364
  function normalizePayload(payload) {
643
365
  const raw = payload?.document ?? payload?.data ?? payload;
@@ -657,30 +379,19 @@ function validateDocumentData(data, operation) {
657
379
  if (data === null || data === void 0) {
658
380
  throw new Error(`${operation}: data cannot be null or undefined`);
659
381
  }
660
- if (typeof data !== "object") {
661
- throw new Error(`${operation}: data must be an object`);
662
- }
663
- if (data instanceof Array2) {
664
- throw new Error(`${operation}: data cannot be an array`);
382
+ if (typeof data !== "object" || Array.isArray(data)) {
383
+ throw new Error(`${operation}: data must be a plain object`);
665
384
  }
666
- const reservedFields = ["id", "_id", "_createdAt", "_updatedAt", "_deleted"];
385
+ const reservedFields = ["_id", "_createdAt", "_updatedAt", "_deleted", "_v", "_s"];
667
386
  for (const field of reservedFields) {
668
387
  if (field in data) {
669
388
  throw new Error(`${operation}: '${field}' is a reserved field and cannot be set manually`);
670
389
  }
671
390
  }
672
- const dataSize = JSON.stringify(data).length;
673
- if (dataSize > 1024 * 1024) {
674
- throw new Error(`${operation}: document size (${Math.round(dataSize / 1024)}KB) exceeds maximum allowed size (1MB)`);
675
- }
676
- try {
677
- JSON.stringify(data);
678
- } catch {
679
- throw new Error(`${operation}: data contains circular references`);
680
- }
681
391
  }
682
392
  var DocumentRef = class {
683
393
  constructor(app, collection, id) {
394
+ this._isUpdating = false;
684
395
  this.app = app;
685
396
  this.collection = collection;
686
397
  this.id = id;
@@ -688,58 +399,60 @@ var DocumentRef = class {
688
399
  this.syncEngine = app.offline().syncEngine;
689
400
  this.localStore = app.offline().localStore;
690
401
  }
402
+ // ====================== GET ======================
691
403
  async get() {
692
404
  if (this.persistence) {
693
405
  try {
694
406
  const localSnap = await this.persistence.getDocSnapshot(this.collection, this.id);
695
407
  if (localSnap)
696
- return localSnap;
697
- return null;
698
- } catch (error) {
699
- console.error("[EdmaxLabs] Error reading from cache:", error);
700
- }
701
- }
702
- return await this.app.getDatabase.collection(this.collection).doc(this.id).get();
703
- }
704
- async set(data) {
705
- validateDocumentData(data, "DocumentRef.set");
706
- if (!this.persistence) {
707
- const res = await new HttpsRequest({
708
- method: "POST" /* POST */,
709
- endpoint: `${this.app.getBaseUrl()}/db/create`,
710
- headers: { authorization: this.app.getConfig().token, "x-project": this.app.getConfig().project },
711
- body: { collection: this.collection, data: { ...data, id: this.id } }
712
- }).sendRequest();
713
- return res?.success ? DocumentSnapshot.fromMap(data) : null;
714
- }
715
- const updated = await this.persistence.upsertDoc({
716
- collection: this.collection,
717
- id: this.id,
718
- data: { ...data, id: this.id },
719
- exists: true,
720
- deleted: false,
721
- pending: 1,
722
- localOnly: false,
723
- status: "pending"
724
- });
725
- await this.persistence.enqueueMutation({
726
- mutationId: this.persistence.createMutationId(),
727
- collection: this.collection,
728
- documentId: this.id,
729
- type: "insert",
730
- // or "create" if you want to distinguish truly new docs
731
- payload: data
732
- });
733
- const snap = DocumentSnapshot.fromMap(updated.data);
734
- this.localStore?.emitDocument(this.collection, this.id, snap, "update");
735
- const currentCollection = await this.persistence.getCollectionSnapshots(this.collection);
736
- this.localStore?.emitCollection(this.collection, currentCollection, "update", this.id);
737
- this.syncEngine?.flush().catch(console.error);
738
- return snap;
408
+ return localSnap;
409
+ } catch (error) {
410
+ console.error("[EdmaxLabs] Cache read error:", error);
411
+ }
412
+ }
413
+ try {
414
+ const res = await new HttpsRequest({
415
+ method: "POST" /* POST */,
416
+ endpoint: `${this.app.getBaseUrl()}/db/read`,
417
+ headers: {
418
+ authorization: this.app.getConfig().token,
419
+ "x-project": this.app.getConfig().project
420
+ },
421
+ body: {
422
+ collection: this.collection,
423
+ id: this.id,
424
+ single: true
425
+ }
426
+ }).sendRequest();
427
+ if (!res?.success || !res.document)
428
+ return null;
429
+ return DocumentSnapshot.fromMap(res.document);
430
+ } catch (error) {
431
+ console.error(`[DocumentRef] get(${this.collection}/${this.id}) failed:`, error);
432
+ return null;
433
+ }
739
434
  }
435
+ // ====================== UPDATE ======================
740
436
  async update(data) {
741
- validateDocumentData(data, "DocumentRef.update");
742
- if (this.persistence) {
437
+ if (this._isUpdating) {
438
+ console.warn(`[DocumentRef] update recursion blocked on ${this.collection}/${this.id}`);
439
+ return null;
440
+ }
441
+ this._isUpdating = true;
442
+ try {
443
+ validateDocumentData(data, "DocumentRef.update");
444
+ if (!this.persistence) {
445
+ const res = await new HttpsRequest({
446
+ method: "POST" /* POST */,
447
+ endpoint: `${this.app.getBaseUrl()}/db/update`,
448
+ headers: {
449
+ authorization: this.app.getConfig().token,
450
+ "x-project": this.app.getConfig().project
451
+ },
452
+ body: { collection: this.collection, id: this.id, data }
453
+ }).sendRequest();
454
+ return res?.success ? DocumentSnapshot.fromMap({ ...data, id: this.id }) : null;
455
+ }
743
456
  const old = await this.persistence.getDoc(this.collection, this.id);
744
457
  if (!old || old.deleted)
745
458
  return null;
@@ -769,9 +482,49 @@ var DocumentRef = class {
769
482
  this.localStore?.notifyCollectionChanged(this.collection, this.id, "update");
770
483
  this.syncEngine?.flush().catch(console.error);
771
484
  return snap;
485
+ } finally {
486
+ this._isUpdating = false;
487
+ }
488
+ }
489
+ // ====================== SET ======================
490
+ async set(data) {
491
+ validateDocumentData(data, "DocumentRef.set");
492
+ if (!this.persistence) {
493
+ const res = await new HttpsRequest({
494
+ method: "POST" /* POST */,
495
+ endpoint: `${this.app.getBaseUrl()}/db/create`,
496
+ headers: {
497
+ authorization: this.app.getConfig().token,
498
+ "x-project": this.app.getConfig().project
499
+ },
500
+ body: { collection: this.collection, data: { ...data, id: this.id } }
501
+ }).sendRequest();
502
+ return res?.success ? DocumentSnapshot.fromMap({ ...data, id: this.id }) : null;
772
503
  }
773
- return await this.app.getDatabase.collection(this.collection).doc(this.id).update(data);
504
+ const updated = await this.persistence.upsertDoc({
505
+ collection: this.collection,
506
+ id: this.id,
507
+ data: { ...data, id: this.id },
508
+ exists: true,
509
+ deleted: false,
510
+ pending: 1,
511
+ localOnly: false,
512
+ status: "pending"
513
+ });
514
+ await this.persistence.enqueueMutation({
515
+ mutationId: this.persistence.createMutationId(),
516
+ collection: this.collection,
517
+ documentId: this.id,
518
+ type: "insert",
519
+ payload: data
520
+ });
521
+ const snap = DocumentSnapshot.fromMap(updated.data);
522
+ this.localStore?.emitDocument(this.collection, this.id, snap, "update");
523
+ this.localStore?.notifyCollectionChanged(this.collection, this.id, "update");
524
+ this.syncEngine?.flush().catch(console.error);
525
+ return snap;
774
526
  }
527
+ // ====================== DELETE ======================
775
528
  async delete() {
776
529
  if (this.persistence) {
777
530
  await this.persistence.markDeleted(this.collection, this.id, 1);
@@ -787,19 +540,22 @@ var DocumentRef = class {
787
540
  this.syncEngine?.flush().catch(console.error);
788
541
  return true;
789
542
  }
790
- return await this.app.getDatabase.collection(this.collection).doc(this.id).delete();
543
+ const res = await new HttpsRequest({
544
+ method: "POST" /* POST */,
545
+ endpoint: `${this.app.getBaseUrl()}/db/delete`,
546
+ headers: {
547
+ authorization: this.app.getConfig().token,
548
+ "x-project": this.app.getConfig().project
549
+ },
550
+ body: { collection: this.collection, id: this.id }
551
+ }).sendRequest();
552
+ return !!res?.success;
791
553
  }
792
- // array(key: string): Array {
793
- // return new Array(this.app, this.collection, key, this.id);
794
- // }
554
+ // ====================== SNAPSHOT ======================
795
555
  onSnapshot(callback) {
796
- if (this.app.offline().persistence && this.app.offline().localStore && this.app.offline().realtimeBridge) {
556
+ if (this.persistence && this.localStore && this.app.offline().realtimeBridge) {
797
557
  this.app.offline().realtimeBridge?.watchDocument(this.collection, this.id);
798
- return this.app.offline().localStore?.subscribeToDocument(
799
- this.collection,
800
- this.id,
801
- callback
802
- ) || (() => {
558
+ return this.localStore.subscribeToDocument(this.collection, this.id, callback) || (() => {
803
559
  });
804
560
  }
805
561
  return this.app.rtdb().subscribeToDocumentRaw(this.collection, this.id, (snapshot, change) => {
@@ -1027,15 +783,42 @@ var CollectionRef = class {
1027
783
  }
1028
784
  return local;
1029
785
  }
1030
- ;
1031
- return await this.app.getDatabase.collection(this.collection).get();
786
+ try {
787
+ const res = await new HttpsRequest({
788
+ method: "POST" /* POST */,
789
+ endpoint: `${this.app.getBaseUrl()}/db/read`,
790
+ headers: {
791
+ authorization: this.app.getConfig().token,
792
+ "x-project": this.app.getConfig().project
793
+ },
794
+ body: {
795
+ collection: this.collection,
796
+ filter: {}
797
+ }
798
+ }).sendRequest();
799
+ if (!res?.success || !Array.isArray(res.documents)) {
800
+ return [];
801
+ }
802
+ return res.documents.map((d) => {
803
+ delete d.token;
804
+ d.id = d.id ?? d._id;
805
+ delete d._id;
806
+ return DocumentSnapshot.fromMap(d);
807
+ });
808
+ } catch (error) {
809
+ console.error("[EdmaxLabs] Collection get failed:", error);
810
+ return [];
811
+ }
1032
812
  }
1033
813
  async refreshFromRemote() {
1034
814
  try {
1035
815
  const res = await new HttpsRequest({
1036
816
  method: "POST" /* POST */,
1037
817
  endpoint: `${this.app.getBaseUrl()}/db/read`,
1038
- headers: { authorization: this.app.getConfig().token, "x-project": this.app.getConfig().project },
818
+ headers: {
819
+ authorization: this.app.getConfig().token,
820
+ "x-project": this.app.getConfig().project
821
+ },
1039
822
  body: {
1040
823
  collection: this.collection,
1041
824
  filter: {}
@@ -1061,7 +844,8 @@ var CollectionRef = class {
1061
844
  delete d._id;
1062
845
  return DocumentSnapshot.fromMap(d);
1063
846
  });
1064
- } catch {
847
+ } catch (error) {
848
+ console.error("[EdmaxLabs] refreshFromRemote failed:", error);
1065
849
  return [];
1066
850
  }
1067
851
  }
@@ -1091,13 +875,24 @@ var CollectionRef = class {
1091
875
  this.syncEngine?.flush().catch(console.error);
1092
876
  return snap;
1093
877
  }
1094
- ;
1095
- return await this.app.getDatabase.collection(this.collection).add(data);
878
+ const res = await new HttpsRequest({
879
+ method: "POST" /* POST */,
880
+ endpoint: `${this.app.getBaseUrl()}/db/create`,
881
+ headers: {
882
+ authorization: this.app.getConfig().token,
883
+ "x-project": this.app.getConfig().project
884
+ },
885
+ body: { collection: this.collection, data: { ...data, id: "" } }
886
+ // server will generate id
887
+ }).sendRequest();
888
+ if (!res?.success || !res.document)
889
+ return null;
890
+ return DocumentSnapshot.fromMap(res.document);
1096
891
  }
1097
892
  onSnapshot(callback) {
1098
- if (this.app.offline().persistence) {
893
+ if (this.persistence && this.localStore && this.app.offline().realtimeBridge) {
1099
894
  this.app.offline().realtimeBridge?.watchCollection(this.collection);
1100
- return this.app.offline().localStore?.subscribeToCollection(this.collection, callback) ?? (() => {
895
+ return this.localStore.subscribeToCollection(this.collection, callback) ?? (() => {
1101
896
  });
1102
897
  }
1103
898
  return this.app.rtdb().subscribeToCollectionRaw(this.collection, (payload, changes) => {
@@ -2690,6 +2485,164 @@ var _EdmaxLabs = class _EdmaxLabs {
2690
2485
  _EdmaxLabs.instance = null;
2691
2486
  var EdmaxLabs = _EdmaxLabs;
2692
2487
 
2488
+ // src/database/Timestamp.ts
2489
+ var Timestamp = class _Timestamp {
2490
+ constructor(seconds, nanoseconds = 0) {
2491
+ this.seconds = seconds;
2492
+ this.nanoseconds = nanoseconds;
2493
+ }
2494
+ static now() {
2495
+ return _Timestamp.fromMillis(Date.now());
2496
+ }
2497
+ static fromDate(date) {
2498
+ return new _Timestamp(
2499
+ Math.floor(date.getTime() / 1e3),
2500
+ date.getTime() % 1e3 * 1e6
2501
+ );
2502
+ }
2503
+ static fromMillis(ms) {
2504
+ return new _Timestamp(Math.floor(ms / 1e3), ms % 1e3 * 1e6);
2505
+ }
2506
+ static fromMongo(dateObj) {
2507
+ return _Timestamp.fromMillis(dateObj.getTime());
2508
+ }
2509
+ static fromJSON(obj) {
2510
+ return new _Timestamp(obj.seconds, obj.nanoseconds);
2511
+ }
2512
+ toDate() {
2513
+ return new Date(this.toMillis());
2514
+ }
2515
+ toMillis() {
2516
+ return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
2517
+ }
2518
+ sinceEpoch() {
2519
+ return this.seconds;
2520
+ }
2521
+ toMongo() {
2522
+ return new Date(this.toMillis());
2523
+ }
2524
+ format(pattern = "dd-MM-yyyy HH:mm:ss") {
2525
+ const d = this.toDate();
2526
+ const monthsShort = [
2527
+ "Jan",
2528
+ "Feb",
2529
+ "Mar",
2530
+ "Apr",
2531
+ "May",
2532
+ "Jun",
2533
+ "Jul",
2534
+ "Aug",
2535
+ "Sep",
2536
+ "Oct",
2537
+ "Nov",
2538
+ "Dec"
2539
+ ];
2540
+ const monthsLong = [
2541
+ "January",
2542
+ "February",
2543
+ "March",
2544
+ "April",
2545
+ "May",
2546
+ "June",
2547
+ "July",
2548
+ "August",
2549
+ "September",
2550
+ "October",
2551
+ "November",
2552
+ "December"
2553
+ ];
2554
+ const hours = d.getHours();
2555
+ const hours12 = hours % 12 === 0 ? 12 : hours % 12;
2556
+ const ampm = hours < 12 ? "AM" : "PM";
2557
+ const map = {
2558
+ dd: String(d.getDate()).padStart(2, "0"),
2559
+ MM: String(d.getMonth() + 1).padStart(2, "0"),
2560
+ yyyy: d.getFullYear(),
2561
+ HH: String(d.getHours()).padStart(2, "0"),
2562
+ mm: String(d.getMinutes()).padStart(2, "0"),
2563
+ ss: String(d.getSeconds()).padStart(2, "0"),
2564
+ SSS: String(d.getMilliseconds()).padStart(3, "0"),
2565
+ MMM: monthsShort[d.getMonth()],
2566
+ MMMM: monthsLong[d.getMonth()],
2567
+ a: ampm.toLowerCase(),
2568
+ // am/pm
2569
+ A: ampm
2570
+ // AM/PM
2571
+ };
2572
+ return pattern.replace(
2573
+ /MMMM|MMM|dd|MM|yyyy|HH|mm|ss|SSS|a|A/g,
2574
+ (match) => String(map[match])
2575
+ );
2576
+ }
2577
+ compare(other) {
2578
+ if (this.seconds !== other.seconds) {
2579
+ return this.seconds - other.seconds;
2580
+ }
2581
+ return this.nanoseconds - other.nanoseconds;
2582
+ }
2583
+ relative(fmt = "dd-MM-yyyy HH:mm:ss") {
2584
+ const now = Date.now();
2585
+ const diff = now - this.toMillis();
2586
+ const sec = Math.floor(diff / 1e3);
2587
+ if (sec < 60)
2588
+ return `${sec}s ago`;
2589
+ const min = Math.floor(sec / 60);
2590
+ if (min < 60)
2591
+ return `${min}m ago`;
2592
+ const hrs = Math.floor(min / 60);
2593
+ if (hrs < 24)
2594
+ return `${hrs}h ago`;
2595
+ const days = Math.floor(hrs / 24);
2596
+ if (days < 3)
2597
+ return `${days}d ago`;
2598
+ return this.format(fmt);
2599
+ }
2600
+ add({
2601
+ seconds = 0,
2602
+ minutes = 0,
2603
+ hours = 0,
2604
+ days = 0
2605
+ }) {
2606
+ const totalMs = seconds * 1e3 + minutes * 6e4 + hours * 36e5 + days * 864e5;
2607
+ return _Timestamp.fromMillis(this.toMillis() + totalMs);
2608
+ }
2609
+ weekdayName() {
2610
+ return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][this.toDate().getDay()];
2611
+ }
2612
+ toJSON() {
2613
+ return {
2614
+ _type: "timestamp",
2615
+ seconds: this.seconds,
2616
+ nanoseconds: this.nanoseconds
2617
+ };
2618
+ }
2619
+ toString() {
2620
+ return this.format("yyyy-MM-dd HH:mm:ss");
2621
+ }
2622
+ };
2623
+
2624
+ // src/database/ArraySnapshot.ts
2625
+ var ArraySnapshot = class _ArraySnapshot {
2626
+ constructor(id = "", index, data, dt) {
2627
+ this.data = data;
2628
+ this.id = id;
2629
+ this.dt = dt ?? Timestamp.now();
2630
+ }
2631
+ static fromMap(map) {
2632
+ const index = map.index ?? map.index ?? -1;
2633
+ const id = map.id ?? map._id ?? "";
2634
+ const document2 = map.data ?? map.document ?? map;
2635
+ return new _ArraySnapshot(id, index, document2);
2636
+ }
2637
+ toMap() {
2638
+ return {
2639
+ id: this.id ?? "",
2640
+ data: this.data,
2641
+ dt: this.dt
2642
+ };
2643
+ }
2644
+ };
2645
+
2693
2646
  // src/index.ts
2694
2647
  var src_default = EdmaxLabs;
2695
2648
  export {