diff --git a/examples/minimal/packages/client-react/src/mud/setupNetwork.ts b/examples/minimal/packages/client-react/src/mud/setupNetwork.ts index 1353c0803c..d8d3498303 100644 --- a/examples/minimal/packages/client-react/src/mud/setupNetwork.ts +++ b/examples/minimal/packages/client-react/src/mud/setupNetwork.ts @@ -4,7 +4,14 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs"; import { getNetworkConfig } from "./getNetworkConfig"; import { world } from "./world"; import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json"; -import { ContractWrite, createBurnerAccount, getContract, transportObserver } from "@latticexyz/common"; +import { + ContractWrite, + createBurnerAccount, + getContract, + hexToResource, + resourceToHex, + transportObserver, +} from "@latticexyz/common"; import { Subject, share } from "rxjs"; import mudConfig from "contracts/mud.config"; import { createClient as createFaucetClient } from "@latticexyz/faucet"; @@ -40,6 +47,19 @@ export async function setupNetwork() { const { components, latestBlock$, storedBlockLogs$, waitForTransaction } = await syncToRecs({ world, config: mudConfig, + tables: { + KeysWithValue: { + namespace: "keywval", + name: "Inventory", + tableId: resourceToHex({ type: "table", namespace: "keywval", name: "Inventory" }), + keySchema: { + valueHash: "bytes32", + }, + valueSchema: { + keysWithValue: "bytes32[]", + }, + }, + }, address: networkConfig.worldAddress as Hex, publicClient, startBlock: BigInt(networkConfig.initialBlockNumber), diff --git a/examples/minimal/packages/contracts/mud.config.ts b/examples/minimal/packages/contracts/mud.config.ts index dfeeaffb52..c2432def66 100644 --- a/examples/minimal/packages/contracts/mud.config.ts +++ b/examples/minimal/packages/contracts/mud.config.ts @@ -37,12 +37,11 @@ export default mudConfig({ valueSchema: { amount: "uint32" }, }, }, - // KeysWithValue doesn't seem to like singleton keys - // modules: [ - // { - // name: "KeysWithValueModule", - // root: true, - // args: [resolveTableId("CounterTable")], - // }, - // ], + modules: [ + { + name: "KeysWithValueModule", + root: true, + args: [resolveTableId("Inventory")], + }, + ], }); diff --git a/packages/store-sync/src/recs/common.ts b/packages/store-sync/src/recs/common.ts index 09622f2fe8..8f4157ad58 100644 --- a/packages/store-sync/src/recs/common.ts +++ b/packages/store-sync/src/recs/common.ts @@ -3,6 +3,8 @@ import { Component as RecsComponent, Metadata as RecsMetadata, Type as RecsType import { SchemaAbiTypeToRecsType } from "./schemaAbiTypeToRecsType"; import { SchemaAbiType } from "@latticexyz/schema-type"; import { KeySchema, ValueSchema } from "@latticexyz/protocol-parser"; +import { Table } from "../common"; +import { Hex } from "viem"; export type StoreComponentMetadata = RecsMetadata & { componentName: string; @@ -11,21 +13,33 @@ export type StoreComponentMetadata = RecsMetadata & { valueSchema: ValueSchema; }; -export type ConfigToRecsComponents = { - [tableName in keyof TConfig["tables"] & string]: RecsComponent< - { - __staticData: RecsType.OptionalString; - __encodedLengths: RecsType.OptionalString; - __dynamicData: RecsType.OptionalString; - } & { - [fieldName in keyof TConfig["tables"][tableName]["valueSchema"] & string]: RecsType & - SchemaAbiTypeToRecsType; - }, - StoreComponentMetadata & { - componentName: tableName; - tableName: `${TConfig["namespace"]}:${tableName}`; - keySchema: TConfig["tables"][tableName]["keySchema"]; - valueSchema: TConfig["tables"][tableName]["valueSchema"]; - } - >; +export type TableToRecsComponent> = RecsComponent< + { + __staticData: RecsType.OptionalString; + __encodedLengths: RecsType.OptionalString; + __dynamicData: RecsType.OptionalString; + } & { + [fieldName in keyof table["valueSchema"] & string]: RecsType & + SchemaAbiTypeToRecsType; + }, + StoreComponentMetadata & { + componentName: table["name"]; + tableName: `${table["namespace"]}:${table["name"]}`; + keySchema: table["keySchema"]; + valueSchema: table["valueSchema"]; + } +>; + +export type TablesToRecsComponents>> = { + [tableName in keyof tables]: TableToRecsComponent; +}; + +export type ConfigToRecsComponents = { + [tableName in keyof config["tables"] & string]: TableToRecsComponent<{ + tableId: Hex; + namespace: config["namespace"]; + name: tableName; + keySchema: config["tables"][tableName]["keySchema"] & KeySchema; + valueSchema: config["tables"][tableName]["valueSchema"] & ValueSchema; + }>; }; diff --git a/packages/store-sync/src/recs/configToRecsComponents.ts b/packages/store-sync/src/recs/configToRecsComponents.ts index 1b22f69a98..925ced2127 100644 --- a/packages/store-sync/src/recs/configToRecsComponents.ts +++ b/packages/store-sync/src/recs/configToRecsComponents.ts @@ -1,45 +1,34 @@ -import { StoreConfig } from "@latticexyz/store"; -import { SchemaAbiType } from "@latticexyz/schema-type"; +import { StoreConfig, resolveUserTypes } from "@latticexyz/store"; import { resourceToHex } from "@latticexyz/common"; -import { World, defineComponent, Type } from "@latticexyz/recs"; +import { World } from "@latticexyz/recs"; import { ConfigToRecsComponents } from "./common"; -import { schemaAbiTypeToRecsType } from "./schemaAbiTypeToRecsType"; +import { tableToRecsComponent } from "./tableToRecsComponent"; +import { KeySchema } from "@latticexyz/protocol-parser"; export function configToRecsComponents( world: World, config: TConfig ): ConfigToRecsComponents { + const userTypes = { + ...config.userTypes, + ...Object.fromEntries(Object.entries(config.enums).map(([key]) => [key, { internalType: "uint8" }] as const)), + }; + return Object.fromEntries( Object.entries(config.tables).map(([tableName, table]) => [ tableName, - defineComponent( - world, - { - ...Object.fromEntries( - Object.entries(table.valueSchema).map(([fieldName, schemaAbiType]) => [ - fieldName, - schemaAbiTypeToRecsType[schemaAbiType as SchemaAbiType], - ]) - ), - __staticData: Type.OptionalString, - __encodedLengths: Type.OptionalString, - __dynamicData: Type.OptionalString, - }, - { - // TODO: support table namespaces https://github.com/latticexyz/mud/issues/994 - id: resourceToHex({ - type: table.offchainOnly ? "offchainTable" : "table", - namespace: config.namespace, - name: tableName, - }), - metadata: { - componentName: tableName, - tableName: `${config.namespace}:${tableName}`, - keySchema: table.keySchema, - valueSchema: table.valueSchema, - }, - } - ), + tableToRecsComponent(world, { + tableId: resourceToHex({ + type: table.offchainOnly ? "offchainTable" : "table", + namespace: config.namespace, + name: table.name, // using the parsed config's `table.name` here rather than `tableName` key because that's what's used by codegen + }), + namespace: config.namespace, + name: tableName, + // TODO: refine to static ABI types so we don't have to cast + keySchema: resolveUserTypes(table.keySchema, userTypes) as KeySchema, + valueSchema: resolveUserTypes(table.valueSchema, userTypes), + }), ]) ) as ConfigToRecsComponents; } diff --git a/packages/store-sync/src/recs/recsStorage.ts b/packages/store-sync/src/recs/recsStorage.ts index 10eca49779..780cafb6b2 100644 --- a/packages/store-sync/src/recs/recsStorage.ts +++ b/packages/store-sync/src/recs/recsStorage.ts @@ -9,41 +9,60 @@ import { Hex, size } from "viem"; import { isTableRegistrationLog } from "../isTableRegistrationLog"; import { logToTable } from "../logToTable"; import { hexKeyTupleToEntity } from "./hexKeyTupleToEntity"; -import { ConfigToRecsComponents } from "./common"; -import { StorageAdapter, StorageAdapterBlock } from "../common"; +import { ConfigToRecsComponents, TablesToRecsComponents } from "./common"; +import { StorageAdapter, StorageAdapterBlock, Table } from "../common"; import { configToRecsComponents } from "./configToRecsComponents"; import { singletonEntity } from "./singletonEntity"; import storeConfig from "@latticexyz/store/mud.config"; import worldConfig from "@latticexyz/world/mud.config"; +import { tableToRecsComponent } from "./tableToRecsComponent"; -export type RecsStorageOptions = { +export type RecsStorageOptions< + config extends StoreConfig, + tables extends Record> | undefined +> = { world: RecsWorld; - // TODO: make config optional? - config: TConfig; + config: config; + tables?: tables; shouldSkipUpdateStream?: () => boolean; }; -export type RecsStorageAdapter = { +export type RecsStorageAdapter< + config extends StoreConfig, + tables extends Record> | undefined +> = { storageAdapter: StorageAdapter; - components: ConfigToRecsComponents & + components: ConfigToRecsComponents & + (tables extends Record> + ? ConfigToRecsComponents & TablesToRecsComponents + : ConfigToRecsComponents) & ConfigToRecsComponents & ConfigToRecsComponents & ReturnType; }; -export function recsStorage({ +export function recsStorage< + config extends StoreConfig = StoreConfig, + tables extends Record> | undefined = undefined +>({ world, config, + tables, shouldSkipUpdateStream, -}: RecsStorageOptions): RecsStorageAdapter { +}: RecsStorageOptions): RecsStorageAdapter { world.registerEntity({ id: singletonEntity }); const components = { ...configToRecsComponents(world, config), + ...(tables + ? Object.fromEntries( + Object.entries(tables).map(([tableName, table]) => [tableName, tableToRecsComponent(world, table)]) + ) + : null), ...configToRecsComponents(world, storeConfig), ...configToRecsComponents(world, worldConfig), ...defineInternalComponents(world), - }; + } as RecsStorageAdapter["components"]; async function recsStorageAdapter({ logs }: StorageAdapterBlock): Promise { const newTables = logs.filter(isTableRegistrationLog).map(logToTable); diff --git a/packages/store-sync/src/recs/syncToRecs.ts b/packages/store-sync/src/recs/syncToRecs.ts index 4759e5f538..51caf76e6a 100644 --- a/packages/store-sync/src/recs/syncToRecs.ts +++ b/packages/store-sync/src/recs/syncToRecs.ts @@ -1,25 +1,36 @@ import { StoreConfig } from "@latticexyz/store"; import { Component as RecsComponent, World as RecsWorld, getComponentValue, setComponent } from "@latticexyz/recs"; -import { SyncOptions, SyncResult } from "../common"; +import { SyncOptions, SyncResult, Table } from "../common"; import { RecsStorageAdapter, recsStorage } from "./recsStorage"; import { createStoreSync } from "../createStoreSync"; import { singletonEntity } from "./singletonEntity"; import { SyncStep } from "../SyncStep"; -type SyncToRecsOptions = SyncOptions & { +type SyncToRecsOptions< + config extends StoreConfig, + tables extends Record> | undefined +> = SyncOptions & { world: RecsWorld; - config: TConfig; + config: config; + tables?: tables; startSync?: boolean; }; -type SyncToRecsResult = SyncResult & { - components: RecsStorageAdapter["components"]; +type SyncToRecsResult< + config extends StoreConfig, + tables extends Record> | undefined +> = SyncResult & { + components: RecsStorageAdapter["components"]; stopSync: () => void; }; -export async function syncToRecs({ +export async function syncToRecs< + config extends StoreConfig, + tables extends Record> | undefined +>({ world, config, + tables, address, publicClient, startBlock, @@ -27,10 +38,11 @@ export async function syncToRecs({ initialState, indexerUrl, startSync = true, -}: SyncToRecsOptions): Promise> { +}: SyncToRecsOptions): Promise> { const { storageAdapter, components } = recsStorage({ world, config, + tables, shouldSkipUpdateStream: (): boolean => getComponentValue(components.SyncProgress, singletonEntity)?.step !== SyncStep.LIVE, }); diff --git a/packages/store-sync/src/recs/tableToRecsComponent.ts b/packages/store-sync/src/recs/tableToRecsComponent.ts new file mode 100644 index 0000000000..f5920cce79 --- /dev/null +++ b/packages/store-sync/src/recs/tableToRecsComponent.ts @@ -0,0 +1,34 @@ +import { Type, World, defineComponent } from "@latticexyz/recs"; +import { Table } from "../common"; +import { TableToRecsComponent } from "./common"; +import { schemaAbiTypeToRecsType } from "./schemaAbiTypeToRecsType"; +import { SchemaAbiType } from "@latticexyz/schema-type"; + +export function tableToRecsComponent
>( + world: World, + table: table +): TableToRecsComponent
{ + return defineComponent( + world, + { + ...Object.fromEntries( + Object.entries(table.valueSchema).map(([fieldName, schemaAbiType]) => [ + fieldName, + schemaAbiTypeToRecsType[schemaAbiType as SchemaAbiType], + ]) + ), + __staticData: Type.OptionalString, + __encodedLengths: Type.OptionalString, + __dynamicData: Type.OptionalString, + }, + { + id: table.tableId, + metadata: { + componentName: table.name, + tableName: `${table.namespace}:${table.name}`, + keySchema: table.keySchema, + valueSchema: table.valueSchema, + }, + } + ) as TableToRecsComponent
; +} diff --git a/packages/store-sync/src/recs/tablesToRecsComponent.ts b/packages/store-sync/src/recs/tablesToRecsComponent.ts new file mode 100644 index 0000000000..855f5d3a17 --- /dev/null +++ b/packages/store-sync/src/recs/tablesToRecsComponent.ts @@ -0,0 +1,13 @@ +import { World } from "@latticexyz/recs"; +import { Table } from "../common"; +import { TablesToRecsComponents } from "./common"; +import { tableToRecsComponent } from "./tableToRecsComponent"; + +export function tablesToRecsComponents>>( + world: World, + tables: tables +): TablesToRecsComponents { + return Object.fromEntries( + Object.entries(tables).map(([tableName, table]) => [tableName, tableToRecsComponent(world, table)]) + ) as TablesToRecsComponents; +}