From 2adc56de5966983ca1bf7b773657f0bfc3a3cf9a Mon Sep 17 00:00:00 2001 From: ponderingdemocritus Date: Thu, 19 Dec 2024 18:27:46 +1100 Subject: [PATCH 1/2] debounce --- client/src/dojo/debouncedQueries.ts | 114 ++++++++++++++++++ client/src/dojo/queries.ts | 10 +- .../resources/InventoryResources.tsx | 4 +- client/src/ui/layouts/World.tsx | 16 ++- 4 files changed, 131 insertions(+), 13 deletions(-) create mode 100644 client/src/dojo/debouncedQueries.ts diff --git a/client/src/dojo/debouncedQueries.ts b/client/src/dojo/debouncedQueries.ts new file mode 100644 index 000000000..83121766d --- /dev/null +++ b/client/src/dojo/debouncedQueries.ts @@ -0,0 +1,114 @@ +import { Component, Metadata, Schema } from "@dojoengine/recs"; +import { ToriiClient } from "@dojoengine/torii-client"; +import debounce from "lodash/debounce"; +import { + addMarketSubscription, + addToSubscription, + addToSubscriptionOneKeyModelbyRealmEntityId, + addToSubscriptionTwoKeyModelbyRealmEntityId, + syncPosition, +} from "./queries"; + +// Queue class to manage requests +class RequestQueue { + private queue: Array<() => Promise> = []; + private processing = false; + private batchSize = 3; // Number of concurrent requests + private batchDelayMs = 100; // Delay between batches + + async add(request: () => Promise) { + this.queue.push(request); + if (!this.processing) { + this.processing = true; + this.processQueue(); + } + } + + private async processQueue() { + while (this.queue.length > 0) { + const batch = this.queue.splice(0, this.batchSize); + + try { + await Promise.all(batch.map((request) => request())); + } catch (error) { + console.error("Error processing request batch:", error); + } + + if (this.queue.length > 0) { + // Add delay between batches to prevent overwhelming the server + await new Promise((resolve) => setTimeout(resolve, this.batchDelayMs)); + } + } + this.processing = false; + } + + clear() { + this.queue = []; + } +} + +// Create separate queues for different types of requests +const positionQueue = new RequestQueue(); +const subscriptionQueue = new RequestQueue(); +const marketQueue = new RequestQueue(); + +// Debounced functions that add to queues +export const debouncedSyncPosition = debounce( + async (client: ToriiClient, components: Component[], entityID: string) => { + await positionQueue.add(() => syncPosition(client, components, entityID)); + }, + 100, + { leading: true }, // Add leading: true to execute immediately on first call +); + +export const debouncedAddToSubscriptionTwoKey = debounce( + async ( + client: ToriiClient, + components: Component[], + entityID: string[], + ) => { + await subscriptionQueue.add(() => addToSubscriptionTwoKeyModelbyRealmEntityId(client, components, entityID)); + }, + 250, + { leading: true }, +); + +export const debouncedAddToSubscriptionOneKey = debounce( + async ( + client: ToriiClient, + components: Component[], + entityID: string[], + ) => { + await subscriptionQueue.add(() => addToSubscriptionOneKeyModelbyRealmEntityId(client, components, entityID)); + }, + 250, + { leading: true }, +); + +export const debouncedAddToSubscription = debounce( + async ( + client: ToriiClient, + components: Component[], + entityID: string[], + position?: { x: number; y: number }[], + ) => { + await subscriptionQueue.add(() => addToSubscription(client, components, entityID, position)); + }, + 250, + { leading: true }, +); + +export const debouncedAddMarketSubscription = debounce( + async (client: ToriiClient, components: Component[]) => { + await marketQueue.add(() => addMarketSubscription(client, components)); + }, + 500, + { leading: true }, +); + +// Utility function to clear all queues if needed +export const clearAllQueues = () => { + positionQueue.clear(); + subscriptionQueue.clear(); + marketQueue.clear(); +}; diff --git a/client/src/dojo/queries.ts b/client/src/dojo/queries.ts index c1620c90e..9dff82a6d 100644 --- a/client/src/dojo/queries.ts +++ b/client/src/dojo/queries.ts @@ -25,7 +25,7 @@ export const syncPosition = async ( components, [], [], - 30_000, + 5_000, ); }; @@ -53,7 +53,7 @@ export const addToSubscriptionTwoKeyModelbyRealmEntityId = async ( components as any, [], [], - 30_000, + 5_000, ); const end = performance.now(); console.log("AddToSubscriptionEnd", end - start); @@ -145,7 +145,7 @@ export const addMarketSubscription = async ( components, [], [], - 30_000, + 5_000, false, ); const end = performance.now(); diff --git a/client/src/ui/components/resources/InventoryResources.tsx b/client/src/ui/components/resources/InventoryResources.tsx index 0b4b7cd2f..235d28010 100644 --- a/client/src/ui/components/resources/InventoryResources.tsx +++ b/client/src/ui/components/resources/InventoryResources.tsx @@ -1,4 +1,4 @@ -import { addToSubscription } from "@/dojo/queries"; +import { debouncedAddToSubscription } from "@/dojo/debouncedQueries"; import { useDojo } from "@/hooks/context/DojoContext"; import { useResourceBalance, useResourcesUtils } from "@/hooks/helpers/useResources"; import { ResourceCost } from "@/ui/elements/ResourceCost"; @@ -52,7 +52,7 @@ export const InventoryResources = ({ setIsSyncing(true); try { console.log("AddToSubscriptionStart - 4"); - await addToSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any, [ + await debouncedAddToSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any, [ entityId.toString(), ]); localStorage.setItem(cacheKey, now.toString()); diff --git a/client/src/ui/layouts/World.tsx b/client/src/ui/layouts/World.tsx index 3a2132791..22a06988e 100644 --- a/client/src/ui/layouts/World.tsx +++ b/client/src/ui/layouts/World.tsx @@ -3,7 +3,11 @@ import { lazy, Suspense, useEffect, useMemo, useState } from "react"; import { Redirect } from "wouter"; import useUIStore from "../../hooks/store/useUIStore"; -import { addMarketSubscription, addToSubscription, addToSubscriptionOneKeyModelbyRealmEntityId } from "@/dojo/queries"; +import { + debouncedAddMarketSubscription, + debouncedAddToSubscription, + debouncedAddToSubscriptionOneKey, +} from "@/dojo/debouncedQueries"; import { useDojo } from "@/hooks/context/DojoContext"; import { PlayerStructure, useEntities } from "@/hooks/helpers/useEntities"; import { useStructureEntityId } from "@/hooks/helpers/useStructureEntityId"; @@ -143,7 +147,7 @@ export const World = ({ backgroundImage }: { backgroundImage: string }) => { console.log("AddToSubscriptionStart - 1"); try { await Promise.all([ - addToSubscription( + debouncedAddToSubscription( dojo.network.toriiClient, dojo.network.contractComponents as any, [structureEntityId.toString()], @@ -164,13 +168,13 @@ export const World = ({ backgroundImage }: { backgroundImage: string }) => { try { setWorldLoading(true); console.log("AddToSubscriptionStart - 2"); - addToSubscription( + debouncedAddToSubscription( dojo.network.toriiClient, dojo.network.contractComponents as any, [...filteredStructures.map((structure) => structure.entity_id.toString())], [...filteredStructures.map((structure) => ({ x: structure.position.x, y: structure.position.y }))], ); - addToSubscriptionOneKeyModelbyRealmEntityId(dojo.network.toriiClient, dojo.network.contractComponents as any, [ + debouncedAddToSubscriptionOneKey(dojo.network.toriiClient, dojo.network.contractComponents as any, [ ...filteredStructures.map((structure) => structure.entity_id.toString()), ]); } catch (error) { @@ -184,10 +188,10 @@ export const World = ({ backgroundImage }: { backgroundImage: string }) => { try { setMarketLoading(true); console.log("AddToSubscriptionStart - 3"); - addToSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any, [ + debouncedAddToSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any, [ ADMIN_BANK_ENTITY_ID.toString(), ]); - addMarketSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any); + debouncedAddMarketSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any); } catch (error) { console.error("Fetch failed", error); } finally { From 37e8a96b805dddda50936adbe138259ad52e487b Mon Sep 17 00:00:00 2001 From: ponderingdemocritus Date: Thu, 19 Dec 2024 20:58:00 +1100 Subject: [PATCH 2/2] fix: --- client/src/dojo/setup.ts | 2 +- .../hooks/helpers/use-resource-arrivals.tsx | 2 + client/src/hooks/helpers/useEntities.tsx | 92 +++---------------- client/src/hooks/helpers/useRealm.tsx | 12 +-- .../ui/components/trading/TransferView.tsx | 55 ++++++++++- 5 files changed, 73 insertions(+), 90 deletions(-) diff --git a/client/src/dojo/setup.ts b/client/src/dojo/setup.ts index 55768ea65..03f4befd8 100644 --- a/client/src/dojo/setup.ts +++ b/client/src/dojo/setup.ts @@ -107,7 +107,7 @@ export async function setup({ ...config }: DojoConfig) { }, { Keys: { - keys: [HYPERSTRUCTURE_CONFIG_ID.toString(), undefined], + keys: [HYPERSTRUCTURE_CONFIG_ID.toString()], pattern_matching: "VariableLen", models: [], }, diff --git a/client/src/hooks/helpers/use-resource-arrivals.tsx b/client/src/hooks/helpers/use-resource-arrivals.tsx index ef7af8933..053e5e142 100644 --- a/client/src/hooks/helpers/use-resource-arrivals.tsx +++ b/client/src/hooks/helpers/use-resource-arrivals.tsx @@ -62,6 +62,8 @@ const usePlayerArrivals = () => { const arrivals = positions.flatMap((position) => { return Array.from(runQuery([HasValue(Position, { x: position.x, y: position.y }), ...queryFragments])); }); + + console.log("arrivals", arrivals); return arrivals; }, []); diff --git a/client/src/hooks/helpers/useEntities.tsx b/client/src/hooks/helpers/useEntities.tsx index d32d1ca0c..c145b0c36 100644 --- a/client/src/hooks/helpers/useEntities.tsx +++ b/client/src/hooks/helpers/useEntities.tsx @@ -9,7 +9,7 @@ import { type ID, } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; -import { Has, getComponentValue, type ComponentValue } from "@dojoengine/recs"; +import { Has, HasValue, getComponentValue, type ComponentValue } from "@dojoengine/recs"; import { useMemo } from "react"; import { shortString } from "starknet"; import { useDojo } from "../context/DojoContext"; @@ -23,7 +23,7 @@ export type PlayerStructure = ComponentValue; }; -type RealmWithPosition = ComponentValue & { +export type RealmWithPosition = ComponentValue & { position: ComponentValue; name: string; owner: ComponentValue; @@ -43,53 +43,18 @@ export const useEntities = () => { const { getEntityName } = useEntitiesUtils(); // Get all realms - const allRealms = useEntityQuery([Has(Realm)]); - - const filterPlayerRealms = useMemo(() => { - return allRealms.filter((id) => { - const owner = getComponentValue(Owner, id); - return owner && ContractAddress(owner.address) === ContractAddress(address); - }); - }, [allRealms, address]); - - const filterOtherRealms = useMemo(() => { - return allRealms.filter((id) => { - const owner = getComponentValue(Owner, id); - return owner && ContractAddress(owner.address) !== ContractAddress(address); - }); - }, [allRealms, address]); + const playerRealmsQuery = useEntityQuery([Has(Realm), HasValue(Owner, { address: address })]); // Get all structures - const allStructures = useEntityQuery([Has(Structure), Has(Position), Has(Owner)]); - - const filterPlayerStructures = useMemo(() => { - return allStructures.filter((id) => { - const owner = getComponentValue(Owner, id); - return owner && ContractAddress(owner.address) === ContractAddress(address); - }); - }, [allStructures, address]); - - const filterOtherStructures = useMemo(() => { - return allStructures.filter((id) => { - const owner = getComponentValue(Owner, id); - return owner && ContractAddress(owner.address) !== ContractAddress(address); - }); - }, [allStructures, address]); + const playerStructuresQuery = useEntityQuery([ + Has(Structure), + Has(Position), + Has(Owner), + HasValue(Owner, { address: address }), + ]); const playerRealms = useMemo(() => { - return filterPlayerRealms.map((id) => { - const realm = getComponentValue(Realm, id); - return { - ...realm, - position: getComponentValue(Position, id), - name: getRealmNameById(realm!.realm_id), - owner: getComponentValue(Owner, id), - } as RealmWithPosition; - }); - }, [filterPlayerRealms]); - - const otherRealms = useMemo(() => { - return filterOtherRealms.map((id) => { + return playerRealmsQuery.map((id) => { const realm = getComponentValue(Realm, id); return { ...realm, @@ -98,10 +63,10 @@ export const useEntities = () => { owner: getComponentValue(Owner, id), } as RealmWithPosition; }); - }, [filterOtherRealms]); + }, [playerRealmsQuery]); const playerStructures = useMemo(() => { - return filterPlayerStructures + return playerStructuresQuery .map((id) => { const structure = getComponentValue(Structure, id); if (!structure) return; @@ -121,24 +86,7 @@ export const useEntities = () => { if (b.category === StructureType[StructureType.Realm]) return 1; return a.category.localeCompare(b.category); }); - }, [filterPlayerStructures]); - - const otherStructures = useMemo(() => { - return filterOtherStructures - .map((id) => { - const structure = getComponentValue(Structure, id); - if (!structure || structure.category === StructureType[StructureType.Realm]) return; - - const position = getComponentValue(Position, id); - - const structureName = getEntityName(structure.entity_id); - - const name = structureName ? `${structure?.category} ${structureName}` : structure.category || ""; - return { ...structure, position: position!, name, owner: getComponentValue(Owner, id) }; - }) - .filter((structure): structure is PlayerStructure => structure !== undefined) - .sort((a, b) => a.category.localeCompare(b.category)); - }, [filterOtherStructures]); + }, [playerStructuresQuery]); const getPlayerRealms = (filterFn?: (realm: RealmWithPosition) => boolean) => { return useMemo(() => { @@ -147,12 +95,6 @@ export const useEntities = () => { }, [playerRealms, filterFn]); }; - const getOtherRealms = (filterFn?: (realm: RealmWithPosition) => boolean) => { - return useMemo(() => { - return filterFn ? otherRealms.filter(filterFn) : otherRealms; - }, [otherRealms, filterFn]); - }; - const getPlayerStructures = (filterFn?: (structure: PlayerStructure) => boolean) => { return useMemo(() => { const structures = filterFn ? playerStructures.filter(filterFn) : playerStructures; @@ -160,17 +102,9 @@ export const useEntities = () => { }, [playerStructures, filterFn]); }; - const getOtherStructures = (filterFn?: (structure: PlayerStructure) => boolean) => { - return useMemo(() => { - return filterFn ? otherStructures.filter(filterFn) : otherStructures; - }, [otherStructures, filterFn]); - }; - return { playerRealms: getPlayerRealms, - otherRealms: getOtherRealms, playerStructures: getPlayerStructures, - otherStructures: getOtherStructures, }; }; diff --git a/client/src/hooks/helpers/useRealm.tsx b/client/src/hooks/helpers/useRealm.tsx index 88e2690d9..a62fb4bf7 100644 --- a/client/src/hooks/helpers/useRealm.tsx +++ b/client/src/hooks/helpers/useRealm.tsx @@ -2,12 +2,12 @@ import { type ClientComponents } from "@/dojo/createClientComponents"; import { configManager } from "@/dojo/setup"; import { ContractAddress, - type ID, getOrderName, getQuestResources as getStartingResources, + type ID, } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; -import { type ComponentValue, type Entity, Has, HasValue, getComponentValue, runQuery } from "@dojoengine/recs"; +import { Has, HasValue, getComponentValue, runQuery, type ComponentValue, type Entity } from "@dojoengine/recs"; import { useMemo } from "react"; import { shortString } from "starknet"; import realmIdsByOrder from "../../data/realmids_by_order.json"; @@ -265,7 +265,7 @@ export function getRealms(): RealmInfo[] { const position = getComponentValue(Position, entity); const population = getComponentValue(Population, entity); - if (!realm || !owner || !position) return; + if (!realm || !owner || !position) return null; const { realm_id, entity_id, produced_resources, order } = realm; @@ -291,7 +291,7 @@ export function getRealms(): RealmInfo[] { hasWonder: realm.has_wonder, }; }) - .filter((realm) => realm !== undefined); + .filter((realm): realm is RealmInfo => realm !== null); } export function usePlayerRealms(): RealmInfo[] { @@ -312,7 +312,7 @@ export function usePlayerRealms(): RealmInfo[] { const position = getComponentValue(Position, entity); const population = getComponentValue(Population, entity); - if (!realm || !owner || !position) return; + if (!realm || !owner || !position) return null; const { realm_id, entity_id, produced_resources, order } = realm; @@ -338,7 +338,7 @@ export function usePlayerRealms(): RealmInfo[] { hasWonder: realm.has_wonder, }; }) - .filter((realm) => realm !== undefined); + .filter((realm): realm is RealmInfo => realm !== null); }, [realmEntities]); return realms; } diff --git a/client/src/ui/components/trading/TransferView.tsx b/client/src/ui/components/trading/TransferView.tsx index 13161bb6c..4ff0933b4 100644 --- a/client/src/ui/components/trading/TransferView.tsx +++ b/client/src/ui/components/trading/TransferView.tsx @@ -1,15 +1,22 @@ import { useDojo } from "@/hooks/context/DojoContext"; -import { useEntities } from "@/hooks/helpers/useEntities"; +import { PlayerStructure, RealmWithPosition, useEntities, useEntitiesUtils } from "@/hooks/helpers/useEntities"; import { useGuilds } from "@/hooks/helpers/useGuilds"; +import { getRealmNameById } from "@/ui/utils/realms"; +import { ContractAddress, StructureType } from "@bibliothecadao/eternum"; +import { useEntityQuery } from "@dojoengine/react"; +import { Has, NotValue, getComponentValue } from "@dojoengine/recs"; import { useMemo, useState } from "react"; import { TransferBetweenEntities } from "./TransferBetweenEntities"; export const TransferView = () => { const { account: { account }, + setup: { + components: { Structure, Position, Owner, Realm }, + }, } = useDojo(); - const { playerRealms, playerStructures, otherRealms, otherStructures } = useEntities(); + const { playerRealms, playerStructures } = useEntities(); const [guildOnly, setGuildOnly] = useState(false); @@ -19,6 +26,46 @@ export const TransferView = () => { return getPlayersInPlayersGuild(BigInt(account.address)).map((a) => BigInt(a.address)); }, [account.address]); + const { getEntityName } = useEntitiesUtils(); + + const otherStructuresQuery = useEntityQuery([ + Has(Structure), + Has(Position), + Has(Owner), + NotValue(Owner, { address: ContractAddress(account.address) }), + ]); + + const otherStructures = useMemo(() => { + return otherStructuresQuery + .map((id) => { + const structure = getComponentValue(Structure, id); + if (!structure || structure.category === StructureType[StructureType.Realm]) return; + + const position = getComponentValue(Position, id); + + const structureName = getEntityName(structure.entity_id); + + const name = structureName ? `${structure?.category} ${structureName}` : structure.category || ""; + return { ...structure, position: position!, name, owner: getComponentValue(Owner, id) }; + }) + .filter((structure): structure is PlayerStructure => structure !== undefined) + .sort((a, b) => a.category.localeCompare(b.category)); + }, [otherStructuresQuery]); + + const otherRealmsQuery = useEntityQuery([Has(Realm), NotValue(Owner, { address: ContractAddress(account.address) })]); + + const otherRealms = useMemo(() => { + return otherRealmsQuery.map((id) => { + const realm = getComponentValue(Realm, id); + return { + ...realm, + position: getComponentValue(Position, id), + name: getRealmNameById(realm!.realm_id), + owner: getComponentValue(Owner, id), + } as RealmWithPosition; + }); + }, [otherRealmsQuery]); + return ( { name: "Your Banks", }, { - entities: otherRealms((a) => + entities: otherRealms.filter((a) => guildOnly ? playersInPlayersGuildAddress.includes(a.owner.address) : !playersInPlayersGuildAddress.includes(a.owner.address), @@ -46,7 +93,7 @@ export const TransferView = () => { name: "Other Realms", }, { - entities: otherStructures((a) => + entities: otherStructures.filter((a) => guildOnly ? playersInPlayersGuildAddress.includes(a.owner.address) : !playersInPlayersGuildAddress.includes(a.owner.address),