From 31b4bd95a1904f98ace24eea903e9e5502c0965a Mon Sep 17 00:00:00 2001 From: Eric <5089238+emizzle@users.noreply.github.com> Date: Tue, 2 Jul 2024 16:22:08 +1000 Subject: [PATCH] Move events to their own store for notifications centre --- src/stores/events.js | 175 ++++++++++++++++++++++++++++ src/stores/requests.js | 256 +++++++++++------------------------------ src/utils/events.js | 26 +++++ 3 files changed, 270 insertions(+), 187 deletions(-) create mode 100644 src/stores/events.js create mode 100644 src/utils/events.js diff --git a/src/stores/events.js b/src/stores/events.js new file mode 100644 index 0000000..5ed08b4 --- /dev/null +++ b/src/stores/events.js @@ -0,0 +1,175 @@ +import { ref, inject } from 'vue' +import { defineStore } from 'pinia' +import { slotId } from '@/utils/ids' +import { RequestState } from '@/utils/requests' +import { SlotState } from '@/utils/slots' +import { StorageEvent } from '@/utils/events' +import { useRequestsStore } from './requests' +import serializer from './serializer' + +export const useEventsStore = defineStore( + 'events', + () => { + // let fetched = false + const marketplace = inject('marketplace') + const requests = useRequestsStore() + let { + StorageRequested, + RequestFulfilled, + RequestCancelled, + RequestFailed, + SlotFilled, + SlotFreed + } = marketplace.filters + const events = ref([]) // {event: 'SlotFreed',blockNumber,requestId,slotIdx,state: 'Free'} + // const subscriptions = ref({}) + // const request = computed(() => count.value * 2) + + // onStorageRequested => add request to requests ref, along with slots + // => add to storageRequestedEvents {blockNumber, requestId} + // => add blockNumber to blockNumbers + // onRequestFulfilled => update requests[requestId].state with marketplace.getRequestState(requestId) + // => add to requestStartedEvents {blockNumber, requestId} + // => add blockNumber to blockNumbers + // onRequestCancelled => update requests[requestId].state with marketplace.getRequestState(requestId) + // => add to requestCancelledEvents {blockNumber, requestId} + // => add blockNumber to blockNumbers + // onRequestFailed => update requests[requestId].state with marketplace.getRequestState(requestId) + // => add to requestFailedEvents {blockNumber, requestId} + // => add blockNumber to blockNumbers + // onRequestFinished => update requests[requestId].state with marketplace.getRequestState(requestId) + // => add to requestFinishedEvents {blockNumber, requestId} + // => add blockNumber to blockNumbers + // onSlotFilled => update request.slots[slotId].state with getSlotState + // => add to slotFilledEvents {blockNumber, slotId, slotIdx} + // => add blockNumber to blockNumbers + // onSlotFreed => update slots[slotId].state with getSlotState + // => add to slotFreedEvents {blockNumber, slotId, slotIdx} + // => add blockNumber to blockNumbers + + function add({ event, blockNumber, requestId, slotIdx, state }) { + events.value.push({ event, blockNumber, requestId, slotIdx, state }) + } + + async function listenForNewEvents() { + async function onStorageRequested(requestId, ask, expiry, event) { + let { blockNumber, blockHash } = event.log + const request = await requests.add(requestId, ask, expiry, blockHash) + + add({ + event: StorageEvent.StorageRequested, + blockNumber, + requestId, + state: RequestState.New + }) + } + + async function onRequestFulfilled(requestId, event) { + let state = RequestState.Fulfilled + try { + requests.updateRequestState(requestId, state) + } catch (error) { + if (error instanceof requests.RequestNotFoundError) { + await requests.fetchRequestDetails(requestId) + } + } + + let { blockNumber } = event.log + add({ event: StorageEvent.RequestFulfilled, blockNumber, requestId, state }) + } + + async function onRequestCancelled(requestId, event) { + let state = RequestState.Cancelled + try { + requests.updateRequestState(requestId, state) + requests.cancelWaitForRequestFinished(requestId) + } catch (error) { + if (error instanceof requests.RequestNotFoundError) { + await requests.fetchRequestDetails(requestId) + } + } + + let { blockNumber } = event.log + add({ event: StorageEvent.RequestCancelled, blockNumber, requestId, state }) + } + + async function onRequestFailed(requestId, event) { + let state = RequestState.Failed + try { + requests.updateRequestState(requestId, state) + requests.cancelWaitForRequestFinished(requestId) + } catch (error) { + if (error instanceof requests.RequestNotFoundError) { + await requests.fetchRequestDetails(requestId) + } + } + + let { blockNumber } = event.log + add({ event: StorageEvent.RequestFailed, blockNumber, requestId, state }) + } + + async function onSlotFreed(requestId, slotIdx, event) { + let state = SlotState.Free + try { + requests.updateRequestSlotState(requestId, slotIdx, state) + requests.updateRequestSlotProvider(requestId, slotIdx, null) + } catch (error) { + if (error instanceof requests.RequestNotFoundError) { + await requests.fetchRequestDetails(requestId) + } + } + + let { blockNumber } = event.log + add({ event: StorageEvent.SlotFreed, blockNumber, requestId, slotIdx, state }) + } + + async function onSlotFilled(requestId, slotIdx, event) { + let state = SlotState.Filled + try { + requests.updateRequestSlotState(requestId, slotIdx, state) + let id = slotId(requestId, slotIdx) + let provider = await marketplace.getHost(id) + requests.updateRequestSlotProvider(requestId, slotIdx, provider) + } catch (error) { + if (error instanceof requests.RequestNotFoundError) { + await requests.fetchRequestDetails(requestId) + } + } + + let { blockNumber } = event.log + add({ event: StorageEvent.SlotFilled, blockNumber, requestId, slotIdx, state }) + } + + await marketplace.removeAllListeners(StorageRequested) + await marketplace.on(StorageRequested, onStorageRequested) + // subscriptions.value[StorageEvent.StorageRequested] = { subscribed: true } + + await marketplace.removeAllListeners(RequestFulfilled) + await marketplace.on(RequestFulfilled, onRequestFulfilled) + + await marketplace.removeAllListeners(RequestCancelled) + await marketplace.on(RequestCancelled, onRequestCancelled) + + await marketplace.removeAllListeners(RequestFailed) + await marketplace.on(RequestFailed, onRequestFailed) + + await marketplace.removeAllListeners(SlotFreed) + await marketplace.on(SlotFreed, onSlotFreed) + + await marketplace.removeAllListeners(SlotFilled) + await marketplace.on(SlotFilled, onSlotFilled) + } + + return { + events, + listenForNewEvents, + add + } + }, + { + persist: { + serializer, + paths: ['events'] + } + } +) diff --git a/src/stores/requests.js b/src/stores/requests.js index afcd789..b9f62b3 100644 --- a/src/stores/requests.js +++ b/src/stores/requests.js @@ -1,9 +1,11 @@ import { ref, inject } from 'vue' import { defineStore } from 'pinia' import { slotId } from '../utils/ids' -import { arrayToObject, requestState, timestampsFor } from '@/utils/requests' -import { slotState } from '@/utils/slots' +import { arrayToObject, toRequestState, timestampsFor } from '@/utils/requests' +import { toSlotState } from '@/utils/slots' +import { RequestState } from '@/utils/requests' import serializer from './serializer' +import { useEventsStore } from './events' class RequestNotFoundError extends Error { constructor(requestId, ...params) { @@ -24,64 +26,25 @@ class RequestNotFoundError extends Error { export const useRequestsStore = defineStore( 'requests', () => { - // let fetched = false const marketplace = inject('marketplace') const ethProvider = inject('ethProvider') - let { - StorageRequested, - RequestFulfilled, - RequestCancelled, - RequestFailed, - SlotFilled, - SlotFreed - } = marketplace.filters + const events = useEventsStore() + let { StorageRequested } = marketplace.filters const requests = ref({}) // key: requestId, val: {request, state, slots: [{slotId, slotIdx, state}]} - // const slots = ref(new Map()) // key: slotId, val: {requestId, slotIdx, state} - // const blockNumbers = ref(new Set()) // includes blocks that had events - // const storageRequestedEvents = ref([]) // {blockNumber, requestId} - // const slotFilledEvents = ref([]) // {blockNumber, requestId, slotIdx, slotId} - // const slotFreedEvents = ref([]) // {blockNumber, requestId, slotIdx, slotId} - // const requestFulfilledEvents = ref([]) // {blockNumber, requestId} - // const requestCancelledEvents = ref([]) // {blockNumber, requestId} - // const requestFailedEvents = ref([]) // {blockNumber, requestId} - // const requestFinishedEvents = ref([]) // {blockNumber, requestId} const loading = ref(false) const loadingRecent = ref(false) const loadingRequestStates = ref(false) const fetched = ref(false) // indicates if past events were fetched const blocks = ref({}) - // const request = computed(() => count.value * 2) - - // onStorageRequested => add request to requests ref, along with slots - // => add to storageRequestedEvents {blockNumber, requestId} - // => add blockNumber to blockNumbers - // onRequestFulfilled => update requests[requestId].state with marketplace.getRequestState(requestId) - // => add to requestStartedEvents {blockNumber, requestId} - // => add blockNumber to blockNumbers - // onRequestCancelled => update requests[requestId].state with marketplace.getRequestState(requestId) - // => add to requestCancelledEvents {blockNumber, requestId} - // => add blockNumber to blockNumbers - // onRequestFailed => update requests[requestId].state with marketplace.getRequestState(requestId) - // => add to requestFailedEvents {blockNumber, requestId} - // => add blockNumber to blockNumbers - // onRequestFinished => update requests[requestId].state with marketplace.getRequestState(requestId) - // => add to requestFinishedEvents {blockNumber, requestId} - // => add blockNumber to blockNumbers - // onSlotFilled => update request.slots[slotId].state with getSlotState - // => add to slotFilledEvents {blockNumber, slotId, slotIdx} - // => add blockNumber to blockNumbers - // onSlotFreed => update slots[slotId].state with getSlotState - // => add to slotFreedEvents {blockNumber, slotId, slotIdx} - // => add blockNumber to blockNumbers const getRequestState = async (requestId) => { let stateIdx = await marketplace.requestState(requestId) - return requestState[stateIdx] + return toRequestState(Number(stateIdx)) } const getSlotState = async (slotId) => { let stateIdx = await marketplace.slotState(slotId) - return slotState[stateIdx] + return toSlotState(Number(stateIdx)) } const getSlots = async (requestId, numSlots) => { @@ -117,31 +80,12 @@ export const useRequestsStore = defineStore( } } - async function addRequest(requestId, ask, expiry, blockHash) { + async function add(requestId, ask, expiry, blockHash) { let state = await getRequestState(requestId) let { timestamp } = await getBlock(blockHash) + let requestFinishedId = waitForRequestFinished(requestId, ask, expiry, state, timestamp) let reqExisting = requests.value[requestId] || {} // just in case it already exists - let requestFinishedId = null - if (['Fulfilled', 'New'].includes(state)) { - try { - // set request state to finished at the end of the request -- there's no - // other way to know when a request finishes - let { endsAt } = timestampsFor(ask, expiry, timestamp) - let msFromNow = endsAt * 1000 - Date.now() // time remaining until finish, in ms - requestFinishedId = waitForRequestFinished( - requestId, - msFromNow, - onRequestFinishedCallback - ) - } catch (e) { - console.error( - `Failed to set timeout for RequestFinished event for request ${requestId}. There will be no alert for this event.`, - e - ) - } - } - let request = { ...reqExisting, state, @@ -154,12 +98,6 @@ export const useRequestsStore = defineStore( return request } - async function handleStorageRequestEvent(event) { - let { requestId, ask, expiry } = event.args - let { blockHash, blockNumber } = event - await addRequest(requestId, ask, expiry, blockHash) - } - // Returns an array of Promises, where each Promise represents the fetching // of one StorageRequested event async function fetchPastRequestsFrom(fromBlock = null) { @@ -171,7 +109,11 @@ export const useRequestsStore = defineStore( return [] } - return events.map((event, i) => handleStorageRequestEvent(event)) //{ + return events.map(async (event, i) => { + let { requestId, ask, expiry } = event.args + let { blockHash, blockNumber } = event + await add(requestId, ask, expiry, blockHash) + }) } catch (error) { console.error(`failed to load past contract events: ${error.message}`) return [] @@ -181,6 +123,17 @@ export const useRequestsStore = defineStore( async function refetchRequestStates() { async function refetchRequestState(requestId) { requests.value[requestId].state = await getRequestState(requestId) + let { ask, expiry, state, requestedAt } = requests.value[requestId] + // refetching of requests states happen on page load, so if we're + // loading the page, we need to reset any timeouts for RequestFinished + // events + requests.value[requestId].requestFinishedId = waitForRequestFinished( + requestId, + ask, + expiry, + state, + requestedAt + ) } // array of asynchronously-executed Promises, each requesting a request // state @@ -294,7 +247,29 @@ export const useRequestsStore = defineStore( requests.value[requestId] = { slots, ...rest } } - function waitForRequestFinished(requestId, msFromNow, onRequestFinished) { + function updateRequestSlotProvider(requestId, slotIdx, provider) { + if (!Object.keys(requests.value).includes(requestId)) { + throw new RequestNotFoundError(requestId, `Request not found`) + } + let { slots, ...rest } = requests.value[requestId] + slots = slots.map((slot) => { + if (slot.slotIdx == slotIdx) { + slot.provider = provider + } + return slot + }) + requests.value[requestId] = { slots, ...rest } + } + + function waitForRequestFinished(requestId, ask, expiry, state, requestedAt) { + if (!['Fulfilled', 'New'].includes(state)) { + return null + } + // set request state to finished at the end of the request -- there's no + // other way to know when a request finishes + let { endsAt } = timestampsFor(ask, expiry, requestedAt) + let msFromNow = endsAt * 1000 - Date.now() // time remaining until finish, in ms + return setTimeout(async () => { try { // the state may actually have been cancelled, but RequestCancelled @@ -305,13 +280,18 @@ export const useRequestsStore = defineStore( } catch (error) { if (error instanceof RequestNotFoundError) { await fetchRequestDetails(requestId) + } else { + throw error } } - if (onRequestFinished) { - let blockNumber = await ethProvider.getBlockNumber() - onRequestFinished(blockNumber, requestId) - } - }, msFromNow) + let blockNumber = await ethProvider.getBlockNumber() + events.add({ + event: 'RequestFinished', + blockNumber, + requestId, + state: RequestState.Finished + }) + }, msFromNow + 1000) // add additional second to ensure state has changed } function cancelWaitForRequestFinished(requestId) { @@ -324,127 +304,29 @@ export const useRequestsStore = defineStore( } } - let onRequestFinishedCallback - async function listenForNewEvents( - onStorageRequested, - onRequestFulfilled, - onRequestCancelled, - onRequestFailed, - onRequestFinished, - onSlotFreed, - onSlotFilled - ) { - onRequestFinishedCallback = onRequestFinished - - marketplace.on(StorageRequested, async (requestId, ask, expiry, event) => { - let { blockNumber, blockHash } = event.log - const request = await addRequest(requestId, ask, expiry, blockHash) - - // callback - if (onStorageRequested) { - onStorageRequested(blockNumber, requestId, request.state) - } - }) - - marketplace.on(RequestFulfilled, async (requestId, event) => { - try { - updateRequestState(requestId, 'Fulfilled') - } catch (error) { - if (error instanceof RequestNotFoundError) { - await fetchRequestDetails(requestId) - } - } - - let { blockNumber } = event.log - if (onRequestFulfilled) { - onRequestFulfilled(blockNumber, requestId) - } - }) - marketplace.on(RequestCancelled, async (requestId, event) => { - try { - updateRequestState(requestId, 'Cancelled') - cancelWaitForRequestFinished(requestId) - } catch (error) { - if (error instanceof RequestNotFoundError) { - await fetchRequestDetails(requestId) - } - } - - let { blockNumber } = event.log - if (onRequestCancelled) { - onRequestCancelled(blockNumber, requestId) - } - }) - marketplace.on(RequestFailed, async (requestId, event) => { - try { - updateRequestState(requestId, 'Failed') - cancelWaitForRequestFinished(requestId) - } catch (error) { - if (error instanceof RequestNotFoundError) { - await fetchRequestDetails(requestId) - } - } - - let { blockNumber } = event.log - if (onRequestFailed) { - onRequestFailed(blockNumber, requestId) - } - }) - marketplace.on(SlotFreed, async (requestId, slotIdx, event) => { - try { - updateRequestSlotState(requestId, slotIdx, 'Free') - } catch (error) { - if (error instanceof RequestNotFoundError) { - await fetchRequestDetails(requestId) - } - } - - let { blockNumber } = event.log - if (onSlotFreed) { - onSlotFreed(blockNumber, requestId, slotIdx) - } - }) - marketplace.on(SlotFilled, async (requestId, slotIdx, event) => { - try { - updateRequestSlotState(requestId, slotIdx, 'Filled') - } catch (error) { - if (error instanceof RequestNotFoundError) { - await fetchRequestDetails(requestId) - } - } - - let { blockNumber } = event.log - if (onSlotFilled) { - onSlotFilled(blockNumber, requestId, slotIdx) - } - }) - } - return { requests, blocks, - // slots, - // blockNumbers, - // storageRequestedEvents, - // slotFilledEvents, - // slotFreedEvents, - // requestStartedEvents, - // requestCancelledEvents, - // requestFailedEvents, - // requestFinishedEvents, + add, fetchPastRequests, refetchRequestStates, fetchRequestDetails, - listenForNewEvents, + updateRequestState, + updateRequestSlotState, + updateRequestSlotProvider, + updateRequestFinishedId, + cancelWaitForRequestFinished, loading, loadingRecent, loadingRequestStates, - fetched + fetched, + RequestNotFoundError } }, { persist: { - serializer + serializer, + paths: ['requests', 'blocks', 'fetched', 'loading', 'loadingRecent'] } } ) diff --git a/src/utils/events.js b/src/utils/events.js new file mode 100644 index 0000000..5b99e99 --- /dev/null +++ b/src/utils/events.js @@ -0,0 +1,26 @@ +export const StorageEvent = { + StorageRequested: 'StorageRequested', // storage was requested + RequestFulfilled: 'RequestFulfilled', // request has started (all slots filled) + RequestCancelled: 'RequestCancelled', // request was cancelled when not all slots were filled by the expiry + RequestFailed: 'RequestFailed', // the request has failed (too many slots freed) + SlotFilled: 'SlotFilled', // slot was filled by provider (providing proof and collateral) + SlotFreed: 'SlotFreed' // slot freed after provider missed too many proofs +} + +export function toSlotState(idx) { + return Object.values(SlotState).at(idx) +} + +export function getStateColour(state) { + if (state === SlotState.Free) { + return 'yellow' + } else if (state === SlotState.Filled) { + return 'green' + } else if (state === SlotState.Finished) { + return 'blue' + } else if (state === SlotState.Paid) { + return 'gray' + } else { + return 'red' + } +}