-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ECO-2404] Add localStorage caching of events (#354)
Co-authored-by: Matt <[email protected]>
- Loading branch information
Showing
6 changed files
with
88 additions
and
173 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
194 changes: 28 additions & 166 deletions
194
src/typescript/frontend/src/lib/store/event/local-storage.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,177 +1,39 @@ | ||
import { | ||
type DatabaseModels, | ||
type AnyEventModel, | ||
type MarketLatestStateEventModel, | ||
type MarketRegistrationEventModel, | ||
type GlobalStateEventModel, | ||
} from "@sdk/indexer-v2/types"; | ||
import { type TableName } from "@sdk/indexer-v2/types/json-types"; | ||
import { LOCALSTORAGE_EXPIRY_TIME_MS } from "const"; | ||
import { type AnyEventModel } from "@sdk/indexer-v2/types"; | ||
import { parseJSON, stringifyJSON } from "utils"; | ||
import { type MarketEventStore, type EventState, type SymbolString } from "./types"; | ||
import { createInitialMarketState } from "./utils"; | ||
import { type DeepWritable } from "@sdk/utils/utility-types"; | ||
|
||
const shouldKeepItem = <T extends AnyEventModel>(event: T) => { | ||
const eventTime = Number(event.transaction.timestamp); | ||
const now = new Date().getTime(); | ||
return eventTime > now - LOCALSTORAGE_EXPIRY_TIME_MS; | ||
}; | ||
export const LOCAL_STORAGE_EXPIRATION_TIME = 1000 * 60; // 10 minutes. | ||
|
||
export const addToLocalStorage = <T extends AnyEventModel>(event: T) => { | ||
const { eventName } = event; | ||
const events = localStorage.getItem(eventName) ?? "[]"; | ||
const filtered: T[] = parseJSON<T[]>(events).filter(shouldKeepItem); | ||
const guids = new Set(filtered.map((e) => e.guid)); | ||
if (!guids.has(event.guid)) { | ||
filtered.push(event); | ||
} | ||
localStorage.setItem(eventName, stringifyJSON(filtered)); | ||
}; | ||
export const LOCAL_STORAGE_EVENT_TYPES = [ | ||
"swap", | ||
"chat", | ||
"liquidity", | ||
"market", | ||
"periodic", | ||
] as const; | ||
type EventLocalStorageKey = (typeof LOCAL_STORAGE_EVENT_TYPES)[number]; | ||
|
||
type EventArraysByModelType = { | ||
Swap: Array<DatabaseModels[TableName.SwapEvents]>; | ||
Chat: Array<DatabaseModels[TableName.ChatEvents]>; | ||
MarketRegistration: Array<DatabaseModels[TableName.MarketRegistrationEvents]>; | ||
PeriodicState: Array<DatabaseModels[TableName.PeriodicStateEvents]>; | ||
State: Array<DatabaseModels[TableName.MarketLatestStateEvent]>; | ||
GlobalState: Array<DatabaseModels[TableName.GlobalStateEvents]>; | ||
Liquidity: Array<DatabaseModels[TableName.LiquidityEvents]>; | ||
const shouldKeep = (e: AnyEventModel) => { | ||
const now = new Date().getTime(); | ||
// The time at which all events that occurred prior to are considered stale. | ||
const staleTimeBoundary = new Date(now - LOCAL_STORAGE_EXPIRATION_TIME); | ||
return e.transaction.timestamp > staleTimeBoundary; | ||
}; | ||
|
||
const emptyEventArraysByModelType: () => EventArraysByModelType = () => ({ | ||
Swap: [] as EventArraysByModelType["Swap"], | ||
Chat: [] as EventArraysByModelType["Chat"], | ||
MarketRegistration: [] as EventArraysByModelType["MarketRegistration"], | ||
PeriodicState: [] as EventArraysByModelType["PeriodicState"], | ||
State: [] as EventArraysByModelType["State"], | ||
GlobalState: [] as EventArraysByModelType["GlobalState"], | ||
Liquidity: [] as EventArraysByModelType["Liquidity"], | ||
}); | ||
|
||
type MarketEventTypes = | ||
| DatabaseModels["swap_events"] | ||
| DatabaseModels["chat_events"] | ||
| MarketLatestStateEventModel | ||
| DatabaseModels["liquidity_events"] | ||
| DatabaseModels["periodic_state_events"]; | ||
|
||
export const initialStatePatch = (): EventState => { | ||
return { | ||
guids: new Set<string>(), | ||
stateFirehose: [], | ||
marketRegistrations: [], | ||
markets: new Map(), | ||
globalStateEvents: [], | ||
}; | ||
export const updateLocalStorage = (key: EventLocalStorageKey, event: AnyEventModel) => { | ||
const str = localStorage.getItem(key) ?? "[]"; | ||
const data: AnyEventModel[] = parseJSON(str); | ||
data.unshift(event); | ||
localStorage.setItem(key, stringifyJSON(data)); | ||
}; | ||
|
||
export const initialStateFromLocalStorage = (): EventState => { | ||
// Purge stale events then load up the remaining ones. | ||
const events = getEventsFromLocalStorage(); | ||
|
||
// Sort each event that has a market by its market. | ||
const markets: Map<SymbolString, DeepWritable<MarketEventStore>> = new Map(); | ||
const guids: Set<AnyEventModel["guid"]> = new Set(); | ||
|
||
const addGuidAndGetMarket = (event: MarketEventTypes) => { | ||
// Before ensuring the market is initialized, add the incoming event to the set of guids. | ||
guids.add(event.guid); | ||
|
||
const { market } = event; | ||
const symbol = market.symbolData.symbol; | ||
if (!markets.has(symbol)) { | ||
markets.set(symbol, createInitialMarketState(market)); | ||
} | ||
return markets.get(symbol)!; | ||
}; | ||
|
||
const marketRegistrations: MarketRegistrationEventModel[] = []; | ||
const globalStateEvents: GlobalStateEventModel[] = []; | ||
|
||
events.Chat.forEach((e) => { | ||
addGuidAndGetMarket(e).chatEvents.push(e); | ||
}); | ||
events.Liquidity.forEach((e) => { | ||
addGuidAndGetMarket(e).liquidityEvents.push(e); | ||
}); | ||
events.State.forEach((e) => { | ||
addGuidAndGetMarket(e).stateEvents.push(e); | ||
}); | ||
events.Swap.forEach((e) => { | ||
addGuidAndGetMarket(e).swapEvents.push(e); | ||
}); | ||
events.PeriodicState.forEach((e) => { | ||
addGuidAndGetMarket(e)[e.periodicMetadata.period].candlesticks.push(e); | ||
}); | ||
events.MarketRegistration.forEach((e) => { | ||
marketRegistrations.push(e); | ||
guids.add(e.guid); | ||
}); | ||
events.GlobalState.forEach((e) => { | ||
globalStateEvents.push(e); | ||
guids.add(e.guid); | ||
}); | ||
|
||
const stateFirehose: MarketLatestStateEventModel[] = []; | ||
|
||
for (const { stateEvents } of markets.values()) { | ||
stateFirehose.push(...(stateEvents as Array<MarketLatestStateEventModel>)); | ||
} | ||
|
||
// Sort the state firehose by bump time, then market ID, then market nonce. | ||
stateFirehose.sort(({ market: a }, { market: b }) => { | ||
if (a.time === b.time) { | ||
if (a.marketID === b.marketID) { | ||
if (a.marketNonce === b.marketNonce) return 0; | ||
if (a.marketNonce < b.marketNonce) return 1; | ||
return -1; | ||
} else if (a.marketID < b.marketID) { | ||
return 1; | ||
} | ||
return -1; | ||
} else if (a.time < b.time) { | ||
return -1; | ||
} | ||
return 1; | ||
}); | ||
|
||
return { | ||
guids, | ||
stateFirehose, | ||
marketRegistrations, | ||
markets: markets as unknown as Map<SymbolString, MarketEventStore>, | ||
globalStateEvents, | ||
}; | ||
export const cleanReadLocalStorage = (key: EventLocalStorageKey) => { | ||
const str = localStorage.getItem(key) ?? "[]"; | ||
const data: AnyEventModel[] = parseJSON(str); | ||
const relevantItems = data.filter(shouldKeep); | ||
localStorage.setItem(key, stringifyJSON(relevantItems)); | ||
return relevantItems; | ||
}; | ||
|
||
/** | ||
* Purges old local storage events and returns any that remain. | ||
*/ | ||
export const getEventsFromLocalStorage = () => { | ||
const res = emptyEventArraysByModelType(); | ||
const guids = new Set<string>(); | ||
|
||
// Filter the events in local storage, then return them. | ||
Object.entries(res).forEach((entry) => { | ||
const eventName = entry[0] as keyof EventArraysByModelType; | ||
const existing = localStorage.getItem(eventName) ?? "[]"; | ||
const filtered = | ||
parseJSON<EventArraysByModelType[typeof eventName]>(existing).filter(shouldKeepItem); | ||
const reduced = filtered.reduce( | ||
(acc, curr) => { | ||
if (!guids.has(curr.guid)) { | ||
acc.push(curr); | ||
guids.add(curr.guid); | ||
} | ||
return acc; | ||
}, | ||
[] as typeof filtered | ||
); | ||
const events = entry[1] as typeof filtered; | ||
events.push(...reduced); | ||
localStorage.setItem(eventName, stringifyJSON(filtered)); | ||
}); | ||
|
||
return res; | ||
export const clearLocalStorage = (key: EventLocalStorageKey) => { | ||
localStorage.setItem(key, "[]"); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters