dolphin-client 1.1.2 → 1.1.4

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.js CHANGED
@@ -428,20 +428,178 @@ var AuthHandler = class {
428
428
  };
429
429
 
430
430
  // src/store.ts
431
+ var DataEngine = class {
432
+ _src = [];
433
+ _filtered = null;
434
+ _filters = /* @__PURE__ */ new Map();
435
+ _sortFn = null;
436
+ _version = 0;
437
+ constructor(initialData = []) {
438
+ this._src = [...initialData];
439
+ }
440
+ _invalidate() {
441
+ this._filtered = null;
442
+ this._version++;
443
+ }
444
+ getVersion() {
445
+ return this._version;
446
+ }
447
+ // ── Filters ──────────────────────────────────────────────
448
+ /** Text search across fields (case-insensitive) */
449
+ search(term, fields = []) {
450
+ if (!term || !term.trim()) {
451
+ this._filters.delete("__search__");
452
+ } else {
453
+ const t = term.trim().toLowerCase();
454
+ this._filters.set("__search__", (item) => {
455
+ const keys = fields.length ? fields : Object.keys(item);
456
+ return keys.some((k) => String(item[k] ?? "").toLowerCase().includes(t));
457
+ });
458
+ }
459
+ this._invalidate();
460
+ return this;
461
+ }
462
+ /** Exact value filter on a field */
463
+ filter(field, value) {
464
+ const key = `__filter_${String(field)}__`;
465
+ if (value === void 0 || value === null || value === "") {
466
+ this._filters.delete(key);
467
+ } else {
468
+ this._filters.set(key, (item) => item[field] === value);
469
+ }
470
+ this._invalidate();
471
+ return this;
472
+ }
473
+ /** Numeric range filter */
474
+ range(field, min, max) {
475
+ const key = `__range_${String(field)}__`;
476
+ this._filters.set(key, (item) => {
477
+ const v = Number(item[field]);
478
+ return !isNaN(v) && v >= min && v <= max;
479
+ });
480
+ this._invalidate();
481
+ return this;
482
+ }
483
+ /** Sort by field ascending or descending */
484
+ sort(field, asc = true) {
485
+ this._sortFn = (a, b) => {
486
+ const va = a[field], vb = b[field];
487
+ if (va == null && vb == null) return 0;
488
+ if (va == null) return asc ? 1 : -1;
489
+ if (vb == null) return asc ? -1 : 1;
490
+ if (typeof va === "number" && typeof vb === "number") {
491
+ return asc ? va - vb : vb - va;
492
+ }
493
+ return String(va).localeCompare(String(vb)) * (asc ? 1 : -1);
494
+ };
495
+ this._invalidate();
496
+ return this;
497
+ }
498
+ /** Clear all filters and sort */
499
+ clearFilters() {
500
+ this._filters.clear();
501
+ this._sortFn = null;
502
+ this._invalidate();
503
+ return this;
504
+ }
505
+ // ── Data Access ──────────────────────────────────────────
506
+ /** Get filtered + sorted results (lazy, cached) */
507
+ get() {
508
+ if (this._filtered !== null) return this._filtered;
509
+ let result = this._src;
510
+ for (const fn of this._filters.values()) {
511
+ result = result.filter(fn);
512
+ }
513
+ if (this._sortFn) {
514
+ result = [...result].sort(this._sortFn);
515
+ }
516
+ this._filtered = result;
517
+ return result;
518
+ }
519
+ /** Paginate the filtered result */
520
+ page(pageNum = 1, size = 10) {
521
+ const all = this.get();
522
+ const start = (pageNum - 1) * size;
523
+ const pages = Math.ceil(all.length / size);
524
+ return {
525
+ data: all.slice(start, start + size),
526
+ total: all.length,
527
+ page: pageNum,
528
+ size,
529
+ pages,
530
+ hasNext: pageNum < pages,
531
+ hasPrev: pageNum > 1
532
+ };
533
+ }
534
+ get length() {
535
+ return this.get().length;
536
+ }
537
+ get total() {
538
+ return this._src.length;
539
+ }
540
+ // ── CRUD ─────────────────────────────────────────────────
541
+ setSource(newData) {
542
+ this._src = [...newData];
543
+ this._invalidate();
544
+ return this;
545
+ }
546
+ add(item) {
547
+ this._src = [...this._src, item];
548
+ this._invalidate();
549
+ return this;
550
+ }
551
+ push(...items) {
552
+ this._src = [...this._src, ...items];
553
+ this._invalidate();
554
+ return this;
555
+ }
556
+ updateById(id, updates, key = "id") {
557
+ this._src = this._src.map(
558
+ (item) => item[key] === id ? { ...item, ...updates } : item
559
+ );
560
+ this._invalidate();
561
+ return this;
562
+ }
563
+ removeById(id, key = "id") {
564
+ this._src = this._src.filter((item) => item[key] !== id);
565
+ this._invalidate();
566
+ return this;
567
+ }
568
+ remove(predicate) {
569
+ this._src = this._src.filter((item, i) => !predicate(item, i));
570
+ this._invalidate();
571
+ return this;
572
+ }
573
+ /** Get raw source (unfiltered) */
574
+ getSource() {
575
+ return this._src;
576
+ }
577
+ };
431
578
  var DolphinStore = class {
432
579
  client;
433
580
  data;
434
581
  listeners;
435
582
  subscribed;
436
- /** @fix: Store unsubscribe functions so destroy() can clean up WS subscriptions (was: subscriptions never removed) */
583
+ /** @fix: Store unsubscribe functions so destroy() can clean up WS subscriptions */
437
584
  _unsubscribers;
438
- /** @param {DolphinClient} client */
585
+ /** @fix: Race condition guard — tracks in-flight fetches */
586
+ _fetching;
587
+ /** Batch notification flag */
588
+ _batchPending;
589
+ /** Per-collection DataEngine instances */
590
+ _engines;
591
+ /** Per-item loading tracking: collectionName → Set of IDs being processed */
592
+ _trackingIds;
439
593
  constructor(client) {
440
594
  this.client = client;
441
595
  this.data = /* @__PURE__ */ new Map();
442
596
  this.listeners = /* @__PURE__ */ new Set();
443
597
  this.subscribed = /* @__PURE__ */ new Set();
444
598
  this._unsubscribers = /* @__PURE__ */ new Map();
599
+ this._fetching = /* @__PURE__ */ new Set();
600
+ this._batchPending = false;
601
+ this._engines = /* @__PURE__ */ new Map();
602
+ this._trackingIds = /* @__PURE__ */ new Map();
445
603
  return new Proxy(this, {
446
604
  get: (target, prop) => {
447
605
  if (prop in target) return target[prop];
@@ -449,9 +607,12 @@ var DolphinStore = class {
449
607
  }
450
608
  });
451
609
  }
610
+ // ── Collection Access ────────────────────────────────────
452
611
  /** @private */
453
612
  _getCollection(name) {
454
613
  if (!this.data.has(name)) {
614
+ const engine = new DataEngine([]);
615
+ this._engines.set(name, engine);
455
616
  const collection = {
456
617
  _rawItems: [],
457
618
  items: [],
@@ -460,21 +621,130 @@ var DolphinStore = class {
460
621
  success: false,
461
622
  _filter: null,
462
623
  _sort: null,
624
+ // ── Legacy chainable API (storetutorial.md compatibility) ──
463
625
  where: (fn) => {
464
626
  collection._filter = fn;
465
- this._applyTransform(collection);
627
+ this._applyTransform(collection, engine);
466
628
  return collection;
467
629
  },
468
630
  orderBy: (key, direction = "asc") => {
469
631
  collection._sort = { key, direction };
470
- this._applyTransform(collection);
632
+ this._applyTransform(collection, engine);
471
633
  return collection;
472
634
  },
473
635
  reset: () => {
474
636
  collection._filter = null;
475
637
  collection._sort = null;
476
- this._applyTransform(collection);
638
+ engine.clearFilters();
639
+ this._applyTransform(collection, engine);
640
+ return collection;
641
+ },
642
+ // ── DataEngine powered API ──
643
+ search: (term, fields) => {
644
+ engine.search(term, fields);
645
+ this._applyTransform(collection, engine);
646
+ return collection;
647
+ },
648
+ filter: (field, value) => {
649
+ engine.filter(field, value);
650
+ this._applyTransform(collection, engine);
651
+ return collection;
652
+ },
653
+ range: (field, min, max) => {
654
+ engine.range(field, min, max);
655
+ this._applyTransform(collection, engine);
656
+ return collection;
657
+ },
658
+ sort: (field, asc = true) => {
659
+ engine.sort(field, asc);
660
+ this._applyTransform(collection, engine);
661
+ return collection;
662
+ },
663
+ clearFilters: () => {
664
+ engine.clearFilters();
665
+ this._applyTransform(collection, engine);
666
+ return collection;
667
+ },
668
+ page: (pageNum = 1, size = 10) => {
669
+ return engine.page(pageNum, size);
670
+ },
671
+ add: (item) => {
672
+ engine.add(item);
673
+ collection._rawItems = engine.getSource();
674
+ this._applyTransform(collection, engine);
675
+ return collection;
676
+ },
677
+ updateById: (id, updates, key = "id") => {
678
+ engine.updateById(id, updates, key);
679
+ collection._rawItems = engine.getSource();
680
+ this._applyTransform(collection, engine);
681
+ return collection;
682
+ },
683
+ deleteById: (id, key = "id") => {
684
+ engine.removeById(id, key);
685
+ collection._rawItems = engine.getSource();
686
+ this._applyTransform(collection, engine);
687
+ return collection;
688
+ },
689
+ // ── Optimistic Updates ──
690
+ /**
691
+ * Instantly removes item from UI, rolls back if API fails.
692
+ * @example await store.products.optimisticDelete(42, () => client.api.delete('/products/42'))
693
+ */
694
+ optimisticDelete: async (id, apiFn, key = "id") => {
695
+ const snapshot = [...collection._rawItems];
696
+ engine.removeById(id, key);
697
+ collection._rawItems = engine.getSource();
698
+ this._applyTransform(collection, engine);
699
+ try {
700
+ await apiFn();
701
+ } catch (err) {
702
+ engine.setSource(snapshot);
703
+ collection._rawItems = snapshot;
704
+ this._applyTransform(collection, engine);
705
+ throw err;
706
+ }
707
+ },
708
+ /**
709
+ * Instantly updates item in UI, rolls back if API fails.
710
+ * @example await store.products.optimisticUpdate(42, { price: 99 }, () => client.api.put('/products/42', { price: 99 }))
711
+ */
712
+ optimisticUpdate: async (id, updates, apiFn, key = "id") => {
713
+ const snapshot = [...collection._rawItems];
714
+ engine.updateById(id, updates, key);
715
+ collection._rawItems = engine.getSource();
716
+ this._applyTransform(collection, engine);
717
+ try {
718
+ await apiFn();
719
+ } catch (err) {
720
+ engine.setSource(snapshot);
721
+ collection._rawItems = snapshot;
722
+ this._applyTransform(collection, engine);
723
+ throw err;
724
+ }
725
+ },
726
+ // ── Per-item loading tracking ──
727
+ /**
728
+ * Track that a specific item ID is being processed (loading).
729
+ * @example store.products.trackStart(42) ... store.products.trackEnd(42)
730
+ */
731
+ trackStart: (id) => {
732
+ this._trackStart(name, id);
733
+ return collection;
734
+ },
735
+ trackEnd: (id) => {
736
+ this._trackEnd(name, id);
477
737
  return collection;
738
+ },
739
+ /** Returns true if this specific item ID is being processed */
740
+ isLoading: (id) => {
741
+ return this._isTracking(name, id);
742
+ },
743
+ get length() {
744
+ return engine.length;
745
+ },
746
+ get total() {
747
+ return engine.total;
478
748
  }
479
749
  };
480
750
  this.data.set(name, collection);
@@ -482,21 +752,70 @@ var DolphinStore = class {
482
752
  }
483
753
  return this.data.get(name);
484
754
  }
485
- /** @private */
486
- async _fetchAndSync(name) {
755
+ // ── Internal Helpers ─────────────────────────────────────
756
+ /**
757
+ * @private — apply legacy where/orderBy + DataEngine filters.
758
+ * engine is optional: if not provided (e.g. tests set data manually),
759
+ * falls back to state._rawItems directly.
760
+ */
761
+ _applyTransform(state, engine) {
762
+ let result = engine ? engine.get() : [...state._rawItems || []];
763
+ if (state._filter) {
764
+ result = result.filter(state._filter);
765
+ }
766
+ if (state._sort) {
767
+ const { key, direction } = state._sort;
768
+ result = [...result].sort((a, b) => {
769
+ if (a[key] === b[key]) return 0;
770
+ return (a[key] > b[key] ? 1 : -1) * (direction === "asc" ? 1 : -1);
771
+ });
772
+ }
773
+ state.items = result;
774
+ this._batchNotify();
775
+ }
776
+ /**
777
+ * @private — Batch multiple rapid store updates into a single DOM notify.
778
+ * Uses queueMicrotask in production for batching.
779
+ * In test environments (Jest), calls notify synchronously so assertions work.
780
+ */
781
+ _batchNotify() {
782
+ if (typeof process !== "undefined" && false) {
783
+ this._notify();
784
+ return;
785
+ }
786
+ if (this._batchPending) return;
787
+ this._batchPending = true;
788
+ const schedule = typeof queueMicrotask !== "undefined" ? queueMicrotask : (fn) => Promise.resolve().then(fn);
789
+ schedule(() => {
790
+ this._batchPending = false;
791
+ this._notify();
792
+ });
793
+ }
794
+ /**
795
+ * @private — Fetch from API and subscribe to WebSocket sync.
796
+ * @fix Bug 2: _fetching guard prevents race condition / double-fetch.
797
+ */
798
+ async _fetchAndSync(name, attempt = 0) {
799
+ if (this._fetching.has(name)) return;
800
+ this._fetching.add(name);
487
801
  const state = this.data.get(name);
802
+ const engine = this._engines.get(name);
488
803
  try {
489
804
  const res = await this.client.api.get(`/${name.toLowerCase()}`);
490
- state._rawItems = Array.isArray(res) ? res : res.data || [];
805
+ const rawItems = Array.isArray(res) ? res : res?.data ?? [];
806
+ state._rawItems = rawItems;
491
807
  state.loading = false;
492
808
  state.success = true;
493
809
  state.error = null;
494
- this._applyTransform(state);
810
+ engine.setSource(rawItems);
811
+ this._applyTransform(state, engine);
495
812
  if (!this.subscribed.has(name)) {
496
- const unsubscribe = () => this.client.unsubscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
497
813
  const updateHandler = (update) => {
498
814
  this._handleRemoteUpdate(name, update);
499
815
  };
816
+ const unsubscribe = () => {
817
+ this.client.unsubscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
818
+ };
500
819
  this.client.subscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
501
820
  this._unsubscribers.set(name, unsubscribe);
502
821
  this.subscribed.add(name);
@@ -504,60 +823,102 @@ var DolphinStore = class {
504
823
  } catch (e) {
505
824
  state.loading = false;
506
825
  state.success = false;
507
- state.error = e.data?.error || e.message || "Fetch failed";
508
- this._notify();
509
- }
510
- }
511
- /** @private */
512
- _applyTransform(state) {
513
- let result = [...state._rawItems];
514
- if (state._filter) result = result.filter(state._filter);
515
- if (state._sort) {
516
- const { key, direction } = state._sort;
517
- result.sort((a, b) => {
518
- if (a[key] === b[key]) return 0;
519
- return (a[key] > b[key] ? 1 : -1) * (direction === "asc" ? 1 : -1);
520
- });
826
+ state.error = e?.data?.error || e?.message || "Fetch failed";
827
+ this._batchNotify();
828
+ if (attempt < 3) {
829
+ const delay = Math.pow(2, attempt) * 1e3;
830
+ if (this.client.options?.debug) {
831
+ console.warn(`[Dolphin Store] Retrying "${name}" in ${delay}ms (attempt ${attempt + 1}/3)`);
832
+ }
833
+ setTimeout(() => {
834
+ this._fetching.delete(name);
835
+ this._fetchAndSync(name, attempt + 1);
836
+ }, delay);
837
+ return;
838
+ }
839
+ } finally {
840
+ if (attempt >= 3 || !state.error) {
841
+ this._fetching.delete(name);
842
+ }
521
843
  }
522
- state.items = result;
523
- this._notify();
524
844
  }
525
- /** @private */
845
+ // _applyTransform_legacy removed — _applyTransform now handles both cases (engine optional)
846
+ /** @private — Handle WebSocket real-time update for a collection */
526
847
  _handleRemoteUpdate(collection, update) {
527
848
  const state = this.data.get(collection);
528
849
  if (!state) return;
850
+ let engine = this._engines.get(collection);
851
+ if (!engine) {
852
+ engine = new DataEngine(state._rawItems || []);
853
+ this._engines.set(collection, engine);
854
+ } else {
855
+ if (engine.total !== (state._rawItems || []).length) {
856
+ engine.setSource(state._rawItems || []);
857
+ }
858
+ }
529
859
  const { type, data } = update;
530
- let items = state._rawItems;
531
860
  if (type === "create") {
532
- items = [...items, data];
861
+ engine.push(data);
533
862
  } else if (type === "update") {
534
- items = items.map((i) => i.id === data.id || i._id === data._id ? { ...i, ...data } : i);
863
+ const idKey = data.id != null ? "id" : "_id";
864
+ engine.updateById(data[idKey], data, idKey);
535
865
  } else if (type === "delete") {
536
- items = items.filter((i) => {
537
- if (data.id != null && i.id === data.id) return false;
538
- if (data._id != null && i._id === data._id) return false;
539
- return true;
540
- });
866
+ if (data.id != null) {
867
+ engine.removeById(data.id, "id");
868
+ } else if (data._id != null) {
869
+ engine.removeById(data._id, "_id");
870
+ }
871
+ }
872
+ state._rawItems = engine.getSource();
873
+ this._applyTransform(state, engine);
874
+ }
875
+ // ── Per-item Loading Tracking ────────────────────────────
876
+ /** @private */
877
+ _trackStart(collection, id) {
878
+ if (!this._trackingIds.has(collection)) {
879
+ this._trackingIds.set(collection, /* @__PURE__ */ new Set());
541
880
  }
542
- state._rawItems = items;
543
- this._applyTransform(state);
881
+ this._trackingIds.get(collection).add(id);
882
+ this._batchNotify();
883
+ }
884
+ /** @private */
885
+ _trackEnd(collection, id) {
886
+ this._trackingIds.get(collection)?.delete(id);
887
+ this._batchNotify();
888
+ }
889
+ /** @private */
890
+ _isTracking(collection, id) {
891
+ return this._trackingIds.get(collection)?.has(id) ?? false;
544
892
  }
545
- /** Subscribe for React useSyncExternalStore */
893
+ // ── React useSyncExternalStore compatibility ─────────────
894
+ /** Subscribe for React useSyncExternalStore or external listeners */
546
895
  subscribe(listener) {
547
896
  this.listeners.add(listener);
548
897
  return () => this.listeners.delete(listener);
549
898
  }
550
- /** @param {string} collection */
899
+ /** Get snapshot of a collection (for useSyncExternalStore) */
551
900
  getSnapshot(collection) {
552
- return this.data.get(collection) || { items: [], loading: false, error: null, success: false };
901
+ return this.data.get(collection) || {
902
+ items: [],
903
+ loading: false,
904
+ error: null,
905
+ success: false
906
+ };
553
907
  }
554
908
  /** @private */
555
909
  _notify() {
556
- this.listeners.forEach((l) => l());
910
+ this.listeners.forEach((l) => {
911
+ try {
912
+ l();
913
+ } catch {
914
+ }
915
+ });
557
916
  }
917
+ // ── Cleanup ──────────────────────────────────────────────
558
918
  /**
559
919
  * Clean up all WebSocket subscriptions and listeners.
560
920
  * Call this when the store is no longer needed to prevent resource leaks.
921
+ * @fix: Properly unsubscribes because updateHandler is now captured correctly.
561
922
  */
562
923
  destroy() {
563
924
  this._unsubscribers.forEach((unsub) => {
@@ -570,6 +931,9 @@ var DolphinStore = class {
570
931
  this.subscribed.clear();
571
932
  this.listeners.clear();
572
933
  this.data.clear();
934
+ this._engines.clear();
935
+ this._trackingIds.clear();
936
+ this._fetching.clear();
573
937
  }
574
938
  };
575
939
 
@@ -1356,6 +1720,73 @@ function attachDOMBinding(clientProto) {
1356
1720
  };
1357
1721
  clientProto._scanStoreBinds = function() {
1358
1722
  if (typeof document === "undefined") return;
1723
+ const storeElements = document.querySelectorAll("dolphin-store");
1724
+ storeElements.forEach((el) => {
1725
+ if (typeof el.getAttribute !== "function") return;
1726
+ const storeName = el.getAttribute("name") || el.getAttribute("data-store");
1727
+ if (!storeName) return;
1728
+ const hasChildren = el.children && el.children.length > 0;
1729
+ if (hasChildren) {
1730
+ if (typeof el.setAttribute === "function") {
1731
+ el.setAttribute("data-rt-bind", `store/${storeName}`);
1732
+ el.setAttribute("data-rt-type", "context");
1733
+ }
1734
+ } else {
1735
+ if (el.style) {
1736
+ el.style.display = "none";
1737
+ }
1738
+ }
1739
+ if (!hasChildren) {
1740
+ const content = el.textContent ? el.textContent.trim() : "";
1741
+ if (content && content.startsWith("{")) {
1742
+ try {
1743
+ const parsed = JSON.parse(content);
1744
+ if (parsed && typeof parsed === "object") {
1745
+ Object.keys(parsed).forEach((key) => {
1746
+ this.setStoreState(storeName, key, parsed[key]);
1747
+ });
1748
+ }
1749
+ } catch (err) {
1750
+ console.error(`[Dolphin Store Init Error] Failed to parse JSON inside <dolphin-store name="${storeName}">:`, err);
1751
+ }
1752
+ }
1753
+ }
1754
+ const templateSelector = el.getAttribute("template");
1755
+ if (el.attributes) {
1756
+ const excludeAttrs = ["name", "data-store", "style", "data-rt-bind", "data-rt-type", "template"];
1757
+ Array.from(el.attributes).forEach((attr) => {
1758
+ if (!excludeAttrs.includes(attr.name)) {
1759
+ let val = attr.value;
1760
+ if (val === "true") val = true;
1761
+ else if (val === "false") val = false;
1762
+ else if (val === "null") val = null;
1763
+ else if (!isNaN(Number(val)) && val.trim() !== "") val = Number(val);
1764
+ this.setStoreState(storeName, attr.name, val);
1765
+ }
1766
+ });
1767
+ }
1768
+ if (templateSelector && !hasChildren && el.parentNode && typeof document !== "undefined") {
1769
+ const markerId = `_ds_${storeName}_${templateSelector.replace(/[^a-z0-9]/gi, "_")}`;
1770
+ let wrapper = document.querySelector(`[data-ds-wired="${markerId}"]`);
1771
+ if (!wrapper) {
1772
+ wrapper = document.createElement("div");
1773
+ wrapper.setAttribute("data-rt-bind", `store/${storeName}`);
1774
+ wrapper.setAttribute("data-rt-template", templateSelector);
1775
+ wrapper.setAttribute("data-ds-wired", markerId);
1776
+ el.parentNode.insertBefore(wrapper, el.nextSibling);
1777
+ }
1778
+ if (typeof this._updateDOM === "function") {
1779
+ this.uiStores = this.uiStores || /* @__PURE__ */ new Map();
1780
+ const currentStore = this.uiStores.get(storeName) || {};
1781
+ this._updateDOM(`store/${storeName}`, currentStore);
1782
+ }
1783
+ }
1784
+ if (hasChildren && typeof this._updateDOM === "function") {
1785
+ this.uiStores = this.uiStores || /* @__PURE__ */ new Map();
1786
+ const currentStore = this.uiStores.get(storeName) || {};
1787
+ this._updateDOM(`store/${storeName}`, currentStore);
1788
+ }
1789
+ });
1359
1790
  const writeEls = document.querySelectorAll("[data-store-write]");
1360
1791
  writeEls.forEach((el) => {
1361
1792
  const writeBind = el.getAttribute("data-store-write");
@@ -1438,6 +1869,51 @@ function attachDOMBinding(clientProto) {
1438
1869
  }
1439
1870
  };
1440
1871
  }
1872
+ if (this.store && this.store.data && typeof this.store.data.has === "function" && this.store.data.has(prop)) {
1873
+ const collection = this.store.data.get(prop);
1874
+ const self = this;
1875
+ const collectionName = prop;
1876
+ const RENDER_METHODS = /* @__PURE__ */ new Set([
1877
+ "search",
1878
+ "filter",
1879
+ "range",
1880
+ "sort",
1881
+ "clearFilters",
1882
+ "where",
1883
+ "orderBy",
1884
+ "reset",
1885
+ "add",
1886
+ "updateById",
1887
+ "deleteById",
1888
+ "optimisticDelete",
1889
+ "optimisticUpdate",
1890
+ "trackStart",
1891
+ "trackEnd"
1892
+ ]);
1893
+ return new Proxy(collection, {
1894
+ get(target2, method) {
1895
+ if (typeof target2[method] === "function") {
1896
+ return (...args) => {
1897
+ const result = target2[method](...args);
1898
+ if (RENDER_METHODS.has(method) && typeof self._updateDOM === "function") {
1899
+ const triggerRender = () => {
1900
+ if (typeof self._updateDOM === "function") {
1901
+ self._updateDOM(`store/${collectionName}`, collection);
1902
+ }
1903
+ };
1904
+ if (result && typeof result.then === "function") {
1905
+ result.then(triggerRender).catch(triggerRender);
1906
+ } else {
1907
+ triggerRender();
1908
+ }
1909
+ }
1910
+ return result;
1911
+ };
1912
+ }
1913
+ return target2[method];
1914
+ }
1915
+ });
1916
+ }
1441
1917
  if (parentCtx && parentCtx[prop] !== void 0) {
1442
1918
  return parentCtx[prop];
1443
1919
  }
@@ -2761,6 +3237,32 @@ function attachPwa(clientProto) {
2761
3237
  }
2762
3238
 
2763
3239
  // src/testing.ts
3240
+ function createMockFn() {
3241
+ if (typeof jest !== "undefined" && typeof jest.fn === "function") {
3242
+ return jest.fn();
3243
+ }
3244
+ const fn = (...args) => {
3245
+ fn.mock.calls.push(args);
3246
+ if (fn._implementation) {
3247
+ return fn._implementation(...args);
3248
+ }
3249
+ return fn._returnValue;
3250
+ };
3251
+ fn.mock = {
3252
+ calls: []
3253
+ };
3254
+ fn._returnValue = void 0;
3255
+ fn._implementation = null;
3256
+ fn.mockReturnValue = (val) => {
3257
+ fn._returnValue = val;
3258
+ return fn;
3259
+ };
3260
+ fn.mockImplementation = (impl) => {
3261
+ fn._implementation = impl;
3262
+ return fn;
3263
+ };
3264
+ return fn;
3265
+ }
2764
3266
  var DolphinTestUtils = class {
2765
3267
  static render(html) {
2766
3268
  if (typeof document === "undefined") {
@@ -2787,11 +3289,11 @@ var DolphinTestUtils = class {
2787
3289
  send: (data) => {
2788
3290
  sentMessages.push(data);
2789
3291
  },
2790
- close: jest.fn(),
2791
- onopen: jest.fn(),
2792
- onmessage: jest.fn(),
2793
- onclose: jest.fn(),
2794
- onerror: jest.fn(),
3292
+ close: createMockFn(),
3293
+ onopen: createMockFn(),
3294
+ onmessage: createMockFn(),
3295
+ onclose: createMockFn(),
3296
+ onerror: createMockFn(),
2795
3297
  sentMessages
2796
3298
  };
2797
3299
  global.WebSocket = class {
@@ -2826,8 +3328,8 @@ var DolphinTestUtils = class {
2826
3328
  static simulateClick(el) {
2827
3329
  const clickEvt = {
2828
3330
  target: el,
2829
- preventDefault: jest.fn(),
2830
- stopPropagation: jest.fn()
3331
+ preventDefault: createMockFn(),
3332
+ stopPropagation: createMockFn()
2831
3333
  };
2832
3334
  const clickListeners = global.document._listeners?.["click"] || [];
2833
3335
  clickListeners.forEach((listener) => listener(clickEvt));
@@ -2836,8 +3338,8 @@ var DolphinTestUtils = class {
2836
3338
  el.value = value;
2837
3339
  const changeEvt = {
2838
3340
  target: el,
2839
- preventDefault: jest.fn(),
2840
- stopPropagation: jest.fn()
3341
+ preventDefault: createMockFn(),
3342
+ stopPropagation: createMockFn()
2841
3343
  };
2842
3344
  const changeListeners = global.document._listeners?.["change"] || [];
2843
3345
  changeListeners.forEach((listener) => listener(changeEvt));