refactor requests store request structure

The purpose was to emulate the structure of a request in the contract, so that the entire request object could be overwritten, and then the app state, eg moderated, would be separately set. Other state properties are also only fetched if not already fetched.
This commit is contained in:
Eric 2024-07-03 17:12:23 +10:00
parent ea98b760fe
commit 65de6aaf10
No known key found for this signature in database
10 changed files with 220 additions and 131 deletions

View File

@ -17,7 +17,7 @@ import { useEventsStore } from './stores/events'
const eventsStore = useEventsStore()
const requestsStore = useRequestsStore()
const { loadingRecent, loadingRequestStates } = storeToRefs(requestsStore)
const { loading } = storeToRefs(requestsStore)
const { events } = storeToRefs(eventsStore)
const codexApi = inject('codexApi')
const ethProvider = inject('ethProvider')
@ -27,8 +27,10 @@ window.name = generateUniqueId()
onMounted(() => {
initDrawers()
initDismisses()
requestsStore.refetchRequestStates()
requestsStore.fetchPastRequests()
requestsStore.$hydrate()
requestsStore.refetchRequestStates().then(() => {
requestsStore.fetchPastRequests()
})
eventsStore.listenForNewEvents()
window.addEventListener('storage', handleStorageEvent)
@ -111,11 +113,11 @@ onUnmounted(() => {
</footer>
<div id="toast-container" class="fixed bottom-5 right-5 flex flex-col space-y-2">
<ToastNotification
v-if="loadingRecent"
v-if="loading.recent"
text="Loading recent storage requests..."
></ToastNotification>
<ToastNotification
v-if="loadingRequestStates"
v-if="loading.states"
text="Loading latest request states..."
></ToastNotification>
</div>

View File

@ -1,15 +1,29 @@
<script setup>
import { RouterLink, useRoute } from 'vue-router'
const route = useRoute()
</script>
<template>
<section class="bg-white dark:bg-gray-900">
<div class="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
<div class="mx-auto max-w-screen-sm text-center">
<h1 class="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-primary-600 dark:text-primary-500">404</h1>
<p class="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl dark:text-white">Something's missing.</p>
<p class="mb-4 text-lg font-light text-gray-500 dark:text-gray-400">Sorry, we can't find that page. You'll find lots to explore on the home page. </p>
<a href="#" class="inline-flex text-white bg-primary-600 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:focus:ring-primary-900 my-4">Back to Homepage</a>
<h1
class="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-primary-600 dark:text-primary-500 dark:text-white"
>
404
</h1>
<p class="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl dark:text-white">
Something's missing.
</p>
<p class="mb-4 text-lg font-light text-gray-500 dark:text-gray-400">
Sorry, we can't find that page. You'll find lots to explore on the home page.
</p>
<RouterLink
:to="{ path: `/`, query: route.query }"
class="inline-flex text-white bg-primary-600 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:focus:ring-primary-900 my-4"
>Back to Homepage</RouterLink
>
</div>
</div>
</section>
</template>
</template>

View File

@ -37,29 +37,28 @@ onMounted(() => {
initTooltips()
})
const totalPrice = computed(() => price(request.value))
const maxSlotLoss = computed(() => autoPluralize(request.value.ask.maxSlotLoss, 'slot'))
const slots = computed(() => autoPluralize(request.value.ask.slots, 'slot'))
const totalPrice = computed(() => price(request.value.request))
const maxSlotLoss = computed(() => autoPluralize(request.value.request.ask.maxSlotLoss, 'slot'))
const slots = computed(() => autoPluralize(request.value.request.ask.slots, 'slot'))
const stateColour = computed(() => getStateColour(request.value.state))
const timestamps = computed(() => {
let { requestedAt, endsAt, expiresAt } = timestampsFor(
request.value.ask,
request.value.expiry,
request.value.requestedAt
)
let { requestedAt } = request.value
let { ask, expiry } = request.value.request
let { endsAt, expiresAt } = timestampsFor(ask, expiry, requestedAt)
return {
requested: new Date(requestedAt * 1000),
expires: new Date(expiresAt * 1000),
ends: new Date(endsAt * 1000)
}
})
const requestDetails = computed(() => request.value.request)
</script>
<template>
<div class="flex flex-wrap">
<CodexImage
class="flex-initial mx-auto my-4 min-w-sm max-w-md w-full rounded"
:cid="request.content.cid"
:cid="requestDetails.content.cid"
:moderated="enableModeration ? 'approved' : request.moderated"
></CodexImage>
<div class="py-4 px-4 ml-4 max-w-2xl flex-1">
@ -127,19 +126,19 @@ const timestamps = computed(() => {
<dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Dataset CID</dt>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
{{ request.content.cid }}
{{ requestDetails.content.cid }}
</dd>
</dl>
<dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Client</dt>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
{{ request.client }}
{{ requestDetails.client }}
</dd>
</dl>
<dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Merkle root</dt>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
{{ request.content.merkleRoot }}
{{ requestDetails.content.merkleRoot }}
</dd>
</dl>
<div class="relative overflow-x-auto overflow-y-auto max-h-screen mb-10">
@ -153,7 +152,7 @@ const timestamps = computed(() => {
>
Expiry
</td>
<td class="px-6 py-2 font-light">{{ request.expiry }} seconds</td>
<td class="px-6 py-2 font-light">{{ requestDetails.expiry }} seconds</td>
</tr>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
<td
@ -161,7 +160,7 @@ const timestamps = computed(() => {
>
Duration
</td>
<td class="px-6 py-2 font-light">{{ request.ask.duration }} seconds</td>
<td class="px-6 py-2 font-light">{{ requestDetails.ask.duration }} seconds</td>
</tr>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
<td
@ -169,7 +168,7 @@ const timestamps = computed(() => {
>
Slot size
</td>
<td class="px-6 py-2 font-light">{{ request.ask.slotSize }} bytes</td>
<td class="px-6 py-2 font-light">{{ requestDetails.ask.slotSize }} bytes</td>
</tr>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
<td
@ -177,7 +176,7 @@ const timestamps = computed(() => {
>
Proof probability
</td>
<td class="px-6 py-2 font-light">{{ request.ask.proofProbability }}</td>
<td class="px-6 py-2 font-light">{{ requestDetails.ask.proofProbability }}</td>
</tr>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
<td
@ -185,7 +184,7 @@ const timestamps = computed(() => {
>
Reward
</td>
<td class="px-6 py-2 font-light">{{ request.ask.reward }} CDX</td>
<td class="px-6 py-2 font-light">{{ requestDetails.ask.reward }} CDX</td>
</tr>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
<td
@ -193,7 +192,7 @@ const timestamps = computed(() => {
>
Collateral
</td>
<td class="px-6 py-2 font-light">{{ request.ask.collateral }} CDX</td>
<td class="px-6 py-2 font-light">{{ requestDetails.ask.collateral }} CDX</td>
</tr>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
<td

View File

@ -131,8 +131,7 @@ onMounted(() => {
</div>
</div>
<div
class="relative overflow-x-auto overflow-y-hidden max-h-screen shadow-md
sm:rounded-lg border-t border-gray-50 h-full"
class="relative overflow-x-auto overflow-y-hidden max-h-screen shadow-md sm:rounded-lg border-t border-gray-50 h-full"
>
<table
class="w-full relative text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 rounded-lg"
@ -149,8 +148,10 @@ onMounted(() => {
</thead>
<tbody>
<tr
v-for="([requestId, { requestedAt, moderated, state }], idx) in requestsOrdered"
:key="{ requestId }"
v-for="(
[requestId, { request, requestedAt, moderated, state }], idx
) in requestsOrdered"
:key="requestId"
class="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-600 dark:bg-gray-800"
@click="router.push({ path: `/request/${requestId}`, query: { enableModeration } })"
>

View File

@ -37,7 +37,7 @@ export const useEventsStore = defineStore(
async function listenForNewEvents() {
async function onStorageRequested(requestId, ask, expiry, event) {
let { blockNumber, blockHash } = event.log
const request = await requests.add(requestId, ask, expiry, blockHash)
const request = await requests.addFromEvent(requestId, ask, expiry, blockHash)
add({
event: StorageEvent.StorageRequested,

View File

@ -31,15 +31,104 @@ export const useRequestsStore = defineStore(
const events = useEventsStore()
let { StorageRequested } = marketplace.filters
const requests = ref({}) // key: requestId, val: {request, state, slots: [{slotId, slotIdx, state}]}
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 loading = ref({
past: false,
recent: false,
states: false
})
const fetched = ref({
past: false
})
// Request structure
// {
// request: {
// client,
// ask: {
// slots,
// slotSize,
// duration,
// proofProbability,
// reward,
// collateral,
// maxSlotLoss
// },
// content: {
// cid,
// merkleRoot
// },
// expiry,
// nonce,
// },
// state,
// moderated: false,
// requestedAt: Number (timestamp in seconds),
// requestFinishedId: Number (setTimeout id for request completion update)
// slots: [{slotId, slotIdx, state}],
// loading: {
// request: false,
// slots: false,
// state: false
// },
// fetched: {
// request: false,
// slots: false
// }
// }
// fetch request details if not previously fetched
const getRequest = async (requestId) => {
try {
let request = requests.value[requestId]
if (exists(requestId) && request.fetched.request === true) {
console.log('request', requestId, ' details already fetched')
return request
}
requests.value[requestId] = {
loading: {
request: true,
slots: false,
state: false
},
fetched: {
request: false,
slots: false
}
}
request = arrayToObject(await marketplace.getRequest(requestId))
await getRequestState(requestId)
requests.value[requestId] = {
...requests.value[requestId],
request,
moderated: 'pending'
// requestedAt: will be set in addFromEvent, as we don't have a block
// requestFinishedId: will be set in addFromEvent as we don't have
// requestedAt yet
// state: state is set in getRequestState
}
requests.value[requestId].loading.request = false
requests.value[requestId].fetched.request = true
return requests.value[requestId]
} catch (e) {
delete requests.value[requestId]
throw new Error(`failed to get request for ${requestId}: ${e.message}`)
}
}
const getRequestState = async (requestId) => {
let stateIdx = await marketplace.requestState(requestId)
return toRequestState(Number(stateIdx))
if (!exists(requestId)) {
throw new RequestNotFoundError(requestId, `Request not found`)
}
try {
requests.value[requestId].loading.state = true
const stateIdx = await marketplace.requestState(requestId)
const state = toRequestState(Number(stateIdx))
requests.value[requestId].state = state
} catch (e) {
throw new Error(`failed to get request state for ${requestId}: ${e.message}`)
} finally {
requests.value[requestId].loading.state = false
}
}
const getSlotState = async (slotId) => {
@ -48,30 +137,42 @@ export const useRequestsStore = defineStore(
}
const getSlots = async (requestId, numSlots) => {
console.log(`fetching ${numSlots} slots`)
let start = Date.now()
let slots = []
for (let slotIdx = 0; slotIdx < numSlots; slotIdx++) {
try {
let id = slotId(requestId, slotIdx)
const startSlotState = Date.now()
let state = await getSlotState(id)
console.log(`fetched slot state in ${(Date.now() - startSlotState) / 1000}s`)
const startGetHost = Date.now()
let provider = await marketplace.getHost(id)
console.log(`fetched slot provider in ${(Date.now() - startGetHost) / 1000}s`)
slots.push({ slotId: id, slotIdx, state, provider })
} catch (e) {
console.error('error getting slot details', e)
continue
}
if (!exists(requestId)) {
throw new RequestNotFoundError(requestId, `Request not found`)
}
requests.value[requestId].loading.slots = true
try {
console.log(`fetching ${numSlots} slots`)
let start = Date.now()
let slots = []
for (let slotIdx = 0; slotIdx < numSlots; slotIdx++) {
try {
let id = slotId(requestId, slotIdx)
const startSlotState = Date.now()
let state = await getSlotState(id)
console.log(`fetched slot state in ${(Date.now() - startSlotState) / 1000}s`)
const startGetHost = Date.now()
let provider = await marketplace.getHost(id)
console.log(`fetched slot provider in ${(Date.now() - startGetHost) / 1000}s`)
slots.push({ slotId: id, slotIdx, state, provider })
} catch (e) {
console.error('error getting slot details', e)
continue
}
}
console.log(`fetched ${numSlots} slots in ${(Date.now() - start) / 1000}s`)
requests.value[requestId].slots = slots
return slots
} catch (e) {
throw new Error(`error fetching slots for ${requestId}: ${e.message}`)
} finally {
requests.value[requestId].loading.slots = false
requests.value[requestId].fetched.slots = true
}
console.log(`fetched ${numSlots} slots in ${(Date.now() - start) / 1000}s`)
return slots
}
const getBlock = async (blockHash) => {
if (Object.keys(blocks.value).includes(blockHash)) {
if (blockHash in blocks.value) {
return blocks.value[blockHash]
} else {
let { number, timestamp } = await ethProvider.getBlock(blockHash)
@ -80,24 +181,22 @@ export const useRequestsStore = defineStore(
}
}
async function add(requestId, ask, expiry, blockHash) {
let state = await getRequestState(requestId)
const addFromEvent = async (requestId, ask, expiry, blockHash) => {
let request = await getRequest(requestId)
let { state } = request.request
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 request = {
...reqExisting,
state,
requestedAt: timestamp,
requestFinishedId,
detailsFetched: false,
moderated: 'pending'
}
requests.value[requestId] = request
requests.value[requestId].requestedAt = timestamp
requests.value[requestId].requestFinishedId = requestFinishedId
return request
}
const exists = (requestId) => {
return requestId in requests.value
}
// Returns an array of Promises, where each Promise represents the fetching
// of one StorageRequested event
async function fetchPastRequestsFrom(fromBlock = null) {
@ -112,7 +211,7 @@ export const useRequestsStore = defineStore(
return events.map(async (event, i) => {
let { requestId, ask, expiry } = event.args
let { blockHash, blockNumber } = event
await add(requestId, ask, expiry, blockHash)
await addFromEvent(requestId, ask, expiry, blockHash)
})
} catch (error) {
console.error(`failed to load past contract events: ${error.message}`)
@ -122,7 +221,7 @@ export const useRequestsStore = defineStore(
async function refetchRequestStates() {
async function refetchRequestState(requestId) {
requests.value[requestId].state = await getRequestState(requestId)
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
@ -137,7 +236,7 @@ export const useRequestsStore = defineStore(
}
// array of asynchronously-executed Promises, each requesting a request
// state
loadingRequestStates.value = true
loading.value.states = true
try {
const fetches = Object.entries(requests.value).map(([requestId, request]) =>
refetchRequestState(requestId)
@ -146,7 +245,7 @@ export const useRequestsStore = defineStore(
} catch (e) {
console.error(`failure requesting latest request states:`, e)
} finally {
loadingRequestStates.value = false
loading.value.states = false
}
}
@ -158,52 +257,32 @@ export const useRequestsStore = defineStore(
const lastBlockNumber = blocksSorted.length ? blocksSorted[0].number : null
if (lastBlockNumber) {
loadingRecent.value = true
loading.value.recent = true
} else {
loading.value = true
loading.value.past = true
}
await Promise.all(await fetchPastRequestsFrom(lastBlockNumber + 1))
if (lastBlockNumber) {
loadingRecent.value = false
loading.value.recent = false
} else {
loading.value = false
fetched.value = true
loading.value.past = false
fetched.value.past = true
}
}
async function fetchRequestDetails(requestId) {
try {
let request = requests.value[requestId] || {}
if (requestId in requests.value) {
requests.value[requestId].detailsLoading = true
}
// fetch request details if not previously fetched
if (request?.detailsFetched !== true) {
console.log('request', requestId, ' details already fetched')
request = arrayToObject(await marketplace.getRequest(requestId))
}
const { request } = await getRequest(requestId)
// always fetch state
const state = await getRequestState(requestId)
await getRequestState(requestId)
// always fetch slots
// always fetch slots - fire async but don't wait
console.log('fetching slots for request', requestId)
getSlots(requestId, request.ask.slots).then((slots) => {
requests.value[requestId].slots = slots
requests.value[requestId].slotsLoading = false
requests.value[requestId].slotsFetched = true
})
// update state
requests.value[requestId] = {
...requests.value[requestId], // state, requestedAt, requestFinishedId (null)
...request,
state,
slotsLoading: true,
detailsFetched: true,
detailsLoading: false
}
getSlots(requestId, request.ask.slots)
} catch (error) {
if (
!error.message.includes('Unknown request') &&
@ -216,25 +295,21 @@ export const useRequestsStore = defineStore(
}
function updateRequestState(requestId, newState) {
if (!Object.keys(requests.value).includes(requestId)) {
if (!exists(requestId)) {
throw new RequestNotFoundError(requestId, `Request not found`)
}
let { state, ...rest } = requests.value[requestId]
state = newState
requests.value[requestId] = { state, ...rest }
requests.value[requestId].state = newState
}
function updateRequestFinishedId(requestId, newRequestFinishedId) {
if (!Object.keys(requests.value).includes(requestId)) {
if (!exists(requestId)) {
throw new RequestNotFoundError(requestId, `Request not found`)
}
let { requestFinishedId, ...rest } = requests.value[requestId]
requestFinishedId = newRequestFinishedId
requests.value[requestId] = { requestFinishedId, ...rest }
requests.value[requestId].state = newRequestFinishedId
}
function updateRequestSlotState(requestId, slotIdx, newState) {
if (!Object.keys(requests.value).includes(requestId)) {
if (!exists(requestId)) {
throw new RequestNotFoundError(requestId, `Request not found`)
}
let { slots, ...rest } = requests.value[requestId]
@ -248,7 +323,7 @@ export const useRequestsStore = defineStore(
}
function updateRequestSlotProvider(requestId, slotIdx, provider) {
if (!Object.keys(requests.value).includes(requestId)) {
if (!exists(requestId)) {
throw new RequestNotFoundError(requestId, `Request not found`)
}
let { slots, ...rest } = requests.value[requestId]
@ -274,8 +349,7 @@ export const useRequestsStore = defineStore(
try {
// the state may actually have been cancelled, but RequestCancelled
// may not have fired yet, so get the updated state
const state = await getRequestState(requestId)
updateRequestState(requestId, state)
await getRequestState(requestId)
updateRequestFinishedId(requestId, null)
} catch (error) {
if (error instanceof RequestNotFoundError) {
@ -296,7 +370,7 @@ export const useRequestsStore = defineStore(
}
function cancelWaitForRequestFinished(requestId) {
if (!Object.keys(requests.value).includes(requestId)) {
if (!exists(requestId)) {
throw new RequestNotFoundError(requestId, `Request not found`)
}
let { requestFinishedId } = requests.value[requestId]
@ -308,7 +382,8 @@ export const useRequestsStore = defineStore(
return {
requests,
blocks,
add,
addFromEvent,
exists,
getBlock,
fetchPastRequests,
refetchRequestStates,
@ -319,16 +394,14 @@ export const useRequestsStore = defineStore(
updateRequestFinishedId,
cancelWaitForRequestFinished,
loading,
loadingRecent,
loadingRequestStates,
fetched,
RequestNotFoundError
}
},
{
persist: {
serializer,
paths: ['requests', 'blocks', 'fetched', 'loading', 'loadingRecent']
serializer
// paths: ['requests', 'blocks', 'fetched', 'loading']
}
}
)

View File

@ -10,7 +10,7 @@ const { loading } = storeToRefs(requestsStore)
<template>
<div>
<SkeletonLoading v-if="loading" type="text" />
<SkeletonLoading v-if="loading.past" type="text" />
<StorageRequests :enable-moderation="true" v-else />
</div>
</template>

View File

@ -7,7 +7,7 @@ import { computed } from 'vue'
const requestsStore = useRequestsStore()
const { loading, requests } = storeToRefs(requestsStore)
const isLoading = computed(() => loading.value || !requests.value)
const isLoading = computed(() => loading.value.past || !requests.value)
</script>
<template>

View File

@ -20,27 +20,27 @@ async function fetchRequest(requestId) {
error.message.includes('Unknown request') ||
error.message.includes('invalid BytesLike value')
) {
router.push({ path: '/404' })
router.push({ path: '/404', query: route.query })
}
}
}
const request = computed(() => requests.value[route.params.requestId])
const detailsLoading = computed(() => requests.value[route.params.requestId]?.detailsLoading)
const detailsLoading = computed(() => requests.value[route.params.requestId]?.loading?.request)
watch(() => route.params.requestId, fetchRequest, { immediate: true })
</script>
<template>
<div>
<SkeletonLoading v-if="loading || detailsLoading" type="image" />
<SkeletonLoading v-if="loading.past || detailsLoading" type="image" />
<StorageRequest
v-else-if="!!request"
:requestId="route.params.requestId"
v-model="request"
:enableModeration="route.query.enableModeration === 'true'"
:slotsLoading="request.slotsLoading"
:slotsFetched="request.slotsFetched"
:slotsLoading="request.loading.slots"
:slotsFetched="request.fetched.slots"
/>
</div>
</template>

View File

@ -10,7 +10,7 @@ const { loading } = storeToRefs(requestsStore)
<template>
<div>
<SkeletonLoading v-if="loading" type="text" />
<SkeletonLoading v-if="loading.past" type="text" />
<StorageRequests v-else />
</div>
</template>