Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ECO-2874): Add arena broker events to the state store #636

Open
wants to merge 4 commits into
base: arena
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/rust/broker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,18 @@ Arena events don't follow the same rules as the other subscriptions.
To subscribe to arena events, simply pass `{ "arena": true }`. The default value
is `false`; i.e., no subscription to arena events.

To subscribe to arena candlesticks, pass `{ "arena_candlesticks": true }`. The
default value is `false`.

```json5
// Subscribe to every single event type.
{ "arena": true, "markets": [], "event_types": [] }
{ "arena": true, "arena_candlesticks": true, "markets": [], "event_types": [] }

// Subscribe to all non-arena event types.
// Both of the JSON objects below are equivalent.
// All of the JSON objects below are equivalent.
{ "markets": [], "event_types": [] }
{ "arena": false, "markets": [], "event_types": [] }
{ "arena_candlesticks": false, "markets": [], "event_types": [] }
```

### All markets, all non-arena event types
Expand Down
2 changes: 2 additions & 0 deletions src/rust/broker/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ pub struct Subscription {
pub event_types: Vec<EmojicoinDbEventType>,
#[serde(default)]
pub arena: bool,
#[serde(default)]
pub arena_candlesticks: bool,
}
1 change: 1 addition & 0 deletions src/rust/broker/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub fn is_match(subscription: &Subscription, event: &EmojicoinDbEvent) -> bool {
EmojicoinDbEventType::ArenaMelee => subscription.arena,
EmojicoinDbEventType::ArenaSwap => subscription.arena,
EmojicoinDbEventType::ArenaVaultBalanceUpdate => subscription.arena,
EmojicoinDbEventType::ArenaCandlestick => subscription.arena_candlesticks,
EmojicoinDbEventType::GlobalState => {
subscription.event_types.is_empty() || subscription.event_types.contains(&event_type)
}
Expand Down
2 changes: 2 additions & 0 deletions src/typescript/frontend/src/app/home/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SubscribeToHomePageEvents } from "@/components/pages/home/components/SubscribeToHomePageEvents";
import { ARENA_MODULE_ADDRESS } from "@sdk/const";
import {
type ArenaInfoModel,
Expand Down Expand Up @@ -67,6 +68,7 @@ export default async function HomePageComponent({
sortBy={sortBy}
searchBytes={searchBytes}
/>
<SubscribeToHomePageEvents info={meleeData?.melee} />
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type ArenaPositionModel,
} from "@sdk/indexer-v2/types";
import { ROUTES } from "router/routes";
import { useReliableSubscribe } from "@hooks/use-reliable-subscribe";

const RewardsRemainingBox = ({ rewardsRemaining }: { rewardsRemaining: bigint }) => {
const { isMobile } = useMatchBreakpoints();
Expand Down Expand Up @@ -97,6 +98,8 @@ export const ArenaClient = (props: ArenaProps) => {
const [position, setPosition] = useState<ArenaPositionModel | undefined | null>(null);
const [history, setHistory] = useState<ArenaLeaderboardHistoryWithArenaInfoModel[]>([]);

useReliableSubscribe({ eventTypes: ["Swap"], arena: true, arenaCandlesticks: true });

const r = useMemo(
() =>
isMobile ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import { useReliableSubscribe } from "@hooks/use-reliable-subscribe";
import { type ArenaInfoModel } from "@sdk/indexer-v2";
import { useEventStore } from "context/event-store-context";
import { useEffect } from "react";

export const SubscribeToHomePageEvents = ({ info }: { info?: ArenaInfoModel }) => {
const loadArenaInfoFromServer = useEventStore((s) => s.loadArenaInfoFromServer);
useEffect(() => {
if (info) {
loadArenaInfoFromServer(info);
}
}, [loadArenaInfoFromServer, info]);

useReliableSubscribe({
arena: true,
eventTypes: ["MarketLatestState"],
});

return <></>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { Text } from "components/text";
import Link from "next/link";
import { ROUTES } from "router/routes";
import { type HomePageProps } from "app/home/HomePage";
import { useReliableSubscribe } from "@hooks/use-reliable-subscribe";
import { SortMarketsBy } from "@sdk/indexer-v2/types/common";
import { Emoji } from "utils/emoji";

Expand Down Expand Up @@ -99,10 +98,6 @@ const EmojiTable = (props: EmojiTableProps) => {

const rowLength = useGridRowLength();

useReliableSubscribe({
eventTypes: ["MarketLatestState"],
});

return (
<>
<ButtonsBlock
Expand Down
17 changes: 17 additions & 0 deletions src/typescript/frontend/src/hooks/use-latest-melee-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEventStore } from "context/event-store-context";
import { useMemo } from "react";

export const useLatestMeleeID = () => {
const melees = useEventStore((s) => s.meleeEvents);

const latest = useMemo(
() =>
melees
.map(({ melee }) => melee.meleeID)
.sort()
.at(-1) ?? -1n,
[melees]
);

return latest;
};
25 changes: 11 additions & 14 deletions src/typescript/frontend/src/hooks/use-reliable-subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,35 @@ import { useEffect } from "react";
export type ReliableSubscribeArgs = {
eventTypes: Array<SubscribableBrokerEvents>;
arena?: boolean;
arenaCandlesticks?: boolean;
};

/**
* Helper hook to manage the subscriptions to a set of topics while the component using it is
* mounted. It automatically cleans up subscriptions when the component is unmounted.
*/
export const useReliableSubscribe = (args: ReliableSubscribeArgs) => {
const { eventTypes, arena } = args;
const { eventTypes, arena, arenaCandlesticks } = args;
const subscribeEvents = useEventStore((s) => s.subscribeEvents);
const unsubscribeEvents = useEventStore((s) => s.unsubscribeEvents);
const subscribeArena = useEventStore((s) => s.subscribeArena);
const unsubscribeArena = useEventStore((s) => s.unsubscribeArena);

useEffect(() => {
// Don't subscribe right away, to let other components unmounting time to unsubscribe, that way
// components unmounting don't undo/overwrite another component subscribing.
const timeout = window.setTimeout(() => {
subscribeEvents(eventTypes);
if (arena) {
subscribeArena();
} else {
unsubscribeArena();
}
subscribeEvents(eventTypes, {
baseEvents: arena,
candlesticks: arenaCandlesticks,
});
}, 250);

// Unsubscribe from all topics passed into the hook when the component unmounts.
return () => {
clearTimeout(timeout);
unsubscribeEvents(eventTypes);
if (arena) {
unsubscribeArena();
}
unsubscribeEvents(eventTypes, {
baseEvents: arena,
candlesticks: arenaCandlesticks,
});
};
}, [eventTypes, arena, subscribeEvents, unsubscribeEvents, subscribeArena, unsubscribeArena]);
}, [eventTypes, arena, arenaCandlesticks, subscribeEvents, unsubscribeEvents]);
};
36 changes: 36 additions & 0 deletions src/typescript/frontend/src/lib/store/arena/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
type ArenaMeleeModel,
type ArenaEnterModel,
type ArenaExitModel,
type ArenaSwapModel,
type ArenaInfoModel,
} from "@sdk/indexer-v2";
import { type WritableDraft } from "immer";

export type MeleeState = {
swaps: readonly ArenaSwapModel[];
enters: readonly ArenaEnterModel[];
exits: readonly ArenaExitModel[];
};

export type ArenaState = {
arenaInfoFromServer?: ArenaInfoModel;
meleeEvents: readonly ArenaMeleeModel[];
melees: Readonly<Map<bigint, MeleeState>>;
};

export type ArenaActions = {
loadArenaInfoFromServer: (info: ArenaInfoModel) => void;
};

export const createInitialMeleeState = (): WritableDraft<MeleeState> => ({
swaps: [],
enters: [],
exits: [],
});

export const initializeArenaStore = (): ArenaState => ({
arenaInfoFromServer: undefined,
meleeEvents: [],
melees: new Map(),
});
67 changes: 67 additions & 0 deletions src/typescript/frontend/src/lib/store/arena/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { type MarketEmojiData, toMarketEmojiData } from "@sdk/emoji_data";
import { type AccountAddressString } from "@sdk/emojicoin_dot_fun";
import {
type ArenaEventModels,
type ArenaVaultBalanceUpdateModel,
type ArenaInfoModel,
type ArenaEventModelWithMeleeID,
} from "@sdk/indexer-v2";
import { isArenaEnterModel, isArenaExitModel, isArenaMeleeModel } from "@sdk/types/arena-types";

export type ArenaMarketPair = {
market0: {
marketID: bigint;
marketAddress: AccountAddressString;
symbol: string;
} & MarketEmojiData;
market1: {
marketID: bigint;
marketAddress: AccountAddressString;
symbol: string;
} & MarketEmojiData;
};

export const toArenaMarketPair = (info: ArenaInfoModel): ArenaMarketPair => {
const symbol0 = info.emojicoin0Symbols.join("");
const symbol1 = info.emojicoin1Symbols.join("");
return {
market0: {
marketID: info.emojicoin0MarketID,
marketAddress: info.emojicoin0MarketAddress,
symbol: symbol0,
...toMarketEmojiData(symbol0),
},
market1: {
marketID: info.emojicoin1MarketID,
marketAddress: info.emojicoin1MarketAddress,
symbol: symbol1,
...toMarketEmojiData(symbol1),
},
};
};

export const toMappedMelees = <T extends ArenaEventModelWithMeleeID>(models: T[]) => {
const map = new Map<bigint, T[]>();

models.forEach((model) => {
const id = getMeleeIDFromArenaModel(model);
if (!map.has(id)) {
map.set(id, []);
}
map.get(id)!.push(model);
});
return map;
};

export const getMeleeIDFromArenaModel = (
model: Exclude<ArenaEventModels, ArenaVaultBalanceUpdateModel>
): bigint => {
if (isArenaMeleeModel(model)) {
return model.melee.meleeID;
} else if (isArenaEnterModel(model)) {
return model.enter.meleeID;
} else if (isArenaExitModel(model)) {
return model.exit.meleeID;
}
return model.swap.meleeID;
};
43 changes: 41 additions & 2 deletions src/typescript/frontend/src/lib/store/event/event-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
pushPeriodicStateEvents,
toMappedMarketEvents,
initialState,
ensureMeleeInStore,
} from "./utils";
import { periodEnumToRawDuration } from "@sdk/const";
import { createWebSocketClientStore, type WebSocketClientStore } from "../websocket/store";
Expand All @@ -29,15 +30,30 @@ import {
clearLocalStorage,
LOCAL_STORAGE_EVENT_TYPES,
} from "./local-storage";
import { initializeArenaStore } from "../arena/store";
import {
isArenaEnterModel,
isArenaEventModelWithMeleeID,
isArenaExitModel,
isArenaMeleeModel,
isArenaSwapModel,
} from "@sdk/types/arena-types";
import { getMeleeIDFromArenaModel, toMappedMelees } from "../arena/utils";

export const createEventStore = () => {
const store = createStore<EventStore & WebSocketClientStore>()(
immer((set, get) => ({
...initialState(),
...initializeArenaStore(),
getMarket: (emojis) => get().markets.get(emojis.join("")),
getRegisteredMarkets: () => {
return get().markets;
},
loadArenaInfoFromServer: (info) => {
set((state) => {
state.arenaInfoFromServer = info;
});
},
loadMarketStateFromServer: (states) => {
const filtered = states.filter((e) => {
const marketEmojis = e.market.symbolEmojis;
Expand Down Expand Up @@ -83,6 +99,17 @@ export const createEventStore = () => {
pushPeriodicStateEvents(market, extractFilter(marketEvents, isPeriodicStateEventModel));
DEBUG_ASSERT(() => marketEvents.length === 0);
});

const arenaEvents = events.filter(isArenaEventModelWithMeleeID);
const arenaMap = toMappedMelees(arenaEvents);
Array.from(arenaMap.entries()).forEach(([meleeID, events]) => {
ensureMeleeInStore(state, meleeID);
const melee = state.melees.get(meleeID)!;
melee.enters.push(...extractFilter(events, isArenaEnterModel));
melee.exits.push(...extractFilter(events, isArenaExitModel));
melee.swaps.push(...extractFilter(events, isArenaSwapModel));
});
state.meleeEvents.push(...extractFilter(arenaEvents, isArenaMeleeModel));
});
},
pushEventsFromClient: (eventsIn: BrokerEventModels[], pushToLocalStorage = false) => {
Expand Down Expand Up @@ -121,8 +148,20 @@ export const createEventStore = () => {
maybeUpdateLocalStorage(pushToLocalStorage, "periodic", event);
}
} else {
// Arena events.
// TODO.
if (isArenaEventModelWithMeleeID(event)) {
const meleeID = getMeleeIDFromArenaModel(event);
ensureMeleeInStore(state, meleeID);
const melee = state.melees.get(meleeID)!;
if (isArenaMeleeModel(event)) {
state.meleeEvents.unshift(event);
} else if (isArenaEnterModel(event)) {
melee.enters.unshift(event);
} else if (isArenaExitModel(event)) {
melee.exits.unshift(event);
} else if (isArenaSwapModel(event)) {
melee.swaps.unshift(event);
}
}
}
}
});
Expand Down
Loading
Loading