diff --git a/.changeset/silver-nails-explain.md b/.changeset/silver-nails-explain.md
new file mode 100644
index 0000000000..593e518a95
--- /dev/null
+++ b/.changeset/silver-nails-explain.md
@@ -0,0 +1,23 @@
+---
+"@latticexyz/store-sync": minor
+---
+
+Added a Zustand storage adapter and corresponding `syncToZustand` method for use in vanilla and React apps. It's used much like the other sync methods, except it returns a bound store and set of typed tables.
+
+```ts
+import { syncToZustand } from "@latticexyz/store-sync/zustand";
+import config from "contracts/mud.config";
+
+const { tables, useStore, latestBlock$, storedBlockLogs$, waitForTransaction } = await syncToZustand({
+ config,
+ ...
+});
+
+// in vanilla apps
+const positions = useStore.getState().getRecords(tables.Position);
+
+// in React apps
+const positions = useStore((state) => state.getRecords(tables.Position));
+```
+
+This change will be shortly followed by an update to our templates that uses Zustand as the default client data store and sync method.
diff --git a/examples/minimal/packages/client-vanilla/index.html b/examples/minimal/packages/client-vanilla/index.html
index 017fdd9c4b..2be5aca84d 100644
--- a/examples/minimal/packages/client-vanilla/index.html
+++ b/examples/minimal/packages/client-vanilla/index.html
@@ -8,7 +8,7 @@
-
Counter: 0
+
Counter: ??
diff --git a/examples/minimal/packages/client-vanilla/src/index.ts b/examples/minimal/packages/client-vanilla/src/index.ts
index 8666b71a68..07db012b23 100644
--- a/examples/minimal/packages/client-vanilla/src/index.ts
+++ b/examples/minimal/packages/client-vanilla/src/index.ts
@@ -1,31 +1,33 @@
import { setup } from "./mud/setup";
import mudConfig from "contracts/mud.config";
-const { components, network } = await setup();
-const { worldContract, waitForTransaction } = network;
+const {
+ network,
+ network: { tables, useStore, worldContract, waitForTransaction },
+ systemCalls,
+} = await setup();
-// Components expose a stream that triggers when the component is updated.
-components.CounterTable.update$.subscribe((update) => {
- const [nextValue, prevValue] = update.value;
- console.log("Counter updated", update, { nextValue, prevValue });
- document.getElementById("counter")!.innerHTML = String(nextValue?.value ?? "unset");
+// TODO: provide slice helpers and show subscribing to slices
+useStore.subscribe((state) => {
+ const value = state.getValue(tables.CounterTable, {});
+ if (value) {
+ document.getElementById("counter")!.innerHTML = String(value.value);
+ }
});
-components.MessageTable.update$.subscribe((update) => {
- console.log("Message received", update);
- const [nextValue] = update.value;
-
- const ele = document.getElementById("chat-output")!;
- ele.innerHTML = ele.innerHTML + `${new Date().toLocaleString()}: ${nextValue?.value}\n`;
+// TODO: provide slice helpers and show subscribing to slices
+useStore.subscribe((state, prevState) => {
+ const record = state.getRecord(tables.MessageTable, {});
+ if (record && record !== prevState.records[record.id]) {
+ document.getElementById("chat-output")!.innerHTML += `${new Date().toLocaleString()}: ${record?.value.value}\n`;
+ }
});
// Just for demonstration purposes: we create a global function that can be
// called to invoke the Increment system contract via the world. (See IncrementSystem.sol.)
(window as any).increment = async () => {
- const tx = await worldContract.write.increment();
-
- console.log("increment tx", tx);
- console.log("increment result", await waitForTransaction(tx));
+ const result = await systemCalls.increment();
+ console.log("increment result", result);
};
(window as any).willRevert = async () => {
@@ -33,7 +35,6 @@ components.MessageTable.update$.subscribe((update) => {
const tx = await worldContract.write.willRevert({ gas: 100000n });
console.log("willRevert tx", tx);
- console.log("willRevert result", await waitForTransaction(tx));
};
(window as any).sendMessage = async () => {
@@ -46,7 +47,6 @@ components.MessageTable.update$.subscribe((update) => {
const tx = await worldContract.write.sendMessage([msg]);
console.log("sendMessage tx", tx);
- console.log("sendMessage result", await waitForTransaction(tx));
};
document.getElementById("chat-form")?.addEventListener("submit", (e) => {
@@ -66,6 +66,5 @@ if (import.meta.env.DEV) {
worldAddress: network.worldContract.address,
worldAbi: network.worldContract.abi,
write$: network.write$,
- recsWorld: network.world,
});
}
diff --git a/examples/minimal/packages/client-vanilla/src/mud/createClientComponents.ts b/examples/minimal/packages/client-vanilla/src/mud/createClientComponents.ts
deleted file mode 100644
index 5058cc380b..0000000000
--- a/examples/minimal/packages/client-vanilla/src/mud/createClientComponents.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { SetupNetworkResult } from "./setupNetwork";
-
-export type ClientComponents = ReturnType;
-
-export function createClientComponents({ components }: SetupNetworkResult) {
- return {
- ...components,
- // add your client components or overrides here
- };
-}
diff --git a/examples/minimal/packages/client-vanilla/src/mud/createSystemCalls.ts b/examples/minimal/packages/client-vanilla/src/mud/createSystemCalls.ts
index 1e0c674320..bcf7166250 100644
--- a/examples/minimal/packages/client-vanilla/src/mud/createSystemCalls.ts
+++ b/examples/minimal/packages/client-vanilla/src/mud/createSystemCalls.ts
@@ -1,18 +1,12 @@
-import { getComponentValue } from "@latticexyz/recs";
-import { ClientComponents } from "./createClientComponents";
import { SetupNetworkResult } from "./setupNetwork";
-import { singletonEntity } from "@latticexyz/store-sync/recs";
export type SystemCalls = ReturnType;
-export function createSystemCalls(
- { worldContract, waitForTransaction }: SetupNetworkResult,
- { CounterTable }: ClientComponents
-) {
+export function createSystemCalls({ tables, useStore, worldContract, waitForTransaction }: SetupNetworkResult) {
const increment = async () => {
const tx = await worldContract.write.increment();
await waitForTransaction(tx);
- return getComponentValue(CounterTable, singletonEntity);
+ return useStore.getState().getRecord(tables.CounterTable, {})?.value.value;
};
return {
diff --git a/examples/minimal/packages/client-vanilla/src/mud/setup.ts b/examples/minimal/packages/client-vanilla/src/mud/setup.ts
index 4f79edd8f3..8ff1037428 100644
--- a/examples/minimal/packages/client-vanilla/src/mud/setup.ts
+++ b/examples/minimal/packages/client-vanilla/src/mud/setup.ts
@@ -1,4 +1,3 @@
-import { createClientComponents } from "./createClientComponents";
import { createSystemCalls } from "./createSystemCalls";
import { setupNetwork } from "./setupNetwork";
@@ -6,11 +5,9 @@ export type SetupResult = Awaited>;
export async function setup() {
const network = await setupNetwork();
- const components = createClientComponents(network);
- const systemCalls = createSystemCalls(network, components);
+ const systemCalls = createSystemCalls(network);
return {
network,
- components,
systemCalls,
};
}
diff --git a/examples/minimal/packages/client-vanilla/src/mud/setupNetwork.ts b/examples/minimal/packages/client-vanilla/src/mud/setupNetwork.ts
index 6c8e716ef3..4574062b9d 100644
--- a/examples/minimal/packages/client-vanilla/src/mud/setupNetwork.ts
+++ b/examples/minimal/packages/client-vanilla/src/mud/setupNetwork.ts
@@ -1,8 +1,7 @@
import { createPublicClient, fallback, webSocket, http, createWalletClient, Hex, parseEther, ClientConfig } from "viem";
import { createFaucetService } from "@latticexyz/services/faucet";
-import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs";
+import { syncToZustand } from "@latticexyz/store-sync/zustand";
import { getNetworkConfig } from "./getNetworkConfig";
-import { world } from "./world";
import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json";
import { createBurnerAccount, getContract, transportObserver, ContractWrite } from "@latticexyz/common";
import { Subject, share } from "rxjs";
@@ -36,8 +35,7 @@ export async function setupNetwork() {
onWrite: (write) => write$.next(write),
});
- const { components, latestBlock$, storedBlockLogs$, waitForTransaction } = await syncToRecs({
- world,
+ const { tables, useStore, latestBlock$, storedBlockLogs$, waitForTransaction } = await syncToZustand({
config: mudConfig,
address: networkConfig.worldAddress as Hex,
publicClient,
@@ -69,9 +67,8 @@ export async function setupNetwork() {
}
return {
- world,
- components,
- playerEntity: encodeEntity({ address: "address" }, { address: burnerWalletClient.account.address }),
+ tables,
+ useStore,
publicClient,
walletClient: burnerWalletClient,
latestBlock$,
diff --git a/examples/minimal/packages/client-vanilla/src/mud/world.ts b/examples/minimal/packages/client-vanilla/src/mud/world.ts
deleted file mode 100644
index ef9fb2be24..0000000000
--- a/examples/minimal/packages/client-vanilla/src/mud/world.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { createWorld } from "@latticexyz/recs";
-
-export const world = createWorld();
diff --git a/examples/minimal/packages/contracts/worlds.json b/examples/minimal/packages/contracts/worlds.json
index 2d47ae4158..5006b1be1d 100644
--- a/examples/minimal/packages/contracts/worlds.json
+++ b/examples/minimal/packages/contracts/worlds.json
@@ -4,6 +4,6 @@
"blockNumber": 21817970
},
"31337": {
- "address": "0x6e9474e9c83676b9a71133ff96db43e7aa0a4342"
+ "address": "0x6d584a9c0bd815104ab1fe92087c74450f7845d4"
}
}
\ No newline at end of file
diff --git a/packages/store-sync/package.json b/packages/store-sync/package.json
index 4d4bb772b1..3939841bc0 100644
--- a/packages/store-sync/package.json
+++ b/packages/store-sync/package.json
@@ -14,7 +14,8 @@
"./postgres": "./dist/postgres/index.js",
"./recs": "./dist/recs/index.js",
"./sqlite": "./dist/sqlite/index.js",
- "./trpc-indexer": "./dist/trpc-indexer/index.js"
+ "./trpc-indexer": "./dist/trpc-indexer/index.js",
+ "./zustand": "./dist/zustand/index.js"
},
"typesVersions": {
"*": {
@@ -32,6 +33,9 @@
],
"trpc-indexer": [
"./src/trpc-indexer/index.ts"
+ ],
+ "zustand": [
+ "./src/zustand/index.ts"
]
}
},
@@ -63,7 +67,8 @@
"sql.js": "^1.8.0",
"superjson": "^1.12.4",
"viem": "1.14.0",
- "zod": "^3.21.4"
+ "zod": "^3.21.4",
+ "zustand": "^4.3.7"
},
"devDependencies": {
"@types/debug": "^4.1.7",
diff --git a/packages/store-sync/src/common.ts b/packages/store-sync/src/common.ts
index 6caa2f46d7..fa4989dca5 100644
--- a/packages/store-sync/src/common.ts
+++ b/packages/store-sync/src/common.ts
@@ -1,10 +1,16 @@
import { Address, Block, Hex, Log, PublicClient } from "viem";
-import { StoreConfig, StoreEventsAbiItem, StoreEventsAbi, resolveUserTypes } from "@latticexyz/store";
-import storeConfig from "@latticexyz/store/mud.config";
+import { StoreConfig, StoreEventsAbiItem, StoreEventsAbi, resolveConfig } from "@latticexyz/store";
import { Observable } from "rxjs";
-import { resourceToHex } from "@latticexyz/common";
import { UnionPick } from "@latticexyz/common/type-utils";
import { KeySchema, TableRecord, ValueSchema } from "@latticexyz/protocol-parser";
+import storeConfig from "@latticexyz/store/mud.config";
+import worldConfig from "@latticexyz/world/mud.config";
+import { flattenSchema } from "./flattenSchema";
+
+/** @internal Temporary workaround until we redo our config parsing and can pull this directly from the config (https://github.com/latticexyz/mud/issues/1668) */
+export const storeTables = resolveConfig(storeConfig).tables;
+/** @internal Temporary workaround until we redo our config parsing and can pull this directly from the config (https://github.com/latticexyz/mud/issues/1668) */
+export const worldTables = resolveConfig(worldConfig).tables;
export type ChainId = number;
export type WorldId = `${ChainId}:${Address}`;
@@ -101,15 +107,10 @@ export type StorageAdapterLog = Partial & UnionPick Promise;
-// TODO: adjust when we get namespace support (https://github.com/latticexyz/mud/issues/994) and when table has namespace key (https://github.com/latticexyz/mud/issues/1201)
-// TODO: adjust when schemas are automatically resolved
+export const schemasTableId = storeTables.Tables.tableId;
export const schemasTable = {
- ...storeConfig.tables.Tables,
- valueSchema: resolveUserTypes(storeConfig.tables.Tables.valueSchema, storeConfig.userTypes),
+ ...storeTables.Tables,
+ // TODO: remove once we've got everything using the new Table shape
+ keySchema: flattenSchema(storeTables.Tables.keySchema),
+ valueSchema: flattenSchema(storeTables.Tables.valueSchema),
};
-
-export const schemasTableId = resourceToHex({
- type: schemasTable.offchainOnly ? "offchainTable" : "table",
- namespace: storeConfig.namespace,
- name: schemasTable.name,
-});
diff --git a/packages/store-sync/src/flattenSchema.ts b/packages/store-sync/src/flattenSchema.ts
new file mode 100644
index 0000000000..7cc9a92d90
--- /dev/null
+++ b/packages/store-sync/src/flattenSchema.ts
@@ -0,0 +1,8 @@
+import { mapObject } from "@latticexyz/common/utils";
+import { ValueSchema } from "@latticexyz/store";
+
+export function flattenSchema(
+ schema: schema
+): { readonly [k in keyof schema]: schema[k]["type"] } {
+ return mapObject(schema, (value) => value.type);
+}
diff --git a/packages/store-sync/src/isTableRegistrationLog.ts b/packages/store-sync/src/isTableRegistrationLog.ts
index b14f8911b5..c71ad1f3e4 100644
--- a/packages/store-sync/src/isTableRegistrationLog.ts
+++ b/packages/store-sync/src/isTableRegistrationLog.ts
@@ -1,7 +1,7 @@
-import { StorageAdapterLog, schemasTableId } from "./common";
+import { StorageAdapterLog, storeTables } from "./common";
export function isTableRegistrationLog(
log: StorageAdapterLog
): log is StorageAdapterLog & { eventName: "Store_SetRecord" } {
- return log.eventName === "Store_SetRecord" && log.args.tableId === schemasTableId;
+ return log.eventName === "Store_SetRecord" && log.args.tableId === storeTables.Tables.tableId;
}
diff --git a/packages/store-sync/src/logToTable.test.ts b/packages/store-sync/src/logToTable.test.ts
index 8fb7475552..f50e813bb5 100644
--- a/packages/store-sync/src/logToTable.test.ts
+++ b/packages/store-sync/src/logToTable.test.ts
@@ -13,7 +13,7 @@ describe("logToTable", () => {
staticData:
// eslint-disable-next-line max-len
"0x0060030220202000000000000000000000000000000000000000000000000000002001005f000000000000000000000000000000000000000000000000000000006003025f5f5fc4c40000000000000000000000000000000000000000000000",
- encodedLengths: "0x000000000000000000000000000000000000022000000000a0000000000002c0", // "0x00000000000000000000000000000000000000a00000000220000000000002c0",
+ encodedLengths: "0x000000000000000000000000000000000000022000000000a0000000000002c0",
dynamicData:
// eslint-disable-next-line max-len
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000077461626c654964000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000000000000000b6669656c644c61796f757400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000096b6579536368656d610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b76616c7565536368656d610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012616269456e636f6465644b65794e616d657300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014616269456e636f6465644669656c644e616d6573000000000000000000000000",
diff --git a/packages/store-sync/src/zustand/common.ts b/packages/store-sync/src/zustand/common.ts
new file mode 100644
index 0000000000..80598c24bb
--- /dev/null
+++ b/packages/store-sync/src/zustand/common.ts
@@ -0,0 +1,21 @@
+import { Table, SchemaToPrimitives } from "@latticexyz/store";
+import { Hex } from "viem";
+
+export type RawRecord = {
+ /** Internal unique ID */
+ readonly id: string;
+ readonly tableId: Hex;
+ readonly keyTuple: readonly Hex[];
+ readonly staticData: Hex;
+ readonly encodedLengths: Hex;
+ readonly dynamicData: Hex;
+};
+
+export type TableRecord