@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.mjs CHANGED
@@ -678,7 +678,7 @@ function extractMethodFromSelector(fn) {
678
678
  return fn.__selectorMethod;
679
679
  }
680
680
 
681
- // src/state/manager.ts
681
+ // src/state/utils.ts
682
682
  function createInitialState() {
683
683
  return {
684
684
  data: void 0,
@@ -694,10 +694,13 @@ function generateSelfTagFromKey(key) {
694
694
  return void 0;
695
695
  }
696
696
  }
697
+
698
+ // src/state/manager.ts
697
699
  function createStateManager() {
698
700
  const cache = /* @__PURE__ */ new Map();
699
701
  const subscribers = /* @__PURE__ */ new Map();
700
702
  const pendingPromises = /* @__PURE__ */ new Map();
703
+ const dataChangeCallbacks = /* @__PURE__ */ new Set();
701
704
  const notifySubscribers = (key) => {
702
705
  const subs = subscribers.get(key);
703
706
  subs?.forEach((cb) => cb());
@@ -717,6 +720,7 @@ function createStateManager() {
717
720
  },
718
721
  setCache(key, entry) {
719
722
  const existing = cache.get(key);
723
+ const oldData = existing?.state.data;
720
724
  if (existing) {
721
725
  existing.state = { ...existing.state, ...entry.state };
722
726
  if (entry.tags) {
@@ -729,6 +733,10 @@ function createStateManager() {
729
733
  existing.stale = entry.stale;
730
734
  }
731
735
  notifySubscribers(key);
736
+ const newData = existing.state.data;
737
+ if (oldData !== newData) {
738
+ dataChangeCallbacks.forEach((cb) => cb(key, oldData, newData));
739
+ }
732
740
  } else {
733
741
  const newEntry = {
734
742
  state: entry.state ?? createInitialState(),
@@ -740,6 +748,10 @@ function createStateManager() {
740
748
  };
741
749
  cache.set(key, newEntry);
742
750
  notifySubscribers(key);
751
+ const newData = newEntry.state.data;
752
+ if (oldData !== newData) {
753
+ dataChangeCallbacks.forEach((cb) => cb(key, oldData, newData));
754
+ }
743
755
  }
744
756
  },
745
757
  deleteCache(key) {
@@ -844,10 +856,17 @@ function createStateManager() {
844
856
  getPendingPromise(key) {
845
857
  return pendingPromises.get(key);
846
858
  },
859
+ onDataChange(callback) {
860
+ dataChangeCallbacks.add(callback);
861
+ return () => {
862
+ dataChangeCallbacks.delete(callback);
863
+ };
864
+ },
847
865
  clear() {
848
866
  cache.clear();
849
867
  subscribers.clear();
850
868
  pendingPromises.clear();
869
+ dataChangeCallbacks.clear();
851
870
  }
852
871
  };
853
872
  }
@@ -1208,7 +1227,7 @@ function createClient(baseUrl, defaultOptions) {
1208
1227
  });
1209
1228
  }
1210
1229
 
1211
- // src/operations/controller.ts
1230
+ // src/controllers/base/controller.ts
1212
1231
  function createOperationController(options) {
1213
1232
  const {
1214
1233
  operationType,
@@ -1372,23 +1391,7 @@ function createOperationController(options) {
1372
1391
  return controller;
1373
1392
  }
1374
1393
 
1375
- // src/operations/infinite-controller.ts
1376
- function createTrackerKey(path, method, baseOptions) {
1377
- return JSON.stringify({
1378
- path,
1379
- method,
1380
- baseOptions,
1381
- type: "infinite-tracker"
1382
- });
1383
- }
1384
- function createPageKey(path, method, baseOptions, pageRequest) {
1385
- return JSON.stringify({
1386
- path,
1387
- method,
1388
- baseOptions,
1389
- pageRequest
1390
- });
1391
- }
1394
+ // src/controllers/infinite/utils.ts
1392
1395
  function shallowMergeRequest(initial, override) {
1393
1396
  return {
1394
1397
  query: override.query ? { ...initial.query, ...override.query } : initial.query,
@@ -1396,38 +1399,26 @@ function shallowMergeRequest(initial, override) {
1396
1399
  body: override.body !== void 0 ? override.body : initial.body
1397
1400
  };
1398
1401
  }
1399
- function collectPageData(pageKeys, stateManager, pageRequests, initialRequest) {
1400
- const allResponses = [];
1401
- const allRequests = [];
1402
- for (const key of pageKeys) {
1403
- const cached = stateManager.getCache(key);
1404
- if (cached?.state?.data !== void 0) {
1405
- allResponses.push(cached.state.data);
1406
- allRequests.push(pageRequests.get(key) ?? initialRequest);
1407
- }
1408
- }
1409
- return { allResponses, allRequests };
1410
- }
1411
1402
  function createInitialInfiniteState() {
1412
1403
  return {
1413
1404
  data: void 0,
1414
- allResponses: void 0,
1415
- allRequests: void 0,
1405
+ pages: [],
1416
1406
  canFetchNext: false,
1417
1407
  canFetchPrev: false,
1418
1408
  error: void 0
1419
1409
  };
1420
1410
  }
1411
+
1412
+ // src/controllers/infinite/controller.ts
1421
1413
  function createInfiniteReadController(options) {
1422
1414
  const {
1423
1415
  path,
1424
1416
  method,
1425
1417
  tags,
1426
1418
  initialRequest,
1427
- baseOptionsForKey,
1428
- canFetchNext,
1419
+ canFetchNext = () => false,
1429
1420
  canFetchPrev,
1430
- nextPageRequest,
1421
+ nextPageRequest = () => ({}),
1431
1422
  prevPageRequest,
1432
1423
  merger,
1433
1424
  stateManager,
@@ -1444,32 +1435,10 @@ function createInfiniteReadController(options) {
1444
1435
  let pluginOptions = void 0;
1445
1436
  let fetchingDirection = null;
1446
1437
  let latestError = void 0;
1438
+ let activeInitialRequest = initialRequest;
1447
1439
  let cachedState = createInitialInfiniteState();
1448
- const trackerKey = createTrackerKey(path, method, baseOptionsForKey);
1449
1440
  let pageSubscriptions = [];
1450
- let trackerSubscription = null;
1451
1441
  let refetchUnsubscribe = null;
1452
- const loadFromTracker = () => {
1453
- const cached = stateManager.getCache(trackerKey);
1454
- const trackerData = cached?.state?.data;
1455
- if (trackerData) {
1456
- pageKeys = trackerData.pageKeys;
1457
- pageRequests = new Map(Object.entries(trackerData.pageRequests));
1458
- }
1459
- };
1460
- const saveToTracker = () => {
1461
- stateManager.setCache(trackerKey, {
1462
- state: {
1463
- data: {
1464
- pageKeys,
1465
- pageRequests: Object.fromEntries(pageRequests)
1466
- },
1467
- error: void 0,
1468
- timestamp: Date.now()
1469
- },
1470
- tags
1471
- });
1472
- };
1473
1442
  const computeState = () => {
1474
1443
  if (pageKeys.length === 0) {
1475
1444
  return {
@@ -1477,41 +1446,44 @@ function createInfiniteReadController(options) {
1477
1446
  error: latestError
1478
1447
  };
1479
1448
  }
1480
- const { allResponses, allRequests } = collectPageData(
1481
- pageKeys,
1482
- stateManager,
1483
- pageRequests,
1484
- initialRequest
1449
+ const computedPages = pageKeys.map(
1450
+ (key) => {
1451
+ const cached = stateManager.getCache(key);
1452
+ const meta = cached?.meta ? Object.fromEntries(cached.meta) : void 0;
1453
+ const input = pageRequests.get(key) ?? activeInitialRequest;
1454
+ let status = "pending";
1455
+ if (pendingFetches.has(key)) {
1456
+ status = "loading";
1457
+ } else if (cached?.state?.error) {
1458
+ status = "error";
1459
+ } else if (cached?.state?.data !== void 0) {
1460
+ status = cached?.stale ? "stale" : "success";
1461
+ }
1462
+ return {
1463
+ status,
1464
+ data: cached?.state?.data,
1465
+ error: cached?.state?.error,
1466
+ meta,
1467
+ input
1468
+ };
1469
+ }
1485
1470
  );
1486
- if (allResponses.length === 0) {
1487
- return {
1488
- data: void 0,
1489
- allResponses: void 0,
1490
- allRequests: void 0,
1491
- canFetchNext: false,
1492
- canFetchPrev: false,
1493
- error: latestError
1494
- };
1495
- }
1496
- const lastResponse = allResponses.at(-1);
1497
- const firstResponse = allResponses.at(0);
1498
- const lastRequest = allRequests.at(-1) ?? initialRequest;
1499
- const firstRequest = allRequests.at(0) ?? initialRequest;
1471
+ const lastPage = computedPages.at(-1);
1472
+ const firstPage = computedPages.at(0);
1500
1473
  const canNext = canFetchNext({
1501
- response: lastResponse,
1502
- allResponses,
1503
- request: lastRequest
1474
+ lastPage,
1475
+ pages: computedPages,
1476
+ request: lastPage?.input ?? activeInitialRequest
1504
1477
  });
1505
1478
  const canPrev = canFetchPrev ? canFetchPrev({
1506
- response: firstResponse,
1507
- allResponses,
1508
- request: firstRequest
1479
+ firstPage,
1480
+ pages: computedPages,
1481
+ request: firstPage?.input ?? activeInitialRequest
1509
1482
  }) : false;
1510
- const mergedData = merger(allResponses);
1483
+ const mergedData = computedPages.length > 0 ? merger(computedPages) : void 0;
1511
1484
  return {
1512
1485
  data: mergedData,
1513
- allResponses,
1514
- allRequests,
1486
+ pages: computedPages,
1515
1487
  canFetchNext: canNext,
1516
1488
  canFetchPrev: canPrev,
1517
1489
  error: latestError
@@ -1529,7 +1501,7 @@ function createInfiniteReadController(options) {
1529
1501
  };
1530
1502
  const createContext = (pageKey, requestOptions) => {
1531
1503
  return pluginExecutor.createContext({
1532
- operationType: "infiniteRead",
1504
+ operationType: "pages",
1533
1505
  path,
1534
1506
  method,
1535
1507
  queryKey: pageKey,
@@ -1549,13 +1521,15 @@ function createInfiniteReadController(options) {
1549
1521
  });
1550
1522
  };
1551
1523
  const doFetch = async (direction, requestOverride) => {
1552
- const mergedRequest = shallowMergeRequest(initialRequest, requestOverride);
1553
- const pageKey = createPageKey(
1524
+ const mergedRequest = shallowMergeRequest(
1525
+ activeInitialRequest,
1526
+ requestOverride
1527
+ );
1528
+ const pageKey = stateManager.createQueryKey({
1554
1529
  path,
1555
1530
  method,
1556
- baseOptionsForKey,
1557
- mergedRequest
1558
- );
1531
+ options: mergedRequest
1532
+ });
1559
1533
  const pendingPromise = stateManager.getPendingPromise(pageKey);
1560
1534
  if (pendingPromise || pendingFetches.has(pageKey)) {
1561
1535
  return;
@@ -1569,7 +1543,7 @@ function createInfiniteReadController(options) {
1569
1543
  const context = createContext(pageKey, mergedRequest);
1570
1544
  const coreFetch = async () => {
1571
1545
  try {
1572
- const response = await fetchFn(mergedRequest, signal);
1546
+ const response = await fetchFn(context.request, signal);
1573
1547
  if (signal.aborted) {
1574
1548
  return {
1575
1549
  status: 0,
@@ -1596,7 +1570,7 @@ function createInfiniteReadController(options) {
1596
1570
  }
1597
1571
  };
1598
1572
  const middlewarePromise = pluginExecutor.executeMiddleware(
1599
- "infiniteRead",
1573
+ "pages",
1600
1574
  context,
1601
1575
  coreFetch
1602
1576
  );
@@ -1617,7 +1591,6 @@ function createInfiniteReadController(options) {
1617
1591
  pageKeys = [pageKey, ...pageKeys];
1618
1592
  }
1619
1593
  }
1620
- saveToTracker();
1621
1594
  subscribeToPages();
1622
1595
  stateManager.setCache(pageKey, {
1623
1596
  state: {
@@ -1649,112 +1622,190 @@ function createInfiniteReadController(options) {
1649
1622
  await doFetch("next", {});
1650
1623
  return;
1651
1624
  }
1652
- const { allResponses, allRequests } = collectPageData(
1653
- pageKeys,
1654
- stateManager,
1655
- pageRequests,
1656
- initialRequest
1657
- );
1658
- if (allResponses.length === 0) return;
1659
- const lastResponse = allResponses.at(-1);
1660
- const lastRequest = allRequests.at(-1) ?? initialRequest;
1625
+ const state = computeState();
1626
+ const { pages } = state;
1627
+ if (pages.length === 0) return;
1628
+ const lastPage = pages.at(-1);
1661
1629
  const canNext = canFetchNext({
1662
- response: lastResponse,
1663
- allResponses,
1664
- request: lastRequest
1630
+ lastPage,
1631
+ pages,
1632
+ request: lastPage?.input ?? activeInitialRequest
1665
1633
  });
1666
1634
  if (!canNext) return;
1667
1635
  const nextRequest = nextPageRequest({
1668
- response: lastResponse,
1669
- allResponses,
1670
- request: lastRequest
1636
+ lastPage,
1637
+ pages,
1638
+ request: lastPage?.input ?? activeInitialRequest
1671
1639
  });
1672
1640
  await doFetch("next", nextRequest);
1673
1641
  },
1674
1642
  async fetchPrev() {
1675
1643
  if (!canFetchPrev || !prevPageRequest) return;
1676
1644
  if (pageKeys.length === 0) return;
1677
- const { allResponses, allRequests } = collectPageData(
1678
- pageKeys,
1679
- stateManager,
1680
- pageRequests,
1681
- initialRequest
1682
- );
1683
- if (allResponses.length === 0) return;
1684
- const firstResponse = allResponses.at(0);
1685
- const firstRequest = allRequests.at(0) ?? initialRequest;
1645
+ const state = computeState();
1646
+ const { pages } = state;
1647
+ if (pages.length === 0) return;
1648
+ const firstPage = pages.at(0);
1686
1649
  const canPrev = canFetchPrev({
1687
- response: firstResponse,
1688
- allResponses,
1689
- request: firstRequest
1650
+ firstPage,
1651
+ pages,
1652
+ request: firstPage?.input ?? activeInitialRequest
1690
1653
  });
1691
1654
  if (!canPrev) return;
1692
1655
  const prevRequest = prevPageRequest({
1693
- response: firstResponse,
1694
- allResponses,
1695
- request: firstRequest
1656
+ firstPage,
1657
+ pages,
1658
+ request: firstPage?.input ?? activeInitialRequest
1696
1659
  });
1697
1660
  await doFetch("prev", prevRequest);
1698
1661
  },
1699
- async refetch() {
1662
+ async trigger(options2) {
1663
+ const { force = true, ...requestOverride } = options2 ?? {};
1664
+ if (abortController) {
1665
+ abortController.abort();
1666
+ abortController = null;
1667
+ }
1700
1668
  for (const key of pageKeys) {
1701
- stateManager.deleteCache(key);
1669
+ stateManager.setPendingPromise(key, void 0);
1702
1670
  }
1703
- pageKeys = [];
1704
- pageRequests.clear();
1671
+ if (force) {
1672
+ const allPathCaches = stateManager.getCacheEntriesBySelfTag(path);
1673
+ for (const { key } of allPathCaches) {
1674
+ stateManager.setCache(key, { stale: true });
1675
+ }
1676
+ }
1677
+ pendingFetches.clear();
1678
+ fetchingDirection = null;
1679
+ if (requestOverride && Object.keys(requestOverride).length > 0) {
1680
+ activeInitialRequest = shallowMergeRequest(
1681
+ initialRequest,
1682
+ requestOverride
1683
+ );
1684
+ } else {
1685
+ activeInitialRequest = initialRequest;
1686
+ }
1687
+ const newFirstPageKey = stateManager.createQueryKey({
1688
+ path,
1689
+ method,
1690
+ options: activeInitialRequest
1691
+ });
1705
1692
  pageSubscriptions.forEach((unsub) => unsub());
1706
1693
  pageSubscriptions = [];
1694
+ pageKeys = [];
1695
+ pageRequests = /* @__PURE__ */ new Map();
1707
1696
  latestError = void 0;
1708
- saveToTracker();
1709
1697
  fetchingDirection = "next";
1710
1698
  notify();
1711
- await doFetch("next", {});
1699
+ abortController = new AbortController();
1700
+ const signal = abortController.signal;
1701
+ const context = createContext(newFirstPageKey, activeInitialRequest);
1702
+ if (force) {
1703
+ context.forceRefetch = true;
1704
+ }
1705
+ const coreFetch = async () => {
1706
+ try {
1707
+ const response = await fetchFn(context.request, signal);
1708
+ if (signal.aborted) {
1709
+ return {
1710
+ status: 0,
1711
+ data: void 0,
1712
+ aborted: true
1713
+ };
1714
+ }
1715
+ return response;
1716
+ } catch (err) {
1717
+ if (signal.aborted) {
1718
+ return {
1719
+ status: 0,
1720
+ data: void 0,
1721
+ aborted: true
1722
+ };
1723
+ }
1724
+ return {
1725
+ status: 0,
1726
+ error: err,
1727
+ data: void 0
1728
+ };
1729
+ }
1730
+ };
1731
+ const middlewarePromise = pluginExecutor.executeMiddleware(
1732
+ "pages",
1733
+ context,
1734
+ coreFetch
1735
+ );
1736
+ stateManager.setPendingPromise(newFirstPageKey, middlewarePromise);
1737
+ const finalResponse = await middlewarePromise;
1738
+ pendingFetches.delete(newFirstPageKey);
1739
+ fetchingDirection = null;
1740
+ stateManager.setPendingPromise(newFirstPageKey, void 0);
1741
+ if (finalResponse.data !== void 0 && !finalResponse.error) {
1742
+ pageKeys = [newFirstPageKey];
1743
+ pageRequests = /* @__PURE__ */ new Map([[newFirstPageKey, activeInitialRequest]]);
1744
+ stateManager.setCache(newFirstPageKey, {
1745
+ state: {
1746
+ data: finalResponse.data,
1747
+ error: void 0,
1748
+ timestamp: Date.now()
1749
+ },
1750
+ tags,
1751
+ stale: false
1752
+ });
1753
+ subscribeToPages();
1754
+ latestError = void 0;
1755
+ } else if (finalResponse.error) {
1756
+ latestError = finalResponse.error;
1757
+ }
1758
+ notify();
1712
1759
  },
1713
1760
  abort() {
1714
1761
  abortController?.abort();
1715
1762
  abortController = null;
1716
1763
  },
1717
1764
  mount() {
1718
- loadFromTracker();
1719
1765
  cachedState = computeState();
1720
1766
  subscribeToPages();
1721
- trackerSubscription = stateManager.subscribeCache(trackerKey, notify);
1722
- const context = createContext(trackerKey, initialRequest);
1723
- pluginExecutor.executeLifecycle("onMount", "infiniteRead", context);
1767
+ const firstPageKey = stateManager.createQueryKey({
1768
+ path,
1769
+ method,
1770
+ options: initialRequest
1771
+ });
1772
+ const context = createContext(firstPageKey, initialRequest);
1773
+ pluginExecutor.executeLifecycle("onMount", "pages", context);
1724
1774
  refetchUnsubscribe = eventEmitter.on("refetch", (event) => {
1725
- const isRelevant = event.queryKey === trackerKey || pageKeys.includes(event.queryKey);
1726
- if (isRelevant) {
1727
- controller.refetch();
1775
+ if (pageKeys.includes(event.queryKey)) {
1776
+ controller.trigger();
1728
1777
  }
1729
1778
  });
1730
- const isStale = pageKeys.some((key) => {
1731
- const cached = stateManager.getCache(key);
1732
- return cached?.stale === true;
1733
- });
1734
- if (isStale) {
1735
- controller.refetch();
1736
- }
1737
1779
  },
1738
1780
  unmount() {
1739
- const context = createContext(trackerKey, initialRequest);
1740
- pluginExecutor.executeLifecycle("onUnmount", "infiniteRead", context);
1781
+ const firstPageKey = stateManager.createQueryKey({
1782
+ path,
1783
+ method,
1784
+ options: initialRequest
1785
+ });
1786
+ const context = createContext(firstPageKey, initialRequest);
1787
+ pluginExecutor.executeLifecycle("onUnmount", "pages", context);
1741
1788
  pageSubscriptions.forEach((unsub) => unsub());
1742
1789
  pageSubscriptions = [];
1743
- trackerSubscription?.();
1744
- trackerSubscription = null;
1745
1790
  refetchUnsubscribe?.();
1746
1791
  refetchUnsubscribe = null;
1747
1792
  },
1748
1793
  update(previousContext) {
1749
- const context = createContext(trackerKey, initialRequest);
1750
- pluginExecutor.executeUpdateLifecycle(
1751
- "infiniteRead",
1752
- context,
1753
- previousContext
1754
- );
1794
+ const firstPageKey = stateManager.createQueryKey({
1795
+ path,
1796
+ method,
1797
+ options: activeInitialRequest
1798
+ });
1799
+ const context = createContext(firstPageKey, activeInitialRequest);
1800
+ pluginExecutor.executeUpdateLifecycle("pages", context, previousContext);
1755
1801
  },
1756
1802
  getContext() {
1757
- return createContext(trackerKey, initialRequest);
1803
+ const firstPageKey = stateManager.createQueryKey({
1804
+ path,
1805
+ method,
1806
+ options: activeInitialRequest
1807
+ });
1808
+ return createContext(firstPageKey, activeInitialRequest);
1758
1809
  },
1759
1810
  setPluginOptions(opts) {
1760
1811
  pluginOptions = opts;
@@ -1763,7 +1814,7 @@ function createInfiniteReadController(options) {
1763
1814
  return controller;
1764
1815
  }
1765
1816
 
1766
- // src/queue/semaphore.ts
1817
+ // src/controllers/queue/semaphore.ts
1767
1818
  var Semaphore = class {
1768
1819
  constructor(max) {
1769
1820
  this.max = max;
@@ -1812,7 +1863,7 @@ var Semaphore = class {
1812
1863
  }
1813
1864
  };
1814
1865
 
1815
- // src/queue/controller.ts
1866
+ // src/controllers/queue/controller.ts
1816
1867
  var DEFAULT_CONCURRENCY = 3;
1817
1868
  function createQueueController(config, context) {
1818
1869
  const { path, method, operationType, hookOptions = {} } = config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/core",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "license": "MIT",
5
5
  "description": "Type-safe API toolkit with plugin middleware system",
6
6
  "keywords": [