diff --git a/src/hooks/useEventInCommon.ts b/src/hooks/useEventInCommon.ts index 7202567..bc8b764 100644 --- a/src/hooks/useEventInCommon.ts +++ b/src/hooks/useEventInCommon.ts @@ -2,10 +2,10 @@ import { useCallback, useEffect, useState } from 'react' import { AbortedError } from 'models/error' import { Drop } from 'models/drop' import { POAP } from 'models/poap' -import { Progress } from 'models/http' +import { CountProgress, DownloadProgress } from 'models/http' import { InCommon } from 'models/api' import { filterInCommon } from 'models/in-common' -import { getInCommonEventsWithProgress } from 'loaders/api' +import { getInCommonEventsWithEvents, getInCommonEventsWithProgress } from 'loaders/api' import { scanAddress } from 'loaders/poap' function useEventInCommon( @@ -13,10 +13,12 @@ function useEventInCommon( owners: string[], force: boolean = false, local: boolean = false, + stream: boolean = false, ): { completedEventInCommon: boolean loadingEventInCommon: boolean - loadedInCommonProgress: { progress: number; estimated: number | null; rate: number | null } | null + loadedInCommon: CountProgress | null + loadedInCommonDownload: DownloadProgress | null loadedOwners: number ownersErrors: Array<{ address: string; error: Error }> inCommon: InCommon @@ -27,7 +29,8 @@ function useEventInCommon( } { const [completed, setCompleted] = useState(false) const [loading, setLoading] = useState(false) - const [loadedProgress, setLoadedProgress] = useState(null) + const [loadedInCommon, setLoadedInCommon] = useState(null) + const [loadedProgress, setLoadedProgress] = useState(null) const [loadedOwners, setLoadedOwners] = useState(0) const [errors, setErrors] = useState>([]) const [inCommon, setInCommon] = useState({}) @@ -144,24 +147,39 @@ function useEventInCommon( } else { setLoading(true) setLoadedOwners(0) + setLoadedInCommon(null) setLoadedProgress(null) - getInCommonEventsWithProgress( - eventId, - /*abortSignal*/undefined, - /*onProgress*/({ progress, estimated, rate }) => { - if (progress != null) { - setLoadedProgress({ - progress, - estimated: estimated ?? null, - rate: rate ?? null, - }) - } else { - setLoadedProgress(null) - } - }, - /*refresh*/force + ;( + stream + ? getInCommonEventsWithEvents( + eventId, + /*refresh*/force, + /*onProgress*/(received, total) => { + setLoadedInCommon({ + count: received, + total, + }) + }, + ) + : getInCommonEventsWithProgress( + eventId, + /*abortSignal*/undefined, + /*onProgress*/({ progress, estimated, rate }) => { + if (progress != null) { + setLoadedProgress({ + progress, + estimated: estimated ?? null, + rate: rate ?? null, + }) + } else { + setLoadedProgress(null) + } + }, + /*refresh*/force + ) ).then( (result) => { + setLoadedInCommon(null) setLoadedProgress(null) if (!result) { return fetchOwnersInCommon(controllers) @@ -175,6 +193,7 @@ function useEventInCommon( setCachedTs(result.ts) }, (err) => { + setLoadedInCommon(null) setLoadedProgress(null) console.error(err) return fetchOwnersInCommon(controllers) @@ -189,13 +208,14 @@ function useEventInCommon( } setCompleted(false) setLoading(false) + setLoadedInCommon(null) setLoadedProgress(null) setLoadedOwners(0) setErrors([]) setInCommon({}) } }, - [eventId, owners, force, local, fetchOwnersInCommon] + [eventId, owners, force, local, stream, fetchOwnersInCommon] ) function retryAddress(address: string): () => void { @@ -212,7 +232,8 @@ function useEventInCommon( return { completedEventInCommon: completed, loadingEventInCommon: loading, - loadedInCommonProgress: loadedProgress, + loadedInCommon, + loadedInCommonDownload: loadedProgress, loadedOwners, ownersErrors: errors, inCommon, diff --git a/src/hooks/useEventsInCommon.ts b/src/hooks/useEventsInCommon.ts index f4a4c57..42fe01d 100644 --- a/src/hooks/useEventsInCommon.ts +++ b/src/hooks/useEventsInCommon.ts @@ -1,10 +1,10 @@ import { useCallback, useEffect, useState } from 'react' -import { getInCommonEventsWithProgress } from 'loaders/api' +import { getInCommonEventsWithEvents, getInCommonEventsWithProgress } from 'loaders/api' import { scanAddress } from 'loaders/poap' import { AbortedError } from 'models/error' import { POAP } from 'models/poap' import { Drop } from 'models/drop' -import { Progress } from 'models/http' +import { CountProgress, DownloadProgress } from 'models/http' import { EventsInCommon, InCommon } from 'models/api' import { filterInCommon } from 'models/in-common' @@ -14,12 +14,14 @@ function useEventsInCommon( all: boolean = false, force: boolean = false, local: boolean = false, + stream: boolean = false, ): { completedEventsInCommon: boolean completedInCommonEvents: Record loadingInCommonEvents: Record eventsInCommonErrors: Record> - loadedEventsProgress: Record + loadedEventsInCommon: Record + loadedEventsProgress: Record loadedEventsOwners: Record eventsInCommon: Record fetchEventsInCommon: () => () => void @@ -28,7 +30,8 @@ function useEventsInCommon( const [completed, setCompleted] = useState>({}) const [loading, setLoading] = useState>({}) const [errors, setErrors] = useState>>({}) - const [loadedProgress, setLoadedProgress] = useState>({}) + const [loadedInCommon, setLoadedInCommon] = useState>({}) + const [loadedProgress, setLoadedProgress] = useState>({}) const [loadedOwners, setLoadedOwners] = useState>({}) const [inCommon, setInCommon] = useState>({}) @@ -141,6 +144,31 @@ function useEventsInCommon( }) } + function updateLoadedInCommon( + eventId: number, + { count, total }: CountProgress, + ) { + setLoadedInCommon((prevLoadedInCommon) => ({ + ...(prevLoadedInCommon ?? {}), + [eventId]: { count, total }, + })) + } + + function removeLoadedInCommon(eventId: number): void { + setLoadedInCommon((prevLoadedInCommon) => { + if (prevLoadedInCommon == null) { + return {} + } + const newProgress: Record = {} + for (const [loadingEventId, progress] of Object.entries(prevLoadedInCommon)) { + if (String(eventId) !== String(loadingEventId)) { + newProgress[loadingEventId] = progress + } + } + return newProgress + }) + } + function addLoadedProgress(eventId: number): void { setLoadedProgress((alsoProgress) => ({ ...alsoProgress, @@ -154,7 +182,7 @@ function useEventsInCommon( function updateLoadedProgress( eventId: number, - { progress, estimated, rate }: Progress, + { progress, estimated, rate }: DownloadProgress, ): void { setLoadedProgress((alsoProgress) => { if (alsoProgress[eventId] != null) { @@ -176,7 +204,7 @@ function useEventsInCommon( if (alsoProgress == null) { return {} } - const newProgress: Record = {} + const newProgress: Record = {} for (const [loadingEventId, progress] of Object.entries(alsoProgress)) { if (String(eventId) !== String(loadingEventId)) { newProgress[loadingEventId] = progress @@ -383,22 +411,35 @@ function useEventsInCommon( } else { removeCompleted(eventId) addLoading(eventId) - addLoadedProgress(eventId) let result: EventsInCommon | null = null try { - result = await getInCommonEventsWithProgress( - eventId, - controller.signal, - /*onProgress*/({ progress, estimated, rate }) => { - if (progress != null) { - updateLoadedProgress(eventId, { progress, estimated, rate }) - } else { - removeLoadedProgress(eventId) - } - }, - /*refresh*/force - ) + if (stream) { + result = await getInCommonEventsWithEvents( + eventId, + /*refresh*/force, + /*onProgress*/(received, total) => { + updateLoadedInCommon(eventId, { count: received, total }) + }, + ) + removeLoadedInCommon(eventId) + } else { + addLoadedProgress(eventId) + result = await getInCommonEventsWithProgress( + eventId, + controller.signal, + /*onProgress*/({ progress, estimated, rate }) => { + if (progress != null) { + updateLoadedProgress(eventId, { progress, estimated, rate }) + } else { + removeLoadedProgress(eventId) + } + }, + /*refresh*/force + ) + removeLoadedProgress(eventId) + } } catch (err: unknown) { + removeLoadedInCommon(eventId) removeLoadedProgress(eventId) if (!(err instanceof AbortedError)) { console.error(err) @@ -408,6 +449,7 @@ function useEventsInCommon( } return } + removeLoadedInCommon(eventId) removeLoadedProgress(eventId) if (result == null) { await processEvent(eventId, addresses, controllers) @@ -421,7 +463,7 @@ function useEventsInCommon( } } }, - [force, local, processEvent] + [force, local, stream, processEvent] ) const fetchEventsInCommon = useCallback( @@ -430,6 +472,7 @@ function useEventsInCommon( setCompleted({}) setLoading({}) setErrors({}) + setLoadedInCommon({}) setLoadedProgress({}) setLoadedOwners({}) setInCommon({}) @@ -469,6 +512,7 @@ function useEventsInCommon( setCompleted({}) setLoading({}) setErrors({}) + setLoadedInCommon({}) setLoadedProgress({}) setLoadedOwners({}) setInCommon({}) @@ -497,6 +541,7 @@ function useEventsInCommon( completedInCommonEvents: completed, loadingInCommonEvents: loading, eventsInCommonErrors: errors, + loadedEventsInCommon: loadedInCommon, loadedEventsProgress: loadedProgress, loadedEventsOwners: loadedOwners, eventsInCommon: inCommon, diff --git a/src/hooks/useEventsOwnersAndMetrics.ts b/src/hooks/useEventsOwnersAndMetrics.ts index f2d227d..7c819a1 100644 --- a/src/hooks/useEventsOwnersAndMetrics.ts +++ b/src/hooks/useEventsOwnersAndMetrics.ts @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react' import { filterInvalidOwners } from 'models/address' import { AbortedError } from 'models/error' -import { InCommon } from 'models/api' +import { EventAndOwners, InCommon } from 'models/api' import { fetchPOAPs } from 'loaders/poap' import { getEventAndOwners, @@ -117,7 +117,7 @@ function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record { removeError(eventId) addLoading(eventId) - let eventAndOwners + let eventAndOwners: EventAndOwners | null = null try { eventAndOwners = await getEventAndOwners( eventId, diff --git a/src/loaders/api.ts b/src/loaders/api.ts index 553c4eb..a2e58b7 100644 --- a/src/loaders/api.ts +++ b/src/loaders/api.ts @@ -7,11 +7,12 @@ import { CachedEvent, Feedback, EventsInCommon, + EventAndOwners, } from 'models/api' import { encodeExpiryDates } from 'models/event' import { parseDrop, parseDropMetrics, parseDropOwners, Drop, DropMetrics, DropOwners } from 'models/drop' import { AbortedError, HttpError } from 'models/error' -import { Progress } from 'models/http' +import { DownloadProgress } from 'models/http' export async function getEventAndOwners( eventId: number, @@ -19,12 +20,7 @@ export async function getEventAndOwners( includeDescription: boolean = false, includeMetrics: boolean = true, refresh: boolean = false, -): Promise<{ - event: Drop - owners: string[] - ts: number - metrics: DropMetrics | null -} | null> { +): Promise { if (!FAMILY_API_KEY) { throw new Error( `Drop ${eventId} and owners could not be fetched, ` + @@ -197,7 +193,7 @@ export async function getInCommonEvents( export async function getInCommonEventsWithProgress( eventId: number, abortSignal: AbortSignal, - onProgress: (progressEvent: Partial) => void, + onProgress: (progressEvent: Partial) => void, refresh: boolean = false, ): Promise { if (!FAMILY_API_KEY) { @@ -295,6 +291,105 @@ export async function getInCommonEventsWithProgress( } } +export async function getInCommonEventsWithEvents( + eventId: number, + refresh: boolean = false, + onProgress: (received: number, total: number) => void, + onTs?: (ts: number) => void, + onEventIds?: (eventIds: number[]) => void, + onInCommon?: (eventId: number, owners: string[]) => void, +): Promise { + let total: number + let received = 0 + const inCommon: EventsInCommon = { + events: {}, + inCommon: {}, + ts: null, + } + const inCommonEvents = new EventSource( + `${FAMILY_API_URL}/event/${eventId}/in-common` + + `/stream${refresh ? '?refresh=true' : ''}` + ) + return new Promise((resolve, reject) => { + inCommonEvents.addEventListener('error', (ev) => { + reject(new Error(`Something happen: ${ev}`)) + }) + + inCommonEvents.addEventListener('message', (ev) => { + let data: unknown | undefined + try { + data = JSON.parse(ev.data) + } catch { + reject(new Error(`Cannot parse data '${ev.data}'`)) + return + } + if (!data || typeof data !== 'object') { + reject(new Error('Malformed data')) + return + } + if ('ts' in data && data.ts && typeof data.ts === 'number') { + inCommon.ts = data.ts + onTs?.(data.ts) + } + if ( + 'eventIds' in data && + data.eventIds && + Array.isArray(data.eventIds) && + data.eventIds.every( + (eventId) => eventId && typeof eventId === 'number' + ) + ) { + total = data.eventIds.length + for (const eventId of data.eventIds) { + inCommon.inCommon[eventId] = [] + } + onEventIds?.(data.eventIds) + } + if ( + 'eventId' in data && + data.eventId && + typeof data.eventId === 'number' && + 'owners' in data && + data.owners && + Array.isArray(data.owners) && + data.owners.every( + (owner) => owner && typeof owner === 'string' + ) + ) { + received++ + inCommon.inCommon[data.eventId] = data.owners + onInCommon?.(data.eventId, data.owners) + if (total) { + onProgress(received, total) + } + } + if ( + 'events' in data && + data.events && + typeof data.events === 'object' + ) { + inCommon.events = Object.fromEntries( + Object.entries(data.events).map( + ([eventIdRaw, eventData]) => [ + eventIdRaw, + parseDrop(eventData, /*includeDescription*/false), + ] + ) + ) + } + if ( + inCommon.ts != null && + received === total && + Object.values(inCommon.inCommon).every((owners) => owners.length > 0) && + Object.keys(inCommon.events).length > 0 + ) { + resolve(inCommon) + return + } + }) + }) +} + export async function getLastEvents( page: number = 1, qty: number = 3, diff --git a/src/models/api.ts b/src/models/api.ts index a0d01f3..c93cd6f 100644 --- a/src/models/api.ts +++ b/src/models/api.ts @@ -1,16 +1,10 @@ -import { Drop } from './drop' +import { Drop, DropMetrics } from 'models/drop' export const FAMILY_API_URL = process.env.REACT_APP_FAMILY_API_URL ?? 'https://api.poap.family' export const FAMILY_API_KEY = process.env.REACT_APP_FAMILY_API_KEY export type InCommon = Record -export interface EventsInCommon { - events: Record - inCommon: InCommon - ts: number | null -} - export function parseInCommon(inCommon: unknown): InCommon { if ( inCommon == null || @@ -33,6 +27,19 @@ export function parseInCommon(inCommon: unknown): InCommon { return inCommon } +export interface EventsInCommon { + events: Record + inCommon: InCommon + ts: number | null +} + +export interface EventAndOwners { + event: Drop + owners: string[] + ts: number + metrics: DropMetrics | null +} + export interface CachedEvent { id: number name: string diff --git a/src/models/http.ts b/src/models/http.ts index 6cf0e13..1169b56 100644 --- a/src/models/http.ts +++ b/src/models/http.ts @@ -1,5 +1,10 @@ -export interface Progress { +export interface DownloadProgress { progress: number rate: number | null estimated: number | null -} \ No newline at end of file +} + +export interface CountProgress { + count: number + total: number +} diff --git a/src/pages/Event.tsx b/src/pages/Event.tsx index 6d43908..110dd23 100644 --- a/src/pages/Event.tsx +++ b/src/pages/Event.tsx @@ -46,7 +46,8 @@ function Event() { const { completedEventInCommon, loadingEventInCommon, - loadedInCommonProgress, + loadedInCommon, + loadedInCommonDownload, loadedOwners, ownersErrors, inCommon, @@ -54,7 +55,13 @@ function Event() { cachedTs, fetchEventInCommon, retryAddress, - } = useEventInCommon(event.id, owners, force, /*local*/false) + } = useEventInCommon( + event.id, + owners, + /*refresh*/force, + /*local*/false, + /*stream*/true + ) const eventIds = useMemo( () => [event.id], @@ -174,15 +181,24 @@ function Event() { {loadedOwners > 0 ? : ( - loadedInCommonProgress != null + loadedInCommon != null ? ( - - ) - : + + ) + : ( + loadedInCommonDownload != null + ? ( + + ) + : + ) ) } diff --git a/src/pages/Events.tsx b/src/pages/Events.tsx index 7400807..797b7cd 100644 --- a/src/pages/Events.tsx +++ b/src/pages/Events.tsx @@ -78,12 +78,20 @@ function Events() { completedInCommonEvents, loadingInCommonEvents, eventsInCommonErrors, + loadedEventsInCommon, loadedEventsProgress, loadedEventsOwners, eventsInCommon, fetchEventsInCommon, retryEventAddressInCommon, - } = useEventsInCommon(eventIds, eventsOwners, all, force, /*local*/false) + } = useEventsInCommon( + eventIds, + eventsOwners, + all, + /*refresh*/force, + /*local*/false, + /*stream*/true + ) const { loadingCollections, @@ -363,6 +371,7 @@ function Events() { {( loadingInCommonEvents[event.id] != null && + loadedEventsInCommon[event.id] == null && loadedEventsProgress[event.id] == null && loadedEventsOwners[event.id] != null && eventsOwners[event.id] != null @@ -373,7 +382,20 @@ function Events() { showValue={loadedEventsOwners[event.id] > 0} /> )} - {loadedEventsProgress[event.id] != null && ( + {( + loadedEventsInCommon[event.id] != null && + loadedEventsProgress[event.id] == null + ) && ( + 0} + /> + )} + {( + loadedEventsInCommon[event.id] == null && + loadedEventsProgress[event.id] != null + ) && (