@spoosh/core 0.14.0 → 0.15.0

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
@@ -747,7 +747,7 @@ function extractMethodFromSelector(fn) {
747
747
  return fn.__selectorMethod;
748
748
  }
749
749
 
750
- // src/state/manager.ts
750
+ // src/state/utils.ts
751
751
  function createInitialState() {
752
752
  return {
753
753
  data: void 0,
@@ -763,10 +763,13 @@ function generateSelfTagFromKey(key) {
763
763
  return void 0;
764
764
  }
765
765
  }
766
+
767
+ // src/state/manager.ts
766
768
  function createStateManager() {
767
769
  const cache = /* @__PURE__ */ new Map();
768
770
  const subscribers = /* @__PURE__ */ new Map();
769
771
  const pendingPromises = /* @__PURE__ */ new Map();
772
+ const dataChangeCallbacks = /* @__PURE__ */ new Set();
770
773
  const notifySubscribers = (key) => {
771
774
  const subs = subscribers.get(key);
772
775
  subs?.forEach((cb) => cb());
@@ -786,6 +789,7 @@ function createStateManager() {
786
789
  },
787
790
  setCache(key, entry) {
788
791
  const existing = cache.get(key);
792
+ const oldData = existing?.state.data;
789
793
  if (existing) {
790
794
  existing.state = { ...existing.state, ...entry.state };
791
795
  if (entry.tags) {
@@ -798,6 +802,10 @@ function createStateManager() {
798
802
  existing.stale = entry.stale;
799
803
  }
800
804
  notifySubscribers(key);
805
+ const newData = existing.state.data;
806
+ if (oldData !== newData) {
807
+ dataChangeCallbacks.forEach((cb) => cb(key, oldData, newData));
808
+ }
801
809
  } else {
802
810
  const newEntry = {
803
811
  state: entry.state ?? createInitialState(),
@@ -809,6 +817,10 @@ function createStateManager() {
809
817
  };
810
818
  cache.set(key, newEntry);
811
819
  notifySubscribers(key);
820
+ const newData = newEntry.state.data;
821
+ if (oldData !== newData) {
822
+ dataChangeCallbacks.forEach((cb) => cb(key, oldData, newData));
823
+ }
812
824
  }
813
825
  },
814
826
  deleteCache(key) {
@@ -913,10 +925,17 @@ function createStateManager() {
913
925
  getPendingPromise(key) {
914
926
  return pendingPromises.get(key);
915
927
  },
928
+ onDataChange(callback) {
929
+ dataChangeCallbacks.add(callback);
930
+ return () => {
931
+ dataChangeCallbacks.delete(callback);
932
+ };
933
+ },
916
934
  clear() {
917
935
  cache.clear();
918
936
  subscribers.clear();
919
937
  pendingPromises.clear();
938
+ dataChangeCallbacks.clear();
920
939
  }
921
940
  };
922
941
  }
@@ -1277,7 +1296,7 @@ function createClient(baseUrl, defaultOptions) {
1277
1296
  });
1278
1297
  }
1279
1298
 
1280
- // src/operations/controller.ts
1299
+ // src/controllers/base/controller.ts
1281
1300
  function createOperationController(options) {
1282
1301
  const {
1283
1302
  operationType,
@@ -1441,23 +1460,7 @@ function createOperationController(options) {
1441
1460
  return controller;
1442
1461
  }
1443
1462
 
1444
- // src/operations/infinite-controller.ts
1445
- function createTrackerKey(path, method, baseOptions) {
1446
- return JSON.stringify({
1447
- path,
1448
- method,
1449
- baseOptions,
1450
- type: "infinite-tracker"
1451
- });
1452
- }
1453
- function createPageKey(path, method, baseOptions, pageRequest) {
1454
- return JSON.stringify({
1455
- path,
1456
- method,
1457
- baseOptions,
1458
- pageRequest
1459
- });
1460
- }
1463
+ // src/controllers/infinite/utils.ts
1461
1464
  function shallowMergeRequest(initial, override) {
1462
1465
  return {
1463
1466
  query: override.query ? { ...initial.query, ...override.query } : initial.query,
@@ -1465,38 +1468,26 @@ function shallowMergeRequest(initial, override) {
1465
1468
  body: override.body !== void 0 ? override.body : initial.body
1466
1469
  };
1467
1470
  }
1468
- function collectPageData(pageKeys, stateManager, pageRequests, initialRequest) {
1469
- const allResponses = [];
1470
- const allRequests = [];
1471
- for (const key of pageKeys) {
1472
- const cached = stateManager.getCache(key);
1473
- if (cached?.state?.data !== void 0) {
1474
- allResponses.push(cached.state.data);
1475
- allRequests.push(pageRequests.get(key) ?? initialRequest);
1476
- }
1477
- }
1478
- return { allResponses, allRequests };
1479
- }
1480
1471
  function createInitialInfiniteState() {
1481
1472
  return {
1482
1473
  data: void 0,
1483
- allResponses: void 0,
1484
- allRequests: void 0,
1474
+ pages: [],
1485
1475
  canFetchNext: false,
1486
1476
  canFetchPrev: false,
1487
1477
  error: void 0
1488
1478
  };
1489
1479
  }
1480
+
1481
+ // src/controllers/infinite/controller.ts
1490
1482
  function createInfiniteReadController(options) {
1491
1483
  const {
1492
1484
  path,
1493
1485
  method,
1494
1486
  tags,
1495
1487
  initialRequest,
1496
- baseOptionsForKey,
1497
- canFetchNext,
1488
+ canFetchNext = () => false,
1498
1489
  canFetchPrev,
1499
- nextPageRequest,
1490
+ nextPageRequest = () => ({}),
1500
1491
  prevPageRequest,
1501
1492
  merger,
1502
1493
  stateManager,
@@ -1513,32 +1504,10 @@ function createInfiniteReadController(options) {
1513
1504
  let pluginOptions = void 0;
1514
1505
  let fetchingDirection = null;
1515
1506
  let latestError = void 0;
1507
+ let activeInitialRequest = initialRequest;
1516
1508
  let cachedState = createInitialInfiniteState();
1517
- const trackerKey = createTrackerKey(path, method, baseOptionsForKey);
1518
1509
  let pageSubscriptions = [];
1519
- let trackerSubscription = null;
1520
1510
  let refetchUnsubscribe = null;
1521
- const loadFromTracker = () => {
1522
- const cached = stateManager.getCache(trackerKey);
1523
- const trackerData = cached?.state?.data;
1524
- if (trackerData) {
1525
- pageKeys = trackerData.pageKeys;
1526
- pageRequests = new Map(Object.entries(trackerData.pageRequests));
1527
- }
1528
- };
1529
- const saveToTracker = () => {
1530
- stateManager.setCache(trackerKey, {
1531
- state: {
1532
- data: {
1533
- pageKeys,
1534
- pageRequests: Object.fromEntries(pageRequests)
1535
- },
1536
- error: void 0,
1537
- timestamp: Date.now()
1538
- },
1539
- tags
1540
- });
1541
- };
1542
1511
  const computeState = () => {
1543
1512
  if (pageKeys.length === 0) {
1544
1513
  return {
@@ -1546,41 +1515,44 @@ function createInfiniteReadController(options) {
1546
1515
  error: latestError
1547
1516
  };
1548
1517
  }
1549
- const { allResponses, allRequests } = collectPageData(
1550
- pageKeys,
1551
- stateManager,
1552
- pageRequests,
1553
- initialRequest
1518
+ const computedPages = pageKeys.map(
1519
+ (key) => {
1520
+ const cached = stateManager.getCache(key);
1521
+ const meta = cached?.meta ? Object.fromEntries(cached.meta) : void 0;
1522
+ const input = pageRequests.get(key) ?? activeInitialRequest;
1523
+ let status = "pending";
1524
+ if (pendingFetches.has(key)) {
1525
+ status = "loading";
1526
+ } else if (cached?.state?.error) {
1527
+ status = "error";
1528
+ } else if (cached?.state?.data !== void 0) {
1529
+ status = cached?.stale ? "stale" : "success";
1530
+ }
1531
+ return {
1532
+ status,
1533
+ data: cached?.state?.data,
1534
+ error: cached?.state?.error,
1535
+ meta,
1536
+ input
1537
+ };
1538
+ }
1554
1539
  );
1555
- if (allResponses.length === 0) {
1556
- return {
1557
- data: void 0,
1558
- allResponses: void 0,
1559
- allRequests: void 0,
1560
- canFetchNext: false,
1561
- canFetchPrev: false,
1562
- error: latestError
1563
- };
1564
- }
1565
- const lastResponse = allResponses.at(-1);
1566
- const firstResponse = allResponses.at(0);
1567
- const lastRequest = allRequests.at(-1) ?? initialRequest;
1568
- const firstRequest = allRequests.at(0) ?? initialRequest;
1540
+ const lastPage = computedPages.at(-1);
1541
+ const firstPage = computedPages.at(0);
1569
1542
  const canNext = canFetchNext({
1570
- response: lastResponse,
1571
- allResponses,
1572
- request: lastRequest
1543
+ lastPage,
1544
+ pages: computedPages,
1545
+ request: lastPage?.input ?? activeInitialRequest
1573
1546
  });
1574
1547
  const canPrev = canFetchPrev ? canFetchPrev({
1575
- response: firstResponse,
1576
- allResponses,
1577
- request: firstRequest
1548
+ firstPage,
1549
+ pages: computedPages,
1550
+ request: firstPage?.input ?? activeInitialRequest
1578
1551
  }) : false;
1579
- const mergedData = merger(allResponses);
1552
+ const mergedData = computedPages.length > 0 ? merger(computedPages) : void 0;
1580
1553
  return {
1581
1554
  data: mergedData,
1582
- allResponses,
1583
- allRequests,
1555
+ pages: computedPages,
1584
1556
  canFetchNext: canNext,
1585
1557
  canFetchPrev: canPrev,
1586
1558
  error: latestError
@@ -1598,7 +1570,7 @@ function createInfiniteReadController(options) {
1598
1570
  };
1599
1571
  const createContext = (pageKey, requestOptions) => {
1600
1572
  return pluginExecutor.createContext({
1601
- operationType: "infiniteRead",
1573
+ operationType: "pages",
1602
1574
  path,
1603
1575
  method,
1604
1576
  queryKey: pageKey,
@@ -1618,13 +1590,15 @@ function createInfiniteReadController(options) {
1618
1590
  });
1619
1591
  };
1620
1592
  const doFetch = async (direction, requestOverride) => {
1621
- const mergedRequest = shallowMergeRequest(initialRequest, requestOverride);
1622
- const pageKey = createPageKey(
1593
+ const mergedRequest = shallowMergeRequest(
1594
+ activeInitialRequest,
1595
+ requestOverride
1596
+ );
1597
+ const pageKey = stateManager.createQueryKey({
1623
1598
  path,
1624
1599
  method,
1625
- baseOptionsForKey,
1626
- mergedRequest
1627
- );
1600
+ options: mergedRequest
1601
+ });
1628
1602
  const pendingPromise = stateManager.getPendingPromise(pageKey);
1629
1603
  if (pendingPromise || pendingFetches.has(pageKey)) {
1630
1604
  return;
@@ -1638,7 +1612,7 @@ function createInfiniteReadController(options) {
1638
1612
  const context = createContext(pageKey, mergedRequest);
1639
1613
  const coreFetch = async () => {
1640
1614
  try {
1641
- const response = await fetchFn(mergedRequest, signal);
1615
+ const response = await fetchFn(context.request, signal);
1642
1616
  if (signal.aborted) {
1643
1617
  return {
1644
1618
  status: 0,
@@ -1665,7 +1639,7 @@ function createInfiniteReadController(options) {
1665
1639
  }
1666
1640
  };
1667
1641
  const middlewarePromise = pluginExecutor.executeMiddleware(
1668
- "infiniteRead",
1642
+ "pages",
1669
1643
  context,
1670
1644
  coreFetch
1671
1645
  );
@@ -1686,7 +1660,6 @@ function createInfiniteReadController(options) {
1686
1660
  pageKeys = [pageKey, ...pageKeys];
1687
1661
  }
1688
1662
  }
1689
- saveToTracker();
1690
1663
  subscribeToPages();
1691
1664
  stateManager.setCache(pageKey, {
1692
1665
  state: {
@@ -1718,112 +1691,190 @@ function createInfiniteReadController(options) {
1718
1691
  await doFetch("next", {});
1719
1692
  return;
1720
1693
  }
1721
- const { allResponses, allRequests } = collectPageData(
1722
- pageKeys,
1723
- stateManager,
1724
- pageRequests,
1725
- initialRequest
1726
- );
1727
- if (allResponses.length === 0) return;
1728
- const lastResponse = allResponses.at(-1);
1729
- const lastRequest = allRequests.at(-1) ?? initialRequest;
1694
+ const state = computeState();
1695
+ const { pages } = state;
1696
+ if (pages.length === 0) return;
1697
+ const lastPage = pages.at(-1);
1730
1698
  const canNext = canFetchNext({
1731
- response: lastResponse,
1732
- allResponses,
1733
- request: lastRequest
1699
+ lastPage,
1700
+ pages,
1701
+ request: lastPage?.input ?? activeInitialRequest
1734
1702
  });
1735
1703
  if (!canNext) return;
1736
1704
  const nextRequest = nextPageRequest({
1737
- response: lastResponse,
1738
- allResponses,
1739
- request: lastRequest
1705
+ lastPage,
1706
+ pages,
1707
+ request: lastPage?.input ?? activeInitialRequest
1740
1708
  });
1741
1709
  await doFetch("next", nextRequest);
1742
1710
  },
1743
1711
  async fetchPrev() {
1744
1712
  if (!canFetchPrev || !prevPageRequest) return;
1745
1713
  if (pageKeys.length === 0) return;
1746
- const { allResponses, allRequests } = collectPageData(
1747
- pageKeys,
1748
- stateManager,
1749
- pageRequests,
1750
- initialRequest
1751
- );
1752
- if (allResponses.length === 0) return;
1753
- const firstResponse = allResponses.at(0);
1754
- const firstRequest = allRequests.at(0) ?? initialRequest;
1714
+ const state = computeState();
1715
+ const { pages } = state;
1716
+ if (pages.length === 0) return;
1717
+ const firstPage = pages.at(0);
1755
1718
  const canPrev = canFetchPrev({
1756
- response: firstResponse,
1757
- allResponses,
1758
- request: firstRequest
1719
+ firstPage,
1720
+ pages,
1721
+ request: firstPage?.input ?? activeInitialRequest
1759
1722
  });
1760
1723
  if (!canPrev) return;
1761
1724
  const prevRequest = prevPageRequest({
1762
- response: firstResponse,
1763
- allResponses,
1764
- request: firstRequest
1725
+ firstPage,
1726
+ pages,
1727
+ request: firstPage?.input ?? activeInitialRequest
1765
1728
  });
1766
1729
  await doFetch("prev", prevRequest);
1767
1730
  },
1768
- async refetch() {
1731
+ async trigger(options2) {
1732
+ const { force = true, ...requestOverride } = options2 ?? {};
1733
+ if (abortController) {
1734
+ abortController.abort();
1735
+ abortController = null;
1736
+ }
1769
1737
  for (const key of pageKeys) {
1770
- stateManager.deleteCache(key);
1738
+ stateManager.setPendingPromise(key, void 0);
1771
1739
  }
1772
- pageKeys = [];
1773
- pageRequests.clear();
1740
+ if (force) {
1741
+ const allPathCaches = stateManager.getCacheEntriesBySelfTag(path);
1742
+ for (const { key } of allPathCaches) {
1743
+ stateManager.setCache(key, { stale: true });
1744
+ }
1745
+ }
1746
+ pendingFetches.clear();
1747
+ fetchingDirection = null;
1748
+ if (requestOverride && Object.keys(requestOverride).length > 0) {
1749
+ activeInitialRequest = shallowMergeRequest(
1750
+ initialRequest,
1751
+ requestOverride
1752
+ );
1753
+ } else {
1754
+ activeInitialRequest = initialRequest;
1755
+ }
1756
+ const newFirstPageKey = stateManager.createQueryKey({
1757
+ path,
1758
+ method,
1759
+ options: activeInitialRequest
1760
+ });
1774
1761
  pageSubscriptions.forEach((unsub) => unsub());
1775
1762
  pageSubscriptions = [];
1763
+ pageKeys = [];
1764
+ pageRequests = /* @__PURE__ */ new Map();
1776
1765
  latestError = void 0;
1777
- saveToTracker();
1778
1766
  fetchingDirection = "next";
1779
1767
  notify();
1780
- await doFetch("next", {});
1768
+ abortController = new AbortController();
1769
+ const signal = abortController.signal;
1770
+ const context = createContext(newFirstPageKey, activeInitialRequest);
1771
+ if (force) {
1772
+ context.forceRefetch = true;
1773
+ }
1774
+ const coreFetch = async () => {
1775
+ try {
1776
+ const response = await fetchFn(context.request, signal);
1777
+ if (signal.aborted) {
1778
+ return {
1779
+ status: 0,
1780
+ data: void 0,
1781
+ aborted: true
1782
+ };
1783
+ }
1784
+ return response;
1785
+ } catch (err) {
1786
+ if (signal.aborted) {
1787
+ return {
1788
+ status: 0,
1789
+ data: void 0,
1790
+ aborted: true
1791
+ };
1792
+ }
1793
+ return {
1794
+ status: 0,
1795
+ error: err,
1796
+ data: void 0
1797
+ };
1798
+ }
1799
+ };
1800
+ const middlewarePromise = pluginExecutor.executeMiddleware(
1801
+ "pages",
1802
+ context,
1803
+ coreFetch
1804
+ );
1805
+ stateManager.setPendingPromise(newFirstPageKey, middlewarePromise);
1806
+ const finalResponse = await middlewarePromise;
1807
+ pendingFetches.delete(newFirstPageKey);
1808
+ fetchingDirection = null;
1809
+ stateManager.setPendingPromise(newFirstPageKey, void 0);
1810
+ if (finalResponse.data !== void 0 && !finalResponse.error) {
1811
+ pageKeys = [newFirstPageKey];
1812
+ pageRequests = /* @__PURE__ */ new Map([[newFirstPageKey, activeInitialRequest]]);
1813
+ stateManager.setCache(newFirstPageKey, {
1814
+ state: {
1815
+ data: finalResponse.data,
1816
+ error: void 0,
1817
+ timestamp: Date.now()
1818
+ },
1819
+ tags,
1820
+ stale: false
1821
+ });
1822
+ subscribeToPages();
1823
+ latestError = void 0;
1824
+ } else if (finalResponse.error) {
1825
+ latestError = finalResponse.error;
1826
+ }
1827
+ notify();
1781
1828
  },
1782
1829
  abort() {
1783
1830
  abortController?.abort();
1784
1831
  abortController = null;
1785
1832
  },
1786
1833
  mount() {
1787
- loadFromTracker();
1788
1834
  cachedState = computeState();
1789
1835
  subscribeToPages();
1790
- trackerSubscription = stateManager.subscribeCache(trackerKey, notify);
1791
- const context = createContext(trackerKey, initialRequest);
1792
- pluginExecutor.executeLifecycle("onMount", "infiniteRead", context);
1836
+ const firstPageKey = stateManager.createQueryKey({
1837
+ path,
1838
+ method,
1839
+ options: initialRequest
1840
+ });
1841
+ const context = createContext(firstPageKey, initialRequest);
1842
+ pluginExecutor.executeLifecycle("onMount", "pages", context);
1793
1843
  refetchUnsubscribe = eventEmitter.on("refetch", (event) => {
1794
- const isRelevant = event.queryKey === trackerKey || pageKeys.includes(event.queryKey);
1795
- if (isRelevant) {
1796
- controller.refetch();
1844
+ if (pageKeys.includes(event.queryKey)) {
1845
+ controller.trigger();
1797
1846
  }
1798
1847
  });
1799
- const isStale = pageKeys.some((key) => {
1800
- const cached = stateManager.getCache(key);
1801
- return cached?.stale === true;
1802
- });
1803
- if (isStale) {
1804
- controller.refetch();
1805
- }
1806
1848
  },
1807
1849
  unmount() {
1808
- const context = createContext(trackerKey, initialRequest);
1809
- pluginExecutor.executeLifecycle("onUnmount", "infiniteRead", context);
1850
+ const firstPageKey = stateManager.createQueryKey({
1851
+ path,
1852
+ method,
1853
+ options: initialRequest
1854
+ });
1855
+ const context = createContext(firstPageKey, initialRequest);
1856
+ pluginExecutor.executeLifecycle("onUnmount", "pages", context);
1810
1857
  pageSubscriptions.forEach((unsub) => unsub());
1811
1858
  pageSubscriptions = [];
1812
- trackerSubscription?.();
1813
- trackerSubscription = null;
1814
1859
  refetchUnsubscribe?.();
1815
1860
  refetchUnsubscribe = null;
1816
1861
  },
1817
1862
  update(previousContext) {
1818
- const context = createContext(trackerKey, initialRequest);
1819
- pluginExecutor.executeUpdateLifecycle(
1820
- "infiniteRead",
1821
- context,
1822
- previousContext
1823
- );
1863
+ const firstPageKey = stateManager.createQueryKey({
1864
+ path,
1865
+ method,
1866
+ options: activeInitialRequest
1867
+ });
1868
+ const context = createContext(firstPageKey, activeInitialRequest);
1869
+ pluginExecutor.executeUpdateLifecycle("pages", context, previousContext);
1824
1870
  },
1825
1871
  getContext() {
1826
- return createContext(trackerKey, initialRequest);
1872
+ const firstPageKey = stateManager.createQueryKey({
1873
+ path,
1874
+ method,
1875
+ options: activeInitialRequest
1876
+ });
1877
+ return createContext(firstPageKey, activeInitialRequest);
1827
1878
  },
1828
1879
  setPluginOptions(opts) {
1829
1880
  pluginOptions = opts;
@@ -1832,7 +1883,7 @@ function createInfiniteReadController(options) {
1832
1883
  return controller;
1833
1884
  }
1834
1885
 
1835
- // src/queue/semaphore.ts
1886
+ // src/controllers/queue/semaphore.ts
1836
1887
  var Semaphore = class {
1837
1888
  constructor(max) {
1838
1889
  this.max = max;
@@ -1881,7 +1932,7 @@ var Semaphore = class {
1881
1932
  }
1882
1933
  };
1883
1934
 
1884
- // src/queue/controller.ts
1935
+ // src/controllers/queue/controller.ts
1885
1936
  var DEFAULT_CONCURRENCY = 3;
1886
1937
  function createQueueController(config, context) {
1887
1938
  const { path, method, operationType, hookOptions = {} } = config;