Skip to content

Commit

Permalink
feat(store-sync): extra table definitions (#1840)
Browse files Browse the repository at this point in the history
  • Loading branch information
holic authored Oct 31, 2023
1 parent 6a091d9 commit de47d69
Show file tree
Hide file tree
Showing 18 changed files with 239 additions and 193 deletions.
27 changes: 27 additions & 0 deletions .changeset/fair-buckets-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@latticexyz/store-sync": minor
---

Added an optional `tables` option to `syncToRecs` to allow you to sync from tables that may not be expressed by your MUD config. This will be useful for namespaced tables used by [ERC20](https://github.com/latticexyz/mud/pull/1789) and [ERC721](https://github.com/latticexyz/mud/pull/1844) token modules until the MUD config gains [namespace support](https://github.com/latticexyz/mud/issues/994).

Here's how we use this in our example project with the `KeysWithValue` module:

```ts
syncToRecs({
...
tables: {
KeysWithValue: {
namespace: "keywval",
name: "Inventory",
tableId: resourceToHex({ type: "table", namespace: "keywval", name: "Inventory" }),
keySchema: {
valueHash: { type: "bytes32" },
},
valueSchema: {
keysWithValue: { type: "bytes32[]" },
},
},
},
...
});
```
17 changes: 15 additions & 2 deletions examples/minimal/packages/client-react/src/mud/setupNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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, resourceToHex, transportObserver } from "@latticexyz/common";
import { Subject, share } from "rxjs";
import mudConfig from "contracts/mud.config";
import { createClient as createFaucetClient } from "@latticexyz/faucet";
Expand Down Expand Up @@ -40,10 +40,23 @@ 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: { type: "bytes32" },
},
valueSchema: {
keysWithValue: { type: "bytes32[]" },
},
},
},
address: networkConfig.worldAddress as Hex,
publicClient,
startBlock: BigInt(networkConfig.initialBlockNumber),
});
} as const);

try {
console.log("creating faucet client");
Expand Down
15 changes: 7 additions & 8 deletions examples/minimal/packages/contracts/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")],
},
],
});
31 changes: 0 additions & 31 deletions packages/store-sync/src/recs/common.test-d.ts

This file was deleted.

27 changes: 3 additions & 24 deletions packages/store-sync/src/recs/common.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,10 @@
import { StoreConfig } from "@latticexyz/store";
import { Component as RecsComponent, Metadata as RecsMetadata, Type as RecsType } from "@latticexyz/recs";
import { SchemaAbiTypeToRecsType } from "./schemaAbiTypeToRecsType";
import { SchemaAbiType } from "@latticexyz/schema-type";
import { Metadata } from "@latticexyz/recs";
import { KeySchema, ValueSchema } from "@latticexyz/protocol-parser";

export type StoreComponentMetadata = RecsMetadata & {
export type StoreComponentMetadata = Metadata & {
componentName: string;
tableName: string;
// TODO: migrate to store's KeySchema/ValueSchema
keySchema: KeySchema;
valueSchema: ValueSchema;
};

export type ConfigToRecsComponents<TConfig extends StoreConfig> = {
[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<SchemaAbiType & TConfig["tables"][tableName]["valueSchema"][fieldName]>;
},
StoreComponentMetadata & {
componentName: tableName;
tableName: `${TConfig["namespace"]}:${tableName}`;
keySchema: TConfig["tables"][tableName]["keySchema"];
valueSchema: TConfig["tables"][tableName]["valueSchema"];
}
>;
};
45 changes: 0 additions & 45 deletions packages/store-sync/src/recs/configToRecsComponents.ts

This file was deleted.

8 changes: 5 additions & 3 deletions packages/store-sync/src/recs/recsStorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { groupLogsByBlockNumber } from "@latticexyz/block-logs-stream";
import { StoreEventsLog } from "../common";
import { singletonEntity } from "./singletonEntity";
import { RpcLog, formatLog, decodeEventLog, Hex } from "viem";
import { storeEventsAbi } from "@latticexyz/store";
import { resolveConfig, storeEventsAbi } from "@latticexyz/store";

const tables = resolveConfig(mudConfig).tables;

// TODO: make test-data a proper package and export this
const blocks = groupLogsByBlockNumber(
Expand All @@ -25,15 +27,15 @@ const blocks = groupLogsByBlockNumber(
describe("recsStorage", () => {
it("creates components", async () => {
const world = createWorld();
const { components } = recsStorage({ world, config: mudConfig });
const { components } = recsStorage({ world, tables });
expect(components.NumberList.id).toMatchInlineSnapshot(
'"0x746200000000000000000000000000004e756d6265724c697374000000000000"'
);
});

it("sets component values from logs", async () => {
const world = createWorld();
const { storageAdapter, components } = recsStorage({ world, config: mudConfig });
const { storageAdapter, components } = recsStorage({ world, tables });

for (const block of blocks) {
await storageAdapter(block);
Expand Down
33 changes: 17 additions & 16 deletions packages/store-sync/src/recs/recsStorage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StoreConfig } from "@latticexyz/store";
import { Table, resolveConfig } from "@latticexyz/store";
import { debug } from "./debug";
import { World as RecsWorld, getComponentValue, hasComponent, removeComponent, setComponent } from "@latticexyz/recs";
import { defineInternalComponents } from "./defineInternalComponents";
Expand All @@ -9,39 +9,40 @@ 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 } from "./configToRecsComponents";
import { singletonEntity } from "./singletonEntity";
import storeConfig from "@latticexyz/store/mud.config";
import worldConfig from "@latticexyz/world/mud.config";
import { TablesToComponents, tablesToComponents } from "./tablesToComponents";

export type RecsStorageOptions<TConfig extends StoreConfig = StoreConfig> = {
const storeTables = resolveConfig(storeConfig).tables;
const worldTables = resolveConfig(worldConfig).tables;

export type RecsStorageOptions<tables extends Record<string, Table>> = {
world: RecsWorld;
// TODO: make config optional?
config: TConfig;
tables: tables;
shouldSkipUpdateStream?: () => boolean;
};

export type RecsStorageAdapter<TConfig extends StoreConfig = StoreConfig> = {
export type RecsStorageAdapter<tables extends Record<string, Table>> = {
storageAdapter: StorageAdapter;
components: ConfigToRecsComponents<TConfig> &
ConfigToRecsComponents<typeof storeConfig> &
ConfigToRecsComponents<typeof worldConfig> &
components: TablesToComponents<tables> &
TablesToComponents<typeof storeTables> &
TablesToComponents<typeof worldTables> &
ReturnType<typeof defineInternalComponents>;
};

export function recsStorage<TConfig extends StoreConfig = StoreConfig>({
export function recsStorage<tables extends Record<string, Table>>({
world,
config,
tables,
shouldSkipUpdateStream,
}: RecsStorageOptions<TConfig>): RecsStorageAdapter<TConfig> {
}: RecsStorageOptions<tables>): RecsStorageAdapter<tables> {
world.registerEntity({ id: singletonEntity });

const components = {
...configToRecsComponents(world, config),
...configToRecsComponents(world, storeConfig),
...configToRecsComponents(world, worldConfig),
...tablesToComponents(world, tables),
...tablesToComponents(world, storeTables),
...tablesToComponents(world, worldTables),
...defineInternalComponents(world),
};

Expand Down
23 changes: 15 additions & 8 deletions packages/store-sync/src/recs/syncToRecs.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
import { StoreConfig } from "@latticexyz/store";
import { StoreConfig, Table, ResolvedStoreConfig, resolveConfig } from "@latticexyz/store";
import { Component as RecsComponent, World as RecsWorld, getComponentValue, setComponent } from "@latticexyz/recs";
import { SyncOptions, SyncResult } from "../common";
import { RecsStorageAdapter, recsStorage } from "./recsStorage";
import { createStoreSync } from "../createStoreSync";
import { singletonEntity } from "./singletonEntity";
import { SyncStep } from "../SyncStep";

type SyncToRecsOptions<TConfig extends StoreConfig = StoreConfig> = SyncOptions<TConfig> & {
type SyncToRecsOptions<config extends StoreConfig, extraTables extends Record<string, Table>> = SyncOptions<config> & {
world: RecsWorld;
config: TConfig;
config: config;
tables?: extraTables;
startSync?: boolean;
};

type SyncToRecsResult<TConfig extends StoreConfig = StoreConfig> = SyncResult & {
components: RecsStorageAdapter<TConfig>["components"];
type SyncToRecsResult<config extends StoreConfig, extraTables extends Record<string, Table>> = SyncResult & {
components: RecsStorageAdapter<ResolvedStoreConfig<config>["tables"] & extraTables>["components"];
stopSync: () => void;
};

export async function syncToRecs<TConfig extends StoreConfig = StoreConfig>({
export async function syncToRecs<config extends StoreConfig, extraTables extends Record<string, Table>>({
world,
config,
tables: extraTables,
startSync = true,
...syncOptions
}: SyncToRecsOptions<TConfig>): Promise<SyncToRecsResult<TConfig>> {
}: SyncToRecsOptions<config, extraTables>): Promise<SyncToRecsResult<config, extraTables>> {
const tables = {
...resolveConfig(config).tables,
...extraTables,
} as ResolvedStoreConfig<config>["tables"] & extraTables;

const { storageAdapter, components } = recsStorage({
world,
config,
tables,
shouldSkipUpdateStream: (): boolean =>
getComponentValue(components.SyncProgress, singletonEntity)?.step !== SyncStep.LIVE,
});
Expand Down
49 changes: 49 additions & 0 deletions packages/store-sync/src/recs/tableToComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Component, Type, World, defineComponent } from "@latticexyz/recs";
import { StoreComponentMetadata } from "./common";
import { SchemaAbiTypeToRecsType, schemaAbiTypeToRecsType } from "./schemaAbiTypeToRecsType";
import { SchemaAbiType } from "@latticexyz/schema-type";
import { Table } from "@latticexyz/store";
import { mapObject } from "@latticexyz/common/utils";

export type TableToComponent<table extends Table> = Component<
{
__staticData: Type.OptionalString;
__encodedLengths: Type.OptionalString;
__dynamicData: Type.OptionalString;
} & {
[fieldName in keyof table["valueSchema"] & string]: Type &
SchemaAbiTypeToRecsType<SchemaAbiType & table["valueSchema"][fieldName]["type"]>;
},
StoreComponentMetadata & {
componentName: table["name"];
tableName: `${table["namespace"]}:${table["name"]}`;
keySchema: { [name in keyof table["keySchema"] & string]: table["keySchema"][name]["type"] };
valueSchema: { [name in keyof table["valueSchema"] & string]: table["valueSchema"][name]["type"] };
}
>;

export function tableToComponent<table extends Table>(world: World, table: table): TableToComponent<table> {
return defineComponent(
world,
{
...Object.fromEntries(
Object.entries(table.valueSchema).map(([fieldName, { type: 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: mapObject(table.keySchema, ({ type }) => type),
valueSchema: mapObject(table.valueSchema, ({ type }) => type),
},
}
) as TableToComponent<table>;
}
15 changes: 15 additions & 0 deletions packages/store-sync/src/recs/tablesToComponents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Table } from "@latticexyz/store";
import { TableToComponent, tableToComponent } from "./tableToComponent";
import { mapObject } from "@latticexyz/common/utils";
import { World } from "@latticexyz/recs";

export type TablesToComponents<tables extends Record<string, Table>> = {
[tableName in keyof tables]: TableToComponent<tables[tableName]>;
};

export function tablesToComponents<tables extends Record<string, Table>>(
world: World,
tables: tables
): TablesToComponents<tables> {
return mapObject(tables, (table) => tableToComponent(world, table));
}
Loading

0 comments on commit de47d69

Please sign in to comment.