dolphin-client 1.1.3 → 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/README.md +60 -16
- package/dist/dolphin-client.js +450 -41
- package/dist/dolphin-client.min.js +18 -18
- package/dist/index.cjs +450 -41
- package/dist/index.js +450 -41
- package/dist/store.d.ts +84 -10
- package/fulltutorial.md +108 -9
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -453,20 +453,178 @@ var AuthHandler = class {
|
|
|
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
|
|
608
|
+
/** @fix: Store unsubscribe functions so destroy() can clean up WS subscriptions */
|
|
462
609
|
_unsubscribers;
|
|
463
|
-
/** @
|
|
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 DolphinStore = class {
|
|
|
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 DolphinStore = class {
|
|
|
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
|
-
|
|
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);
|
|
502
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);
|
|
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 DolphinStore = class {
|
|
|
507
777
|
}
|
|
508
778
|
return this.data.get(name);
|
|
509
779
|
}
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 DolphinStore = class {
|
|
|
529
848
|
} catch (e) {
|
|
530
849
|
state.loading = false;
|
|
531
850
|
state.success = false;
|
|
532
|
-
state.error = e
|
|
533
|
-
this.
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
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
|
-
|
|
886
|
+
engine.push(data);
|
|
558
887
|
} else if (type === "update") {
|
|
559
|
-
|
|
888
|
+
const idKey = data.id != null ? "id" : "_id";
|
|
889
|
+
engine.updateById(data[idKey], data, idKey);
|
|
560
890
|
} else if (type === "delete") {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
568
|
-
this.
|
|
906
|
+
this._trackingIds.get(collection).add(id);
|
|
907
|
+
this._batchNotify();
|
|
569
908
|
}
|
|
570
|
-
/**
|
|
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;
|
|
917
|
+
}
|
|
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
|
-
/**
|
|
924
|
+
/** Get snapshot of a collection (for useSyncExternalStore) */
|
|
576
925
|
getSnapshot(collection) {
|
|
577
|
-
return this.data.get(collection) || {
|
|
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) =>
|
|
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 DolphinStore = class {
|
|
|
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
|
|
|
@@ -1530,6 +1894,51 @@ function attachDOMBinding(clientProto) {
|
|
|
1530
1894
|
}
|
|
1531
1895
|
};
|
|
1532
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
|
+
}
|
|
1533
1942
|
if (parentCtx && parentCtx[prop] !== void 0) {
|
|
1534
1943
|
return parentCtx[prop];
|
|
1535
1944
|
}
|