edmaxlabs-core 2.5.0 → 2.5.1

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.cjs CHANGED
@@ -32,12 +32,13 @@ module.exports = __toCommonJS(src_exports);
32
32
 
33
33
  // src/authentication/Credentials.ts
34
34
  var Credentials = class _Credentials {
35
- constructor(uid, displayInfo, createdAt, updatedAt, lastLogged) {
35
+ constructor(uid, displayInfo, createdAt, updatedAt, lastLogged, logged) {
36
36
  this.uid = uid;
37
37
  this.displayInfo = displayInfo;
38
38
  this.createdAt = createdAt;
39
39
  this.updatedAt = updatedAt;
40
40
  this.lastLogged = lastLogged;
41
+ this.logged = logged === true;
41
42
  }
42
43
  static fromMap(map) {
43
44
  const uid = map.id ?? map.uid ?? "";
@@ -46,12 +47,14 @@ var Credentials = class _Credentials {
46
47
  const createdAt = data.createdAt;
47
48
  const updatedAt = data.updatedAt;
48
49
  const lastLogged = data.lastLogged;
50
+ const logged = data.logged;
49
51
  const result = new _Credentials(
50
52
  uid,
51
53
  d_info,
52
54
  createdAt,
53
55
  updatedAt,
54
- lastLogged
56
+ lastLogged,
57
+ logged
55
58
  );
56
59
  return result;
57
60
  }
@@ -61,7 +64,8 @@ var Credentials = class _Credentials {
61
64
  displayInfo: this.displayInfo,
62
65
  createdAt: this.createdAt,
63
66
  updatedAt: this.updatedAt,
64
- lastLogged: this.lastLogged
67
+ lastLogged: this.lastLogged,
68
+ logged: this.logged
65
69
  };
66
70
  }
67
71
  };
@@ -129,91 +133,123 @@ var HttpsRequest = class {
129
133
 
130
134
  // src/authentication/Authentication.ts
131
135
  var _Authentication = class _Authentication {
132
- // Re-entrancy guard
133
136
  constructor() {
134
- this.eUser = null;
135
- this.client = null;
136
- this.app = null;
137
- // Dedicated lightweight instance for auth
138
137
  this.eventListeners = /* @__PURE__ */ new Map();
139
- this.isHandlingChange = false;
140
- // ====================== AUTH METHODS ======================
138
+ this.eventWaiters = /* @__PURE__ */ new Map();
139
+ this.unsubscribers = /* @__PURE__ */ new Set();
140
+ this.isSameCredentials = (a, b) => {
141
+ if (!a && !b)
142
+ return true;
143
+ if (!a || !b)
144
+ return false;
145
+ return JSON.stringify(a.toMap()) === JSON.stringify(b.toMap());
146
+ };
141
147
  this.createUserWithEmailAndPassword = async ({
142
148
  email,
143
149
  password
144
150
  }) => {
151
+ this.eUser = void 0;
145
152
  this.saveCredentials(null);
146
- const app = this.app.getDatabase;
147
- const existing = await app.collection("users").query.where({ key: "email", op: "===", value: email }).get();
148
- if (existing.length > 0) {
153
+ const app = this.app?.getDatabase;
154
+ const data = await app?.collection("users").query.where({
155
+ key: "email",
156
+ op: "===",
157
+ value: email
158
+ }).get();
159
+ if (data.length > 0) {
149
160
  throw new Error("Email Already In Use.");
150
161
  }
151
- const userRef = await app.collection("users").add({
162
+ const userRef = await app?.collection("users").add({
152
163
  email,
153
164
  password,
154
- token: this.client.getConfig().token,
155
165
  logged: true,
156
- lastLogged: Date.now()
166
+ lastLogged: Date.now(),
167
+ displayInfo: {
168
+ firstname: email.split("@")[0],
169
+ lastname: "",
170
+ surname: "",
171
+ profile: "./logo.png",
172
+ email,
173
+ phone: ""
174
+ }
157
175
  });
158
- if (!userRef?.id) {
159
- throw new Error("Something went wrong. Please try again.");
176
+ if (!userRef?.id || userRef?.id === "") {
177
+ throw new Error("Something went wrong try Again.");
160
178
  }
161
- const newCreds = Credentials.fromMap(userRef.toMap());
162
- this.saveCredentials(newCreds);
163
- return newCreds;
179
+ this.eUser = Credentials.fromMap(userRef);
180
+ this.saveCredentials(this.eUser);
181
+ return Credentials.fromMap(userRef.toMap());
164
182
  };
165
183
  this.signInWithEmailAndPassword = async ({
166
184
  email,
167
185
  password
168
186
  }) => {
169
- const app = this.app.getDatabase;
170
- const data = await app.collection("users").query.where({ key: "email", op: "===", value: email }).where({ key: "password", op: "===", value: password }).get();
171
- if (!data || data.length === 0) {
187
+ const app = this.app?.getDatabase;
188
+ const data = await app?.collection("users").query.where({
189
+ key: "email",
190
+ op: "===",
191
+ value: email
192
+ }).where({
193
+ key: "password",
194
+ op: "===",
195
+ value: password
196
+ }).get();
197
+ if (data === void 0) {
198
+ throw new Error("Auth Failed.");
199
+ }
200
+ if (data?.length === 0) {
172
201
  throw new Error("User Not Found.");
173
202
  }
174
- const userDoc = data[0];
175
- await app.collection("users").doc(userDoc.id).update({
203
+ const _data = data[0];
204
+ await app?.collection("users").doc(_data.id).update({
176
205
  logged: true,
177
206
  lastLogged: Date.now()
178
207
  });
179
- const signedInUser = Credentials.fromMap(userDoc.toMap());
180
- this.saveCredentials(signedInUser);
181
- return signedInUser;
182
- };
183
- this.signOut = async () => {
184
- const current = this.currentUser();
185
- if (!current)
186
- return;
187
- const app = this.app.getDatabase;
188
- await app.collection("users").doc(current.uid).update({
189
- logged: false,
190
- lastLogged: Date.now()
191
- });
192
- this.saveCredentials(null);
208
+ this.eUser = Credentials.fromMap(_data);
209
+ this.saveCredentials(this.eUser);
210
+ return Credentials.fromMap(_data);
193
211
  };
194
212
  this.deleteUser = async () => {
195
- if (!this.eUser)
213
+ if (!this.eUser) {
196
214
  throw new Error("No User Signed in");
197
- const app = this.app.getDatabase;
198
- await app.collection("users").doc(this.eUser.uid).delete();
215
+ }
216
+ const app = this.app?.getDatabase;
217
+ const userRef = await app?.collection("users").doc(this.eUser.uid).delete();
218
+ if (userRef) {
219
+ throw new Error("Something went wrong try Again.");
220
+ }
221
+ this.eUser = void 0;
222
+ this.saveCredentials(null);
223
+ };
224
+ this.signOut = async () => {
225
+ const app = this.app?.getDatabase;
226
+ const luid = this.currentUser();
227
+ this.eUser = void 0;
199
228
  this.saveCredentials(null);
229
+ if (luid)
230
+ await app?.collection("users").doc(luid?.uid).update({
231
+ logged: false,
232
+ lastLogged: Date.now()
233
+ });
234
+ return;
200
235
  };
201
- // Optional: Rules verification
202
236
  this.rules = async (path, context) => {
203
237
  const res = await new HttpsRequest({
204
238
  method: "POST" /* POST */,
205
- endpoint: `${this.client.getBaseUrl()}/auth/rules/verify`,
239
+ endpoint: this.client.getBaseUrl() + "/auth/rules/verify",
206
240
  headers: {
207
241
  authorization: this.client.getConfig().token,
208
242
  project: this.client.getConfig().project
209
243
  },
210
- body: { path, context }
244
+ body: {
245
+ path,
246
+ context
247
+ }
211
248
  }).sendRequest();
212
249
  return res;
213
250
  };
214
- if (_Authentication.instance) {
251
+ if (_Authentication.instance)
215
252
  return _Authentication.instance;
216
- }
217
253
  this.client = EdmaxLabs.instance;
218
254
  this.app = new EdmaxLabs({
219
255
  token: "auth",
@@ -222,16 +258,26 @@ var _Authentication = class _Authentication {
222
258
  _Authentication.instance = this;
223
259
  this.restoreSession();
224
260
  }
261
+ restoreSession() {
262
+ const saved = this.currentUser();
263
+ if (saved) {
264
+ this.eUser = saved;
265
+ this.emitValue("creds", saved);
266
+ }
267
+ }
268
+ // Better singleton
225
269
  static getInstance() {
226
270
  if (!_Authentication.instance) {
227
271
  _Authentication.instance = new _Authentication();
228
272
  }
229
273
  return _Authentication.instance;
230
274
  }
231
- // ====================== EVENT SYSTEM ======================
232
275
  emitValue(key, value) {
233
276
  const listeners = this.eventListeners.get(key) || [];
234
- [...listeners].forEach((listener) => listener(value));
277
+ listeners.forEach((l) => l(value));
278
+ const waiters = this.eventWaiters.get(key) || [];
279
+ waiters.forEach((resolve) => resolve(value));
280
+ this.eventWaiters.delete(key);
235
281
  }
236
282
  onValue(key, callback) {
237
283
  const listeners = this.eventListeners.get(key) || [];
@@ -242,7 +288,6 @@ var _Authentication = class _Authentication {
242
288
  this.eventListeners.set(key, filtered);
243
289
  };
244
290
  }
245
- // ====================== CREDENTIALS ======================
246
291
  currentUser() {
247
292
  const data = localStorage.getItem("eauth");
248
293
  if (!data)
@@ -265,71 +310,50 @@ var _Authentication = class _Authentication {
265
310
  this.eUser = credentials;
266
311
  this.emitValue("creds", credentials);
267
312
  }
268
- isSameCredentials(a, b) {
269
- if (!a && !b)
270
- return true;
271
- if (!a || !b)
272
- return false;
273
- return a.uid === b.uid && a.displayInfo.email === b.displayInfo.email && a.lastLogged === b.lastLogged;
274
- }
275
- restoreSession() {
276
- const saved = this.currentUser();
277
- if (saved) {
278
- this.eUser = saved;
279
- }
280
- }
281
- // ====================== MAIN AUTH STATE LISTENER ======================
282
- /**
283
- * Main method to listen to authentication state changes.
284
- * This listener stays alive for the entire app lifetime.
285
- */
313
+ // Main auth state listener - should be called ONCE at app root
286
314
  authState({
287
315
  onChange,
288
316
  onSignOut,
289
317
  onDeleted
290
318
  }) {
291
319
  let userDocUnsubscribe;
292
- let credsUnsub;
293
320
  const handleCredsChange = (creds) => {
294
- if (this.isHandlingChange)
295
- return;
296
- this.isHandlingChange = true;
297
- userDocUnsubscribe?.();
298
- userDocUnsubscribe = void 0;
321
+ if (userDocUnsubscribe) {
322
+ userDocUnsubscribe();
323
+ userDocUnsubscribe = void 0;
324
+ }
299
325
  if (!creds) {
300
326
  this.eUser = null;
301
327
  onSignOut?.();
302
- this.isHandlingChange = false;
303
328
  return;
304
329
  }
305
- const userRef = this.app.getDatabase.collection("users").doc(creds.uid);
330
+ const userRef = this.app?.getDatabase.collection("users").doc(creds.uid);
331
+ if (!userRef)
332
+ return;
306
333
  userDocUnsubscribe = userRef.onSnapshot(
307
334
  (snapshot, change) => {
335
+ console.log(snapshot);
308
336
  if (change === "delete") {
309
337
  this.saveCredentials(null);
310
338
  onSignOut?.();
311
339
  onDeleted?.();
312
- this.isHandlingChange = false;
313
340
  return;
314
341
  }
315
342
  if (change === "insert" || change === "update") {
316
- if (!snapshot?.data) {
317
- this.isHandlingChange = false;
343
+ if (!snapshot)
318
344
  return;
319
- }
320
345
  if (snapshot.data.logged === false || snapshot.data.logged === "false") {
321
346
  this.saveCredentials(null);
322
347
  onSignOut?.();
323
- this.isHandlingChange = false;
324
348
  return;
325
349
  }
326
350
  const newCreds = Credentials.fromMap(snapshot.toMap());
327
351
  if (!this.isSameCredentials(this.eUser, newCreds)) {
352
+ this.eUser = newCreds;
328
353
  this.saveCredentials(newCreds);
329
354
  onChange(newCreds);
330
355
  }
331
356
  }
332
- this.isHandlingChange = false;
333
357
  }
334
358
  );
335
359
  };
@@ -338,10 +362,10 @@ var _Authentication = class _Authentication {
338
362
  this.eUser = initialUser;
339
363
  onChange(initialUser);
340
364
  }
341
- credsUnsub = this.onValue("creds", handleCredsChange);
342
365
  handleCredsChange(initialUser);
366
+ const credsUnsub = this.onValue("creds", handleCredsChange);
343
367
  return () => {
344
- credsUnsub?.();
368
+ credsUnsub();
345
369
  userDocUnsubscribe?.();
346
370
  };
347
371
  }
@@ -368,308 +392,6 @@ var DocumentSnapshot = class _DocumentSnapshot {
368
392
  }
369
393
  };
370
394
 
371
- // src/database/Timestamp.ts
372
- var Timestamp = class _Timestamp {
373
- constructor(seconds, nanoseconds = 0) {
374
- this.seconds = seconds;
375
- this.nanoseconds = nanoseconds;
376
- }
377
- static now() {
378
- return _Timestamp.fromMillis(Date.now());
379
- }
380
- static fromDate(date) {
381
- return new _Timestamp(
382
- Math.floor(date.getTime() / 1e3),
383
- date.getTime() % 1e3 * 1e6
384
- );
385
- }
386
- static fromMillis(ms) {
387
- return new _Timestamp(Math.floor(ms / 1e3), ms % 1e3 * 1e6);
388
- }
389
- static fromMongo(dateObj) {
390
- return _Timestamp.fromMillis(dateObj.getTime());
391
- }
392
- static fromJSON(obj) {
393
- return new _Timestamp(obj.seconds, obj.nanoseconds);
394
- }
395
- toDate() {
396
- return new Date(this.toMillis());
397
- }
398
- toMillis() {
399
- return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
400
- }
401
- sinceEpoch() {
402
- return this.seconds;
403
- }
404
- toMongo() {
405
- return new Date(this.toMillis());
406
- }
407
- format(pattern = "dd-MM-yyyy HH:mm:ss") {
408
- const d = this.toDate();
409
- const monthsShort = [
410
- "Jan",
411
- "Feb",
412
- "Mar",
413
- "Apr",
414
- "May",
415
- "Jun",
416
- "Jul",
417
- "Aug",
418
- "Sep",
419
- "Oct",
420
- "Nov",
421
- "Dec"
422
- ];
423
- const monthsLong = [
424
- "January",
425
- "February",
426
- "March",
427
- "April",
428
- "May",
429
- "June",
430
- "July",
431
- "August",
432
- "September",
433
- "October",
434
- "November",
435
- "December"
436
- ];
437
- const hours = d.getHours();
438
- const hours12 = hours % 12 === 0 ? 12 : hours % 12;
439
- const ampm = hours < 12 ? "AM" : "PM";
440
- const map = {
441
- dd: String(d.getDate()).padStart(2, "0"),
442
- MM: String(d.getMonth() + 1).padStart(2, "0"),
443
- yyyy: d.getFullYear(),
444
- HH: String(d.getHours()).padStart(2, "0"),
445
- mm: String(d.getMinutes()).padStart(2, "0"),
446
- ss: String(d.getSeconds()).padStart(2, "0"),
447
- SSS: String(d.getMilliseconds()).padStart(3, "0"),
448
- MMM: monthsShort[d.getMonth()],
449
- MMMM: monthsLong[d.getMonth()],
450
- a: ampm.toLowerCase(),
451
- // am/pm
452
- A: ampm
453
- // AM/PM
454
- };
455
- return pattern.replace(
456
- /MMMM|MMM|dd|MM|yyyy|HH|mm|ss|SSS|a|A/g,
457
- (match) => String(map[match])
458
- );
459
- }
460
- compare(other) {
461
- if (this.seconds !== other.seconds) {
462
- return this.seconds - other.seconds;
463
- }
464
- return this.nanoseconds - other.nanoseconds;
465
- }
466
- relative(fmt = "dd-MM-yyyy HH:mm:ss") {
467
- const now = Date.now();
468
- const diff = now - this.toMillis();
469
- const sec = Math.floor(diff / 1e3);
470
- if (sec < 60)
471
- return `${sec}s ago`;
472
- const min = Math.floor(sec / 60);
473
- if (min < 60)
474
- return `${min}m ago`;
475
- const hrs = Math.floor(min / 60);
476
- if (hrs < 24)
477
- return `${hrs}h ago`;
478
- const days = Math.floor(hrs / 24);
479
- if (days < 3)
480
- return `${days}d ago`;
481
- return this.format(fmt);
482
- }
483
- add({
484
- seconds = 0,
485
- minutes = 0,
486
- hours = 0,
487
- days = 0
488
- }) {
489
- const totalMs = seconds * 1e3 + minutes * 6e4 + hours * 36e5 + days * 864e5;
490
- return _Timestamp.fromMillis(this.toMillis() + totalMs);
491
- }
492
- weekdayName() {
493
- return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][this.toDate().getDay()];
494
- }
495
- toJSON() {
496
- return {
497
- _type: "timestamp",
498
- seconds: this.seconds,
499
- nanoseconds: this.nanoseconds
500
- };
501
- }
502
- toString() {
503
- return this.format("yyyy-MM-dd HH:mm:ss");
504
- }
505
- };
506
-
507
- // src/database/ArraySnapshot.ts
508
- var ArraySnapshot = class _ArraySnapshot {
509
- constructor(id = "", index, data, dt) {
510
- this.data = data;
511
- this.id = id;
512
- this.dt = dt ?? Timestamp.now();
513
- }
514
- static fromMap(map) {
515
- const index = map.index ?? map.index ?? -1;
516
- const id = map.id ?? map._id ?? "";
517
- const document2 = map.data ?? map.document ?? map;
518
- return new _ArraySnapshot(id, index, document2);
519
- }
520
- toMap() {
521
- return {
522
- id: this.id ?? "",
523
- data: this.data,
524
- dt: this.dt
525
- };
526
- }
527
- };
528
-
529
- // src/database/Array.ts
530
- var Array2 = class {
531
- constructor(app, collection, key, id) {
532
- this.app = app;
533
- this.collection = collection;
534
- this.key = key;
535
- this.docID = id;
536
- this.persistence = app.offline().persistence;
537
- this.syncEngine = app.offline().syncEngine;
538
- this.localStore = app.offline().localStore;
539
- }
540
- /**
541
- * Get current array elements (offline-first: prefers local cache)
542
- */
543
- async show() {
544
- if (this.persistence) {
545
- const doc = await this.persistence.getDoc(this.collection, this.docID);
546
- if (doc?.exists && !doc.deleted) {
547
- const arrayData = doc.data[this.key] || [];
548
- return arrayData.map((item) => ArraySnapshot.fromMap(item));
549
- }
550
- }
551
- if (typeof navigator === "undefined" || navigator.onLine) {
552
- this.refreshFromRemote().catch(() => {
553
- });
554
- }
555
- return [];
556
- }
557
- async refreshFromRemote() {
558
- try {
559
- const res = await new HttpsRequest({
560
- method: "POST" /* POST */,
561
- endpoint: `${this.app.getBaseUrl()}/db/array/show`,
562
- headers: { authorization: this.app.getConfig().token, "x-project": this.app.getConfig().project },
563
- body: {
564
- collection: this.collection,
565
- document: this.docID,
566
- array: this.key
567
- }
568
- }).sendRequest();
569
- if (!res?.success || !globalThis.Array.isArray(res.documents)) {
570
- return [];
571
- }
572
- const snapshots = res.documents.filter((d) => d != null).map((d) => ArraySnapshot.fromMap(d));
573
- if (this.persistence) {
574
- const currentDoc = await this.persistence.getDoc(this.collection, this.docID);
575
- if (currentDoc) {
576
- await this.persistence.upsertDoc({
577
- ...currentDoc,
578
- data: { ...currentDoc.data, [this.key]: res.documents },
579
- pending: 0,
580
- status: "synced",
581
- lastSyncedAt: Date.now()
582
- });
583
- }
584
- }
585
- return snapshots;
586
- } catch {
587
- return [];
588
- }
589
- }
590
- async push(data, id) {
591
- const payload = {
592
- data,
593
- id: id || this.persistence?.createLocalId?.() || void 0,
594
- dt: Timestamp.now()
595
- };
596
- if (this.persistence) {
597
- await this.persistence.enqueueMutation({
598
- mutationId: this.persistence.createMutationId(),
599
- collection: this.collection,
600
- documentId: this.docID,
601
- type: "array_push",
602
- // custom type
603
- payload: { arrayKey: this.key, ...payload }
604
- });
605
- this.syncEngine?.flush().catch(console.error);
606
- const doc = await this.persistence.getDoc(this.collection, this.docID);
607
- if (doc) {
608
- const updatedArray = [...doc.data[this.key] || [], payload];
609
- const snap = ArraySnapshot.fromMap(payload);
610
- this.localStore?.emitDocument(
611
- this.collection,
612
- this.docID,
613
- DocumentSnapshot.fromMap({ ...doc.data, [this.key]: updatedArray }),
614
- "update"
615
- );
616
- }
617
- return ArraySnapshot.fromMap(payload);
618
- }
619
- const res = await new HttpsRequest({
620
- method: "POST" /* POST */,
621
- endpoint: `${this.app.getBaseUrl()}/db/array/push`,
622
- headers: { authorization: this.app.getConfig().token, "x-project": this.app.getConfig().project },
623
- body: {
624
- collection: this.collection,
625
- document: this.docID,
626
- array: this.key,
627
- ...payload
628
- }
629
- }).sendRequest();
630
- return res?.success ? ArraySnapshot.fromMap(res.document) : null;
631
- }
632
- // Similar pattern for update, insert, remove, get...
633
- // (I'll show one more as example; apply the same logic to the rest)
634
- async update(position, data, id) {
635
- if (this.persistence) {
636
- await this.persistence.enqueueMutation({
637
- mutationId: this.persistence.createMutationId(),
638
- collection: this.collection,
639
- documentId: this.docID,
640
- type: "array_update",
641
- payload: { arrayKey: this.key, position, data, id }
642
- });
643
- this.syncEngine?.flush().catch(console.error);
644
- return ArraySnapshot.fromMap({ ...data, id });
645
- }
646
- const res = await new HttpsRequest({
647
- method: "POST" /* POST */,
648
- endpoint: `${this.app.getBaseUrl()}/db/array/update`,
649
- headers: { authorization: this.app.getConfig().token, "x-project": this.app.getConfig().project },
650
- body: {
651
- collection: this.collection,
652
- document: this.docID,
653
- array: this.key,
654
- position,
655
- data,
656
- id
657
- }
658
- }).sendRequest();
659
- return res?.success ? ArraySnapshot.fromMap(res.document) : null;
660
- }
661
- // TODO: Implement insert(), get(), remove() using the exact same offline pattern as push/update
662
- async insert(position, data, id) {
663
- return null;
664
- }
665
- async get(position) {
666
- return null;
667
- }
668
- async remove(position) {
669
- return null;
670
- }
671
- };
672
-
673
395
  // src/utils/documentNomalizer.ts
674
396
  function normalizePayload(payload) {
675
397
  const raw = payload?.document ?? payload?.data ?? payload;
@@ -689,11 +411,8 @@ function validateDocumentData(data, operation) {
689
411
  if (data === null || data === void 0) {
690
412
  throw new Error(`${operation}: data cannot be null or undefined`);
691
413
  }
692
- if (typeof data !== "object") {
693
- throw new Error(`${operation}: data must be an object`);
694
- }
695
- if (data instanceof Array2) {
696
- throw new Error(`${operation}: data cannot be an array`);
414
+ if (typeof data !== "object" || Array.isArray(data)) {
415
+ throw new Error(`${operation}: data must be a plain object`);
697
416
  }
698
417
  const reservedFields = ["id", "_id", "_createdAt", "_updatedAt", "_deleted"];
699
418
  for (const field of reservedFields) {
@@ -701,18 +420,18 @@ function validateDocumentData(data, operation) {
701
420
  throw new Error(`${operation}: '${field}' is a reserved field and cannot be set manually`);
702
421
  }
703
422
  }
704
- const dataSize = JSON.stringify(data).length;
705
- if (dataSize > 1024 * 1024) {
706
- throw new Error(`${operation}: document size (${Math.round(dataSize / 1024)}KB) exceeds maximum allowed size (1MB)`);
707
- }
708
423
  try {
709
- JSON.stringify(data);
424
+ const size = JSON.stringify(data).length;
425
+ if (size > 1024 * 1024) {
426
+ throw new Error(`${operation}: document too large (max 1MB)`);
427
+ }
710
428
  } catch {
711
429
  throw new Error(`${operation}: data contains circular references`);
712
430
  }
713
431
  }
714
432
  var DocumentRef = class {
715
433
  constructor(app, collection, id) {
434
+ this._isUpdating = false;
716
435
  this.app = app;
717
436
  this.collection = collection;
718
437
  this.id = id;
@@ -720,58 +439,60 @@ var DocumentRef = class {
720
439
  this.syncEngine = app.offline().syncEngine;
721
440
  this.localStore = app.offline().localStore;
722
441
  }
442
+ // ====================== GET ======================
723
443
  async get() {
724
444
  if (this.persistence) {
725
445
  try {
726
446
  const localSnap = await this.persistence.getDocSnapshot(this.collection, this.id);
727
447
  if (localSnap)
728
- return localSnap;
729
- return null;
730
- } catch (error) {
731
- console.error("[EdmaxLabs] Error reading from cache:", error);
732
- }
733
- }
734
- return await this.app.getDatabase.collection(this.collection).doc(this.id).get();
735
- }
736
- async set(data) {
737
- validateDocumentData(data, "DocumentRef.set");
738
- if (!this.persistence) {
739
- const res = await new HttpsRequest({
740
- method: "POST" /* POST */,
741
- endpoint: `${this.app.getBaseUrl()}/db/create`,
742
- headers: { authorization: this.app.getConfig().token, "x-project": this.app.getConfig().project },
743
- body: { collection: this.collection, data: { ...data, id: this.id } }
744
- }).sendRequest();
745
- return res?.success ? DocumentSnapshot.fromMap(data) : null;
746
- }
747
- const updated = await this.persistence.upsertDoc({
748
- collection: this.collection,
749
- id: this.id,
750
- data: { ...data, id: this.id },
751
- exists: true,
752
- deleted: false,
753
- pending: 1,
754
- localOnly: false,
755
- status: "pending"
756
- });
757
- await this.persistence.enqueueMutation({
758
- mutationId: this.persistence.createMutationId(),
759
- collection: this.collection,
760
- documentId: this.id,
761
- type: "insert",
762
- // or "create" if you want to distinguish truly new docs
763
- payload: data
764
- });
765
- const snap = DocumentSnapshot.fromMap(updated.data);
766
- this.localStore?.emitDocument(this.collection, this.id, snap, "update");
767
- const currentCollection = await this.persistence.getCollectionSnapshots(this.collection);
768
- this.localStore?.emitCollection(this.collection, currentCollection, "update", this.id);
769
- this.syncEngine?.flush().catch(console.error);
770
- return snap;
448
+ return localSnap;
449
+ } catch (error) {
450
+ console.error("[EdmaxLabs] Cache read error:", error);
451
+ }
452
+ }
453
+ try {
454
+ const res = await new HttpsRequest({
455
+ method: "POST" /* POST */,
456
+ endpoint: `${this.app.getBaseUrl()}/db/read`,
457
+ headers: {
458
+ authorization: this.app.getConfig().token,
459
+ "x-project": this.app.getConfig().project
460
+ },
461
+ body: {
462
+ collection: this.collection,
463
+ id: this.id,
464
+ single: true
465
+ }
466
+ }).sendRequest();
467
+ if (!res?.success || !res.document)
468
+ return null;
469
+ return DocumentSnapshot.fromMap(res.document);
470
+ } catch (error) {
471
+ console.error(`[DocumentRef] get(${this.collection}/${this.id}) failed:`, error);
472
+ return null;
473
+ }
771
474
  }
475
+ // ====================== UPDATE ======================
772
476
  async update(data) {
773
- validateDocumentData(data, "DocumentRef.update");
774
- if (this.persistence) {
477
+ if (this._isUpdating) {
478
+ console.warn(`[DocumentRef] update recursion blocked on ${this.collection}/${this.id}`);
479
+ return null;
480
+ }
481
+ this._isUpdating = true;
482
+ try {
483
+ validateDocumentData(data, "DocumentRef.update");
484
+ if (!this.persistence) {
485
+ const res = await new HttpsRequest({
486
+ method: "POST" /* POST */,
487
+ endpoint: `${this.app.getBaseUrl()}/db/update`,
488
+ headers: {
489
+ authorization: this.app.getConfig().token,
490
+ "x-project": this.app.getConfig().project
491
+ },
492
+ body: { collection: this.collection, id: this.id, data }
493
+ }).sendRequest();
494
+ return res?.success ? DocumentSnapshot.fromMap({ ...data, id: this.id }) : null;
495
+ }
775
496
  const old = await this.persistence.getDoc(this.collection, this.id);
776
497
  if (!old || old.deleted)
777
498
  return null;
@@ -801,9 +522,49 @@ var DocumentRef = class {
801
522
  this.localStore?.notifyCollectionChanged(this.collection, this.id, "update");
802
523
  this.syncEngine?.flush().catch(console.error);
803
524
  return snap;
525
+ } finally {
526
+ this._isUpdating = false;
527
+ }
528
+ }
529
+ // ====================== SET ======================
530
+ async set(data) {
531
+ validateDocumentData(data, "DocumentRef.set");
532
+ if (!this.persistence) {
533
+ const res = await new HttpsRequest({
534
+ method: "POST" /* POST */,
535
+ endpoint: `${this.app.getBaseUrl()}/db/create`,
536
+ headers: {
537
+ authorization: this.app.getConfig().token,
538
+ "x-project": this.app.getConfig().project
539
+ },
540
+ body: { collection: this.collection, data: { ...data, id: this.id } }
541
+ }).sendRequest();
542
+ return res?.success ? DocumentSnapshot.fromMap({ ...data, id: this.id }) : null;
804
543
  }
805
- return await this.app.getDatabase.collection(this.collection).doc(this.id).update(data);
544
+ const updated = await this.persistence.upsertDoc({
545
+ collection: this.collection,
546
+ id: this.id,
547
+ data: { ...data, id: this.id },
548
+ exists: true,
549
+ deleted: false,
550
+ pending: 1,
551
+ localOnly: false,
552
+ status: "pending"
553
+ });
554
+ await this.persistence.enqueueMutation({
555
+ mutationId: this.persistence.createMutationId(),
556
+ collection: this.collection,
557
+ documentId: this.id,
558
+ type: "insert",
559
+ payload: data
560
+ });
561
+ const snap = DocumentSnapshot.fromMap(updated.data);
562
+ this.localStore?.emitDocument(this.collection, this.id, snap, "update");
563
+ this.localStore?.notifyCollectionChanged(this.collection, this.id, "update");
564
+ this.syncEngine?.flush().catch(console.error);
565
+ return snap;
806
566
  }
567
+ // ====================== DELETE ======================
807
568
  async delete() {
808
569
  if (this.persistence) {
809
570
  await this.persistence.markDeleted(this.collection, this.id, 1);
@@ -819,19 +580,22 @@ var DocumentRef = class {
819
580
  this.syncEngine?.flush().catch(console.error);
820
581
  return true;
821
582
  }
822
- return await this.app.getDatabase.collection(this.collection).doc(this.id).delete();
583
+ const res = await new HttpsRequest({
584
+ method: "POST" /* POST */,
585
+ endpoint: `${this.app.getBaseUrl()}/db/delete`,
586
+ headers: {
587
+ authorization: this.app.getConfig().token,
588
+ "x-project": this.app.getConfig().project
589
+ },
590
+ body: { collection: this.collection, id: this.id }
591
+ }).sendRequest();
592
+ return !!res?.success;
823
593
  }
824
- // array(key: string): Array {
825
- // return new Array(this.app, this.collection, key, this.id);
826
- // }
594
+ // ====================== SNAPSHOT ======================
827
595
  onSnapshot(callback) {
828
- if (this.app.offline().persistence && this.app.offline().localStore && this.app.offline().realtimeBridge) {
596
+ if (this.persistence && this.localStore && this.app.offline().realtimeBridge) {
829
597
  this.app.offline().realtimeBridge?.watchDocument(this.collection, this.id);
830
- return this.app.offline().localStore?.subscribeToDocument(
831
- this.collection,
832
- this.id,
833
- callback
834
- ) || (() => {
598
+ return this.localStore.subscribeToDocument(this.collection, this.id, callback) || (() => {
835
599
  });
836
600
  }
837
601
  return this.app.rtdb().subscribeToDocumentRaw(this.collection, this.id, (snapshot, change) => {
@@ -1059,15 +823,42 @@ var CollectionRef = class {
1059
823
  }
1060
824
  return local;
1061
825
  }
1062
- ;
1063
- return await this.app.getDatabase.collection(this.collection).get();
826
+ try {
827
+ const res = await new HttpsRequest({
828
+ method: "POST" /* POST */,
829
+ endpoint: `${this.app.getBaseUrl()}/db/read`,
830
+ headers: {
831
+ authorization: this.app.getConfig().token,
832
+ "x-project": this.app.getConfig().project
833
+ },
834
+ body: {
835
+ collection: this.collection,
836
+ filter: {}
837
+ }
838
+ }).sendRequest();
839
+ if (!res?.success || !Array.isArray(res.documents)) {
840
+ return [];
841
+ }
842
+ return res.documents.map((d) => {
843
+ delete d.token;
844
+ d.id = d.id ?? d._id;
845
+ delete d._id;
846
+ return DocumentSnapshot.fromMap(d);
847
+ });
848
+ } catch (error) {
849
+ console.error("[EdmaxLabs] Collection get failed:", error);
850
+ return [];
851
+ }
1064
852
  }
1065
853
  async refreshFromRemote() {
1066
854
  try {
1067
855
  const res = await new HttpsRequest({
1068
856
  method: "POST" /* POST */,
1069
857
  endpoint: `${this.app.getBaseUrl()}/db/read`,
1070
- headers: { authorization: this.app.getConfig().token, "x-project": this.app.getConfig().project },
858
+ headers: {
859
+ authorization: this.app.getConfig().token,
860
+ "x-project": this.app.getConfig().project
861
+ },
1071
862
  body: {
1072
863
  collection: this.collection,
1073
864
  filter: {}
@@ -1093,7 +884,8 @@ var CollectionRef = class {
1093
884
  delete d._id;
1094
885
  return DocumentSnapshot.fromMap(d);
1095
886
  });
1096
- } catch {
887
+ } catch (error) {
888
+ console.error("[EdmaxLabs] refreshFromRemote failed:", error);
1097
889
  return [];
1098
890
  }
1099
891
  }
@@ -1123,13 +915,24 @@ var CollectionRef = class {
1123
915
  this.syncEngine?.flush().catch(console.error);
1124
916
  return snap;
1125
917
  }
1126
- ;
1127
- return await this.app.getDatabase.collection(this.collection).add(data);
918
+ const res = await new HttpsRequest({
919
+ method: "POST" /* POST */,
920
+ endpoint: `${this.app.getBaseUrl()}/db/create`,
921
+ headers: {
922
+ authorization: this.app.getConfig().token,
923
+ "x-project": this.app.getConfig().project
924
+ },
925
+ body: { collection: this.collection, data: { ...data, id: "" } }
926
+ // server will generate id
927
+ }).sendRequest();
928
+ if (!res?.success || !res.document)
929
+ return null;
930
+ return DocumentSnapshot.fromMap(res.document);
1128
931
  }
1129
932
  onSnapshot(callback) {
1130
- if (this.app.offline().persistence) {
933
+ if (this.persistence && this.localStore && this.app.offline().realtimeBridge) {
1131
934
  this.app.offline().realtimeBridge?.watchCollection(this.collection);
1132
- return this.app.offline().localStore?.subscribeToCollection(this.collection, callback) ?? (() => {
935
+ return this.localStore.subscribeToCollection(this.collection, callback) ?? (() => {
1133
936
  });
1134
937
  }
1135
938
  return this.app.rtdb().subscribeToCollectionRaw(this.collection, (payload, changes) => {
@@ -2722,6 +2525,164 @@ var _EdmaxLabs = class _EdmaxLabs {
2722
2525
  _EdmaxLabs.instance = null;
2723
2526
  var EdmaxLabs = _EdmaxLabs;
2724
2527
 
2528
+ // src/database/Timestamp.ts
2529
+ var Timestamp = class _Timestamp {
2530
+ constructor(seconds, nanoseconds = 0) {
2531
+ this.seconds = seconds;
2532
+ this.nanoseconds = nanoseconds;
2533
+ }
2534
+ static now() {
2535
+ return _Timestamp.fromMillis(Date.now());
2536
+ }
2537
+ static fromDate(date) {
2538
+ return new _Timestamp(
2539
+ Math.floor(date.getTime() / 1e3),
2540
+ date.getTime() % 1e3 * 1e6
2541
+ );
2542
+ }
2543
+ static fromMillis(ms) {
2544
+ return new _Timestamp(Math.floor(ms / 1e3), ms % 1e3 * 1e6);
2545
+ }
2546
+ static fromMongo(dateObj) {
2547
+ return _Timestamp.fromMillis(dateObj.getTime());
2548
+ }
2549
+ static fromJSON(obj) {
2550
+ return new _Timestamp(obj.seconds, obj.nanoseconds);
2551
+ }
2552
+ toDate() {
2553
+ return new Date(this.toMillis());
2554
+ }
2555
+ toMillis() {
2556
+ return this.seconds * 1e3 + Math.floor(this.nanoseconds / 1e6);
2557
+ }
2558
+ sinceEpoch() {
2559
+ return this.seconds;
2560
+ }
2561
+ toMongo() {
2562
+ return new Date(this.toMillis());
2563
+ }
2564
+ format(pattern = "dd-MM-yyyy HH:mm:ss") {
2565
+ const d = this.toDate();
2566
+ const monthsShort = [
2567
+ "Jan",
2568
+ "Feb",
2569
+ "Mar",
2570
+ "Apr",
2571
+ "May",
2572
+ "Jun",
2573
+ "Jul",
2574
+ "Aug",
2575
+ "Sep",
2576
+ "Oct",
2577
+ "Nov",
2578
+ "Dec"
2579
+ ];
2580
+ const monthsLong = [
2581
+ "January",
2582
+ "February",
2583
+ "March",
2584
+ "April",
2585
+ "May",
2586
+ "June",
2587
+ "July",
2588
+ "August",
2589
+ "September",
2590
+ "October",
2591
+ "November",
2592
+ "December"
2593
+ ];
2594
+ const hours = d.getHours();
2595
+ const hours12 = hours % 12 === 0 ? 12 : hours % 12;
2596
+ const ampm = hours < 12 ? "AM" : "PM";
2597
+ const map = {
2598
+ dd: String(d.getDate()).padStart(2, "0"),
2599
+ MM: String(d.getMonth() + 1).padStart(2, "0"),
2600
+ yyyy: d.getFullYear(),
2601
+ HH: String(d.getHours()).padStart(2, "0"),
2602
+ mm: String(d.getMinutes()).padStart(2, "0"),
2603
+ ss: String(d.getSeconds()).padStart(2, "0"),
2604
+ SSS: String(d.getMilliseconds()).padStart(3, "0"),
2605
+ MMM: monthsShort[d.getMonth()],
2606
+ MMMM: monthsLong[d.getMonth()],
2607
+ a: ampm.toLowerCase(),
2608
+ // am/pm
2609
+ A: ampm
2610
+ // AM/PM
2611
+ };
2612
+ return pattern.replace(
2613
+ /MMMM|MMM|dd|MM|yyyy|HH|mm|ss|SSS|a|A/g,
2614
+ (match) => String(map[match])
2615
+ );
2616
+ }
2617
+ compare(other) {
2618
+ if (this.seconds !== other.seconds) {
2619
+ return this.seconds - other.seconds;
2620
+ }
2621
+ return this.nanoseconds - other.nanoseconds;
2622
+ }
2623
+ relative(fmt = "dd-MM-yyyy HH:mm:ss") {
2624
+ const now = Date.now();
2625
+ const diff = now - this.toMillis();
2626
+ const sec = Math.floor(diff / 1e3);
2627
+ if (sec < 60)
2628
+ return `${sec}s ago`;
2629
+ const min = Math.floor(sec / 60);
2630
+ if (min < 60)
2631
+ return `${min}m ago`;
2632
+ const hrs = Math.floor(min / 60);
2633
+ if (hrs < 24)
2634
+ return `${hrs}h ago`;
2635
+ const days = Math.floor(hrs / 24);
2636
+ if (days < 3)
2637
+ return `${days}d ago`;
2638
+ return this.format(fmt);
2639
+ }
2640
+ add({
2641
+ seconds = 0,
2642
+ minutes = 0,
2643
+ hours = 0,
2644
+ days = 0
2645
+ }) {
2646
+ const totalMs = seconds * 1e3 + minutes * 6e4 + hours * 36e5 + days * 864e5;
2647
+ return _Timestamp.fromMillis(this.toMillis() + totalMs);
2648
+ }
2649
+ weekdayName() {
2650
+ return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][this.toDate().getDay()];
2651
+ }
2652
+ toJSON() {
2653
+ return {
2654
+ _type: "timestamp",
2655
+ seconds: this.seconds,
2656
+ nanoseconds: this.nanoseconds
2657
+ };
2658
+ }
2659
+ toString() {
2660
+ return this.format("yyyy-MM-dd HH:mm:ss");
2661
+ }
2662
+ };
2663
+
2664
+ // src/database/ArraySnapshot.ts
2665
+ var ArraySnapshot = class _ArraySnapshot {
2666
+ constructor(id = "", index, data, dt) {
2667
+ this.data = data;
2668
+ this.id = id;
2669
+ this.dt = dt ?? Timestamp.now();
2670
+ }
2671
+ static fromMap(map) {
2672
+ const index = map.index ?? map.index ?? -1;
2673
+ const id = map.id ?? map._id ?? "";
2674
+ const document2 = map.data ?? map.document ?? map;
2675
+ return new _ArraySnapshot(id, index, document2);
2676
+ }
2677
+ toMap() {
2678
+ return {
2679
+ id: this.id ?? "",
2680
+ data: this.data,
2681
+ dt: this.dt
2682
+ };
2683
+ }
2684
+ };
2685
+
2725
2686
  // src/index.ts
2726
2687
  var src_default = EdmaxLabs;
2727
2688
  // Annotate the CommonJS export names for ESM import in node: