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.
@@ -453,20 +453,178 @@ var DolphinModule = (() => {
453
453
  };
454
454
 
455
455
  // src/store.ts
456
+ var DataEngine = class {
457
+ _src = [];
458
+ _filtered = null;
459
+ _filters = /* @__PURE__ */ new Map();
460
+ _sortFn = null;
461
+ _version = 0;
462
+ constructor(initialData = []) {
463
+ this._src = [...initialData];
464
+ }
465
+ _invalidate() {
466
+ this._filtered = null;
467
+ this._version++;
468
+ }
469
+ getVersion() {
470
+ return this._version;
471
+ }
472
+ // ── Filters ──────────────────────────────────────────────
473
+ /** Text search across fields (case-insensitive) */
474
+ search(term, fields = []) {
475
+ if (!term || !term.trim()) {
476
+ this._filters.delete("__search__");
477
+ } else {
478
+ const t = term.trim().toLowerCase();
479
+ this._filters.set("__search__", (item) => {
480
+ const keys = fields.length ? fields : Object.keys(item);
481
+ return keys.some((k) => String(item[k] ?? "").toLowerCase().includes(t));
482
+ });
483
+ }
484
+ this._invalidate();
485
+ return this;
486
+ }
487
+ /** Exact value filter on a field */
488
+ filter(field, value) {
489
+ const key = `__filter_${String(field)}__`;
490
+ if (value === void 0 || value === null || value === "") {
491
+ this._filters.delete(key);
492
+ } else {
493
+ this._filters.set(key, (item) => item[field] === value);
494
+ }
495
+ this._invalidate();
496
+ return this;
497
+ }
498
+ /** Numeric range filter */
499
+ range(field, min, max) {
500
+ const key = `__range_${String(field)}__`;
501
+ this._filters.set(key, (item) => {
502
+ const v = Number(item[field]);
503
+ return !isNaN(v) && v >= min && v <= max;
504
+ });
505
+ this._invalidate();
506
+ return this;
507
+ }
508
+ /** Sort by field ascending or descending */
509
+ sort(field, asc = true) {
510
+ this._sortFn = (a, b) => {
511
+ const va = a[field], vb = b[field];
512
+ if (va == null && vb == null) return 0;
513
+ if (va == null) return asc ? 1 : -1;
514
+ if (vb == null) return asc ? -1 : 1;
515
+ if (typeof va === "number" && typeof vb === "number") {
516
+ return asc ? va - vb : vb - va;
517
+ }
518
+ return String(va).localeCompare(String(vb)) * (asc ? 1 : -1);
519
+ };
520
+ this._invalidate();
521
+ return this;
522
+ }
523
+ /** Clear all filters and sort */
524
+ clearFilters() {
525
+ this._filters.clear();
526
+ this._sortFn = null;
527
+ this._invalidate();
528
+ return this;
529
+ }
530
+ // ── Data Access ──────────────────────────────────────────
531
+ /** Get filtered + sorted results (lazy, cached) */
532
+ get() {
533
+ if (this._filtered !== null) return this._filtered;
534
+ let result = this._src;
535
+ for (const fn of this._filters.values()) {
536
+ result = result.filter(fn);
537
+ }
538
+ if (this._sortFn) {
539
+ result = [...result].sort(this._sortFn);
540
+ }
541
+ this._filtered = result;
542
+ return result;
543
+ }
544
+ /** Paginate the filtered result */
545
+ page(pageNum = 1, size = 10) {
546
+ const all = this.get();
547
+ const start = (pageNum - 1) * size;
548
+ const pages = Math.ceil(all.length / size);
549
+ return {
550
+ data: all.slice(start, start + size),
551
+ total: all.length,
552
+ page: pageNum,
553
+ size,
554
+ pages,
555
+ hasNext: pageNum < pages,
556
+ hasPrev: pageNum > 1
557
+ };
558
+ }
559
+ get length() {
560
+ return this.get().length;
561
+ }
562
+ get total() {
563
+ return this._src.length;
564
+ }
565
+ // ── CRUD ─────────────────────────────────────────────────
566
+ setSource(newData) {
567
+ this._src = [...newData];
568
+ this._invalidate();
569
+ return this;
570
+ }
571
+ add(item) {
572
+ this._src = [...this._src, item];
573
+ this._invalidate();
574
+ return this;
575
+ }
576
+ push(...items) {
577
+ this._src = [...this._src, ...items];
578
+ this._invalidate();
579
+ return this;
580
+ }
581
+ updateById(id, updates, key = "id") {
582
+ this._src = this._src.map(
583
+ (item) => item[key] === id ? { ...item, ...updates } : item
584
+ );
585
+ this._invalidate();
586
+ return this;
587
+ }
588
+ removeById(id, key = "id") {
589
+ this._src = this._src.filter((item) => item[key] !== id);
590
+ this._invalidate();
591
+ return this;
592
+ }
593
+ remove(predicate) {
594
+ this._src = this._src.filter((item, i) => !predicate(item, i));
595
+ this._invalidate();
596
+ return this;
597
+ }
598
+ /** Get raw source (unfiltered) */
599
+ getSource() {
600
+ return this._src;
601
+ }
602
+ };
456
603
  var DolphinStore = class {
457
604
  client;
458
605
  data;
459
606
  listeners;
460
607
  subscribed;
461
- /** @fix: Store unsubscribe functions so destroy() can clean up WS subscriptions (was: subscriptions never removed) */
608
+ /** @fix: Store unsubscribe functions so destroy() can clean up WS subscriptions */
462
609
  _unsubscribers;
463
- /** @param {DolphinClient} client */
610
+ /** @fix: Race condition guard — tracks in-flight fetches */
611
+ _fetching;
612
+ /** Batch notification flag */
613
+ _batchPending;
614
+ /** Per-collection DataEngine instances */
615
+ _engines;
616
+ /** Per-item loading tracking: collectionName → Set of IDs being processed */
617
+ _trackingIds;
464
618
  constructor(client) {
465
619
  this.client = client;
466
620
  this.data = /* @__PURE__ */ new Map();
467
621
  this.listeners = /* @__PURE__ */ new Set();
468
622
  this.subscribed = /* @__PURE__ */ new Set();
469
623
  this._unsubscribers = /* @__PURE__ */ new Map();
624
+ this._fetching = /* @__PURE__ */ new Set();
625
+ this._batchPending = false;
626
+ this._engines = /* @__PURE__ */ new Map();
627
+ this._trackingIds = /* @__PURE__ */ new Map();
470
628
  return new Proxy(this, {
471
629
  get: (target, prop) => {
472
630
  if (prop in target) return target[prop];
@@ -474,9 +632,12 @@ var DolphinModule = (() => {
474
632
  }
475
633
  });
476
634
  }
635
+ // ── Collection Access ────────────────────────────────────
477
636
  /** @private */
478
637
  _getCollection(name) {
479
638
  if (!this.data.has(name)) {
639
+ const engine = new DataEngine([]);
640
+ this._engines.set(name, engine);
480
641
  const collection = {
481
642
  _rawItems: [],
482
643
  items: [],
@@ -485,21 +646,130 @@ var DolphinModule = (() => {
485
646
  success: false,
486
647
  _filter: null,
487
648
  _sort: null,
649
+ // ── Legacy chainable API (storetutorial.md compatibility) ──
488
650
  where: (fn) => {
489
651
  collection._filter = fn;
490
- this._applyTransform(collection);
652
+ this._applyTransform(collection, engine);
491
653
  return collection;
492
654
  },
493
655
  orderBy: (key, direction = "asc") => {
494
656
  collection._sort = { key, direction };
495
- this._applyTransform(collection);
657
+ this._applyTransform(collection, engine);
496
658
  return collection;
497
659
  },
498
660
  reset: () => {
499
661
  collection._filter = null;
500
662
  collection._sort = null;
501
- this._applyTransform(collection);
663
+ engine.clearFilters();
664
+ this._applyTransform(collection, engine);
665
+ return collection;
666
+ },
667
+ // ── DataEngine powered API ──
668
+ search: (term, fields) => {
669
+ engine.search(term, fields);
670
+ this._applyTransform(collection, engine);
671
+ return collection;
672
+ },
673
+ filter: (field, value) => {
674
+ engine.filter(field, value);
675
+ this._applyTransform(collection, engine);
676
+ return collection;
677
+ },
678
+ range: (field, min, max) => {
679
+ engine.range(field, min, max);
680
+ this._applyTransform(collection, engine);
681
+ return collection;
682
+ },
683
+ sort: (field, asc = true) => {
684
+ engine.sort(field, asc);
685
+ this._applyTransform(collection, engine);
686
+ return collection;
687
+ },
688
+ clearFilters: () => {
689
+ engine.clearFilters();
690
+ this._applyTransform(collection, engine);
691
+ return collection;
692
+ },
693
+ page: (pageNum = 1, size = 10) => {
694
+ return engine.page(pageNum, size);
695
+ },
696
+ add: (item) => {
697
+ engine.add(item);
698
+ collection._rawItems = engine.getSource();
699
+ this._applyTransform(collection, engine);
700
+ return collection;
701
+ },
702
+ updateById: (id, updates, key = "id") => {
703
+ engine.updateById(id, updates, key);
704
+ collection._rawItems = engine.getSource();
705
+ this._applyTransform(collection, engine);
706
+ return collection;
707
+ },
708
+ deleteById: (id, key = "id") => {
709
+ engine.removeById(id, key);
710
+ collection._rawItems = engine.getSource();
711
+ this._applyTransform(collection, engine);
712
+ return collection;
713
+ },
714
+ // ── Optimistic Updates ──
715
+ /**
716
+ * Instantly removes item from UI, rolls back if API fails.
717
+ * @example await store.products.optimisticDelete(42, () => client.api.delete('/products/42'))
718
+ */
719
+ optimisticDelete: async (id, apiFn, key = "id") => {
720
+ const snapshot = [...collection._rawItems];
721
+ engine.removeById(id, key);
722
+ collection._rawItems = engine.getSource();
723
+ this._applyTransform(collection, engine);
724
+ try {
725
+ await apiFn();
726
+ } catch (err) {
727
+ engine.setSource(snapshot);
728
+ collection._rawItems = snapshot;
729
+ this._applyTransform(collection, engine);
730
+ throw err;
731
+ }
732
+ },
733
+ /**
734
+ * Instantly updates item in UI, rolls back if API fails.
735
+ * @example await store.products.optimisticUpdate(42, { price: 99 }, () => client.api.put('/products/42', { price: 99 }))
736
+ */
737
+ optimisticUpdate: async (id, updates, apiFn, key = "id") => {
738
+ const snapshot = [...collection._rawItems];
739
+ engine.updateById(id, updates, key);
740
+ collection._rawItems = engine.getSource();
741
+ this._applyTransform(collection, engine);
742
+ try {
743
+ await apiFn();
744
+ } catch (err) {
745
+ engine.setSource(snapshot);
746
+ collection._rawItems = snapshot;
747
+ this._applyTransform(collection, engine);
748
+ throw err;
749
+ }
750
+ },
751
+ // ── Per-item loading tracking ──
752
+ /**
753
+ * Track that a specific item ID is being processed (loading).
754
+ * @example store.products.trackStart(42) ... store.products.trackEnd(42)
755
+ */
756
+ trackStart: (id) => {
757
+ this._trackStart(name, id);
758
+ return collection;
759
+ },
760
+ trackEnd: (id) => {
761
+ this._trackEnd(name, id);
502
762
  return collection;
763
+ },
764
+ /** Returns true if this specific item ID is being processed */
765
+ isLoading: (id) => {
766
+ return this._isTracking(name, id);
767
+ },
768
+ get length() {
769
+ return engine.length;
770
+ },
771
+ get total() {
772
+ return engine.total;
503
773
  }
504
774
  };
505
775
  this.data.set(name, collection);
@@ -507,21 +777,70 @@ var DolphinModule = (() => {
507
777
  }
508
778
  return this.data.get(name);
509
779
  }
510
- /** @private */
511
- async _fetchAndSync(name) {
780
+ // ── Internal Helpers ─────────────────────────────────────
781
+ /**
782
+ * @private — apply legacy where/orderBy + DataEngine filters.
783
+ * engine is optional: if not provided (e.g. tests set data manually),
784
+ * falls back to state._rawItems directly.
785
+ */
786
+ _applyTransform(state, engine) {
787
+ let result = engine ? engine.get() : [...state._rawItems || []];
788
+ if (state._filter) {
789
+ result = result.filter(state._filter);
790
+ }
791
+ if (state._sort) {
792
+ const { key, direction } = state._sort;
793
+ result = [...result].sort((a, b) => {
794
+ if (a[key] === b[key]) return 0;
795
+ return (a[key] > b[key] ? 1 : -1) * (direction === "asc" ? 1 : -1);
796
+ });
797
+ }
798
+ state.items = result;
799
+ this._batchNotify();
800
+ }
801
+ /**
802
+ * @private — Batch multiple rapid store updates into a single DOM notify.
803
+ * Uses queueMicrotask in production for batching.
804
+ * In test environments (Jest), calls notify synchronously so assertions work.
805
+ */
806
+ _batchNotify() {
807
+ if (typeof process !== "undefined" && false) {
808
+ this._notify();
809
+ return;
810
+ }
811
+ if (this._batchPending) return;
812
+ this._batchPending = true;
813
+ const schedule = typeof queueMicrotask !== "undefined" ? queueMicrotask : (fn) => Promise.resolve().then(fn);
814
+ schedule(() => {
815
+ this._batchPending = false;
816
+ this._notify();
817
+ });
818
+ }
819
+ /**
820
+ * @private — Fetch from API and subscribe to WebSocket sync.
821
+ * @fix Bug 2: _fetching guard prevents race condition / double-fetch.
822
+ */
823
+ async _fetchAndSync(name, attempt = 0) {
824
+ if (this._fetching.has(name)) return;
825
+ this._fetching.add(name);
512
826
  const state = this.data.get(name);
827
+ const engine = this._engines.get(name);
513
828
  try {
514
829
  const res = await this.client.api.get(`/${name.toLowerCase()}`);
515
- state._rawItems = Array.isArray(res) ? res : res.data || [];
830
+ const rawItems = Array.isArray(res) ? res : res?.data ?? [];
831
+ state._rawItems = rawItems;
516
832
  state.loading = false;
517
833
  state.success = true;
518
834
  state.error = null;
519
- this._applyTransform(state);
835
+ engine.setSource(rawItems);
836
+ this._applyTransform(state, engine);
520
837
  if (!this.subscribed.has(name)) {
521
- const unsubscribe = () => this.client.unsubscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
522
838
  const updateHandler = (update) => {
523
839
  this._handleRemoteUpdate(name, update);
524
840
  };
841
+ const unsubscribe = () => {
842
+ this.client.unsubscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
843
+ };
525
844
  this.client.subscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
526
845
  this._unsubscribers.set(name, unsubscribe);
527
846
  this.subscribed.add(name);
@@ -529,60 +848,102 @@ var DolphinModule = (() => {
529
848
  } catch (e) {
530
849
  state.loading = false;
531
850
  state.success = false;
532
- state.error = e.data?.error || e.message || "Fetch failed";
533
- this._notify();
534
- }
535
- }
536
- /** @private */
537
- _applyTransform(state) {
538
- let result = [...state._rawItems];
539
- if (state._filter) result = result.filter(state._filter);
540
- if (state._sort) {
541
- const { key, direction } = state._sort;
542
- result.sort((a, b) => {
543
- if (a[key] === b[key]) return 0;
544
- return (a[key] > b[key] ? 1 : -1) * (direction === "asc" ? 1 : -1);
545
- });
851
+ state.error = e?.data?.error || e?.message || "Fetch failed";
852
+ this._batchNotify();
853
+ if (attempt < 3) {
854
+ const delay = Math.pow(2, attempt) * 1e3;
855
+ if (this.client.options?.debug) {
856
+ console.warn(`[Dolphin Store] Retrying "${name}" in ${delay}ms (attempt ${attempt + 1}/3)`);
857
+ }
858
+ setTimeout(() => {
859
+ this._fetching.delete(name);
860
+ this._fetchAndSync(name, attempt + 1);
861
+ }, delay);
862
+ return;
863
+ }
864
+ } finally {
865
+ if (attempt >= 3 || !state.error) {
866
+ this._fetching.delete(name);
867
+ }
546
868
  }
547
- state.items = result;
548
- this._notify();
549
869
  }
550
- /** @private */
870
+ // _applyTransform_legacy removed — _applyTransform now handles both cases (engine optional)
871
+ /** @private — Handle WebSocket real-time update for a collection */
551
872
  _handleRemoteUpdate(collection, update) {
552
873
  const state = this.data.get(collection);
553
874
  if (!state) return;
875
+ let engine = this._engines.get(collection);
876
+ if (!engine) {
877
+ engine = new DataEngine(state._rawItems || []);
878
+ this._engines.set(collection, engine);
879
+ } else {
880
+ if (engine.total !== (state._rawItems || []).length) {
881
+ engine.setSource(state._rawItems || []);
882
+ }
883
+ }
554
884
  const { type, data } = update;
555
- let items = state._rawItems;
556
885
  if (type === "create") {
557
- items = [...items, data];
886
+ engine.push(data);
558
887
  } else if (type === "update") {
559
- items = items.map((i) => i.id === data.id || i._id === data._id ? { ...i, ...data } : i);
888
+ const idKey = data.id != null ? "id" : "_id";
889
+ engine.updateById(data[idKey], data, idKey);
560
890
  } else if (type === "delete") {
561
- items = items.filter((i) => {
562
- if (data.id != null && i.id === data.id) return false;
563
- if (data._id != null && i._id === data._id) return false;
564
- return true;
565
- });
891
+ if (data.id != null) {
892
+ engine.removeById(data.id, "id");
893
+ } else if (data._id != null) {
894
+ engine.removeById(data._id, "_id");
895
+ }
896
+ }
897
+ state._rawItems = engine.getSource();
898
+ this._applyTransform(state, engine);
899
+ }
900
+ // ── Per-item Loading Tracking ────────────────────────────
901
+ /** @private */
902
+ _trackStart(collection, id) {
903
+ if (!this._trackingIds.has(collection)) {
904
+ this._trackingIds.set(collection, /* @__PURE__ */ new Set());
566
905
  }
567
- state._rawItems = items;
568
- this._applyTransform(state);
906
+ this._trackingIds.get(collection).add(id);
907
+ this._batchNotify();
908
+ }
909
+ /** @private */
910
+ _trackEnd(collection, id) {
911
+ this._trackingIds.get(collection)?.delete(id);
912
+ this._batchNotify();
913
+ }
914
+ /** @private */
915
+ _isTracking(collection, id) {
916
+ return this._trackingIds.get(collection)?.has(id) ?? false;
569
917
  }
570
- /** Subscribe for React useSyncExternalStore */
918
+ // ── React useSyncExternalStore compatibility ─────────────
919
+ /** Subscribe for React useSyncExternalStore or external listeners */
571
920
  subscribe(listener) {
572
921
  this.listeners.add(listener);
573
922
  return () => this.listeners.delete(listener);
574
923
  }
575
- /** @param {string} collection */
924
+ /** Get snapshot of a collection (for useSyncExternalStore) */
576
925
  getSnapshot(collection) {
577
- return this.data.get(collection) || { items: [], loading: false, error: null, success: false };
926
+ return this.data.get(collection) || {
927
+ items: [],
928
+ loading: false,
929
+ error: null,
930
+ success: false
931
+ };
578
932
  }
579
933
  /** @private */
580
934
  _notify() {
581
- this.listeners.forEach((l) => l());
935
+ this.listeners.forEach((l) => {
936
+ try {
937
+ l();
938
+ } catch {
939
+ }
940
+ });
582
941
  }
942
+ // ── Cleanup ──────────────────────────────────────────────
583
943
  /**
584
944
  * Clean up all WebSocket subscriptions and listeners.
585
945
  * Call this when the store is no longer needed to prevent resource leaks.
946
+ * @fix: Properly unsubscribes because updateHandler is now captured correctly.
586
947
  */
587
948
  destroy() {
588
949
  this._unsubscribers.forEach((unsub) => {
@@ -595,6 +956,9 @@ var DolphinModule = (() => {
595
956
  this.subscribed.clear();
596
957
  this.listeners.clear();
597
958
  this.data.clear();
959
+ this._engines.clear();
960
+ this._trackingIds.clear();
961
+ this._fetching.clear();
598
962
  }
599
963
  };
600
964
 
@@ -1381,6 +1745,73 @@ var DolphinModule = (() => {
1381
1745
  };
1382
1746
  clientProto._scanStoreBinds = function() {
1383
1747
  if (typeof document === "undefined") return;
1748
+ const storeElements = document.querySelectorAll("dolphin-store");
1749
+ storeElements.forEach((el) => {
1750
+ if (typeof el.getAttribute !== "function") return;
1751
+ const storeName = el.getAttribute("name") || el.getAttribute("data-store");
1752
+ if (!storeName) return;
1753
+ const hasChildren = el.children && el.children.length > 0;
1754
+ if (hasChildren) {
1755
+ if (typeof el.setAttribute === "function") {
1756
+ el.setAttribute("data-rt-bind", `store/${storeName}`);
1757
+ el.setAttribute("data-rt-type", "context");
1758
+ }
1759
+ } else {
1760
+ if (el.style) {
1761
+ el.style.display = "none";
1762
+ }
1763
+ }
1764
+ if (!hasChildren) {
1765
+ const content = el.textContent ? el.textContent.trim() : "";
1766
+ if (content && content.startsWith("{")) {
1767
+ try {
1768
+ const parsed = JSON.parse(content);
1769
+ if (parsed && typeof parsed === "object") {
1770
+ Object.keys(parsed).forEach((key) => {
1771
+ this.setStoreState(storeName, key, parsed[key]);
1772
+ });
1773
+ }
1774
+ } catch (err) {
1775
+ console.error(`[Dolphin Store Init Error] Failed to parse JSON inside <dolphin-store name="${storeName}">:`, err);
1776
+ }
1777
+ }
1778
+ }
1779
+ const templateSelector = el.getAttribute("template");
1780
+ if (el.attributes) {
1781
+ const excludeAttrs = ["name", "data-store", "style", "data-rt-bind", "data-rt-type", "template"];
1782
+ Array.from(el.attributes).forEach((attr) => {
1783
+ if (!excludeAttrs.includes(attr.name)) {
1784
+ let val = attr.value;
1785
+ if (val === "true") val = true;
1786
+ else if (val === "false") val = false;
1787
+ else if (val === "null") val = null;
1788
+ else if (!isNaN(Number(val)) && val.trim() !== "") val = Number(val);
1789
+ this.setStoreState(storeName, attr.name, val);
1790
+ }
1791
+ });
1792
+ }
1793
+ if (templateSelector && !hasChildren && el.parentNode && typeof document !== "undefined") {
1794
+ const markerId = `_ds_${storeName}_${templateSelector.replace(/[^a-z0-9]/gi, "_")}`;
1795
+ let wrapper = document.querySelector(`[data-ds-wired="${markerId}"]`);
1796
+ if (!wrapper) {
1797
+ wrapper = document.createElement("div");
1798
+ wrapper.setAttribute("data-rt-bind", `store/${storeName}`);
1799
+ wrapper.setAttribute("data-rt-template", templateSelector);
1800
+ wrapper.setAttribute("data-ds-wired", markerId);
1801
+ el.parentNode.insertBefore(wrapper, el.nextSibling);
1802
+ }
1803
+ if (typeof this._updateDOM === "function") {
1804
+ this.uiStores = this.uiStores || /* @__PURE__ */ new Map();
1805
+ const currentStore = this.uiStores.get(storeName) || {};
1806
+ this._updateDOM(`store/${storeName}`, currentStore);
1807
+ }
1808
+ }
1809
+ if (hasChildren && typeof this._updateDOM === "function") {
1810
+ this.uiStores = this.uiStores || /* @__PURE__ */ new Map();
1811
+ const currentStore = this.uiStores.get(storeName) || {};
1812
+ this._updateDOM(`store/${storeName}`, currentStore);
1813
+ }
1814
+ });
1384
1815
  const writeEls = document.querySelectorAll("[data-store-write]");
1385
1816
  writeEls.forEach((el) => {
1386
1817
  const writeBind = el.getAttribute("data-store-write");
@@ -1463,6 +1894,51 @@ var DolphinModule = (() => {
1463
1894
  }
1464
1895
  };
1465
1896
  }
1897
+ if (this.store && this.store.data && typeof this.store.data.has === "function" && this.store.data.has(prop)) {
1898
+ const collection = this.store.data.get(prop);
1899
+ const self = this;
1900
+ const collectionName = prop;
1901
+ const RENDER_METHODS = /* @__PURE__ */ new Set([
1902
+ "search",
1903
+ "filter",
1904
+ "range",
1905
+ "sort",
1906
+ "clearFilters",
1907
+ "where",
1908
+ "orderBy",
1909
+ "reset",
1910
+ "add",
1911
+ "updateById",
1912
+ "deleteById",
1913
+ "optimisticDelete",
1914
+ "optimisticUpdate",
1915
+ "trackStart",
1916
+ "trackEnd"
1917
+ ]);
1918
+ return new Proxy(collection, {
1919
+ get(target2, method) {
1920
+ if (typeof target2[method] === "function") {
1921
+ return (...args) => {
1922
+ const result = target2[method](...args);
1923
+ if (RENDER_METHODS.has(method) && typeof self._updateDOM === "function") {
1924
+ const triggerRender = () => {
1925
+ if (typeof self._updateDOM === "function") {
1926
+ self._updateDOM(`store/${collectionName}`, collection);
1927
+ }
1928
+ };
1929
+ if (result && typeof result.then === "function") {
1930
+ result.then(triggerRender).catch(triggerRender);
1931
+ } else {
1932
+ triggerRender();
1933
+ }
1934
+ }
1935
+ return result;
1936
+ };
1937
+ }
1938
+ return target2[method];
1939
+ }
1940
+ });
1941
+ }
1466
1942
  if (parentCtx && parentCtx[prop] !== void 0) {
1467
1943
  return parentCtx[prop];
1468
1944
  }
@@ -2786,6 +3262,32 @@ var DolphinModule = (() => {
2786
3262
  }
2787
3263
 
2788
3264
  // src/testing.ts
3265
+ function createMockFn() {
3266
+ if (typeof jest !== "undefined" && typeof jest.fn === "function") {
3267
+ return jest.fn();
3268
+ }
3269
+ const fn = (...args) => {
3270
+ fn.mock.calls.push(args);
3271
+ if (fn._implementation) {
3272
+ return fn._implementation(...args);
3273
+ }
3274
+ return fn._returnValue;
3275
+ };
3276
+ fn.mock = {
3277
+ calls: []
3278
+ };
3279
+ fn._returnValue = void 0;
3280
+ fn._implementation = null;
3281
+ fn.mockReturnValue = (val) => {
3282
+ fn._returnValue = val;
3283
+ return fn;
3284
+ };
3285
+ fn.mockImplementation = (impl) => {
3286
+ fn._implementation = impl;
3287
+ return fn;
3288
+ };
3289
+ return fn;
3290
+ }
2789
3291
  var DolphinTestUtils = class {
2790
3292
  static render(html) {
2791
3293
  if (typeof document === "undefined") {
@@ -2812,11 +3314,11 @@ var DolphinModule = (() => {
2812
3314
  send: (data) => {
2813
3315
  sentMessages.push(data);
2814
3316
  },
2815
- close: jest.fn(),
2816
- onopen: jest.fn(),
2817
- onmessage: jest.fn(),
2818
- onclose: jest.fn(),
2819
- onerror: jest.fn(),
3317
+ close: createMockFn(),
3318
+ onopen: createMockFn(),
3319
+ onmessage: createMockFn(),
3320
+ onclose: createMockFn(),
3321
+ onerror: createMockFn(),
2820
3322
  sentMessages
2821
3323
  };
2822
3324
  global.WebSocket = class {
@@ -2851,8 +3353,8 @@ var DolphinModule = (() => {
2851
3353
  static simulateClick(el) {
2852
3354
  const clickEvt = {
2853
3355
  target: el,
2854
- preventDefault: jest.fn(),
2855
- stopPropagation: jest.fn()
3356
+ preventDefault: createMockFn(),
3357
+ stopPropagation: createMockFn()
2856
3358
  };
2857
3359
  const clickListeners = global.document._listeners?.["click"] || [];
2858
3360
  clickListeners.forEach((listener) => listener(clickEvt));
@@ -2861,8 +3363,8 @@ var DolphinModule = (() => {
2861
3363
  el.value = value;
2862
3364
  const changeEvt = {
2863
3365
  target: el,
2864
- preventDefault: jest.fn(),
2865
- stopPropagation: jest.fn()
3366
+ preventDefault: createMockFn(),
3367
+ stopPropagation: createMockFn()
2866
3368
  };
2867
3369
  const changeListeners = global.document._listeners?.["change"] || [];
2868
3370
  changeListeners.forEach((listener) => listener(changeEvt));