diff --git a/packages/recs/src/Component.ts b/packages/recs/src/Component.ts index dc4be8750c..662e26f960 100644 --- a/packages/recs/src/Component.ts +++ b/packages/recs/src/Component.ts @@ -110,6 +110,8 @@ export function setComponent( } } component.update$.next({ entity, value: [value, prevValue], component }); + const updateValue = { entity, value: [value, prevValue], component }; + return updateValue; } /** @@ -139,9 +141,9 @@ export function updateComponent( if (initialValue === undefined) { throw new Error(`Can't update component ${getComponentName(component)} without a current value or initial value`); } - setComponent(component, entity, { ...initialValue, ...value }); + return setComponent(component, entity, { ...initialValue, ...value }); } else { - setComponent(component, entity, { ...currentValue, ...value }); + return setComponent(component, entity, { ...currentValue, ...value }); } } @@ -161,6 +163,8 @@ export function removeComponent>; -export function recsStorage({ - components, -}: { - components: ReturnType & - Record>; - config?: TConfig; -}): BlockLogsToStorageOptions { +export type RecsUpdatesHook = ( + blockNumber: BlockLogs["blockNumber"], + componentEntityUpdates: ComponentEntityUpdates +) => Promise; + +export function recsStorage( + { + components, + }: { + components: ReturnType & + Record>; + config?: TConfig; + }, + recsAllUpdatesHook?: RecsUpdatesHook +): BlockLogsToStorageOptions { // TODO: do we need to store block number? const componentsByTableId = Object.fromEntries( @@ -44,7 +57,8 @@ export function recsStorage({ .map((table) => getComponentValue(components.RegisteredTables, getTableEntity(table))?.table) .filter(isDefined); }, - async storeOperations({ operations }) { + async storeOperations({ blockNumber, operations }) { + const componentEntityUpdates: ComponentEntityUpdates = new Map>(); for (const operation of operations) { const table = getComponentValue( components.RegisteredTables, @@ -68,14 +82,15 @@ export function recsStorage({ const entity = encodeEntity(table.keySchema, operation.key); + let newUpdate = undefined; if (operation.type === "SetRecord") { debug("setting component", tableId, entity, operation.value); - setComponent(component, entity, operation.value as ComponentValue); + newUpdate = setComponent(component, entity, operation.value as ComponentValue); } else if (operation.type === "SetField") { debug("updating component", tableId, entity, { [operation.fieldName]: operation.fieldValue, }); - updateComponent( + newUpdate = updateComponent( component, entity, { [operation.fieldName]: operation.fieldValue } as ComponentValue, @@ -83,9 +98,27 @@ export function recsStorage({ ); } else if (operation.type === "DeleteRecord") { debug("deleting component", tableId, entity); - removeComponent(component, entity); + newUpdate = removeComponent(component, entity); + } + + if (newUpdate) { + if (componentEntityUpdates.has(newUpdate.component.id)) { + if (componentEntityUpdates.get(newUpdate.component.id)?.has(newUpdate.entity)) { + componentEntityUpdates.get(newUpdate.component.id)?.get(newUpdate.entity)?.push(newUpdate); + } else { + componentEntityUpdates.get(newUpdate.component.id)?.set(newUpdate.entity, [newUpdate]); + } + } else { + const entityUpdates = new Map(); + entityUpdates.set(newUpdate.entity, [newUpdate]); + componentEntityUpdates.set(newUpdate.component.id, entityUpdates); + } } } + + if (recsAllUpdatesHook !== undefined) { + await recsAllUpdatesHook(blockNumber, componentEntityUpdates); + } }, } as BlockLogsToStorageOptions; } diff --git a/packages/store-sync/src/recs/syncToRecs.ts b/packages/store-sync/src/recs/syncToRecs.ts index d2c2b006e8..e20a8d60cd 100644 --- a/packages/store-sync/src/recs/syncToRecs.ts +++ b/packages/store-sync/src/recs/syncToRecs.ts @@ -1,20 +1,21 @@ -import { StoreConfig } from "@latticexyz/store"; import { World as RecsWorld, getComponentValue, setComponent } from "@latticexyz/recs"; +import { StoreConfig } from "@latticexyz/store"; +import storeConfig from "@latticexyz/store/mud.config.js"; +import worldConfig from "@latticexyz/world/mud.config.js"; +import { SyncStep } from "../SyncStep"; import { SyncOptions, SyncResult } from "../common"; -import { recsStorage } from "./recsStorage"; -import { defineInternalComponents } from "./defineInternalComponents"; import { createStoreSync } from "../createStoreSync"; import { ConfigToRecsComponents } from "./common"; -import storeConfig from "@latticexyz/store/mud.config.js"; -import worldConfig from "@latticexyz/world/mud.config.js"; import { configToRecsComponents } from "./configToRecsComponents"; +import { defineInternalComponents } from "./defineInternalComponents"; +import { RecsUpdatesHook, recsStorage } from "./recsStorage"; import { singletonEntity } from "./singletonEntity"; -import { SyncStep } from "../SyncStep"; type SyncToRecsOptions = SyncOptions & { world: RecsWorld; config: TConfig; startSync?: boolean; + recsAllUpdatesHook?: RecsUpdatesHook; }; type SyncToRecsResult = SyncResult & { @@ -34,6 +35,7 @@ export async function syncToRecs({ maxBlockRange, initialState, indexerUrl, + recsAllUpdatesHook, startSync = true, }: SyncToRecsOptions): Promise> { const components = { @@ -46,7 +48,7 @@ export async function syncToRecs({ world.registerEntity({ id: singletonEntity }); const storeSync = await createStoreSync({ - storageAdapter: recsStorage({ components, config }), + storageAdapter: recsStorage({ components, config }, recsAllUpdatesHook), config, address, publicClient,