From fa8b48d1e996a9ae746e42320e66228b229b2c40 Mon Sep 17 00:00:00 2001 From: Andrea Scartabelli Date: Tue, 14 May 2024 15:44:48 +0200 Subject: [PATCH] explorer: Add a `reset` method to data stores - the `dataStore.getData` method now starts a new call and ignore the previous one - the `pollingDataStore.start` method now starts a new poll and ignore the previous one - updated `StatisticsPanel` to accomodate the new behaviours Resolves #1732 --- .../statistics-panel/StatisticsPanel.svelte | 54 ++-- .../__tests__/createDataStore.spec.js | 267 +++++++++++++++--- .../__tests__/createPollingDataStore.spec.js | 166 ++++++++++- .../lib/dusk/svelte-stores/createDataStore.js | 56 ++-- .../svelte-stores/createPollingDataStore.js | 38 +-- .../lib/dusk/svelte-stores/svelte-stores.d.ts | 2 + 6 files changed, 460 insertions(+), 123 deletions(-) diff --git a/explorer/src/lib/containers/statistics-panel/StatisticsPanel.svelte b/explorer/src/lib/containers/statistics-panel/StatisticsPanel.svelte index fa212eac88..9cbe0e5c2f 100644 --- a/explorer/src/lib/containers/statistics-panel/StatisticsPanel.svelte +++ b/explorer/src/lib/containers/statistics-panel/StatisticsPanel.svelte @@ -7,7 +7,7 @@ mdiCurrencyUsd, mdiSwapVertical, } from "@mdi/js"; - import { onDestroy, onMount } from "svelte"; + import { onDestroy } from "svelte"; import { createCurrencyFormatter, luxToDusk } from "$lib/dusk/currency"; import { createCompactFormatter } from "$lib/dusk/value"; @@ -15,12 +15,12 @@ import { Icon } from "$lib/dusk/components"; import { DataGuard, WorldMap } from "$lib/components"; import { duskAPI } from "$lib/services"; - import { appStore } from "$lib/stores"; import { createDataStore, createPollingDataStore, } from "$lib/dusk/svelte-stores"; import { onNetworkChange } from "$lib/lifecyles"; + import "./StatisticsPanel.css"; const valueFormatter = createCurrencyFormatter("en", "DUSK", 0); @@ -37,36 +37,31 @@ const nodeLocationsStore = createDataStore(duskAPI.getNodeLocations); const marketDataStore = createDataStore(duskAPI.getMarketData); - - const getNodeLocations = () => { - nodeLocationsStore.getData($appStore.network); - }; - - const getMarketData = () => { - marketDataStore.getData($appStore.network); - }; - const pollingStatsDataStore = createPollingDataStore( duskAPI.getStats, STATS_FETCH_INTERVAL ); - onNetworkChange(pollingStatsDataStore.start); - onNetworkChange(getNodeLocations); - onNetworkChange(getMarketData); - onDestroy(pollingStatsDataStore.stop); + onNetworkChange((network) => { + marketDataStore.getData(network); + nodeLocationsStore.getData(network); + pollingStatsDataStore.start(network); + }); - $: ({ data } = $nodeLocationsStore); + onDestroy(pollingStatsDataStore.stop); + $: ({ data: marketData } = $marketDataStore); + $: ({ data: nodesData } = $nodeLocationsStore); + $: ({ data: statsData } = $pollingStatsDataStore); $: statistics = [ [ { - data: $marketDataStore.data?.currentPrice.usd, + data: marketData?.currentPrice.usd, icon: mdiCurrencyUsd, title: "Dusk Price", }, { - data: $marketDataStore.data?.marketCap.usd, + data: marketData?.marketCap.usd, icon: mdiCurrencyUsd, title: "Total Market Cap", }, @@ -74,15 +69,15 @@ [ { - data: $pollingStatsDataStore.data?.activeStake - ? luxToDusk($pollingStatsDataStore.data.activeStake) + data: statsData?.activeStake + ? luxToDusk(statsData.activeStake) : undefined, icon: duskIcon, title: "Current Staked Amount", }, { - data: $pollingStatsDataStore.data?.waitingStake - ? luxToDusk($pollingStatsDataStore.data.waitingStake) + data: statsData?.waitingStake + ? luxToDusk(statsData.waitingStake) : undefined, icon: duskIcon, title: "Next Epoch Staked Amount", @@ -91,12 +86,12 @@ [ { - data: $pollingStatsDataStore.data?.lastBlock, + data: statsData?.lastBlock, icon: mdiCubeOutline, title: "Last Block", }, { - data: $pollingStatsDataStore.data?.txs100blocks.transfers, + data: statsData?.txs100blocks.transfers, icon: mdiSwapVertical, title: "TX Last 100 Blocks", }, @@ -104,22 +99,17 @@ [ { - data: $pollingStatsDataStore.data?.activeProvisioners, + data: statsData?.activeProvisioners, icon: mdiAccountGroupOutline, title: "Provisioners", }, { - data: $pollingStatsDataStore.data?.waitingProvisioners, + data: statsData?.waitingProvisioners, icon: mdiAccountGroupOutline, title: "Next Epoch Provisioners", }, ], ]; - - onMount(() => { - getNodeLocations(); - getMarketData(); - });
@@ -143,6 +133,6 @@ {/each}
- +
diff --git a/explorer/src/lib/dusk/svelte-stores/__tests__/createDataStore.spec.js b/explorer/src/lib/dusk/svelte-stores/__tests__/createDataStore.spec.js index 6e06288170..8fefbc27a0 100644 --- a/explorer/src/lib/dusk/svelte-stores/__tests__/createDataStore.spec.js +++ b/explorer/src/lib/dusk/svelte-stores/__tests__/createDataStore.spec.js @@ -9,13 +9,16 @@ import { } from "vitest"; import { get } from "svelte/store"; +import { rejectAfter, resolveAfter } from "$lib/dusk/promise"; + import { createDataStore } from ".."; describe("createDataStore", () => { - const data = {}; + const data1 = { a: 1 }; + const data2 = { a: 2 }; const error = new Error("some error message"); const args = [1, "a", new Date()]; - const dataRetriever = vi.fn().mockResolvedValue(data); + const dataRetriever = vi.fn().mockResolvedValue(data1); /** @type {DataStore} */ let dataStore; @@ -34,9 +37,10 @@ describe("createDataStore", () => { vi.useRealTimers(); }); - it("should create a readable data store and expose a `getData` service method", () => { + it("should create a readable data store and expose `getData` and `reset` as service method", () => { expect(dataRetriever).not.toHaveBeenCalled(); expect(dataStore).toHaveProperty("getData", expect.any(Function)); + expect(dataStore).toHaveProperty("reset", expect.any(Function)); expect(dataStore).toHaveProperty("subscribe", expect.any(Function)); expect(dataStore).not.toHaveProperty("set"); expect(get(dataStore)).toStrictEqual({ @@ -47,6 +51,11 @@ describe("createDataStore", () => { }); it("should set the loading property to `true` when `getData` is called and then fill the data and set loading to `false` if the promise resolves", async () => { + const expectedState = { + data: data1, + error: null, + isLoading: false, + }; const dataPromise = dataStore.getData(...args); expect(dataRetriever).toHaveBeenCalledTimes(1); @@ -59,17 +68,18 @@ describe("createDataStore", () => { await vi.advanceTimersToNextTimerAsync(); - expect(dataPromise).resolves.toStrictEqual({ - data, - error: null, - isLoading: false, - }); - expect(dataPromise).resolves.toStrictEqual(get(dataStore)); + expect(await dataPromise).toStrictEqual(expectedState); + expect(get(dataStore)).toStrictEqual(expectedState); }); it("should set the loading property to `true` when `getData` is called and then set the error and the loading to `false` if the promise rejects", async () => { dataRetriever.mockRejectedValueOnce(error); + const expectedState = { + data: null, + error, + isLoading: false, + }; const dataPromise = dataStore.getData(...args); expect(dataRetriever).toHaveBeenCalledTimes(1); @@ -82,16 +92,102 @@ describe("createDataStore", () => { await vi.advanceTimersToNextTimerAsync(); - expect(dataPromise).resolves.toStrictEqual({ + expect(await dataPromise).toStrictEqual(expectedState); + expect(get(dataStore)).toStrictEqual(expectedState); + }); + + it("should ignore the previous call if `getData` is called while the promise is still pending", async () => { + dataRetriever + .mockImplementationOnce(() => resolveAfter(1000, data1)) + .mockResolvedValueOnce(data2); + + /** @type {DataStoreContent} */ + let expectedState = { + data: data2, + error: null, + isLoading: false, + }; + + dataStore.getData(...args); + + expect(dataRetriever).toHaveBeenCalledTimes(1); + expect(dataRetriever).toHaveBeenCalledWith(...args); + + let dataPromise = dataStore.getData(...args); + + expect(dataRetriever).toHaveBeenCalledTimes(2); + expect(dataRetriever).toHaveBeenNthCalledWith(2, ...args); + expect(await dataPromise).toStrictEqual(expectedState); + expect(get(dataStore)).toStrictEqual(expectedState); + + // waiting for the first promise to resolve + await vi.advanceTimersToNextTimerAsync(); + + expect(get(dataStore)).toStrictEqual(expectedState); + + dataRetriever + .mockImplementationOnce(() => rejectAfter(1000, error)) + .mockResolvedValueOnce(data2); + + dataStore.getData(...args); + + expect(dataRetriever).toHaveBeenCalledTimes(3); + expect(dataRetriever).toHaveBeenNthCalledWith(3, ...args); + + expectedState = { + data: data2, + error: null, + isLoading: false, + }; + dataPromise = dataStore.getData(...args); + + expect(dataRetriever).toHaveBeenCalledTimes(4); + expect(dataRetriever).toHaveBeenNthCalledWith(4, ...args); + expect(await dataPromise).toStrictEqual(expectedState); + expect(get(dataStore)).toStrictEqual(expectedState); + + // waiting for the first promise to resolve + await vi.advanceTimersToNextTimerAsync(); + + expect(get(dataStore)).toStrictEqual(expectedState); + + dataRetriever + .mockImplementationOnce(() => resolveAfter(1000, data1)) + .mockRejectedValueOnce(error); + + dataStore.getData(...args); + + expect(dataRetriever).toHaveBeenCalledTimes(5); + expect(dataRetriever).toHaveBeenNthCalledWith(5, ...args); + + expectedState = { data: null, error, isLoading: false, - }); - expect(dataPromise).resolves.toStrictEqual(get(dataStore)); + }; + dataPromise = dataStore.getData(...args); + + expect(dataRetriever).toHaveBeenCalledTimes(6); + expect(dataRetriever).toHaveBeenNthCalledWith(6, ...args); + expect(await dataPromise).toStrictEqual(expectedState); + expect(get(dataStore)).toStrictEqual(expectedState); + + // waiting for the first promise to resolve + await vi.advanceTimersToNextTimerAsync(); + + expect(get(dataStore)).toStrictEqual(expectedState); }); - it("should not call the data retriever when `getData` is called and the promise is still pending", async () => { - const dataPromise = dataStore.getData(1); + it("should clear the error and leave the existing data while the promise is pending and clear the data when it ends with a failure", async () => { + dataRetriever.mockRejectedValueOnce(error); + + /** @type {DataStoreContent} */ + let expectedState = { + data: null, + error, + isLoading: false, + }; + let dataPromise = dataStore.getData(); expect(get(dataStore)).toStrictEqual({ data: null, @@ -99,72 +195,153 @@ describe("createDataStore", () => { isLoading: true, }); - const dataPromise2 = dataStore.getData(2); - const dataPromise3 = dataStore.getData(3); - - expect(dataPromise2).toBe(dataPromise); - expect(dataPromise3).toBe(dataPromise); - await vi.advanceTimersToNextTimerAsync(); - expect(dataRetriever).toHaveBeenCalledTimes(1); - expect(dataRetriever).toHaveBeenCalledWith(1); - expect(dataPromise).resolves.toStrictEqual({ - data, + expect(await dataPromise).toStrictEqual(expectedState); + expect(get(dataStore)).toStrictEqual(expectedState); + + expectedState = { + data: data1, error: null, isLoading: false, + }; + dataPromise = dataStore.getData(); + + expect(get(dataStore)).toStrictEqual({ + data: null, + error: null, + isLoading: true, }); - expect(dataPromise).resolves.toStrictEqual(get(dataStore)); - }); - it("should clear the error and leave the existing data while the promise is pending and clear the data when it ends with a failure", async () => { - dataRetriever.mockRejectedValueOnce(error); + await vi.advanceTimersToNextTimerAsync(); - let dataPromise = dataStore.getData(); + expect(await dataPromise).toStrictEqual(expectedState); + expect(get(dataStore)).toStrictEqual(expectedState); - await vi.advanceTimersToNextTimerAsync(); + dataRetriever.mockRejectedValueOnce(error); - expect(dataPromise).resolves.toStrictEqual({ + expectedState = { data: null, error, isLoading: false, - }); - expect(dataPromise).resolves.toStrictEqual(get(dataStore)); - + }; dataPromise = dataStore.getData(); expect(get(dataStore)).toStrictEqual({ - data: null, + data: data1, error: null, isLoading: true, }); await vi.advanceTimersToNextTimerAsync(); - expect(dataPromise).resolves.toStrictEqual({ - data, + expect(await dataPromise).toStrictEqual(expectedState); + expect(get(dataStore)).toStrictEqual(expectedState); + }); + + it("should expose a `reset` method to reset the data to its initial state", async () => { + expect(get(dataStore)).toStrictEqual({ + data: null, error: null, isLoading: false, }); - expect(dataPromise).resolves.toStrictEqual(get(dataStore)); - dataRetriever.mockRejectedValueOnce(error); + await dataStore.getData(...args); - dataPromise = dataStore.getData(); + expect(get(dataStore)).toStrictEqual({ + data: data1, + error: null, + isLoading: false, + }); + + dataStore.reset(); expect(get(dataStore)).toStrictEqual({ - data, + data: null, error: null, - isLoading: true, + isLoading: false, }); - await vi.advanceTimersToNextTimerAsync(); + dataRetriever.mockRejectedValueOnce(error); + + await dataStore.getData(...args); - expect(dataPromise).resolves.toStrictEqual({ + expect(get(dataStore)).toStrictEqual({ data: null, error, isLoading: false, }); - expect(dataPromise).resolves.toStrictEqual(get(dataStore)); + + dataStore.reset(); + + expect(get(dataStore)).toStrictEqual({ + data: null, + error: null, + isLoading: false, + }); + }); + + it("should ignore the pending promise when `reset` is called and have `getData` return the reset store", async () => { + const expectedInitialState = { + data: null, + error: null, + isLoading: false, + }; + + await dataStore.getData(...args); + + let dataPromise = dataStore.getData(...args); + + expect(get(dataStore)).toStrictEqual({ + data: data1, + error: null, + isLoading: true, + }); + + dataStore.reset(); + + expect(await dataPromise).toStrictEqual(expectedInitialState); + expect(get(dataStore)).toStrictEqual(expectedInitialState); + + await dataStore.getData(...args); + + dataRetriever.mockRejectedValueOnce(error); + dataPromise = dataStore.getData(...args); + + expect(get(dataStore)).toStrictEqual({ + data: data1, + error: null, + isLoading: true, + }); + + dataStore.reset(); + + expect(await dataPromise).toStrictEqual(expectedInitialState); + expect(get(dataStore)).toStrictEqual(expectedInitialState); + }); + + it("should ignore the pending promise when `reset` is called and a `getData` is called immediately afterwards and return the new result", async () => { + const expectedState = { + data: data2, + error: null, + isLoading: false, + }; + + dataRetriever + .mockImplementationOnce(() => resolveAfter(1000, data1)) + .mockResolvedValueOnce(data2); + + dataStore.getData(...args); + dataStore.reset(); + + const dataPromise = dataStore.getData(...args); + + expect(await dataPromise).toStrictEqual(expectedState); + expect(get(dataStore)).toStrictEqual(expectedState); + + // waiting for the first promise to resolve + await vi.advanceTimersToNextTimerAsync(); + + expect(get(dataStore)).toStrictEqual(expectedState); }); }); diff --git a/explorer/src/lib/dusk/svelte-stores/__tests__/createPollingDataStore.spec.js b/explorer/src/lib/dusk/svelte-stores/__tests__/createPollingDataStore.spec.js index 242ae10b50..10d6deb61d 100644 --- a/explorer/src/lib/dusk/svelte-stores/__tests__/createPollingDataStore.spec.js +++ b/explorer/src/lib/dusk/svelte-stores/__tests__/createPollingDataStore.spec.js @@ -27,8 +27,9 @@ describe("createPollingDataStore", () => { vi.useRealTimers(); }); - it("should create a readable data store and expose `start` and `stop` as service methods", () => { + it("should create a readable data store and expose `reset`, `start` and `stop` as service methods", () => { expect(dataRetriever).not.toHaveBeenCalled(); + expect(pollingDataStore).toHaveProperty("reset", expect.any(Function)); expect(pollingDataStore).toHaveProperty("start", expect.any(Function)); expect(pollingDataStore).toHaveProperty("stop", expect.any(Function)); expect(pollingDataStore).toHaveProperty("subscribe", expect.any(Function)); @@ -160,35 +161,188 @@ describe("createPollingDataStore", () => { isLoading: false, }); - vi.advanceTimersByTime(fetchInterval - 1); - await tick(); + await vi.advanceTimersByTimeAsync(fetchInterval * 10); expect(dataRetriever).toHaveBeenCalledTimes(2); + expect(get(pollingDataStore)).toStrictEqual({ + data: null, + error, + isLoading: false, + }); }); - it("should not make a new call when the `start` method is invoked and the polling is already running", async () => { + it("should be able to restart polling after an error", async () => { + dataRetriever.mockResolvedValueOnce(data1).mockRejectedValueOnce(error); + pollingDataStore.start(...args); + + await vi.advanceTimersToNextTimerAsync(); + + expect(dataRetriever).toHaveBeenCalledTimes(2); + expect(get(pollingDataStore)).toStrictEqual({ + data: null, + error, + isLoading: false, + }); + + await vi.advanceTimersByTimeAsync(fetchInterval * 10); + + expect(dataRetriever).toHaveBeenCalledTimes(2); + pollingDataStore.start(...args); + await vi.advanceTimersByTimeAsync(fetchInterval * 10); + + expect(dataRetriever).toHaveBeenCalledTimes(13); + + pollingDataStore.stop(); + }); + + it("should start a new poll process and stop the previous one when the `start` method is called and a polling is running", async () => { + dataRetriever.mockImplementation((v) => + Promise.resolve(v === 1 ? data1 : data2) + ); + + pollingDataStore.start(1); + + expect(dataRetriever).toHaveBeenCalledTimes(1); + expect(dataRetriever).toHaveBeenNthCalledWith(1, 1); expect(get(pollingDataStore)).toStrictEqual({ data: null, error: null, isLoading: true, }); + pollingDataStore.start(2); + + expect(dataRetriever).toHaveBeenCalledTimes(2); + expect(dataRetriever).toHaveBeenNthCalledWith(2, 2); + await vi.advanceTimersByTimeAsync(1); + expect(dataRetriever).toHaveBeenCalledTimes(2); + expect(get(pollingDataStore)).toStrictEqual({ + data: data2, + error: null, + isLoading: false, + }); + + await vi.advanceTimersByTimeAsync(fetchInterval * 10); + + expect(dataRetriever).toHaveBeenCalledTimes(12); + + for (let i = 3; i <= 12; i++) { + expect(dataRetriever).toHaveBeenNthCalledWith(i, 2); + } + + pollingDataStore.stop(); + dataRetriever.mockResolvedValue(data1); + }); + + it("should expose a `reset` method that stops the polling and resets the store to its initial state", async () => { + const expectedInitialState = { + data: null, + error: null, + isLoading: false, + }; + + expect(get(pollingDataStore)).toStrictEqual(expectedInitialState); + + dataRetriever.mockResolvedValueOnce(data1).mockResolvedValueOnce(data2); + + pollingDataStore.start(...args); + + await vi.advanceTimersByTimeAsync(fetchInterval - 1); + expect(get(pollingDataStore)).toStrictEqual({ data: data1, error: null, isLoading: false, }); + expect(dataRetriever).toHaveBeenCalledTimes(1); + + pollingDataStore.reset(); + + expect(get(pollingDataStore)).toStrictEqual(expectedInitialState); + + await vi.advanceTimersByTimeAsync(fetchInterval * 10); + + expect(dataRetriever).toHaveBeenCalledTimes(1); + expect(get(pollingDataStore)).toStrictEqual(expectedInitialState); + + dataRetriever.mockRejectedValueOnce(error).mockResolvedValueOnce(data1); + pollingDataStore.start(...args); + + await vi.advanceTimersByTimeAsync(fetchInterval - 1); + + expect(get(pollingDataStore)).toStrictEqual({ + data: data2, + error: null, + isLoading: false, + }); + + await vi.advanceTimersByTimeAsync(fetchInterval - 1); + + expect(get(pollingDataStore)).toStrictEqual({ + data: null, + error, + isLoading: false, + }); + + expect(dataRetriever).toHaveBeenCalledTimes(3); + + pollingDataStore.reset(); + + expect(get(pollingDataStore)).toStrictEqual(expectedInitialState); + + await vi.advanceTimersByTimeAsync(fetchInterval * 10); + + expect(dataRetriever).toHaveBeenCalledTimes(3); + expect(get(pollingDataStore)).toStrictEqual(expectedInitialState); + }); + + it("should be able to restart a polling after a `reset`", async () => { pollingDataStore.start(...args); - expect(dataRetriever).toHaveBeenCalledTimes(1); - expect(dataRetriever).toHaveBeenCalledWith(...args); + await vi.advanceTimersByTimeAsync(fetchInterval * 10); + + expect(dataRetriever).toHaveBeenCalledTimes(11); + + pollingDataStore.reset(); + expect(get(pollingDataStore)).toStrictEqual({ + data: null, + error: null, + isLoading: false, + }); + + await vi.advanceTimersByTimeAsync(fetchInterval * 10); + + expect(get(pollingDataStore)).toStrictEqual({ + data: null, + error: null, + isLoading: false, + }); + + dataRetriever.mockResolvedValueOnce(data2).mockResolvedValueOnce(data3); + pollingDataStore.start(...args); + + await vi.advanceTimersByTimeAsync(fetchInterval - 1); + + expect(get(pollingDataStore)).toStrictEqual({ + data: data2, + error: null, + isLoading: false, + }); + + await vi.advanceTimersByTimeAsync(fetchInterval - 1); + + expect(get(pollingDataStore)).toStrictEqual({ + data: data3, + error: null, + isLoading: false, + }); pollingDataStore.stop(); }); diff --git a/explorer/src/lib/dusk/svelte-stores/createDataStore.js b/explorer/src/lib/dusk/svelte-stores/createDataStore.js index 733f1dbf9a..a8ea73d720 100644 --- a/explorer/src/lib/dusk/svelte-stores/createDataStore.js +++ b/explorer/src/lib/dusk/svelte-stores/createDataStore.js @@ -1,4 +1,4 @@ -import { writable } from "svelte/store"; +import { get, writable } from "svelte/store"; import { getErrorFrom } from "$lib/dusk/error"; @@ -17,48 +17,60 @@ function createDataStore(dataRetriever) { const dataStore = writable(initialState); const { set, subscribe, update } = dataStore; - /** @type {ReturnType | null} */ - let dataPromise = null; + /** @type {number} */ + let currentRetrieveId = 0; /** @type {(...args: Parameters) => Promise} */ const getData = (...args) => { - if (dataPromise) { - return dataPromise; - } + const retrieveId = ++currentRetrieveId; update((store) => ({ ...store, error: null, isLoading: true })); - dataPromise = dataRetriever(...args) + return dataRetriever(...args) .then((data) => { - const newStore = { data, error: null, isLoading: false }; + if (retrieveId === currentRetrieveId) { + const newStoreContent = { data, error: null, isLoading: false }; - set(newStore); + set(newStoreContent); - return newStore; + return newStoreContent; + } else { + return get(dataStore); + } }) .catch( /** @param {any} error */ (error) => { - const newStore = { - data: null, - error: getErrorFrom(error), - isLoading: false, - }; + if (retrieveId === currentRetrieveId) { + const newStoreContent = { + data: null, + error: getErrorFrom(error), + isLoading: false, + }; - set(newStore); + set(newStoreContent); - return newStore; + return newStoreContent; + } else { + return get(dataStore); + } } - ) - .finally(() => { - dataPromise = null; - }); + ); + }; - return dataPromise; + const reset = () => { + /** + * We don't want pending promises to be written + * in the store, and we don't want id clashes + * if `getData` is called immediately after `reset`. + */ + currentRetrieveId++; + set(initialState); }; return { getData, + reset, subscribe, }; } diff --git a/explorer/src/lib/dusk/svelte-stores/createPollingDataStore.js b/explorer/src/lib/dusk/svelte-stores/createPollingDataStore.js index 74d400b17a..0d6dca90b8 100644 --- a/explorer/src/lib/dusk/svelte-stores/createPollingDataStore.js +++ b/explorer/src/lib/dusk/svelte-stores/createPollingDataStore.js @@ -1,5 +1,4 @@ import { derived, get } from "svelte/store"; -import { isNull, keySatisfies, when } from "lamb"; import { resolveAfter } from "$lib/dusk/promise"; @@ -11,37 +10,39 @@ import { createDataStore } from "."; * @returns {PollingDataStore} */ const createPollingDataStore = (dataRetriever, fetchInterval) => { - /** @type {boolean} */ - let isPolling = false; + /** @type {number} */ + let currentPollId = 0; const dataStore = createDataStore(dataRetriever); - /** @type {(...args: any) => void} */ - const poll = (...args) => { - if (isPolling) { + /** @type {(pollId: number, args: Parameters) => void} */ + const poll = (pollId, args) => { + if (pollId === currentPollId) { dataStore .getData(...args) - .then( - when(keySatisfies(isNull, "error"), () => - resolveAfter(fetchInterval, undefined).then(() => poll(...args)) - ) + .then((store) => + store.error === null + ? resolveAfter(fetchInterval, undefined).then(() => + poll(pollId, args) + ) + : stop() ) .catch(stop); } }; + const reset = () => { + stop(); + dataStore.reset(); + }; + const stop = () => { - isPolling = false; + currentPollId++; }; - /** @type {(...args: any) => void} */ + /** @type {(...args: Parameters) => void} */ const start = (...args) => { - if (isPolling) { - return; - } - - isPolling = true; - poll(...args); + poll(++currentPollId, args); }; const pollingDataStore = derived( @@ -53,6 +54,7 @@ const createPollingDataStore = (dataRetriever, fetchInterval) => { ); return { + reset, start, stop, subscribe: pollingDataStore.subscribe, diff --git a/explorer/src/lib/dusk/svelte-stores/svelte-stores.d.ts b/explorer/src/lib/dusk/svelte-stores/svelte-stores.d.ts index 1ad17b3608..1a60944ee4 100644 --- a/explorer/src/lib/dusk/svelte-stores/svelte-stores.d.ts +++ b/explorer/src/lib/dusk/svelte-stores/svelte-stores.d.ts @@ -8,9 +8,11 @@ type DataStoreContent = { type DataStore = Readable & { getData: (...args: any) => Promise; + reset: () => void; }; type PollingDataStore = Readable & { + reset: () => void; start: (...args: any) => void; stop: (...args: any) => void; };