Skip to content

Commit 227db4d

Browse files
authored
feat(store-sync): recs sync adapter (#3486)
1 parent 0812178 commit 227db4d

11 files changed

Lines changed: 196 additions & 84 deletions

.changeset/lucky-maps-tell.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@latticexyz/store-sync": patch
3+
---
4+
5+
Added an RECS sync adapter to be used with `SyncProvider` in React apps.
6+
7+
```tsx
8+
import { WagmiProvider } from "wagmi";
9+
import { QueryClientProvider } from "@tanstack/react-query";
10+
import { SyncProvider } from "@latticexyz/store-sync/react";
11+
import { createSyncAdapter } from "@latticexyz/store-sync/recs";
12+
import { createWorld } from "@latticexyz/recs";
13+
import config from "./mud.config";
14+
15+
const world = createWorld();
16+
const { syncAdapter, components } = createSyncAdapter({ world, config });
17+
18+
export function App() {
19+
return (
20+
<WagmiProvider config={wagmiConfig}>
21+
<QueryClientProvider client={queryClient}>
22+
<SyncProvider chainId={chainId} address={worldAddress} startBlock={startBlock} adapter={syncAdapter}>
23+
{children}
24+
</SyncProvider>
25+
</QueryClientProvider>
26+
</WagmiProvider>
27+
);
28+
}
29+
```

packages/store-sync/src/react/SyncProvider.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,15 @@ export function SyncProvider({ chainId, adapter, children, ...syncOptions }: Pro
3737
});
3838

3939
useEffect(() => {
40-
const sub = result.data?.storedBlockLogs$.subscribe({
40+
if (!result.data) return;
41+
42+
const sub = result.data.storedBlockLogs$.subscribe({
4143
error: (error) => console.error("got sync error", error),
4244
});
4345
return (): void => {
44-
sub?.unsubscribe();
46+
sub.unsubscribe();
4547
};
46-
}, [result.data?.storedBlockLogs$]);
48+
}, [result.data]);
4749

4850
return <SyncContext.Provider value={result}>{children}</SyncContext.Provider>;
4951
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { Metadata } from "@latticexyz/recs";
22
import { KeySchema, ValueSchema } from "@latticexyz/protocol-parser/internal";
3+
import { Table } from "@latticexyz/config";
34

45
export type StoreComponentMetadata = Metadata & {
56
componentName: string;
67
tableName: string;
8+
table: Table;
79
// TODO: migrate to store's KeySchema/ValueSchema
10+
/** @deprecated Derive this schema from `component.metadata.table` instead. */
811
keySchema: KeySchema;
12+
/** @deprecated Derive this schema from `component.metadata.table` instead. */
913
valueSchema: ValueSchema;
1014
};

packages/store-sync/src/recs/createStorageAdapter.ts

Lines changed: 30 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,58 @@
1-
import { Tables } from "@latticexyz/config";
1+
import { Table, Tables } from "@latticexyz/config";
22
import { debug } from "./debug";
3-
import { World as RecsWorld, getComponentValue, hasComponent, removeComponent, setComponent } from "@latticexyz/recs";
4-
import { defineInternalComponents } from "./defineInternalComponents";
5-
import { getTableEntity } from "./getTableEntity";
3+
import { World as RecsWorld, getComponentValue, removeComponent, setComponent } from "@latticexyz/recs";
64
import { hexToResource, resourceToLabel, spliceHex } from "@latticexyz/common";
7-
import { decodeValueArgs } from "@latticexyz/protocol-parser/internal";
5+
import { decodeValueArgs, getSchemaTypes, getValueSchema } from "@latticexyz/protocol-parser/internal";
86
import { Hex, size } from "viem";
9-
import { isTableRegistrationLog } from "../isTableRegistrationLog";
10-
import { logToTable } from "../logToTable";
117
import { hexKeyTupleToEntity } from "./hexKeyTupleToEntity";
128
import { StorageAdapter, StorageAdapterBlock } from "../common";
139
import { singletonEntity } from "./singletonEntity";
1410
import { tablesToComponents } from "./tablesToComponents";
15-
import { merge } from "@ark/util";
1611

17-
export type CreateStorageAdapterOptions<tables extends Tables> = {
12+
export type CreateStorageAdapterOptions<tables extends Tables = {}> = {
1813
world: RecsWorld;
14+
/** @deprecated Use `const components = tablesToComponents(world, tables)` instead. */
1915
tables: tables;
2016
shouldSkipUpdateStream?: () => boolean;
2117
};
2218

23-
export type CreateStorageAdapterResult<tables extends Tables> = {
19+
export type CreateStorageAdapterResult<tables extends Tables = {}> = {
2420
storageAdapter: StorageAdapter;
25-
components: merge<tablesToComponents<tables>, ReturnType<typeof defineInternalComponents>>;
21+
/** @deprecated Use `const components = tablesToComponents(world, tables)` instead. */
22+
components: tablesToComponents<tables>;
2623
};
2724

28-
export function createStorageAdapter<tables extends Tables>({
25+
export function createStorageAdapter<tables extends Tables = {}>({
2926
world,
30-
tables,
27+
tables = {} as tables,
3128
shouldSkipUpdateStream,
3229
}: CreateStorageAdapterOptions<tables>): CreateStorageAdapterResult<tables> {
3330
world.registerEntity({ id: singletonEntity });
3431

35-
const components = {
36-
...tablesToComponents(world, tables),
37-
...defineInternalComponents(world),
38-
} as CreateStorageAdapterResult<tables>["components"];
39-
40-
async function recsStorageAdapter({ logs }: StorageAdapterBlock): Promise<void> {
41-
const newTables = logs.filter(isTableRegistrationLog).map(logToTable);
42-
for (const newTable of newTables) {
43-
const tableEntity = getTableEntity(newTable);
44-
if (hasComponent(components.RegisteredTables, tableEntity)) {
45-
console.warn("table already registered, ignoring", {
46-
newTable,
47-
existingTable: getComponentValue(components.RegisteredTables, tableEntity)?.table,
48-
});
49-
} else {
50-
setComponent(
51-
components.RegisteredTables,
52-
tableEntity,
53-
{ table: newTable },
54-
{ skipUpdateStream: shouldSkipUpdateStream?.() },
55-
);
56-
}
57-
}
32+
// kept for backwards compat
33+
const components = tablesToComponents(world, tables) as CreateStorageAdapterResult<tables>["components"];
5834

35+
async function storageAdapter({ logs }: StorageAdapterBlock): Promise<void> {
5936
for (const log of logs) {
60-
const { namespace, name } = hexToResource(log.args.tableId);
61-
const table = getComponentValue(
62-
components.RegisteredTables,
63-
getTableEntity({ address: log.address, namespace, name }),
64-
)?.table;
65-
if (!table) {
66-
debug(`skipping update for unknown table: ${resourceToLabel({ namespace, name })} at ${log.address}`);
67-
continue;
68-
}
69-
70-
const component = world.components.find((c) => c.id === table.tableId);
37+
const tableId = log.args.tableId;
38+
const component = world.components.find((c) => c.id === tableId);
7139
if (!component) {
7240
debug(
73-
`skipping update for unknown component: ${table.tableId} (${resourceToLabel({
74-
namespace,
75-
name,
76-
})}). Available components: ${Object.keys(components)}`,
41+
`skipping update for unknown component: ${tableId} (${resourceToLabel(hexToResource(tableId))}). Available components: ${Object.keys(components)}`,
7742
);
7843
continue;
7944
}
45+
const table = component.metadata?.table as Table | undefined;
46+
if (!table) {
47+
debug(`skipping update for unknown table: ${resourceToLabel(hexToResource(tableId))} at ${log.address}`);
48+
continue;
49+
}
8050

51+
const valueSchema = getSchemaTypes(getValueSchema(table));
8152
const entity = hexKeyTupleToEntity(log.args.keyTuple);
8253

8354
if (log.eventName === "Store_SetRecord") {
84-
const value = decodeValueArgs(table.valueSchema, log.args);
55+
const value = decodeValueArgs(valueSchema, log.args);
8556
debug("setting component", {
8657
namespace: table.namespace,
8758
name: table.name,
@@ -104,7 +75,7 @@ export function createStorageAdapter<tables extends Tables>({
10475
const previousValue = getComponentValue(component, entity);
10576
const previousStaticData = (previousValue?.__staticData as Hex) ?? "0x";
10677
const newStaticData = spliceHex(previousStaticData, log.args.start, size(log.args.data), log.args.data);
107-
const newValue = decodeValueArgs(table.valueSchema, {
78+
const newValue = decodeValueArgs(valueSchema, {
10879
staticData: newStaticData,
10980
encodedLengths: (previousValue?.__encodedLengths as Hex) ?? "0x",
11081
dynamicData: (previousValue?.__dynamicData as Hex) ?? "0x",
@@ -132,7 +103,7 @@ export function createStorageAdapter<tables extends Tables>({
132103
const previousValue = getComponentValue(component, entity);
133104
const previousDynamicData = (previousValue?.__dynamicData as Hex) ?? "0x";
134105
const newDynamicData = spliceHex(previousDynamicData, log.args.start, log.args.deleteCount, log.args.data);
135-
const newValue = decodeValueArgs(table.valueSchema, {
106+
const newValue = decodeValueArgs(valueSchema, {
136107
staticData: (previousValue?.__staticData as Hex) ?? "0x",
137108
// TODO: handle unchanged encoded lengths
138109
encodedLengths: log.args.encodedLengths,
@@ -168,5 +139,9 @@ export function createStorageAdapter<tables extends Tables>({
168139
}
169140
}
170141

171-
return { storageAdapter: recsStorageAdapter, components };
142+
return {
143+
storageAdapter,
144+
/** @deprecated Use `const components = tablesToComponents(world, tables)` instead. */
145+
components,
146+
};
172147
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Component as RecsComponent, World as RecsWorld, getComponentValue, setComponent } from "@latticexyz/recs";
2+
import { createStorageAdapter } from "./createStorageAdapter";
3+
import { SyncStep } from "../SyncStep";
4+
import { SyncAdapter } from "../common";
5+
import { createStoreSync } from "../createStoreSync";
6+
import { singletonEntity } from "./singletonEntity";
7+
import { Store as StoreConfig } from "@latticexyz/store";
8+
import { registerComponents } from "./registerComponents";
9+
10+
export type CreateSyncAdapterOptions<config extends StoreConfig> = {
11+
world: RecsWorld;
12+
config: config;
13+
};
14+
15+
export function createSyncAdapter<const config extends StoreConfig>({
16+
world,
17+
config,
18+
}: CreateSyncAdapterOptions<config>): {
19+
syncAdapter: SyncAdapter;
20+
components: registerComponents<config>;
21+
} {
22+
const components = registerComponents({ world, config });
23+
24+
const syncAdapter: SyncAdapter = (opts) => {
25+
// TODO: clear component values?
26+
27+
const { storageAdapter } = createStorageAdapter({
28+
world,
29+
tables: {},
30+
shouldSkipUpdateStream: (): boolean => {
31+
const value = getComponentValue(components.SyncProgress, singletonEntity);
32+
console.log("should skip update?", value);
33+
return value?.step !== SyncStep.LIVE;
34+
},
35+
});
36+
37+
return createStoreSync({
38+
...opts,
39+
storageAdapter,
40+
onProgress: ({ step, percentage, latestBlockNumber, lastBlockNumberProcessed, message }) => {
41+
// already live, no need for more progress updates
42+
if (getComponentValue(components.SyncProgress, singletonEntity)?.step === SyncStep.LIVE) return;
43+
44+
console.log("setting component", { step, percentage });
45+
setComponent(components.SyncProgress, singletonEntity, {
46+
step,
47+
percentage,
48+
latestBlockNumber,
49+
lastBlockNumberProcessed,
50+
message,
51+
});
52+
53+
// when we switch to live, trigger update for all entities in all components
54+
if (step === SyncStep.LIVE) {
55+
for (const _component of Object.values(components)) {
56+
// downcast component for easier calling of generic methods on all components
57+
const component = _component as RecsComponent;
58+
for (const entity of component.entities()) {
59+
const value = getComponentValue(component, entity);
60+
component.update$.next({ component, entity, value: [value, value] });
61+
}
62+
}
63+
}
64+
},
65+
});
66+
};
67+
68+
return { syncAdapter, components };
69+
}

packages/store-sync/src/recs/defineInternalComponents.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
import { World, defineComponent, Type, Component, Schema, Metadata } from "@latticexyz/recs";
2-
import { Table } from "../common";
32

43
export type InternalComponents = ReturnType<typeof defineInternalComponents>;
54

65
export function defineInternalComponents(world: World) {
76
return {
8-
RegisteredTables: defineComponent<{ table: Type.T }, Metadata, Table>(
9-
world,
10-
{ table: Type.T },
11-
{ metadata: { componentName: "RegisteredTables" } },
12-
),
137
SyncProgress: defineComponent(
148
world,
159
{
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
export * from "./common";
2+
export * from "./createStorageAdapter";
3+
export * from "./createSyncAdapter";
24
export * from "./decodeEntity";
35
export * from "./encodeEntity";
46
export * from "./entityToHexKeyTuple";
57
export * from "./hexKeyTupleToEntity";
68
export * from "./isStoreComponent";
7-
export * from "./createStorageAdapter";
9+
export * from "./registerComponents";
810
export * from "./singletonEntity";
911
export * from "./syncToRecs";
12+
export * from "./tableToComponent";
13+
export * from "./tablesToComponents";

packages/store-sync/src/recs/isStoreComponent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export function isStoreComponent<S extends Schema = Schema>(
77
return (
88
component.metadata?.componentName != null &&
99
component.metadata?.tableName != null &&
10+
component.metadata?.table != null &&
1011
component.metadata?.keySchema != null &&
1112
component.metadata?.valueSchema != null
1213
);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { World as RecsWorld } from "@latticexyz/recs";
2+
import { mudTables } from "../common";
3+
import { tablesToComponents } from "./tablesToComponents";
4+
import { Store as StoreConfig } from "@latticexyz/store";
5+
import { merge } from "@ark/util";
6+
import { configToTables } from "../configToTables";
7+
import { defineInternalComponents } from "./defineInternalComponents";
8+
import { Tables } from "@latticexyz/config";
9+
10+
export type registerComponents<config extends StoreConfig, extraTables extends Tables = {}> = merge<
11+
merge<
12+
merge<tablesToComponents<configToTables<config>>, tablesToComponents<extraTables>>,
13+
tablesToComponents<mudTables>
14+
>,
15+
ReturnType<typeof defineInternalComponents>
16+
>;
17+
18+
export function registerComponents<const config extends StoreConfig, const extraTables extends Tables = {}>({
19+
world,
20+
config,
21+
extraTables = {} as extraTables,
22+
}: {
23+
world: RecsWorld;
24+
config: config;
25+
extraTables?: extraTables;
26+
}): registerComponents<config, extraTables> {
27+
return {
28+
...tablesToComponents(world, configToTables(config) as configToTables<config>),
29+
...tablesToComponents(world, extraTables),
30+
...tablesToComponents(world, mudTables),
31+
...defineInternalComponents(world),
32+
} as never;
33+
}

packages/store-sync/src/recs/syncToRecs.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { Tables } from "@latticexyz/config";
22
import { Store as StoreConfig } from "@latticexyz/store";
33
import { Component as RecsComponent, World as RecsWorld, getComponentValue, setComponent } from "@latticexyz/recs";
4-
import { SyncOptions, SyncResult, mudTables } from "../common";
5-
import { CreateStorageAdapterResult, createStorageAdapter } from "./createStorageAdapter";
4+
import { SyncOptions, SyncResult } from "../common";
5+
import { createStorageAdapter } from "./createStorageAdapter";
66
import { createStoreSync } from "../createStoreSync";
77
import { singletonEntity } from "./singletonEntity";
88
import { SyncStep } from "../SyncStep";
9-
import { configToTables } from "../configToTables";
10-
import { merge } from "@ark/util";
9+
import { registerComponents } from "./registerComponents";
1110

1211
export type SyncToRecsOptions<
1312
config extends StoreConfig = StoreConfig,
@@ -20,26 +19,22 @@ export type SyncToRecsOptions<
2019
};
2120

2221
export type SyncToRecsResult<config extends StoreConfig, extraTables extends Tables> = SyncResult & {
23-
components: CreateStorageAdapterResult<merge<merge<configToTables<config>, extraTables>, mudTables>>["components"];
22+
components: registerComponents<config, extraTables>;
2423
stopSync: () => void;
2524
};
2625

27-
export async function syncToRecs<config extends StoreConfig, extraTables extends Tables = {}>({
26+
export async function syncToRecs<const config extends StoreConfig, const extraTables extends Tables = {}>({
2827
world,
2928
config,
3029
tables: extraTables = {} as extraTables,
3130
startSync = true,
3231
...syncOptions
3332
}: SyncToRecsOptions<config, extraTables>): Promise<SyncToRecsResult<config, extraTables>> {
34-
const tables = {
35-
...configToTables(config),
36-
...extraTables,
37-
...mudTables,
38-
};
33+
const components = registerComponents({ world, config, extraTables });
3934

40-
const { storageAdapter, components } = createStorageAdapter({
35+
const { storageAdapter } = createStorageAdapter({
4136
world,
42-
tables,
37+
tables: {},
4338
shouldSkipUpdateStream: (): boolean =>
4439
getComponentValue(components.SyncProgress, singletonEntity)?.step !== SyncStep.LIVE,
4540
});

0 commit comments

Comments
 (0)