Skip to content

Commit

Permalink
feat(store-sync): sync to sqlite (#1185)
Browse files Browse the repository at this point in the history
  • Loading branch information
holic committed Jul 21, 2023
1 parent 6de86f1 commit 69a96f1
Show file tree
Hide file tree
Showing 24 changed files with 1,722 additions and 91 deletions.
26 changes: 26 additions & 0 deletions .changeset/soft-boxes-smile.md
@@ -0,0 +1,26 @@
---
"@latticexyz/store-sync": minor
---

`blockLogsToStorage(sqliteStorage(...))` converts block logs to SQLite operations. You can use it like:

```ts
import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
import { createPublicClient } from "viem";
import { blockLogsToStorage } from "@latticexyz/store-sync";
import { sqliteStorage } from "@latticexyz/store-sync/sqlite";

const database = drizzle(new Database('store.db')) as any as BaseSQLiteDatabase<"sync", void>;
const publicClient = createPublicClient({ ... });

blockLogs$
.pipe(
concatMap(blockLogsToStorage(sqliteStorage({ database, publicClient }))),
tap(({ blockNumber, operations }) => {
console.log("stored", operations.length, "operations for block", blockNumber);
})
)
.subscribe();
```
2 changes: 1 addition & 1 deletion packages/block-logs-stream/src/groupLogsByBlockNumber.ts
@@ -1,6 +1,6 @@
import { BlockNumber, Log } from "viem";
import { NonPendingLog, isNonPendingLog } from "./isNonPendingLog";
import { isDefined, bigIntSort } from "@latticexyz/common/utils";
import { bigIntSort, isDefined } from "@latticexyz/common/utils";
import { debug } from "./debug";

export type GroupLogsByBlockNumberResult<TLog extends Log> = {
Expand Down
22 changes: 19 additions & 3 deletions packages/store-sync/package.json
Expand Up @@ -10,9 +10,19 @@
"license": "MIT",
"type": "module",
"exports": {
".": "./dist/index.js"
".": "./dist/index.js",
"./sqlite": "./dist/sqlite/index.js"
},
"typesVersions": {
"*": {
"index": [
"./src/index.ts"
],
"sqlite": [
"./src/sqlite/index.ts"
]
}
},
"types": "src/index.ts",
"scripts": {
"build": "pnpm run build:js",
"build:js": "tsup",
Expand All @@ -29,12 +39,18 @@
"@latticexyz/schema-type": "workspace:*",
"@latticexyz/store": "workspace:*",
"@latticexyz/store-cache": "workspace:*",
"@latticexyz/utils": "workspace:*",
"better-sqlite3": "^8.4.0",
"debug": "^4.3.4",
"drizzle-orm": "^0.27.0",
"kysely": "^0.26.1",
"sql.js": "^1.8.0",
"superjson": "^1.12.4",
"viem": "1.3.1"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.4",
"@types/debug": "^4.1.7",
"@types/sql.js": "^1.4.4",
"tsup": "^6.7.0",
"vitest": "0.31.4"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/store-sync/src/blockLogsToStorage.test.ts
Expand Up @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { BlockLogsToStorageOptions, blockLogsToStorage } from "./blockLogsToStorage";
import storeConfig from "@latticexyz/store/mud.config";
import { isDefined } from "@latticexyz/common/utils";
import { Hex } from "viem";
import { TableId } from "@latticexyz/common";

const mockedCallbacks = {
registerTables: vi.fn<
Expand Down Expand Up @@ -35,6 +35,7 @@ describe("blockLogsToStorage", () => {
if (table.namespace === "" && table.name === "Inventory") {
return {
...table,
tableId: TableId.toHex("", "Inventory"),
keySchema: {
owner: "address",
item: "uint32",
Expand Down
85 changes: 14 additions & 71 deletions packages/store-sync/src/blockLogsToStorage.ts
Expand Up @@ -6,82 +6,27 @@ import {
abiTypesToSchema,
TableSchema,
} from "@latticexyz/protocol-parser";
import { GroupLogsByBlockNumberResult, GetLogsResult } from "@latticexyz/block-logs-stream";
import { StoreEventsAbi, StoreConfig } from "@latticexyz/store";
import { StoreConfig } from "@latticexyz/store";
import { TableId } from "@latticexyz/common";
import { Address, Hex, decodeAbiParameters, parseAbiParameters } from "viem";
import { Address, Hex, decodeAbiParameters, getAddress, parseAbiParameters } from "viem";
import { debug } from "./debug";
// TODO: move these type helpers into store?
import { Key, Value } from "@latticexyz/store-cache";
import { isDefined } from "@latticexyz/common/utils";
import { SchemaAbiType, StaticAbiType } from "@latticexyz/schema-type";
import { BlockLogs, StorageOperation, Table, TableName, TableNamespace } from "./common";

// TODO: change table schema/metadata APIs once we get both schema and field names in the same event (https://github.com/latticexyz/mud/pull/1182)

// TODO: export these from store or world
export const schemaTableId = new TableId("mudstore", "schema");
export const metadataTableId = new TableId("mudstore", "StoreMetadata");

export type StoreEventsLog = GetLogsResult<StoreEventsAbi>[number];
export type BlockLogs = GroupLogsByBlockNumberResult<StoreEventsLog>[number];

export type StoredTable = {
address: Address;
namespace: string;
name: string;
keySchema: Record<string, StaticAbiType>;
valueSchema: Record<string, SchemaAbiType>;
};

export type BaseStorageOperation = {
log: StoreEventsLog;
namespace: string;
};

export type SetRecordOperation<TConfig extends StoreConfig> = BaseStorageOperation & {
type: "SetRecord";
} & {
[TTable in keyof TConfig["tables"]]: {
name: TTable;
key: Key<TConfig, TTable>;
value: Value<TConfig, TTable>;
};
}[keyof TConfig["tables"]];

export type SetFieldOperation<TConfig extends StoreConfig> = BaseStorageOperation & {
type: "SetField";
} & {
[TTable in keyof TConfig["tables"]]: {
name: TTable;
key: Key<TConfig, TTable>;
} & {
[TValue in keyof Value<TConfig, TTable>]: {
fieldName: TValue;
fieldValue: Value<TConfig, TTable>[TValue];
};
}[keyof Value<TConfig, TTable>];
}[keyof TConfig["tables"]];

export type DeleteRecordOperation<TConfig extends StoreConfig> = BaseStorageOperation & {
type: "DeleteRecord";
} & {
[TTable in keyof TConfig["tables"]]: {
name: TTable;
key: Key<TConfig, TTable>;
};
}[keyof TConfig["tables"]];

export type StorageOperation<TConfig extends StoreConfig> =
| SetFieldOperation<TConfig>
| SetRecordOperation<TConfig>
| DeleteRecordOperation<TConfig>;

export type BlockLogsToStorageOptions<TConfig extends StoreConfig = StoreConfig> = {
registerTables: (opts: { blockNumber: BlockLogs["blockNumber"]; tables: StoredTable[] }) => Promise<void>;
registerTables: (opts: { blockNumber: BlockLogs["blockNumber"]; tables: Table[] }) => Promise<void>;
getTables: (opts: {
blockNumber: BlockLogs["blockNumber"];
tables: Pick<StoredTable, "address" | "namespace" | "name">[];
}) => Promise<StoredTable[]>;
tables: Pick<Table, "address" | "namespace" | "name">[];
}) => Promise<Table[]>;
storeOperations: (opts: {
blockNumber: BlockLogs["blockNumber"];
operations: StorageOperation<TConfig>[];
Expand All @@ -93,8 +38,6 @@ export type BlockLogsToStorageResult<TConfig extends StoreConfig = StoreConfig>
operations: StorageOperation<TConfig>[];
}>;

type TableNamespace = string;
type TableName = string;
type TableKey = `${Address}:${TableNamespace}:${TableName}`;

// hacky fix for schema registration + metadata events spanning multiple blocks
Expand Down Expand Up @@ -126,9 +69,9 @@ export function blockLogsToStorage<TConfig extends StoreConfig = StoreConfig>({
const tableId = TableId.fromHex(tableForSchema);
const schema = hexToTableSchema(log.args.data);

const key: TableKey = `${log.address}:${tableId.namespace}:${tableId.name}`;
const key: TableKey = `${getAddress(log.address)}:${tableId.namespace}:${tableId.name}`;
if (!visitedSchemas.has(key)) {
visitedSchemas.set(key, { address: log.address, tableId, schema });
visitedSchemas.set(key, { address: getAddress(log.address), tableId, schema });
newTableKeys.add(key);
}
});
Expand All @@ -152,10 +95,9 @@ export function blockLogsToStorage<TConfig extends StoreConfig = StoreConfig>({
);
const valueNames = decodeAbiParameters(parseAbiParameters("string[]"), abiEncodedFieldNames as Hex)[0];
// TODO: add key names to table registration when we refactor it (https://github.com/latticexyz/mud/pull/1182)

const key: TableKey = `${log.address}:${tableId.namespace}:${tableName}`;
const key: TableKey = `${getAddress(log.address)}:${tableId.namespace}:${tableName}`;
if (!visitedMetadata.has(key)) {
visitedMetadata.set(key, { address: log.address, tableId, keyNames: [], valueNames });
visitedMetadata.set(key, { address: getAddress(log.address), tableId, keyNames: [], valueNames });
newTableKeys.add(key);
}
});
Expand Down Expand Up @@ -194,6 +136,7 @@ export function blockLogsToStorage<TConfig extends StoreConfig = StoreConfig>({

return {
address,
tableId: schema.tableId.toHex(),
namespace: schema.tableId.namespace,
name: schema.tableId.name,
// TODO: replace with proper named key tuple (https://github.com/latticexyz/mud/pull/1182)
Expand All @@ -208,7 +151,7 @@ export function blockLogsToStorage<TConfig extends StoreConfig = StoreConfig>({
new Set(
block.logs.map((log) =>
JSON.stringify({
address: log.address,
address: getAddress(log.address),
...TableId.fromHex(log.args.table),
})
)
Expand All @@ -222,12 +165,12 @@ export function blockLogsToStorage<TConfig extends StoreConfig = StoreConfig>({
tables: tableIds.map((json) => JSON.parse(json)),
})
).map((table) => [`${table.address}:${new TableId(table.namespace, table.name).toHex()}`, table])
) as Record<Hex, StoredTable>;
) as Record<Hex, Table>;

const operations = block.logs
.map((log): StorageOperation<TConfig> | undefined => {
const tableId = TableId.fromHex(log.args.table);
const table = tables[`${log.address}:${log.args.table}`];
const table = tables[`${getAddress(log.address)}:${log.args.table}`];
if (!table) {
debug("no table found for event, skipping", tableId.toString(), log);
return;
Expand Down
80 changes: 80 additions & 0 deletions packages/store-sync/src/common.ts
@@ -0,0 +1,80 @@
import { SchemaAbiType, SchemaAbiTypeToPrimitiveType, StaticAbiType } from "@latticexyz/schema-type";
import { Address, Hex } from "viem";
// TODO: move these type helpers into store?
import { Key, Value } from "@latticexyz/store-cache";
import { GetLogsResult, GroupLogsByBlockNumberResult } from "@latticexyz/block-logs-stream";
import { StoreEventsAbi, StoreConfig } from "@latticexyz/store";

export type ChainId = number;
export type WorldId = `${ChainId}:${Address}`;

export type TableNamespace = string;
export type TableName = string;

export type KeySchema = Record<string, StaticAbiType>;
export type ValueSchema = Record<string, SchemaAbiType>;

export type SchemaToPrimitives<TSchema extends ValueSchema> = {
[key in keyof TSchema]: SchemaAbiTypeToPrimitiveType<TSchema[key]>;
};

export type TableRecord<TKeySchema extends KeySchema = KeySchema, TValueSchema extends ValueSchema = ValueSchema> = {
key: SchemaToPrimitives<TKeySchema>;
value: SchemaToPrimitives<TValueSchema>;
};

export type Table = {
address: Address;
tableId: Hex;
namespace: TableNamespace;
name: TableName;
keySchema: KeySchema;
valueSchema: ValueSchema;
};

export type StoreEventsLog = GetLogsResult<StoreEventsAbi>[number];
export type BlockLogs = GroupLogsByBlockNumberResult<StoreEventsLog>[number];

export type BaseStorageOperation = {
log: StoreEventsLog;
namespace: string;
name: string;
};

export type SetRecordOperation<TConfig extends StoreConfig> = BaseStorageOperation & {
type: "SetRecord";
} & {
[TTable in keyof TConfig["tables"]]: {
name: TTable & string;
key: Key<TConfig, TTable>;
value: Value<TConfig, TTable>;
};
}[keyof TConfig["tables"]];

export type SetFieldOperation<TConfig extends StoreConfig> = BaseStorageOperation & {
type: "SetField";
} & {
[TTable in keyof TConfig["tables"]]: {
name: TTable & string;
key: Key<TConfig, TTable>;
} & {
[TValue in keyof Value<TConfig, TTable>]: {
fieldName: TValue & string;
fieldValue: Value<TConfig, TTable>[TValue];
};
}[keyof Value<TConfig, TTable>];
}[keyof TConfig["tables"]];

export type DeleteRecordOperation<TConfig extends StoreConfig> = BaseStorageOperation & {
type: "DeleteRecord";
} & {
[TTable in keyof TConfig["tables"]]: {
name: TTable & string;
key: Key<TConfig, TTable>;
};
}[keyof TConfig["tables"]];

export type StorageOperation<TConfig extends StoreConfig> =
| SetFieldOperation<TConfig>
| SetRecordOperation<TConfig>
| DeleteRecordOperation<TConfig>;
1 change: 1 addition & 0 deletions packages/store-sync/src/index.ts
@@ -1 +1,2 @@
export * from "./blockLogsToStorage";
export * from "./common";
8 changes: 8 additions & 0 deletions packages/store-sync/src/schemaToDefaults.ts
@@ -0,0 +1,8 @@
import { schemaAbiTypeToDefaultValue } from "@latticexyz/schema-type";
import { ValueSchema, SchemaToPrimitives } from "./common";

export function schemaToDefaults<TSchema extends ValueSchema>(schema: TSchema): SchemaToPrimitives<TSchema> {
return Object.fromEntries(
Object.entries(schema).map(([key, abiType]) => [key, schemaAbiTypeToDefaultValue[abiType]])
) as SchemaToPrimitives<TSchema>;
}

0 comments on commit 69a96f1

Please sign in to comment.