From 811049a072d7ca88b123fed59674e017a06a4576 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Thu, 11 Jul 2024 17:38:51 +0100 Subject: [PATCH] chore: remove unused store-sync/query-cache (#2937) --- packages/store-sync/src/query-cache/common.ts | 55 - .../query-cache/createStorageAdapter.test.ts | 76 - .../src/query-cache/createStorageAdapter.ts | 140 -- .../store-sync/src/query-cache/createStore.ts | 51 - packages/store-sync/src/query-cache/debug.ts | 10 - .../store-sync/src/query-cache/query.test.ts | 438 ---- packages/store-sync/src/query-cache/query.ts | 27 - .../store-sync/src/query-cache/queryToWire.ts | 38 - .../src/query-cache/subjectsToWire.ts | 20 - .../src/query-cache/subscribeToQuery.test.ts | 1873 ----------------- .../src/query-cache/subscribeToQuery.ts | 124 -- .../src/query-cache/syncToQueryCache.ts | 42 - .../query-cache/test/createHydratedStore.ts | 44 - .../src/query-cache/test/minePending.ts | 9 - .../query-cache/test/waitForTransaction.ts | 13 - 15 files changed, 2960 deletions(-) delete mode 100644 packages/store-sync/src/query-cache/common.ts delete mode 100644 packages/store-sync/src/query-cache/createStorageAdapter.test.ts delete mode 100644 packages/store-sync/src/query-cache/createStorageAdapter.ts delete mode 100644 packages/store-sync/src/query-cache/createStore.ts delete mode 100644 packages/store-sync/src/query-cache/debug.ts delete mode 100644 packages/store-sync/src/query-cache/query.test.ts delete mode 100644 packages/store-sync/src/query-cache/query.ts delete mode 100644 packages/store-sync/src/query-cache/queryToWire.ts delete mode 100644 packages/store-sync/src/query-cache/subjectsToWire.ts delete mode 100644 packages/store-sync/src/query-cache/subscribeToQuery.test.ts delete mode 100644 packages/store-sync/src/query-cache/subscribeToQuery.ts delete mode 100644 packages/store-sync/src/query-cache/syncToQueryCache.ts delete mode 100644 packages/store-sync/src/query-cache/test/createHydratedStore.ts delete mode 100644 packages/store-sync/src/query-cache/test/minePending.ts delete mode 100644 packages/store-sync/src/query-cache/test/waitForTransaction.ts diff --git a/packages/store-sync/src/query-cache/common.ts b/packages/store-sync/src/query-cache/common.ts deleted file mode 100644 index 2352e4ec12..0000000000 --- a/packages/store-sync/src/query-cache/common.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Store } from "@latticexyz/store"; -import { Table } from "@latticexyz/config"; -import { SchemaAbiType, SchemaAbiTypeToPrimitiveType } from "@latticexyz/schema-type/internal"; -import { ComparisonCondition, InCondition } from "@latticexyz/query"; - -export type mapTuple = { - [key in keyof tuple]: tuple[key] extends keyof mapping ? mapping[tuple[key]] : never; -}; - -export type subjectSchemaToPrimitive = { - [key in keyof tuple]: tuple[key] extends SchemaAbiType ? SchemaAbiTypeToPrimitiveType : never; -}; - -export type Tables = Store["tables"]; - -export type TableSubjectItem = keyof table["schema"]; - -export type TableSubject
= readonly [ - TableSubjectItem
, - ...TableSubjectItem
[], -]; - -export type schemaAbiTypes> = { - [key in keyof schema]: schema[key]["type"]; -}; - -type tableConditions = { - [field in keyof table["schema"]]: - | [ - `${tableName}.${field & string}`, - ComparisonCondition["op"], - SchemaAbiTypeToPrimitiveType, - ] - | [ - `${tableName}.${field & string}`, - InCondition["op"], - readonly SchemaAbiTypeToPrimitiveType[], - ]; -}[keyof table["schema"]]; - -type queryConditions = { - [tableName in keyof tables]: tableConditions; -}[keyof tables]; - -export type Query = { - readonly from: { - readonly [k in keyof tables]?: readonly [keyof tables[k]["schema"], ...(keyof tables[k]["schema"])[]]; - }; - readonly except?: { - readonly [k in keyof tables]?: readonly [keyof tables[k]["schema"], ...(keyof tables[k]["schema"])[]]; - }; - readonly where?: readonly queryConditions[]; -}; - -export type extractTables = T extends Query ? tables : never; diff --git a/packages/store-sync/src/query-cache/createStorageAdapter.test.ts b/packages/store-sync/src/query-cache/createStorageAdapter.test.ts deleted file mode 100644 index 8df44cfe11..0000000000 --- a/packages/store-sync/src/query-cache/createStorageAdapter.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { beforeAll, describe, expect, it } from "vitest"; -import { storeEventsAbi } from "@latticexyz/store"; -import { createStorageAdapter } from "./createStorageAdapter"; -import { createStore } from "./createStore"; -import { configV2 as config, deployMockGame } from "../../test/mockGame"; -import { fetchAndStoreLogs } from "../fetchAndStoreLogs"; -import { testClient } from "../../test/common"; -import { getBlockNumber } from "viem/actions"; -import { Address } from "viem"; - -describe.skip("createStorageAdapter", async () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let worldAddress: Address; - beforeAll(async () => { - worldAddress = await deployMockGame(); - }); - - it("sets component values from logs", async () => { - const useStore = createStore({ tables: config.tables }); - const storageAdapter = createStorageAdapter({ store: useStore }); - - console.log("fetching blocks"); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for await (const block of fetchAndStoreLogs({ - storageAdapter, - publicClient: testClient, - events: storeEventsAbi, - fromBlock: 0n, - toBlock: await getBlockNumber(testClient), - })) { - // console.log("got block", block.blockNumber); - } - - expect(useStore.getState().records.map((record) => record.fields)).toMatchInlineSnapshot(` - [ - { - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - "x": 1, - "y": -1, - }, - { - "health": 5n, - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - }, - { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - { - "health": 5n, - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - }, - { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - { - "health": 0n, - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - }, - { - "player": "0xdBa86119a787422C593ceF119E40887f396024E2", - "x": 100, - "y": 100, - }, - { - "terrainType": 2, - "x": 3, - "y": 5, - }, - ] - `); - }); -}); diff --git a/packages/store-sync/src/query-cache/createStorageAdapter.ts b/packages/store-sync/src/query-cache/createStorageAdapter.ts deleted file mode 100644 index 4b990b3899..0000000000 --- a/packages/store-sync/src/query-cache/createStorageAdapter.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { StorageAdapter } from "../common"; -import { QueryCacheStore, RawTableRecord, TableRecord } from "./createStore"; -import { hexToResource, resourceToLabel, spliceHex } from "@latticexyz/common"; -import { Hex, concatHex, size } from "viem"; -import { - KeySchema, - decodeKey, - decodeValueArgs, - getKeySchema, - getValueSchema, -} from "@latticexyz/protocol-parser/internal"; -import { flattenSchema } from "../flattenSchema"; -import debug from "debug"; -import { Tables } from "./common"; - -function getRecordId({ tableId, keyTuple }: { tableId: Hex; keyTuple: readonly Hex[] }): string { - return `${tableId}:${concatHex(keyTuple)}`; -} - -export type CreateStorageAdapterOptions = { - store: store; -}; - -// TS isn't happy when we use the strongly typed store for the function definition so we -// overload the strongly typed variant here and allow the more generic version in the function. -export function createStorageAdapter({ - store, -}: CreateStorageAdapterOptions): StorageAdapter; - -export function createStorageAdapter({ store }: CreateStorageAdapterOptions>): StorageAdapter { - return async function queryCacheStorageAdapter({ logs }) { - const touchedIds = new Set(); - - const { tables, rawRecords: previousRawRecords, records: previousRecords } = store.getState(); - const tableList = Object.values(tables); - const updatedRawRecords: { [id: string]: RawTableRecord } = {}; - - for (const log of logs) { - const table = tableList.find((table) => table.tableId === log.args.tableId); - if (!table) { - const { namespace, name } = hexToResource(log.args.tableId); - debug( - `skipping update for unknown table: ${resourceToLabel({ namespace, name })} (${log.args.tableId}) at ${ - log.address - }`, - ); - continue; - } - - const id = getRecordId(log.args); - - if (log.eventName === "Store_SetRecord") { - // debug("setting record", { namespace: table.namespace, name: table.name, id, log }); - updatedRawRecords[id] = { - id, - table, - keyTuple: log.args.keyTuple, - staticData: log.args.staticData, - encodedLengths: log.args.encodedLengths, - dynamicData: log.args.dynamicData, - }; - touchedIds.add(id); - } else if (log.eventName === "Store_SpliceStaticData") { - // debug("splicing static data", { namespace: table.namespace, name: table.name, id, log }); - const previousRecord = - previousRawRecords.find((record) => record.id === id) ?? - ({ - id, - table, - keyTuple: log.args.keyTuple, - staticData: "0x", - encodedLengths: "0x", - dynamicData: "0x", - } satisfies RawTableRecord); - const staticData = spliceHex(previousRecord.staticData, log.args.start, size(log.args.data), log.args.data); - updatedRawRecords[id] = { - ...previousRecord, - staticData, - }; - touchedIds.add(id); - } else if (log.eventName === "Store_SpliceDynamicData") { - // debug("splicing dynamic data", { namespace: table.namespace, name: table.name, id, log }); - const previousRecord = - previousRawRecords.find((record) => record.id === id) ?? - ({ - id, - table, - keyTuple: log.args.keyTuple, - staticData: "0x", - encodedLengths: "0x", - dynamicData: "0x", - } satisfies RawTableRecord); - const encodedLengths = log.args.encodedLengths; - const dynamicData = spliceHex(previousRecord.dynamicData, log.args.start, log.args.deleteCount, log.args.data); - updatedRawRecords[id] = { - ...previousRecord, - encodedLengths, - dynamicData, - }; - touchedIds.add(id); - } else if (log.eventName === "Store_DeleteRecord") { - // debug("deleting record", { namespace: table.namespace, name: table.name, id, log }); - delete updatedRawRecords[id]; - touchedIds.add(id); - } - } - - if (!touchedIds.size) return; - - const rawRecords: readonly RawTableRecord[] = [ - ...previousRawRecords.filter((record) => !touchedIds.has(record.id)), - ...Object.values(updatedRawRecords), - ]; - - const records: readonly TableRecord[] = [ - ...previousRecords.filter((record) => !touchedIds.has(record.id)), - ...Object.values(updatedRawRecords).map((rawRecord): TableRecord => { - const keySchema = flattenSchema(getKeySchema(rawRecord.table)); - const key = decodeKey(keySchema as KeySchema, rawRecord.keyTuple); - const value = decodeValueArgs(flattenSchema(getValueSchema(rawRecord.table)), rawRecord); - - return { - table: rawRecord.table, - id: rawRecord.id, - keyTuple: rawRecord.keyTuple, - // TODO: do something to make sure this stays ordered? - primaryKey: Object.values(key), - key, - value, - fields: { ...key, ...value }, - }; - }), - ]; - - store.setState({ - rawRecords, - records, - }); - }; -} diff --git a/packages/store-sync/src/query-cache/createStore.ts b/packages/store-sync/src/query-cache/createStore.ts deleted file mode 100644 index 8bff3d18a8..0000000000 --- a/packages/store-sync/src/query-cache/createStore.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { StoreApi, UseBoundStore, create } from "zustand"; -import { Table } from "@latticexyz/config"; -import { Tables } from "./common"; -import { Hex } from "viem"; -import { StaticPrimitiveType } from "@latticexyz/schema-type/internal"; -import { SchemaToPrimitives } from "@latticexyz/store/internal"; -import { getKeySchema, getValueSchema } from "@latticexyz/protocol-parser/internal"; - -export type RawTableRecord
= { - readonly table: table; - /** @internal Internal unique ID */ - readonly id: string; - readonly keyTuple: readonly Hex[]; - readonly staticData: Hex; - readonly encodedLengths: Hex; - readonly dynamicData: Hex; -}; - -export type TableRecord
= { - readonly table: table; - /** @internal Internal unique ID */ - readonly id: string; - readonly keyTuple: readonly Hex[]; - readonly primaryKey: readonly StaticPrimitiveType[]; - readonly key: SchemaToPrimitives>; - readonly value: SchemaToPrimitives>; - readonly fields: SchemaToPrimitives; -}; - -export type QueryCacheState = { - readonly tables: tables; - readonly rawRecords: readonly RawTableRecord[]; - readonly records: readonly TableRecord[]; -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type QueryCacheStore = UseBoundStore>>; - -export type extractTables = T extends QueryCacheStore ? tables : never; - -export type CreateStoreOptions = { - tables: tables; -}; - -export function createStore({ tables }: CreateStoreOptions): QueryCacheStore { - return create>(() => ({ - tables, - rawRecords: [], - records: [], - })); -} diff --git a/packages/store-sync/src/query-cache/debug.ts b/packages/store-sync/src/query-cache/debug.ts deleted file mode 100644 index b74e4d9bf0..0000000000 --- a/packages/store-sync/src/query-cache/debug.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { debug as parentDebug } from "../debug"; - -export const debug = parentDebug.extend("query-cache"); -export const error = parentDebug.extend("query-cache"); - -// Pipe debug output to stdout instead of stderr -debug.log = console.debug.bind(console); - -// Pipe error output to stderr -error.log = console.error.bind(console); diff --git a/packages/store-sync/src/query-cache/query.test.ts b/packages/store-sync/src/query-cache/query.test.ts deleted file mode 100644 index 46178132c5..0000000000 --- a/packages/store-sync/src/query-cache/query.test.ts +++ /dev/null @@ -1,438 +0,0 @@ -import { beforeAll, describe, expect, it } from "vitest"; -import { createHydratedStore } from "./test/createHydratedStore"; -import { query } from "./query"; -import { deployMockGame } from "../../test/mockGame"; -import { Address } from "viem"; - -describe.skip("query", async () => { - let worldAddress: Address; - beforeAll(async () => { - worldAddress = await deployMockGame(); - }); - - it("can get players with a position", async () => { - const { store } = await createHydratedStore(worldAddress); - const result = await query(store, { - from: { - Position: ["player"], - }, - }); - - expect(result).toMatchInlineSnapshot(` - { - "subjects": [ - { - "records": [ - { - "fields": { - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - "x": 1, - "y": -1, - }, - "keyTuple": [ - "0x0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - ], - "primaryKey": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - ], - "subject": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "subjectSchema": [ - "address", - ], - }, - { - "records": [ - { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - ], - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "records": [ - { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - ], - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - { - "records": [ - { - "fields": { - "player": "0xdBa86119a787422C593ceF119E40887f396024E2", - "x": 100, - "y": 100, - }, - "keyTuple": [ - "0x000000000000000000000000dba86119a787422c593cef119e40887f396024e2", - ], - "primaryKey": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - ], - "subject": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "subjectSchema": [ - "address", - ], - }, - ], - } - `); - }); - - it("can get players at position (3, 5)", async () => { - const { store } = await createHydratedStore(worldAddress); - const result = await query(store, { - from: { - Position: ["player"], - }, - where: [ - ["Position.x", "=", 3], - ["Position.y", "=", 5], - ], - }); - - expect(result).toMatchInlineSnapshot(` - { - "subjects": [ - { - "records": [ - { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - ], - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "records": [ - { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - ], - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - ], - } - `); - }); - - it("can get players within the bounds of (-5, -5) and (5, 5)", async () => { - const { store } = await createHydratedStore(worldAddress); - const result = await query(store, { - from: { - Position: ["player"], - }, - where: [ - ["Position.x", ">=", -5], - ["Position.x", "<=", 5], - ["Position.y", ">=", -5], - ["Position.y", "<=", 5], - ], - }); - - expect(result).toMatchInlineSnapshot(` - { - "subjects": [ - { - "records": [ - { - "fields": { - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - "x": 1, - "y": -1, - }, - "keyTuple": [ - "0x0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - ], - "primaryKey": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - ], - "subject": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "subjectSchema": [ - "address", - ], - }, - { - "records": [ - { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - ], - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "records": [ - { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - ], - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - ], - } - `); - }); - - it("can get players that are still alive", async () => { - const { store } = await createHydratedStore(worldAddress); - const result = await query(store, { - from: { - Position: ["player"], - Health: ["player"], - }, - where: [["Health.health", "!=", 0n]], - }); - - expect(result).toMatchInlineSnapshot(` - { - "subjects": [ - { - "records": [ - { - "fields": { - "health": 5n, - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - }, - "keyTuple": [ - "0x0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - ], - "primaryKey": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "tableId": "0x746200000000000000000000000000004865616c746800000000000000000000", - }, - ], - "subject": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "subjectSchema": [ - "address", - ], - }, - { - "records": [ - { - "fields": { - "health": 5n, - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x746200000000000000000000000000004865616c746800000000000000000000", - }, - ], - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - ], - } - `); - }); - - it("can get all players in grassland", async () => { - const { store } = await createHydratedStore(worldAddress); - const result = await query(store, { - from: { - Terrain: ["x", "y"], - }, - where: [["Terrain.terrainType", "=", 2]], - }); - - expect(result).toMatchInlineSnapshot(` - { - "subjects": [ - { - "records": [ - { - "fields": { - "terrainType": 2, - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x0000000000000000000000000000000000000000000000000000000000000003", - "0x0000000000000000000000000000000000000000000000000000000000000005", - ], - "primaryKey": [ - 3, - 5, - ], - "tableId": "0x746200000000000000000000000000005465727261696e000000000000000000", - }, - ], - "subject": [ - 3, - 5, - ], - "subjectSchema": [ - "int32", - "int32", - ], - }, - ], - } - `); - }); - - it("can get all players without health (e.g. spectator)", async () => { - const { store } = await createHydratedStore(worldAddress); - const result = await query(store, { - from: { - Position: ["player"], - }, - except: { - Health: ["player"], - }, - }); - - expect(result).toMatchInlineSnapshot(` - { - "subjects": [ - { - "records": [ - { - "fields": { - "player": "0xdBa86119a787422C593ceF119E40887f396024E2", - "x": 100, - "y": 100, - }, - "keyTuple": [ - "0x000000000000000000000000dba86119a787422c593cef119e40887f396024e2", - ], - "primaryKey": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - ], - "subject": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "subjectSchema": [ - "address", - ], - }, - ], - } - `); - }); -}); diff --git a/packages/store-sync/src/query-cache/query.ts b/packages/store-sync/src/query-cache/query.ts deleted file mode 100644 index 9aaf12de56..0000000000 --- a/packages/store-sync/src/query-cache/query.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Query } from "./common"; -import { QueryCacheStore, extractTables } from "./createStore"; -import { SubjectRecords } from "@latticexyz/query"; -import { findSubjects } from "@latticexyz/query/internal"; -import { queryToWire } from "./queryToWire"; - -// TODO: take in query input type so we can narrow result types - -export type QueryResult = { - subjects: readonly SubjectRecords[]; -}; - -export async function query>>( - store: store, - query: query, -): Promise { - const { tables, records } = store.getState(); - - const subjects = findSubjects({ - records, - query: queryToWire(tables, query), - }); - - return { - subjects, - }; -} diff --git a/packages/store-sync/src/query-cache/queryToWire.ts b/packages/store-sync/src/query-cache/queryToWire.ts deleted file mode 100644 index 5ac31030c6..0000000000 --- a/packages/store-sync/src/query-cache/queryToWire.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Query, Tables } from "./common"; -import { QueryCondition as WireQueryCondition, Query as WireQuery } from "@latticexyz/query"; -import { subjectsToWire } from "./subjectsToWire"; - -// TODO: validate query -// - one subject per table -// - underlying subject field types match -// - only keys as subjects for now? -// - subjects and conditions all have valid fields -// - all subjects match -// - can only compare like types? -// - `where` tables are in `from` - -export function queryToWire>( - tables: tables, - query: query, -): WireQuery { - // TODO: move out validation to its own thing - // TODO: validate that all query subjects match in underlying abi types - // TODO: do other validations - - const where = (query.where ?? []).map(([leftTableField, op, right]): WireQueryCondition => { - // TODO: translate table field - const left = leftTableField; - const [tableName, fieldName] = left.split("."); - const table = tables[tableName]; - if (op === "in") { - return { left: { tableId: table.tableId, field: fieldName }, op, right }; - } - return { left: { tableId: table.tableId, field: fieldName }, op, right }; - }); - - return { - from: subjectsToWire(tables, query.from), - except: subjectsToWire(tables, query.except ?? {}), - where, - }; -} diff --git a/packages/store-sync/src/query-cache/subjectsToWire.ts b/packages/store-sync/src/query-cache/subjectsToWire.ts deleted file mode 100644 index c076313e50..0000000000 --- a/packages/store-sync/src/query-cache/subjectsToWire.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TableSubject, Tables } from "./common"; -import { QuerySubject } from "@latticexyz/query"; - -// TODO: validate -// - all subject types match -// - only keys as subjects for now? - -export function subjectsToWire( - tables: Tables, - subjects: { - [tableName in keyof tables]?: TableSubject; - }, -): readonly QuerySubject[] { - // TODO: validate `tables` contains all tables used `subjects` map - // TODO: validate that subject field names exist in table schema - return Object.entries(subjects).map(([tableName, subject]) => ({ - tableId: tables[tableName].tableId, - subject, - })); -} diff --git a/packages/store-sync/src/query-cache/subscribeToQuery.test.ts b/packages/store-sync/src/query-cache/subscribeToQuery.test.ts deleted file mode 100644 index a1ea43e1df..0000000000 --- a/packages/store-sync/src/query-cache/subscribeToQuery.test.ts +++ /dev/null @@ -1,1873 +0,0 @@ -import { beforeAll, describe, expect, it } from "vitest"; -import { createHydratedStore } from "./test/createHydratedStore"; -import { subscribeToQuery } from "./subscribeToQuery"; -import { deployMockGame, worldAbi } from "../../test/mockGame"; -import { writeContract } from "viem/actions"; -import { Address, keccak256, parseEther, stringToHex } from "viem"; -import { privateKeyToAccount } from "viem/accounts"; -import { testClient } from "../../test/common"; -import { combineLatest, filter, firstValueFrom, map, scan, shareReplay } from "rxjs"; -import { waitForTransaction } from "./test/waitForTransaction"; -import { SubjectEvent, SubjectRecord } from "@latticexyz/query"; - -const henryAccount = privateKeyToAccount(keccak256(stringToHex("henry"))); - -describe.skip("subscribeToQuery", async () => { - let worldAddress: Address; - beforeAll(async () => { - await testClient.setBalance({ address: henryAccount.address, value: parseEther("1") }); - worldAddress = await deployMockGame(); - }); - - it("can get players with a position", async () => { - const { store, fetchLatestLogs } = await createHydratedStore(worldAddress); - - const { subjects$, subjectEvents$ } = subscribeToQuery(store, { - from: { - Position: ["player"], - }, - }); - - const latest$ = combineLatest({ - subjects$: subjects$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectRecord[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - subjectEvents$: subjectEvents$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectEvent[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - }).pipe(shareReplay(1)); - - expect(await firstValueFrom(latest$)).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 1, - "value": [ - { - "record": { - "fields": { - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - "x": 1, - "y": -1, - }, - "keyTuple": [ - "0x0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - ], - "primaryKey": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - { - "record": { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - { - "record": { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - { - "record": { - "fields": { - "player": "0xdBa86119a787422C593ceF119E40887f396024E2", - "x": 100, - "y": 100, - }, - "keyTuple": [ - "0x000000000000000000000000dba86119a787422c593cef119e40887f396024e2", - ], - "primaryKey": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - ], - }, - "subjects$": { - "count": 1, - "value": [ - { - "record": { - "fields": { - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - "x": 1, - "y": -1, - }, - "keyTuple": [ - "0x0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - ], - "primaryKey": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0xdBa86119a787422C593ceF119E40887f396024E2", - "x": 100, - "y": 100, - }, - "keyTuple": [ - "0x000000000000000000000000dba86119a787422c593cef119e40887f396024e2", - ], - "primaryKey": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "subjectSchema": [ - "address", - ], - }, - ], - }, - } - `); - - waitForTransaction( - await writeContract(testClient, { - account: henryAccount, - chain: null, - address: worldAddress, - abi: worldAbi, - functionName: "move", - args: [1, 2], - }), - ); - await fetchLatestLogs(); - - expect(await firstValueFrom(latest$)).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 2, - "value": [ - { - "record": { - "fields": { - "player": "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - "x": 1, - "y": 2, - }, - "keyTuple": [ - "0x0000000000000000000000005f2cc8fb10299751348e1b10f5f1ba47820b1cb8", - ], - "primaryKey": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - ], - }, - "subjects$": { - "count": 2, - "value": [ - { - "record": { - "fields": { - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - "x": 1, - "y": -1, - }, - "keyTuple": [ - "0x0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - ], - "primaryKey": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0xdBa86119a787422C593ceF119E40887f396024E2", - "x": 100, - "y": 100, - }, - "keyTuple": [ - "0x000000000000000000000000dba86119a787422c593cef119e40887f396024e2", - ], - "primaryKey": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - "x": 1, - "y": 2, - }, - "keyTuple": [ - "0x0000000000000000000000005f2cc8fb10299751348e1b10f5f1ba47820b1cb8", - ], - "primaryKey": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "subjectSchema": [ - "address", - ], - }, - ], - }, - } - `); - }); - - it("can get players at position (3, 5)", async () => { - const { store, fetchLatestLogs } = await createHydratedStore(worldAddress); - - const { subjects$, subjectEvents$ } = subscribeToQuery(store, { - from: { - Position: ["player"], - }, - where: [ - ["Position.x", "=", 3], - ["Position.y", "=", 5], - ], - }); - - const latest$ = combineLatest({ - subjects$: subjects$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectRecord[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - subjectEvents$: subjectEvents$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectEvent[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - }).pipe(shareReplay(1)); - - expect(await firstValueFrom(latest$)).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 1, - "value": [ - { - "record": { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - { - "record": { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - ], - }, - "subjects$": { - "count": 1, - "value": [ - { - "record": { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - ], - }, - } - `); - - waitForTransaction( - await writeContract(testClient, { - account: henryAccount, - chain: null, - address: worldAddress, - abi: worldAbi, - functionName: "move", - args: [3, 5], - }), - ); - await fetchLatestLogs(); - - expect(await firstValueFrom(latest$)).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 2, - "value": [ - { - "record": { - "fields": { - "player": "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x0000000000000000000000005f2cc8fb10299751348e1b10f5f1ba47820b1cb8", - ], - "primaryKey": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - ], - }, - "subjects$": { - "count": 2, - "value": [ - { - "record": { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x0000000000000000000000005f2cc8fb10299751348e1b10f5f1ba47820b1cb8", - ], - "primaryKey": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "subjectSchema": [ - "address", - ], - }, - ], - }, - } - `); - - waitForTransaction( - await writeContract(testClient, { - account: henryAccount, - chain: null, - address: worldAddress, - abi: worldAbi, - functionName: "move", - args: [2, 4], - }), - ); - await fetchLatestLogs(); - - expect(await firstValueFrom(latest$)).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 3, - "value": [ - { - "record": { - "fields": { - "player": "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x0000000000000000000000005f2cc8fb10299751348e1b10f5f1ba47820b1cb8", - ], - "primaryKey": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "subjectSchema": [ - "address", - ], - "type": "exit", - }, - ], - }, - "subjects$": { - "count": 3, - "value": [ - { - "record": { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - ], - }, - } - `); - }); - - it("can get players within the bounds of (-5, -5) and (5, 5)", async () => { - const { store, fetchLatestLogs } = await createHydratedStore(worldAddress); - - const { subjects$, subjectEvents$ } = subscribeToQuery(store, { - from: { - Position: ["player"], - }, - where: [ - ["Position.x", ">=", -5], - ["Position.x", "<=", 5], - ["Position.y", ">=", -5], - ["Position.y", "<=", 5], - ], - }); - - const latest$ = combineLatest({ - subjects$: subjects$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectRecord[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - subjectEvents$: subjectEvents$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectEvent[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - }).pipe(shareReplay(1)); - - expect(await firstValueFrom(latest$)).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 1, - "value": [ - { - "record": { - "fields": { - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - "x": 1, - "y": -1, - }, - "keyTuple": [ - "0x0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - ], - "primaryKey": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - { - "record": { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - { - "record": { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - ], - }, - "subjects$": { - "count": 1, - "value": [ - { - "record": { - "fields": { - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - "x": 1, - "y": -1, - }, - "keyTuple": [ - "0x0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - ], - "primaryKey": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - ], - }, - } - `); - - waitForTransaction( - await writeContract(testClient, { - account: henryAccount, - chain: null, - address: worldAddress, - abi: worldAbi, - functionName: "move", - args: [3, 5], - }), - ); - await fetchLatestLogs(); - - expect(await firstValueFrom(latest$)).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 2, - "value": [ - { - "record": { - "fields": { - "player": "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x0000000000000000000000005f2cc8fb10299751348e1b10f5f1ba47820b1cb8", - ], - "primaryKey": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - ], - }, - "subjects$": { - "count": 2, - "value": [ - { - "record": { - "fields": { - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - "x": 1, - "y": -1, - }, - "keyTuple": [ - "0x0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - ], - "primaryKey": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x0000000000000000000000005f2cc8fb10299751348e1b10f5f1ba47820b1cb8", - ], - "primaryKey": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "subjectSchema": [ - "address", - ], - }, - ], - }, - } - `); - - waitForTransaction( - await writeContract(testClient, { - account: henryAccount, - chain: null, - address: worldAddress, - abi: worldAbi, - functionName: "move", - args: [100, 100], - }), - ); - await fetchLatestLogs(); - - expect(await firstValueFrom(latest$)).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 3, - "value": [ - { - "record": { - "fields": { - "player": "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x0000000000000000000000005f2cc8fb10299751348e1b10f5f1ba47820b1cb8", - ], - "primaryKey": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "subjectSchema": [ - "address", - ], - "type": "exit", - }, - ], - }, - "subjects$": { - "count": 3, - "value": [ - { - "record": { - "fields": { - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - "x": 1, - "y": -1, - }, - "keyTuple": [ - "0x0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - ], - "primaryKey": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - ], - }, - } - `); - }); - - it("can get players that are still alive", async () => { - const { store } = await createHydratedStore(worldAddress); - - const { subjects$, subjectEvents$ } = subscribeToQuery(store, { - from: { - Position: ["player"], - Health: ["player"], - }, - where: [["Health.health", "!=", 0n]], - }); - - const latest$ = combineLatest({ - subjects$: subjects$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectRecord[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - subjectEvents$: subjectEvents$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectEvent[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - }).pipe(shareReplay(1)); - - expect(await firstValueFrom(latest$)).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 1, - "value": [ - { - "record": { - "fields": { - "health": 5n, - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - }, - "keyTuple": [ - "0x0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - ], - "primaryKey": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "tableId": "0x746200000000000000000000000000004865616c746800000000000000000000", - }, - "subject": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - { - "record": { - "fields": { - "health": 5n, - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x746200000000000000000000000000004865616c746800000000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - ], - }, - "subjects$": { - "count": 1, - "value": [ - { - "record": { - "fields": { - "health": 5n, - "player": "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - }, - "keyTuple": [ - "0x0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - ], - "primaryKey": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "tableId": "0x746200000000000000000000000000004865616c746800000000000000000000", - }, - "subject": [ - "0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "health": 5n, - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x746200000000000000000000000000004865616c746800000000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - ], - }, - } - `); - }); - - it("can get all players in grassland", async () => { - const { store } = await createHydratedStore(worldAddress); - - const { subjects$, subjectEvents$ } = subscribeToQuery(store, { - from: { - Terrain: ["x", "y"], - }, - where: [["Terrain.terrainType", "=", 2]], - }); - - const latest$ = combineLatest({ - subjects$: subjects$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectRecord[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - subjectEvents$: subjectEvents$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectEvent[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - }).pipe(shareReplay(1)); - - expect(await firstValueFrom(latest$)).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 1, - "value": [ - { - "record": { - "fields": { - "terrainType": 2, - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x0000000000000000000000000000000000000000000000000000000000000003", - "0x0000000000000000000000000000000000000000000000000000000000000005", - ], - "primaryKey": [ - 3, - 5, - ], - "tableId": "0x746200000000000000000000000000005465727261696e000000000000000000", - }, - "subject": [ - 3, - 5, - ], - "subjectSchema": [ - "int32", - "int32", - ], - "type": "enter", - }, - ], - }, - "subjects$": { - "count": 1, - "value": [ - { - "record": { - "fields": { - "terrainType": 2, - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x0000000000000000000000000000000000000000000000000000000000000003", - "0x0000000000000000000000000000000000000000000000000000000000000005", - ], - "primaryKey": [ - 3, - 5, - ], - "tableId": "0x746200000000000000000000000000005465727261696e000000000000000000", - }, - "subject": [ - 3, - 5, - ], - "subjectSchema": [ - "int32", - "int32", - ], - }, - ], - }, - } - `); - }); - - it("can get all players without health (e.g. spectator)", async () => { - const { store } = await createHydratedStore(worldAddress); - - const { subjects$, subjectEvents$ } = subscribeToQuery(store, { - from: { - Position: ["player"], - }, - except: { - Health: ["player"], - }, - }); - - const latest$ = combineLatest({ - subjects$: subjects$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectRecord[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - subjectEvents$: subjectEvents$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectEvent[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - }).pipe(shareReplay(1)); - - expect(await firstValueFrom(latest$)).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 1, - "value": [ - { - "record": { - "fields": { - "player": "0xdBa86119a787422C593ceF119E40887f396024E2", - "x": 100, - "y": 100, - }, - "keyTuple": [ - "0x000000000000000000000000dba86119a787422c593cef119e40887f396024e2", - ], - "primaryKey": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - ], - }, - "subjects$": { - "count": 1, - "value": [ - { - "record": { - "fields": { - "player": "0xdBa86119a787422C593ceF119E40887f396024E2", - "x": 100, - "y": 100, - }, - "keyTuple": [ - "0x000000000000000000000000dba86119a787422c593cef119e40887f396024e2", - ], - "primaryKey": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0xdBa86119a787422C593ceF119E40887f396024E2", - ], - "subjectSchema": [ - "address", - ], - }, - ], - }, - } - `); - }); - - it("emits new subjects when initial matching set is empty", async () => { - const { store, fetchLatestLogs } = await createHydratedStore(worldAddress); - - const { subjects$, subjectEvents$ } = subscribeToQuery(store, { - from: { - Position: ["player"], - }, - where: [ - ["Position.x", "=", 999], - ["Position.y", "=", 999], - ], - }); - - const latest$ = combineLatest({ - subjects$: subjects$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectRecord[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - subjectEvents$: subjectEvents$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectEvent[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - }).pipe(shareReplay(1)); - - expect(await firstValueFrom(latest$)).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 1, - "value": [], - }, - "subjects$": { - "count": 1, - "value": [], - }, - } - `); - - waitForTransaction( - await writeContract(testClient, { - account: henryAccount, - chain: null, - address: worldAddress, - abi: worldAbi, - functionName: "move", - args: [999, 999], - }), - ); - await fetchLatestLogs(); - - expect(await firstValueFrom(latest$)).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 2, - "value": [ - { - "record": { - "fields": { - "player": "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - "x": 999, - "y": 999, - }, - "keyTuple": [ - "0x0000000000000000000000005f2cc8fb10299751348e1b10f5f1ba47820b1cb8", - ], - "primaryKey": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - ], - }, - "subjects$": { - "count": 2, - "value": [ - { - "record": { - "fields": { - "player": "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - "x": 999, - "y": 999, - }, - "keyTuple": [ - "0x0000000000000000000000005f2cc8fb10299751348e1b10f5f1ba47820b1cb8", - ], - "primaryKey": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "subjectSchema": [ - "address", - ], - }, - ], - }, - } - `); - }); - - it("emits changed subjects when subscribing some time after initial query", async () => { - const { store, fetchLatestLogs } = await createHydratedStore(worldAddress); - - const { subjects, subjects$, subjectEvents$ } = subscribeToQuery(store, { - from: { - Position: ["player"], - }, - where: [ - ["Position.x", "=", 3], - ["Position.y", "=", 5], - ], - }); - - expect(await subjects).toMatchInlineSnapshot(` - [ - { - "records": [ - { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - ], - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "records": [ - { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - ], - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - ] - `); - - waitForTransaction( - await writeContract(testClient, { - account: henryAccount, - chain: null, - address: worldAddress, - abi: worldAbi, - functionName: "move", - args: [3, 5], - }), - ); - await fetchLatestLogs(); - - const latest$ = combineLatest({ - subjects$: subjects$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectRecord[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - subjectEvents$: subjectEvents$.pipe( - scan((values, value) => [...values, value], [] as readonly (readonly SubjectEvent[])[]), - map((values) => ({ count: values.length, value: values.at(-1) })), - ), - }).pipe(shareReplay(1)); - - // we expect two emissions for by this point: initial subjects + subjects changed since starting the subscriptions - expect( - await firstValueFrom( - latest$.pipe(filter((latest) => latest.subjects$.count === 2 && latest.subjectEvents$.count === 2)), - ), - ).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 2, - "value": [ - { - "record": { - "fields": { - "player": "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x0000000000000000000000005f2cc8fb10299751348e1b10f5f1ba47820b1cb8", - ], - "primaryKey": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "subjectSchema": [ - "address", - ], - "type": "enter", - }, - ], - }, - "subjects$": { - "count": 2, - "value": [ - { - "record": { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x0000000000000000000000005f2cc8fb10299751348e1b10f5f1ba47820b1cb8", - ], - "primaryKey": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "subjectSchema": [ - "address", - ], - }, - ], - }, - } - `); - - waitForTransaction( - await writeContract(testClient, { - account: henryAccount, - chain: null, - address: worldAddress, - abi: worldAbi, - functionName: "move", - args: [2, 4], - }), - ); - await fetchLatestLogs(); - - expect( - await firstValueFrom( - latest$.pipe(filter((latest) => latest.subjects$.count === 3 && latest.subjectEvents$.count === 3)), - ), - ).toMatchInlineSnapshot(` - { - "subjectEvents$": { - "count": 3, - "value": [ - { - "record": { - "fields": { - "player": "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x0000000000000000000000005f2cc8fb10299751348e1b10f5f1ba47820b1cb8", - ], - "primaryKey": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x5f2cC8fb10299751348e1b10f5F1Ba47820B1cB8", - ], - "subjectSchema": [ - "address", - ], - "type": "exit", - }, - ], - }, - "subjects$": { - "count": 3, - "value": [ - { - "record": { - "fields": { - "player": "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6", - ], - "primaryKey": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x328809Bc894f92807417D2dAD6b7C998c1aFdac6", - ], - "subjectSchema": [ - "address", - ], - }, - { - "record": { - "fields": { - "player": "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - "x": 3, - "y": 5, - }, - "keyTuple": [ - "0x000000000000000000000000078cf0753dd50f7c56f20b3ae02719ea199be2eb", - ], - "primaryKey": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "tableId": "0x74620000000000000000000000000000506f736974696f6e0000000000000000", - }, - "subject": [ - "0x078cf0753dd50f7C56F20B3Ae02719EA199BE2eb", - ], - "subjectSchema": [ - "address", - ], - }, - ], - }, - } - `); - }); -}); diff --git a/packages/store-sync/src/query-cache/subscribeToQuery.ts b/packages/store-sync/src/query-cache/subscribeToQuery.ts deleted file mode 100644 index 78b891894c..0000000000 --- a/packages/store-sync/src/query-cache/subscribeToQuery.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Observable, map, scan } from "rxjs"; -import { SubjectEvent, SubjectRecord, SubjectRecords } from "@latticexyz/query"; -import { findSubjects } from "@latticexyz/query/internal"; -import { Query } from "./common"; -import { QueryCacheStore, extractTables } from "./createStore"; -import { queryToWire } from "./queryToWire"; - -function getId({ subject, record }: SubjectRecord): string { - // TODO: memoize - return JSON.stringify([subject, record.primaryKey]); -} - -function flattenSubjectRecords(subjects: readonly SubjectRecords[]): readonly SubjectRecord[] { - return subjects.flatMap((subject) => - subject.records.map((record) => ({ - subject: subject.subject, - subjectSchema: subject.subjectSchema, - record, - })), - ); -} - -function subjectEvents(prev: readonly SubjectRecord[], next: readonly SubjectRecord[]): readonly SubjectEvent[] { - const prevSet = new Set(prev.map((record) => getId(record))); - const nextSet = new Set(next.map((record) => getId(record))); - - const enters = next.filter((record) => !prevSet.has(getId(record))); - const exits = prev.filter((record) => !nextSet.has(getId(record))); - const changes = next.filter((nextRecord) => { - const prevRecord = prev.find((record) => getId(record) === getId(nextRecord)); - // TODO: improve this so we're not dependent on field order - return prevRecord && JSON.stringify(prevRecord.record.fields) !== JSON.stringify(nextRecord.record.fields); - }); - - return [ - ...enters.map((record) => ({ ...record, type: "enter" as const })), - ...exits.map((record) => ({ ...record, type: "exit" as const })), - ...changes.map((record) => ({ ...record, type: "change" as const })), - ]; -} - -// TODO: decide if this whole thing is returned in a promise or just `subjects` -// TODO: return matching records alongside subjects? because the record subset may be smaller than what querying for records with matching subjects -// TODO: stronger types -export type SubscribeToQueryResult = { - /** - * Set of initial matching subjects for query. - */ - subjects: Promise; - /** - * Stream of matching subjects for query. - * First emission has the same data as `subjects`, flattened per record. - */ - subjects$: Observable; - /** - * Stream of subject changes for query. - * First emission will be an `enter` for each item in `subjects`, or an empty array if no matches. - * Each emission after that will only be the subjects that have changed (entered/exited the result set, or the underlying record changed). - */ - subjectEvents$: Observable; -}; - -export function subscribeToQuery>>( - store: store, - query: query, -): SubscribeToQueryResult { - const { tables, records: initialTableRecords } = store.getState(); - const wireQuery = queryToWire(tables, query); - const initialSubjects = findSubjects({ - records: Object.values(initialTableRecords), - query: wireQuery, - }); - - function createSubjectStream(): Observable { - return new Observable(function subscribe(subscriber) { - // return initial results immediately - const initialRecords = flattenSubjectRecords(initialSubjects); - subscriber.next(initialRecords); - - // if records have changed between query and subscription, reevaluate - const { records: tableRecords } = store.getState(); - if (tableRecords !== initialTableRecords) { - const nextSubjectRecords = flattenSubjectRecords( - findSubjects({ - records: Object.values(tableRecords), - query: wireQuery, - }), - ); - subscriber.next(nextSubjectRecords); - } - - // then listen for changes to records and reevaluate - const unsub = store.subscribe((state, prevState) => { - if (state.records !== prevState.records) { - const nextSubjectRecords = flattenSubjectRecords( - findSubjects({ - records: Object.values(state.records), - query: wireQuery, - }), - ); - subscriber.next(nextSubjectRecords); - } - }); - - return () => void unsub(); - }); - } - - const subjects$ = createSubjectStream(); - - const subjectEvents$ = createSubjectStream().pipe( - scan( - (acc, next) => ({ prev: acc.next, next }), - { prev: [], next: [] }, - ), - map(({ prev, next }) => subjectEvents(prev, next)), - ); - - return { - subjects: new Promise((resolve) => resolve(initialSubjects)), - subjects$, - subjectEvents$, - }; -} diff --git a/packages/store-sync/src/query-cache/syncToQueryCache.ts b/packages/store-sync/src/query-cache/syncToQueryCache.ts deleted file mode 100644 index 84f77f5c80..0000000000 --- a/packages/store-sync/src/query-cache/syncToQueryCache.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SyncOptions, SyncResult } from "../common"; -import { createStoreSync } from "../createStoreSync"; -import { Address } from "viem"; -import { Store } from "@latticexyz/store"; -import { createStore } from "./createStore"; -import { createStorageAdapter } from "./createStorageAdapter"; - -type SyncToQueryCacheOptions = Omit & { - // require address for now to keep the data model + retrieval simpler - address: Address; - config: config; - startSync?: boolean; -}; - -type SyncToQueryCacheResult = SyncResult & { - stopSync: () => void; -}; - -export async function syncToQueryCache({ - config, - startSync = true, - ...syncOptions -}: SyncToQueryCacheOptions): Promise { - const useStore = createStore({ tables: config.tables }); - const storageAdapter = createStorageAdapter({ store: useStore }); - - const storeSync = await createStoreSync({ - storageAdapter, - ...syncOptions, - // TODO: sync progress - }); - - const sub = startSync ? storeSync.storedBlockLogs$.subscribe() : null; - const stopSync = (): void => { - sub?.unsubscribe(); - }; - - return { - ...storeSync, - stopSync, - }; -} diff --git a/packages/store-sync/src/query-cache/test/createHydratedStore.ts b/packages/store-sync/src/query-cache/test/createHydratedStore.ts deleted file mode 100644 index 42e7621b51..0000000000 --- a/packages/store-sync/src/query-cache/test/createHydratedStore.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { storeEventsAbi } from "@latticexyz/store"; -import { configV2 as config } from "../../../test/mockGame"; -import { fetchAndStoreLogs } from "../../fetchAndStoreLogs"; -import { testClient } from "../../../test/common"; -import { Address } from "viem"; -import { getBlock, getBlockNumber } from "viem/actions"; -import { QueryCacheStore, createStore } from "../createStore"; -import { createStorageAdapter } from "../createStorageAdapter"; - -export { config }; - -export async function createHydratedStore(worldAddress: Address): Promise<{ - store: QueryCacheStore<(typeof config)["tables"]>; - fetchLatestLogs: () => Promise; -}> { - const store = createStore({ tables: config.tables }); - const storageAdapter = createStorageAdapter({ store }); - - let lastBlockProcessed = (await getBlock(testClient, { blockTag: "earliest" })).number - 1n; - async function fetchLatestLogs(): Promise { - const toBlock = await getBlockNumber(testClient); - if (toBlock > lastBlockProcessed) { - const fromBlock = lastBlockProcessed + 1n; - // console.log("fetching blocks", fromBlock, "to", toBlock); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for await (const block of fetchAndStoreLogs({ - storageAdapter, - publicClient: testClient, - address: worldAddress, - events: storeEventsAbi, - fromBlock, - toBlock, - })) { - // console.log("got block logs", block.blockNumber, block.logs.length); - } - lastBlockProcessed = toBlock; - } - return toBlock; - } - - await fetchLatestLogs(); - - return { store, fetchLatestLogs }; -} diff --git a/packages/store-sync/src/query-cache/test/minePending.ts b/packages/store-sync/src/query-cache/test/minePending.ts deleted file mode 100644 index eb0d1524ee..0000000000 --- a/packages/store-sync/src/query-cache/test/minePending.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { testClient } from "../../../test/common"; - -export async function minePending(): Promise { - const content = await testClient.getTxpoolContent(); - if (!Object.keys(content.pending).length) return; - - await testClient.mine({ blocks: 1 }); - await minePending(); -} diff --git a/packages/store-sync/src/query-cache/test/waitForTransaction.ts b/packages/store-sync/src/query-cache/test/waitForTransaction.ts deleted file mode 100644 index 8f5de8e9b6..0000000000 --- a/packages/store-sync/src/query-cache/test/waitForTransaction.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Hex } from "viem"; -import { getTransactionReceipt } from "viem/actions"; -import { testClient } from "../../../test/common"; -import { minePending } from "./minePending"; - -export async function waitForTransaction(hash: Hex): Promise { - await minePending(); - const receipt = await getTransactionReceipt(testClient, { hash }); - if (receipt.status === "reverted") { - // TODO: better error - throw new Error(`Transaction reverted (${hash})`); - } -}