From f8d4fcf8396a9c9e679740e66d16619305899c5d Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 20 Nov 2024 13:33:48 +0800 Subject: [PATCH 1/8] pull out stash changes from entrykit --- packages/stash/package.json | 15 +-- packages/stash/src/actions/applyUpdates.ts | 82 ++++++++++++ .../stash/src/actions/deleteRecord.test.ts | 12 +- packages/stash/src/actions/deleteRecord.ts | 26 +--- packages/stash/src/actions/getTable.test.ts | 6 + packages/stash/src/actions/runQuery.test.ts | 34 +---- packages/stash/src/actions/setRecord.test.ts | 5 +- packages/stash/src/actions/setRecord.ts | 13 +- packages/stash/src/actions/setRecords.ts | 49 ++------ .../stash/src/actions/subscribeQuery.test.ts | 77 +++++------- .../stash/src/actions/subscribeStash.test.ts | 102 ++++++++++++++- .../stash/src/actions/subscribeTable.test.ts | 6 + packages/stash/src/createStash.test.ts | 19 +++ .../src/decorators/defaultActions.test.ts | 11 +- packages/stash/src/exports/internal.ts | 20 ++- packages/stash/src/react/useStash.test.ts | 119 +++++++++++++++++- packages/stash/src/react/useStash.ts | 2 +- 17 files changed, 424 insertions(+), 174 deletions(-) create mode 100644 packages/stash/src/actions/applyUpdates.ts diff --git a/packages/stash/package.json b/packages/stash/package.json index 4df9785283..619fa048dc 100644 --- a/packages/stash/package.json +++ b/packages/stash/package.json @@ -39,13 +39,12 @@ "test:ci": "pnpm run test" }, "dependencies": { - "@ark/util": "catalog:", + "@ark/util": "0.2.2", "@latticexyz/common": "workspace:*", "@latticexyz/config": "workspace:*", "@latticexyz/protocol-parser": "workspace:*", "@latticexyz/schema-type": "workspace:*", - "@latticexyz/store": "workspace:*", - "viem": "catalog:" + "@latticexyz/store": "workspace:*" }, "devDependencies": { "@testing-library/react": "^16.0.0", @@ -54,12 +53,14 @@ "eslint-plugin-react": "7.31.11", "eslint-plugin-react-hooks": "4.6.0", "fast-deep-equal": "^3.1.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "tsup": "^6.7.0" + "react": "18.2.0", + "react-dom": "18.2.0", + "tsup": "^6.7.0", + "viem": "2.21.19" }, "peerDependencies": { - "react": "18.x" + "react": "18.x", + "viem": "2.x" }, "publishConfig": { "access": "public" diff --git a/packages/stash/src/actions/applyUpdates.ts b/packages/stash/src/actions/applyUpdates.ts new file mode 100644 index 0000000000..6fa2a5a94b --- /dev/null +++ b/packages/stash/src/actions/applyUpdates.ts @@ -0,0 +1,82 @@ +import { schemaAbiTypeToDefaultValue } from "@latticexyz/schema-type/internal"; +import { Key, Stash, StoreUpdates, TableRecord } from "../common"; +import { encodeKey } from "./encodeKey"; +import { Table } from "@latticexyz/config"; +import { registerTable } from "./registerTable"; + +export type StashUpdate = { + table: table; + key: Key
; + value: undefined | Partial>; +}; + +export type ApplyUpdatesArgs = { + stash: Stash; + updates: StashUpdate[]; +}; + +const pendingUpdates = new Map(); + +export function applyUpdates({ stash, updates }: ApplyUpdatesArgs): void { + const storeUpdates = pendingUpdates.get(stash) ?? { config: {}, records: {} }; + if (!pendingUpdates.has(stash)) pendingUpdates.set(stash, storeUpdates); + + for (const { table, key, value } of updates) { + if (stash.get().config[table.namespaceLabel]?.[table.label] == null) { + registerTable({ stash, table }); + } + const tableState = ((stash._.state.records[table.namespaceLabel] ??= {})[table.label] ??= {}); + const encodedKey = encodeKey({ table, key }); + const prevRecord = tableState[encodedKey]; + // create new record, preserving field order + const nextRecord = + value == null + ? undefined + : Object.fromEntries( + Object.entries(table.schema).map(([fieldName, { type }]) => [ + fieldName, + key[fieldName] ?? // Use provided key fields + value[fieldName] ?? // Or provided value fields + prevRecord?.[fieldName] ?? // Keep existing non-overridden fields + schemaAbiTypeToDefaultValue[type], // Default values for new fields + ]), + ); + + // apply update to state + if (nextRecord != null) { + tableState[encodedKey] = nextRecord; + } else { + delete tableState[encodedKey]; + } + + // add update to pending updates for notifying subscribers + const prevUpdate = storeUpdates.records[table.namespaceLabel]?.[table.label]?.[encodedKey]; + const update = { + // preserve the initial prev state if we already have a pending update + // TODO: change subscribers to an array of updates instead of an object + prev: prevUpdate ? prevUpdate.prev : prevRecord, + current: nextRecord, + }; + ((storeUpdates.records[table.namespaceLabel] ??= {})[table.label] ??= {})[encodedKey] ??= update; + } + + // queueMicrotask(() => { + notifySubscribers(stash); + // }); +} + +function notifySubscribers(stash: Stash) { + const storeUpdates = pendingUpdates.get(stash); + if (!storeUpdates) return; + + // Notify table subscribers + for (const [namespaceLabel, tableUpdates] of Object.entries(storeUpdates.records)) { + for (const [label, updates] of Object.entries(tableUpdates)) { + stash._.tableSubscribers[namespaceLabel]?.[label]?.forEach((subscriber) => subscriber(updates)); + } + } + // Notify stash subscribers + stash._.storeSubscribers.forEach((subscriber) => subscriber(storeUpdates)); + + pendingUpdates.delete(stash); +} diff --git a/packages/stash/src/actions/deleteRecord.test.ts b/packages/stash/src/actions/deleteRecord.test.ts index b90a07a617..9298accd1c 100644 --- a/packages/stash/src/actions/deleteRecord.test.ts +++ b/packages/stash/src/actions/deleteRecord.test.ts @@ -45,17 +45,7 @@ describe("deleteRecord", () => { key: { field2: 1, field3: 2 }, }); - attest(stash.get().records).snap({ - namespace1: { - table1: { - "3|1": { - field1: "world", - field2: 3, - field3: 1, - }, - }, - }, - }); + attest(stash.get().records).snap({ namespace1: { table1: { "3|1": { field1: "world", field2: 3, field3: 1 } } } }); }); it("should throw a type error if an invalid key is provided", () => { diff --git a/packages/stash/src/actions/deleteRecord.ts b/packages/stash/src/actions/deleteRecord.ts index df8125d213..5e0b75230a 100644 --- a/packages/stash/src/actions/deleteRecord.ts +++ b/packages/stash/src/actions/deleteRecord.ts @@ -1,7 +1,6 @@ import { Table } from "@latticexyz/config"; import { Key, Stash } from "../common"; -import { encodeKey } from "./encodeKey"; -import { registerTable } from "./registerTable"; +import { applyUpdates } from "./applyUpdates"; export type DeleteRecordArgs
= { stash: Stash; @@ -12,26 +11,5 @@ export type DeleteRecordArgs
= { export type DeleteRecordResult = void; export function deleteRecord
({ stash, table, key }: DeleteRecordArgs
): DeleteRecordResult { - const { namespaceLabel, label } = table; - - if (stash.get().config[namespaceLabel] == null) { - registerTable({ stash, table }); - } - - const encodedKey = encodeKey({ table, key }); - const prevRecord = stash.get().records[namespaceLabel]?.[label]?.[encodedKey]; - - // Early return if this record doesn't exist - if (prevRecord == null) return; - - // Delete record - delete stash._.state.records[namespaceLabel]?.[label]?.[encodedKey]; - - // Notify table subscribers - const updates = { [encodedKey]: { prev: prevRecord && { ...prevRecord }, current: undefined } }; - stash._.tableSubscribers[namespaceLabel]?.[label]?.forEach((subscriber) => subscriber(updates)); - - // Notify stash subscribers - const storeUpdate = { config: {}, records: { [namespaceLabel]: { [label]: updates } } }; - stash._.storeSubscribers.forEach((subscriber) => subscriber(storeUpdate)); + applyUpdates({ stash, updates: [{ table, key, value: undefined }] }); } diff --git a/packages/stash/src/actions/getTable.test.ts b/packages/stash/src/actions/getTable.test.ts index 31002e8f9e..319e4bc367 100644 --- a/packages/stash/src/actions/getTable.test.ts +++ b/packages/stash/src/actions/getTable.test.ts @@ -297,6 +297,8 @@ describe("getTable", () => { describe("subscribe", () => { it("should notify subscriber of table change", () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + const config1 = defineTable({ label: "table1", schema: { a: "address", b: "uint256", c: "uint32" }, @@ -315,6 +317,7 @@ describe("getTable", () => { table1.subscribe({ subscriber }); table1.setRecord({ key: { a: "0x00" }, value: { b: 1n, c: 2 } }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenCalledTimes(1); expect(subscriber).toHaveBeenNthCalledWith(1, { @@ -326,9 +329,12 @@ describe("getTable", () => { // Expect unrelated updates to not notify subscribers table2.setRecord({ key: { a: "0x01" }, value: { b: 1n, c: 2 } }); + vi.advanceTimersToNextTimer(); + expect(subscriber).toHaveBeenCalledTimes(1); table1.setRecord({ key: { a: "0x00" }, value: { b: 1n, c: 3 } }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenCalledTimes(2); expect(subscriber).toHaveBeenNthCalledWith(2, { diff --git a/packages/stash/src/actions/runQuery.test.ts b/packages/stash/src/actions/runQuery.test.ts index 91c2149b93..26ec1bf40b 100644 --- a/packages/stash/src/actions/runQuery.test.ts +++ b/packages/stash/src/actions/runQuery.test.ts @@ -70,12 +70,7 @@ describe("runQuery", () => { it("should return all keys that are in the Position and Health table", () => { const result = runQuery({ stash, query: [In(Position), In(Health)] }); - attest(result).snap({ - keys: { - "0x3": { player: "0x3" }, - "0x4": { player: "0x4" }, - }, - }); + attest(result).snap({ keys: { "0x3": { player: "0x3" }, "0x4": { player: "0x4" } } }); }); it("should return all keys that have Position.x = 4 and are included in Health", () => { @@ -85,13 +80,7 @@ describe("runQuery", () => { it("should return all keys that are in Position but not Health", () => { const result = runQuery({ stash, query: [In(Position), Not(In(Health))] }); - attest(result).snap({ - keys: { - "0x0": { player: "0x0" }, - "0x1": { player: "0x1" }, - "0x2": { player: "0x2" }, - }, - }); + attest(result).snap({ keys: { "0x0": { player: "0x0" }, "0x1": { player: "0x1" }, "0x2": { player: "0x2" } } }); }); it("should return all keys that don't include a gold item in the Inventory table", () => { @@ -116,23 +105,10 @@ describe("runQuery", () => { it("should include all matching records from the tables if includeRecords is set", () => { const result = runQuery({ stash, query: [In(Position), In(Health)], options: { includeRecords: true } }); attest(result).snap({ - keys: { - "0x3": { player: "0x3" }, - "0x4": { player: "0x4" }, - }, + keys: { "0x3": { player: "0x3" }, "0x4": { player: "0x4" } }, records: { - namespace1: { - Position: { - "0x3": { player: "0x3", x: 3, y: 2 }, - "0x4": { player: "0x4", x: 4, y: 1 }, - }, - }, - namespace2: { - Health: { - "0x3": { player: "0x3", health: 3 }, - "0x4": { player: "0x4", health: 4 }, - }, - }, + namespace1: { Position: { "0x3": { player: "0x3", x: 3, y: 2 }, "0x4": { player: "0x4", x: 4, y: 1 } } }, + namespace2: { Health: { "0x3": { player: "0x3", health: 3 }, "0x4": { player: "0x4", health: 4 } } }, }, }); }); diff --git a/packages/stash/src/actions/setRecord.test.ts b/packages/stash/src/actions/setRecord.test.ts index 420f8c5d42..ba28caa897 100644 --- a/packages/stash/src/actions/setRecord.test.ts +++ b/packages/stash/src/actions/setRecord.test.ts @@ -39,10 +39,7 @@ describe("setRecord", () => { attest(stash.get().records).snap({ namespace1: { - table1: { - "1|2": { field1: "hello", field2: 1, field3: 2 }, - "2|1": { field1: "world", field2: 2, field3: 1 }, - }, + table1: { "1|2": { field1: "hello", field2: 1, field3: 2 }, "2|1": { field1: "world", field2: 2, field3: 1 } }, }, }); }); diff --git a/packages/stash/src/actions/setRecord.ts b/packages/stash/src/actions/setRecord.ts index 12df6ce23c..281060a54e 100644 --- a/packages/stash/src/actions/setRecord.ts +++ b/packages/stash/src/actions/setRecord.ts @@ -1,6 +1,6 @@ -import { Key, TableRecord, Stash } from "../common"; -import { setRecords } from "./setRecords"; import { Table } from "@latticexyz/config"; +import { Key, TableRecord, Stash } from "../common"; +import { applyUpdates } from "./applyUpdates"; export type SetRecordArgs
= { stash: Stash; @@ -12,12 +12,5 @@ export type SetRecordArgs
= { export type SetRecordResult = void; export function setRecord
({ stash, table, key, value }: SetRecordArgs
): SetRecordResult { - setRecords({ - stash, - table, - records: [ - // Stored record should include key - { ...value, ...key }, - ], - }); + applyUpdates({ stash, updates: [{ table, key, value }] }); } diff --git a/packages/stash/src/actions/setRecords.ts b/packages/stash/src/actions/setRecords.ts index 110f8332a9..7604027f8b 100644 --- a/packages/stash/src/actions/setRecords.ts +++ b/packages/stash/src/actions/setRecords.ts @@ -1,8 +1,7 @@ -import { dynamicAbiTypeToDefaultValue, staticAbiTypeToDefaultValue } from "@latticexyz/schema-type/internal"; -import { Stash, TableRecord, TableUpdates } from "../common"; -import { encodeKey } from "./encodeKey"; import { Table } from "@latticexyz/config"; -import { registerTable } from "./registerTable"; +import { getKey, getValue } from "@latticexyz/protocol-parser/internal"; +import { Stash, TableRecord } from "../common"; +import { applyUpdates } from "./applyUpdates"; export type SetRecordsArgs
= { stash: Stash; @@ -13,38 +12,12 @@ export type SetRecordsArgs
= { export type SetRecordsResult = void; export function setRecords
({ stash, table, records }: SetRecordsArgs
): SetRecordsResult { - const { namespaceLabel, label, schema } = table; - - if (stash.get().config[namespaceLabel]?.[label] == null) { - registerTable({ stash, table }); - } - - // Construct table updates - const updates: TableUpdates = {}; - for (const record of records) { - const encodedKey = encodeKey({ table, key: record as never }); - const prevRecord = stash.get().records[namespaceLabel]?.[label]?.[encodedKey]; - const newRecord = Object.fromEntries( - Object.keys(schema).map((fieldName) => [ - fieldName, - record[fieldName] ?? // Override provided record fields - prevRecord?.[fieldName] ?? // Keep existing non-overridden fields - staticAbiTypeToDefaultValue[schema[fieldName] as never] ?? // Default values for new fields - dynamicAbiTypeToDefaultValue[schema[fieldName] as never], - ]), - ); - updates[encodedKey] = { prev: prevRecord, current: newRecord }; - } - - // Update records - for (const [encodedKey, { current }] of Object.entries(updates)) { - ((stash._.state.records[namespaceLabel] ??= {})[label] ??= {})[encodedKey] = current as never; - } - - // Notify table subscribers - stash._.tableSubscribers[namespaceLabel]?.[label]?.forEach((subscriber) => subscriber(updates)); - - // Notify stash subscribers - const storeUpdate = { config: {}, records: { [namespaceLabel]: { [label]: updates } } }; - stash._.storeSubscribers.forEach((subscriber) => subscriber(storeUpdate)); + applyUpdates({ + stash, + updates: Object.values(records).map((record) => ({ + table, + key: getKey(table, record), + value: getValue(table, record), + })), + }); } diff --git a/packages/stash/src/actions/subscribeQuery.test.ts b/packages/stash/src/actions/subscribeQuery.test.ts index cf50949889..30a37769cc 100644 --- a/packages/stash/src/actions/subscribeQuery.test.ts +++ b/packages/stash/src/actions/subscribeQuery.test.ts @@ -49,38 +49,32 @@ describe("defineQuery", () => { it("should return the matching keys and keep it updated", () => { const result = subscribeQuery({ stash, query: [In(Position), In(Health)] }); - attest(result.keys).snap({ - "0x3": { player: "0x3" }, - "0x4": { player: "0x4" }, - }); + attest(result.keys).snap({ "0x3": { player: "0x3" }, "0x4": { player: "0x4" } }); setRecord({ stash, table: Health, key: { player: `0x2` }, value: { health: 2 } }); - attest(result.keys).snap({ - "0x2": { player: "0x2" }, - "0x3": { player: "0x3" }, - "0x4": { player: "0x4" }, - }); + attest(result.keys).snap({ "0x3": { player: "0x3" }, "0x4": { player: "0x4" }, "0x2": { player: "0x2" } }); }); it("should notify subscribers when a matching key is updated", () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + let lastUpdate: unknown; const subscriber = vi.fn((update: QueryUpdate) => (lastUpdate = update)); const result = subscribeQuery({ stash, query: [Matches(Position, { x: 4 }), In(Health)] }); result.subscribe(subscriber); + vi.advanceTimersToNextTimer(); + expect(subscriber).toBeCalledTimes(0); + setRecord({ stash, table: Position, key: { player: "0x4" }, value: { y: 2 } }); + vi.advanceTimersToNextTimer(); expect(subscriber).toBeCalledTimes(1); attest(lastUpdate).snap({ records: { namespace1: { - Position: { - "0x4": { - prev: { player: "0x4", x: 4, y: 1 }, - current: { player: "0x4", x: 4, y: 2 }, - }, - }, + Position: { "0x4": { prev: { player: "0x4", x: 4, y: 1 }, current: { player: "0x4", x: 4, y: 2 } } }, }, }, keys: { "0x4": { player: "0x4" } }, @@ -89,75 +83,68 @@ describe("defineQuery", () => { }); it("should notify subscribers when a new key matches", () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + let lastUpdate: unknown; const subscriber = vi.fn((update: QueryUpdate) => (lastUpdate = update)); const result = subscribeQuery({ stash, query: [In(Position), In(Health)] }); result.subscribe(subscriber); + vi.advanceTimersToNextTimer(); + expect(subscriber).toBeCalledTimes(2); + setRecord({ stash, table: Health, key: { player: `0x2` }, value: { health: 2 } }); + vi.advanceTimersToNextTimer(); - expect(subscriber).toBeCalledTimes(1); + expect(subscriber).toBeCalledTimes(3); attest(lastUpdate).snap({ - records: { - namespace1: { - Health: { - "0x2": { - prev: undefined, - current: { player: `0x2`, health: 2 }, - }, - }, - }, - }, + records: { namespace1: { Health: { "0x2": { prev: "(undefined)", current: { player: "0x2", health: 2 } } } } }, keys: { "0x2": { player: "0x2" } }, types: { "0x2": "enter" }, }); }); it("should notify subscribers when a key doesn't match anymore", () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + let lastUpdate: unknown; const subscriber = vi.fn((update: QueryUpdate) => (lastUpdate = update)); const result = subscribeQuery({ stash, query: [In(Position), In(Health)] }); result.subscribe(subscriber); + vi.advanceTimersToNextTimer(); + expect(subscriber).toBeCalledTimes(2); + deleteRecord({ stash, table: Position, key: { player: `0x3` } }); + vi.advanceTimersToNextTimer(); - expect(subscriber).toBeCalledTimes(1); + expect(subscriber).toBeCalledTimes(3); attest(lastUpdate).snap({ - records: { - namespace1: { - Position: { - "0x3": { - prev: { player: "0x3", x: 3, y: 2 }, - current: undefined, - }, - }, - }, - }, + records: { namespace1: { Position: { "0x3": { prev: { player: "0x3", x: 3, y: 2 }, current: "(undefined)" } } } }, keys: { "0x3": { player: "0x3" } }, types: { "0x3": "exit" }, }); }); it("should notify initial subscribers with initial query result", () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + let lastUpdate: unknown; const subscriber = vi.fn((update: QueryUpdate) => (lastUpdate = update)); subscribeQuery({ stash, query: [In(Position), In(Health)], options: { initialSubscribers: [subscriber] } }); expect(subscriber).toBeCalledTimes(1); attest(lastUpdate).snap({ - keys: { - "0x3": { player: "0x3" }, - "0x4": { player: "0x4" }, - }, + keys: { "0x3": { player: "0x3" }, "0x4": { player: "0x4" } }, records: { namespace1: { Position: { - "0x3": { prev: undefined, current: { player: "0x3", x: 3, y: 2 } }, - "0x4": { prev: undefined, current: { player: "0x4", x: 4, y: 1 } }, + "0x3": { prev: "(undefined)", current: { player: "0x3", x: 3, y: 2 } }, + "0x4": { prev: "(undefined)", current: { player: "0x4", x: 4, y: 1 } }, }, Health: { - "0x3": { prev: undefined, current: { player: "0x3", health: 3 } }, - "0x4": { prev: undefined, current: { player: "0x4", health: 4 } }, + "0x3": { prev: "(undefined)", current: { player: "0x3", health: 3 } }, + "0x4": { prev: "(undefined)", current: { player: "0x4", health: 4 } }, }, }, }, diff --git a/packages/stash/src/actions/subscribeStash.test.ts b/packages/stash/src/actions/subscribeStash.test.ts index 887ca1dbc6..51d1f93158 100644 --- a/packages/stash/src/actions/subscribeStash.test.ts +++ b/packages/stash/src/actions/subscribeStash.test.ts @@ -3,9 +3,12 @@ import { describe, expect, it, vi } from "vitest"; import { createStash } from "../createStash"; import { subscribeStash } from "./subscribeStash"; import { setRecord } from "./setRecord"; +import { deleteRecord } from "./deleteRecord"; describe("subscribeStash", () => { - it("should notify subscriber of any stash change", () => { + it("should notify subscriber of any stash change", async () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + const config = defineStore({ namespaces: { namespace1: { @@ -33,6 +36,7 @@ describe("subscribeStash", () => { subscribeStash({ stash, subscriber }); setRecord({ stash, table: config.tables.namespace1__table1, key: { a: "0x00" }, value: { b: 1n, c: 2 } }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenCalledTimes(1); expect(subscriber).toHaveBeenNthCalledWith(1, { @@ -50,6 +54,7 @@ describe("subscribeStash", () => { }); setRecord({ stash, table: config.tables.namespace2__table2, key: { a: "0x01" }, value: { b: 1n, c: 2 } }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenCalledTimes(2); expect(subscriber).toHaveBeenNthCalledWith(2, { @@ -67,6 +72,7 @@ describe("subscribeStash", () => { }); setRecord({ stash, table: config.tables.namespace2__table2, key: { a: "0x01" }, value: { b: 1n, c: 3 } }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenCalledTimes(3); expect(subscriber).toHaveBeenNthCalledWith(3, { @@ -82,5 +88,99 @@ describe("subscribeStash", () => { }, }, }); + + deleteRecord({ stash, table: config.tables.namespace2__table2, key: { a: "0x01" } }); + vi.advanceTimersToNextTimer(); + + expect(subscriber).toHaveBeenCalledTimes(4); + expect(subscriber).toHaveBeenNthCalledWith(4, { + config: {}, + records: { + namespace2: { + table2: { + "0x01": { + prev: { a: "0x01", b: 1n, c: 3 }, + current: undefined, + }, + }, + }, + }, + }); + }); + + it("should notify subscriber of singleton table changes", () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + + const config = defineStore({ + namespaces: { + app: { + tables: { + config: { + schema: { enabled: "bool" }, + key: [], + }, + }, + }, + }, + }); + + const stash = createStash(config); + const subscriber = vi.fn(); + + subscribeStash({ stash, subscriber }); + + setRecord({ stash, table: config.tables.app__config, key: {}, value: { enabled: true } }); + vi.advanceTimersToNextTimer(); + + expect(subscriber).toHaveBeenCalledTimes(1); + expect(subscriber).toHaveBeenNthCalledWith(1, { + config: {}, + records: { + app: { + config: { + "": { + prev: undefined, + current: { enabled: true }, + }, + }, + }, + }, + }); + + setRecord({ stash, table: config.tables.app__config, key: {}, value: { enabled: false } }); + vi.advanceTimersToNextTimer(); + + expect(subscriber).toHaveBeenCalledTimes(2); + expect(subscriber).toHaveBeenNthCalledWith(2, { + config: {}, + records: { + app: { + config: { + "": { + prev: { enabled: true }, + current: { enabled: false }, + }, + }, + }, + }, + }); + + deleteRecord({ stash, table: config.tables.app__config, key: {} }); + vi.advanceTimersToNextTimer(); + + expect(subscriber).toHaveBeenCalledTimes(3); + expect(subscriber).toHaveBeenNthCalledWith(3, { + config: {}, + records: { + app: { + config: { + "": { + prev: { enabled: false }, + current: undefined, + }, + }, + }, + }, + }); }); }); diff --git a/packages/stash/src/actions/subscribeTable.test.ts b/packages/stash/src/actions/subscribeTable.test.ts index a1e6ab1ca4..1495f67d09 100644 --- a/packages/stash/src/actions/subscribeTable.test.ts +++ b/packages/stash/src/actions/subscribeTable.test.ts @@ -6,6 +6,8 @@ import { setRecord } from "./setRecord"; describe("subscribeTable", () => { it("should notify subscriber of table change", () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + const config = defineStore({ namespaces: { namespace1: { @@ -35,6 +37,7 @@ describe("subscribeTable", () => { subscribeTable({ stash, table: table1, subscriber }); setRecord({ stash, table: table1, key: { a: "0x00" }, value: { b: 1n, c: 2 } }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenCalledTimes(1); expect(subscriber).toHaveBeenNthCalledWith(1, { @@ -46,9 +49,12 @@ describe("subscribeTable", () => { // Expect unrelated updates to not notify subscribers setRecord({ stash, table: table2, key: { a: "0x01" }, value: { b: 1n, c: 2 } }); + vi.advanceTimersToNextTimer(); + expect(subscriber).toHaveBeenCalledTimes(1); setRecord({ stash, table: table1, key: { a: "0x00" }, value: { b: 1n, c: 3 } }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenCalledTimes(2); expect(subscriber).toHaveBeenNthCalledWith(2, { diff --git a/packages/stash/src/createStash.test.ts b/packages/stash/src/createStash.test.ts index e92c361839..5ee7ae9b4f 100644 --- a/packages/stash/src/createStash.test.ts +++ b/packages/stash/src/createStash.test.ts @@ -95,6 +95,8 @@ describe("createStash", () => { describe("subscribeTable", () => { it("should notify listeners on table updates", () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + const config = defineStore({ namespace: "namespace1", tables: { @@ -124,6 +126,7 @@ describe("createStash", () => { key: { field2: 1, field3: 2 }, value: { field1: "hello" }, }); + vi.advanceTimersToNextTimer(); expect(listener).toHaveBeenNthCalledWith(1, { "1|2": { @@ -137,6 +140,7 @@ describe("createStash", () => { key: { field2: 1, field3: 2 }, value: { field1: "world" }, }); + vi.advanceTimersToNextTimer(); expect(listener).toHaveBeenNthCalledWith(2, { "1|2": { @@ -149,6 +153,7 @@ describe("createStash", () => { table, key: { field2: 1, field3: 2 }, }); + vi.advanceTimersToNextTimer(); expect(listener).toHaveBeenNthCalledWith(3, { "1|2": { @@ -159,6 +164,8 @@ describe("createStash", () => { }); it("should not notify listeners after they have been removed", () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + const config = defineStore({ namespace: "namespace1", tables: { @@ -188,6 +195,7 @@ describe("createStash", () => { key: { field2: 1, field3: 2 }, value: { field1: "hello" }, }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenNthCalledWith(1, { "1|2": { @@ -203,6 +211,7 @@ describe("createStash", () => { key: { field2: 1, field3: 2 }, value: { field1: "world" }, }); + vi.advanceTimersToNextTimer(); expect(subscriber).toBeCalledTimes(1); }); @@ -210,6 +219,8 @@ describe("createStash", () => { describe("subscribeStash", () => { it("should notify listeners on stash updates", () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + const config = defineStore({ namespace: "namespace1", tables: { @@ -236,6 +247,7 @@ describe("createStash", () => { key: { field2: 1, field3: 2 }, value: { field1: "hello" }, }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenNthCalledWith(1, { config: {}, @@ -256,6 +268,7 @@ describe("createStash", () => { key: { field2: 1, field3: 2 }, value: { field1: "world" }, }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenNthCalledWith(2, { config: {}, @@ -275,6 +288,7 @@ describe("createStash", () => { table, key: { field2: 1, field3: 2 }, }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenNthCalledWith(3, { config: {}, @@ -298,6 +312,7 @@ describe("createStash", () => { key: ["field1"], }), }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenNthCalledWith(4, { config: { @@ -331,6 +346,8 @@ describe("createStash", () => { }); it("should not notify listeners after they have been removed", () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + const config = defineStore({ namespace: "namespace1", tables: { @@ -359,6 +376,7 @@ describe("createStash", () => { key: { field2: 1, field3: 2 }, value: { field1: "hello" }, }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenNthCalledWith(1, { config: {}, @@ -381,6 +399,7 @@ describe("createStash", () => { key: { field2: 1, field3: 2 }, value: { field1: "world" }, }); + vi.advanceTimersToNextTimer(); expect(subscriber).toBeCalledTimes(1); }); diff --git a/packages/stash/src/decorators/defaultActions.test.ts b/packages/stash/src/decorators/defaultActions.test.ts index e3a128c612..8961767515 100644 --- a/packages/stash/src/decorators/defaultActions.test.ts +++ b/packages/stash/src/decorators/defaultActions.test.ts @@ -5,8 +5,8 @@ import { createStash } from "../createStash"; import { defineTable } from "@latticexyz/store/internal"; import { In } from "../queryFragments"; import { Hex } from "viem"; -import { runQuery } from "../actions"; import { StoreRecords, getQueryConfig } from "../common"; +import { runQuery } from "../actions/runQuery"; describe("stash with default actions", () => { describe("decodeKey", () => { @@ -462,6 +462,8 @@ describe("stash with default actions", () => { describe("subscribeStash", () => { it("should notify subscriber of any stash change", () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + const config = defineStore({ namespaces: { namespace1: { @@ -481,6 +483,7 @@ describe("stash with default actions", () => { stash.subscribeStash({ subscriber }); stash.setRecord({ table: config.tables.namespace1__table1, key: { a: "0x00" }, value: { b: 1n, c: 2 } }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenCalledTimes(1); expect(subscriber).toHaveBeenNthCalledWith(1, { @@ -501,6 +504,8 @@ describe("stash with default actions", () => { describe("subscribeTable", () => { it("should notify subscriber of table change", () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + const config = defineStore({ namespaces: { namespace1: { @@ -530,6 +535,7 @@ describe("stash with default actions", () => { stash.subscribeTable({ table: table1, subscriber }); stash.setRecord({ table: table1, key: { a: "0x00" }, value: { b: 1n, c: 2 } }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenCalledTimes(1); expect(subscriber).toHaveBeenNthCalledWith(1, { @@ -541,9 +547,12 @@ describe("stash with default actions", () => { // Expect unrelated updates to not notify subscribers stash.setRecord({ table: table2, key: { a: "0x01" }, value: { b: 1n, c: 2 } }); + vi.advanceTimersToNextTimer(); + expect(subscriber).toHaveBeenCalledTimes(1); stash.setRecord({ table: table1, key: { a: "0x00" }, value: { b: 1n, c: 3 } }); + vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenCalledTimes(2); expect(subscriber).toHaveBeenNthCalledWith(2, { diff --git a/packages/stash/src/exports/internal.ts b/packages/stash/src/exports/internal.ts index 37b08e10f6..15905683b1 100644 --- a/packages/stash/src/exports/internal.ts +++ b/packages/stash/src/exports/internal.ts @@ -1,4 +1,22 @@ export * from "../createStash"; export * from "../common"; export * from "../queryFragments"; -export * from "../actions"; + +export * from "../actions/applyUpdates"; +export * from "../actions/decodeKey"; +export * from "../actions/deleteRecord"; +export * from "../actions/encodeKey"; +export * from "../actions/extend"; +export * from "../actions/getTableConfig"; +export * from "../actions/getKeys"; +export * from "../actions/getRecord"; +export * from "../actions/getRecords"; +export * from "../actions/getTable"; +export * from "../actions/getTables"; +export * from "../actions/registerTable"; +export * from "../actions/runQuery"; +export * from "../actions/setRecord"; +export * from "../actions/setRecords"; +export * from "../actions/subscribeQuery"; +export * from "../actions/subscribeStash"; +export * from "../actions/subscribeTable"; diff --git a/packages/stash/src/react/useStash.test.ts b/packages/stash/src/react/useStash.test.ts index d64fbb4426..94cabedb70 100644 --- a/packages/stash/src/react/useStash.test.ts +++ b/packages/stash/src/react/useStash.test.ts @@ -1,16 +1,19 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi } from "vitest"; import { renderHook, act } from "@testing-library/react-hooks"; import { useStash } from "./useStash"; import { defineStore } from "@latticexyz/store"; import { createStash } from "../createStash"; import isEqual from "fast-deep-equal"; -import { getRecord, getRecords } from "../actions"; import { Hex } from "viem"; +import { getRecords } from "../actions/getRecords"; +import { getRecord } from "../actions/getRecord"; // TODO: migrate to ark/attest snapshots for better formatting + typechecking describe("useStash", () => { it("returns a single record", async () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + const config = defineStore({ namespaces: { game: { @@ -31,12 +34,16 @@ describe("useStash", () => { ({ player }) => useStash(stash, (state) => getRecord({ state, table: Position, key: { player } })), { initialProps: { player } }, ); + vi.advanceTimersToNextTimer(); + expect(result.all.length).toBe(1); expect(result.current).toMatchInlineSnapshot(`undefined`); act(() => { stash.setRecord({ table: Position, key: { player }, value: { x: 1, y: 2 } }); + vi.advanceTimersToNextTimer(); }); + // Expect update to have triggered rerender expect(result.all.length).toBe(2); expect(result.current).toMatchInlineSnapshot(` @@ -49,14 +56,18 @@ describe("useStash", () => { act(() => { stash.setRecord({ table: Position, key: { player: "0x01" }, value: { x: 1, y: 2 } }); + vi.advanceTimersToNextTimer(); }); + // Expect unrelated update to not have triggered rerender expect(result.all.length).toBe(2); // Expect update to have triggered rerender act(() => { stash.setRecord({ table: Position, key: { player }, value: { x: 1, y: 3 } }); + vi.advanceTimersToNextTimer(); }); + expect(result.all.length).toBe(3); expect(result.current).toMatchInlineSnapshot(` { @@ -79,12 +90,15 @@ describe("useStash", () => { act(() => { stash.setRecord({ table: Position, key: { player: "0x0" }, value: { x: 5, y: 0 } }); + vi.advanceTimersToNextTimer(); }); // Expect unrelated update to not have triggered rerender expect(result.all.length).toBe(4); }); it("returns records of a table using equality function", async () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + const config = defineStore({ namespaces: { game: { @@ -104,11 +118,14 @@ describe("useStash", () => { const { result } = renderHook(() => useStash(stash, (state) => Object.values(getRecords({ state, table: Position })), { isEqual }), ); + vi.advanceTimersToNextTimer(); + expect(result.all.length).toBe(1); expect(result.current).toMatchInlineSnapshot(`[]`); act(() => { stash.setRecord({ table: Position, key: { player }, value: { x: 1, y: 2 } }); + vi.advanceTimersToNextTimer(); }); expect(result.all.length).toBe(2); expect(result.current).toMatchInlineSnapshot(` @@ -123,6 +140,7 @@ describe("useStash", () => { act(() => { stash.setRecord({ table: Position, key: { player: "0x01" }, value: { x: 1, y: 2 } }); + vi.advanceTimersToNextTimer(); }); expect(result.all.length).toBe(3); expect(result.current).toMatchInlineSnapshot(` @@ -142,6 +160,7 @@ describe("useStash", () => { act(() => { stash.setRecord({ table: Position, key: { player }, value: { x: 1, y: 3 } }); + vi.advanceTimersToNextTimer(); }); expect(result.all.length).toBe(4); expect(result.current).toMatchInlineSnapshot(` @@ -159,4 +178,100 @@ describe("useStash", () => { ] `); }); + + it("memoizes results", async () => { + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); + + const config = defineStore({ + namespaces: { + game: { + tables: { + Position: { + schema: { player: "address", x: "uint32", y: "uint32" }, + key: ["player"], + }, + Counter: { + schema: { count: "uint32" }, + key: [], + }, + }, + }, + }, + }); + const { Position, Counter } = config.namespaces.game.tables; + const stash = createStash(config); + const player: Hex = "0x00"; + + const { result } = renderHook(() => + useStash( + stash, + (state) => + Object.values(getRecords({ state, table: Position })).filter((position) => position.player === player), + { isEqual }, + ), + ); + vi.advanceTimersToNextTimer(); + + expect(result.all.length).toBe(1); + expect(result.current).toMatchInlineSnapshot(`[]`); + + act(() => { + stash.setRecord({ table: Counter, key: {}, value: { count: 1 } }); + stash.setRecord({ table: Counter, key: {}, value: { count: 2 } }); + vi.advanceTimersToNextTimer(); + }); + act(() => { + stash.setRecord({ table: Counter, key: {}, value: { count: 3 } }); + stash.setRecord({ table: Counter, key: {}, value: { count: 4 } }); + vi.advanceTimersToNextTimer(); + }); + + expect(result.all.length).toBe(1); + expect(result.current).toMatchInlineSnapshot(`[]`); + + act(() => { + stash.setRecord({ table: Position, key: { player }, value: { x: 1, y: 2 } }); + vi.advanceTimersToNextTimer(); + }); + expect(result.all.length).toBe(2); + expect(result.current).toMatchInlineSnapshot(` + [ + { + "player": "0x00", + "x": 1, + "y": 2, + }, + ] + `); + + act(() => { + stash.setRecord({ table: Position, key: { player: "0x01" }, value: { x: 1, y: 2 } }); + vi.advanceTimersToNextTimer(); + }); + expect(result.all.length).toBe(2); + expect(result.current).toMatchInlineSnapshot(` + [ + { + "player": "0x00", + "x": 1, + "y": 2, + }, + ] + `); + + act(() => { + stash.setRecord({ table: Position, key: { player }, value: { x: 1, y: 3 } }); + vi.advanceTimersToNextTimer(); + }); + expect(result.all.length).toBe(3); + expect(result.current).toMatchInlineSnapshot(` + [ + { + "player": "0x00", + "x": 1, + "y": 3, + }, + ] + `); + }); }); diff --git a/packages/stash/src/react/useStash.ts b/packages/stash/src/react/useStash.ts index 700b6cbb47..a410621b11 100644 --- a/packages/stash/src/react/useStash.ts +++ b/packages/stash/src/react/useStash.ts @@ -1,8 +1,8 @@ import { useDebugValue, useSyncExternalStore } from "react"; -import { subscribeStash } from "../actions"; import { StoreConfig, Stash, State } from "../common"; import { isEqual } from "./isEqual"; import { memoize } from "./memoize"; +import { subscribeStash } from "../actions/subscribeStash"; export type UseStashOptions = { /** From cabc8644fa44c7c9a96317f8bd8432a5aa57845b Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 20 Nov 2024 13:52:46 +0800 Subject: [PATCH 2/8] remove unrelated changes --- packages/stash/package.json | 15 +++-- .../stash/src/actions/deleteRecord.test.ts | 12 +++- packages/stash/src/actions/runQuery.test.ts | 34 +++++++++-- packages/stash/src/actions/setRecord.test.ts | 5 +- .../stash/src/actions/subscribeQuery.test.ts | 61 ++++++++++++++----- 5 files changed, 98 insertions(+), 29 deletions(-) diff --git a/packages/stash/package.json b/packages/stash/package.json index 619fa048dc..4df9785283 100644 --- a/packages/stash/package.json +++ b/packages/stash/package.json @@ -39,12 +39,13 @@ "test:ci": "pnpm run test" }, "dependencies": { - "@ark/util": "0.2.2", + "@ark/util": "catalog:", "@latticexyz/common": "workspace:*", "@latticexyz/config": "workspace:*", "@latticexyz/protocol-parser": "workspace:*", "@latticexyz/schema-type": "workspace:*", - "@latticexyz/store": "workspace:*" + "@latticexyz/store": "workspace:*", + "viem": "catalog:" }, "devDependencies": { "@testing-library/react": "^16.0.0", @@ -53,14 +54,12 @@ "eslint-plugin-react": "7.31.11", "eslint-plugin-react-hooks": "4.6.0", "fast-deep-equal": "^3.1.3", - "react": "18.2.0", - "react-dom": "18.2.0", - "tsup": "^6.7.0", - "viem": "2.21.19" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tsup": "^6.7.0" }, "peerDependencies": { - "react": "18.x", - "viem": "2.x" + "react": "18.x" }, "publishConfig": { "access": "public" diff --git a/packages/stash/src/actions/deleteRecord.test.ts b/packages/stash/src/actions/deleteRecord.test.ts index 9298accd1c..b90a07a617 100644 --- a/packages/stash/src/actions/deleteRecord.test.ts +++ b/packages/stash/src/actions/deleteRecord.test.ts @@ -45,7 +45,17 @@ describe("deleteRecord", () => { key: { field2: 1, field3: 2 }, }); - attest(stash.get().records).snap({ namespace1: { table1: { "3|1": { field1: "world", field2: 3, field3: 1 } } } }); + attest(stash.get().records).snap({ + namespace1: { + table1: { + "3|1": { + field1: "world", + field2: 3, + field3: 1, + }, + }, + }, + }); }); it("should throw a type error if an invalid key is provided", () => { diff --git a/packages/stash/src/actions/runQuery.test.ts b/packages/stash/src/actions/runQuery.test.ts index 26ec1bf40b..91c2149b93 100644 --- a/packages/stash/src/actions/runQuery.test.ts +++ b/packages/stash/src/actions/runQuery.test.ts @@ -70,7 +70,12 @@ describe("runQuery", () => { it("should return all keys that are in the Position and Health table", () => { const result = runQuery({ stash, query: [In(Position), In(Health)] }); - attest(result).snap({ keys: { "0x3": { player: "0x3" }, "0x4": { player: "0x4" } } }); + attest(result).snap({ + keys: { + "0x3": { player: "0x3" }, + "0x4": { player: "0x4" }, + }, + }); }); it("should return all keys that have Position.x = 4 and are included in Health", () => { @@ -80,7 +85,13 @@ describe("runQuery", () => { it("should return all keys that are in Position but not Health", () => { const result = runQuery({ stash, query: [In(Position), Not(In(Health))] }); - attest(result).snap({ keys: { "0x0": { player: "0x0" }, "0x1": { player: "0x1" }, "0x2": { player: "0x2" } } }); + attest(result).snap({ + keys: { + "0x0": { player: "0x0" }, + "0x1": { player: "0x1" }, + "0x2": { player: "0x2" }, + }, + }); }); it("should return all keys that don't include a gold item in the Inventory table", () => { @@ -105,10 +116,23 @@ describe("runQuery", () => { it("should include all matching records from the tables if includeRecords is set", () => { const result = runQuery({ stash, query: [In(Position), In(Health)], options: { includeRecords: true } }); attest(result).snap({ - keys: { "0x3": { player: "0x3" }, "0x4": { player: "0x4" } }, + keys: { + "0x3": { player: "0x3" }, + "0x4": { player: "0x4" }, + }, records: { - namespace1: { Position: { "0x3": { player: "0x3", x: 3, y: 2 }, "0x4": { player: "0x4", x: 4, y: 1 } } }, - namespace2: { Health: { "0x3": { player: "0x3", health: 3 }, "0x4": { player: "0x4", health: 4 } } }, + namespace1: { + Position: { + "0x3": { player: "0x3", x: 3, y: 2 }, + "0x4": { player: "0x4", x: 4, y: 1 }, + }, + }, + namespace2: { + Health: { + "0x3": { player: "0x3", health: 3 }, + "0x4": { player: "0x4", health: 4 }, + }, + }, }, }); }); diff --git a/packages/stash/src/actions/setRecord.test.ts b/packages/stash/src/actions/setRecord.test.ts index ba28caa897..420f8c5d42 100644 --- a/packages/stash/src/actions/setRecord.test.ts +++ b/packages/stash/src/actions/setRecord.test.ts @@ -39,7 +39,10 @@ describe("setRecord", () => { attest(stash.get().records).snap({ namespace1: { - table1: { "1|2": { field1: "hello", field2: 1, field3: 2 }, "2|1": { field1: "world", field2: 2, field3: 1 } }, + table1: { + "1|2": { field1: "hello", field2: 1, field3: 2 }, + "2|1": { field1: "world", field2: 2, field3: 1 }, + }, }, }); }); diff --git a/packages/stash/src/actions/subscribeQuery.test.ts b/packages/stash/src/actions/subscribeQuery.test.ts index 30a37769cc..e44d26d624 100644 --- a/packages/stash/src/actions/subscribeQuery.test.ts +++ b/packages/stash/src/actions/subscribeQuery.test.ts @@ -49,11 +49,18 @@ describe("defineQuery", () => { it("should return the matching keys and keep it updated", () => { const result = subscribeQuery({ stash, query: [In(Position), In(Health)] }); - attest(result.keys).snap({ "0x3": { player: "0x3" }, "0x4": { player: "0x4" } }); + attest(result.keys).snap({ + "0x3": { player: "0x3" }, + "0x4": { player: "0x4" }, + }); setRecord({ stash, table: Health, key: { player: `0x2` }, value: { health: 2 } }); - attest(result.keys).snap({ "0x3": { player: "0x3" }, "0x4": { player: "0x4" }, "0x2": { player: "0x2" } }); + attest(result.keys).snap({ + "0x3": { player: "0x3" }, + "0x4": { player: "0x4" }, + "0x2": { player: "0x2" }, + }); }); it("should notify subscribers when a matching key is updated", () => { @@ -74,7 +81,12 @@ describe("defineQuery", () => { attest(lastUpdate).snap({ records: { namespace1: { - Position: { "0x4": { prev: { player: "0x4", x: 4, y: 1 }, current: { player: "0x4", x: 4, y: 2 } } }, + Position: { + "0x4": { + prev: { player: "0x4", x: 4, y: 1 }, + current: { player: "0x4", x: 4, y: 2 }, + }, + }, }, }, keys: { "0x4": { player: "0x4" } }, @@ -91,14 +103,23 @@ describe("defineQuery", () => { result.subscribe(subscriber); vi.advanceTimersToNextTimer(); - expect(subscriber).toBeCalledTimes(2); + expect(subscriber).toBeCalledTimes(0); setRecord({ stash, table: Health, key: { player: `0x2` }, value: { health: 2 } }); vi.advanceTimersToNextTimer(); - expect(subscriber).toBeCalledTimes(3); + expect(subscriber).toBeCalledTimes(1); attest(lastUpdate).snap({ - records: { namespace1: { Health: { "0x2": { prev: "(undefined)", current: { player: "0x2", health: 2 } } } } }, + records: { + namespace1: { + Health: { + "0x2": { + prev: undefined, + current: { player: "0x2", health: 2 }, + }, + }, + }, + }, keys: { "0x2": { player: "0x2" } }, types: { "0x2": "enter" }, }); @@ -113,14 +134,23 @@ describe("defineQuery", () => { result.subscribe(subscriber); vi.advanceTimersToNextTimer(); - expect(subscriber).toBeCalledTimes(2); + expect(subscriber).toBeCalledTimes(0); deleteRecord({ stash, table: Position, key: { player: `0x3` } }); vi.advanceTimersToNextTimer(); - expect(subscriber).toBeCalledTimes(3); + expect(subscriber).toBeCalledTimes(1); attest(lastUpdate).snap({ - records: { namespace1: { Position: { "0x3": { prev: { player: "0x3", x: 3, y: 2 }, current: "(undefined)" } } } }, + records: { + namespace1: { + Position: { + "0x3": { + prev: { player: "0x3", x: 3, y: 2 }, + current: undefined, + }, + }, + }, + }, keys: { "0x3": { player: "0x3" } }, types: { "0x3": "exit" }, }); @@ -135,16 +165,19 @@ describe("defineQuery", () => { expect(subscriber).toBeCalledTimes(1); attest(lastUpdate).snap({ - keys: { "0x3": { player: "0x3" }, "0x4": { player: "0x4" } }, + keys: { + "0x3": { player: "0x3" }, + "0x4": { player: "0x4" }, + }, records: { namespace1: { Position: { - "0x3": { prev: "(undefined)", current: { player: "0x3", x: 3, y: 2 } }, - "0x4": { prev: "(undefined)", current: { player: "0x4", x: 4, y: 1 } }, + "0x3": { prev: undefined, current: { player: "0x3", x: 3, y: 2 } }, + "0x4": { prev: undefined, current: { player: "0x4", x: 4, y: 1 } }, }, Health: { - "0x3": { prev: "(undefined)", current: { player: "0x3", health: 3 } }, - "0x4": { prev: "(undefined)", current: { player: "0x4", health: 4 } }, + "0x3": { prev: undefined, current: { player: "0x3", health: 3 } }, + "0x4": { prev: undefined, current: { player: "0x4", health: 4 } }, }, }, }, From 9d190f83e3e411526d8086fcca9d5b3e0329989e Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 20 Nov 2024 14:52:58 +0800 Subject: [PATCH 3/8] fix tests --- packages/stash/src/actions/applyUpdates.ts | 6 ++--- .../stash/src/actions/subscribeQuery.test.ts | 26 ++++++++----------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/stash/src/actions/applyUpdates.ts b/packages/stash/src/actions/applyUpdates.ts index 6fa2a5a94b..64b148462d 100644 --- a/packages/stash/src/actions/applyUpdates.ts +++ b/packages/stash/src/actions/applyUpdates.ts @@ -60,9 +60,9 @@ export function applyUpdates({ stash, updates }: ApplyUpdatesArgs): void { ((storeUpdates.records[table.namespaceLabel] ??= {})[table.label] ??= {})[encodedKey] ??= update; } - // queueMicrotask(() => { - notifySubscribers(stash); - // }); + queueMicrotask(() => { + notifySubscribers(stash); + }); } function notifySubscribers(stash: Stash) { diff --git a/packages/stash/src/actions/subscribeQuery.test.ts b/packages/stash/src/actions/subscribeQuery.test.ts index e44d26d624..dde36b977e 100644 --- a/packages/stash/src/actions/subscribeQuery.test.ts +++ b/packages/stash/src/actions/subscribeQuery.test.ts @@ -32,6 +32,7 @@ describe("defineQuery", () => { beforeEach(() => { stash = createStash(config); + vi.useFakeTimers({ toFake: ["queueMicrotask"] }); // Add some mock data const items = ["0xgold", "0xsilver"] as const; @@ -45,6 +46,8 @@ describe("defineQuery", () => { setRecord({ stash, table: Inventory, key: { player: `0x${String(i)}`, item }, value: { amount: i } }); } } + + vi.advanceTimersToNextTimer(); }); it("should return the matching keys and keep it updated", () => { @@ -55,23 +58,22 @@ describe("defineQuery", () => { }); setRecord({ stash, table: Health, key: { player: `0x2` }, value: { health: 2 } }); + vi.advanceTimersToNextTimer(); - attest(result.keys).snap({ - "0x3": { player: "0x3" }, - "0x4": { player: "0x4" }, - "0x2": { player: "0x2" }, - }); + attest(result.keys).snap({ "0x3": { player: "0x3" }, "0x4": { player: "0x4" }, "0x2": { player: "0x2" } }); }); it("should notify subscribers when a matching key is updated", () => { - vi.useFakeTimers({ toFake: ["queueMicrotask"] }); - let lastUpdate: unknown; const subscriber = vi.fn((update: QueryUpdate) => (lastUpdate = update)); - const result = subscribeQuery({ stash, query: [Matches(Position, { x: 4 }), In(Health)] }); - result.subscribe(subscriber); + const result = subscribeQuery({ + stash, + query: [Matches(Position, { x: 4 }), In(Health)], + }); + result.subscribe(subscriber); vi.advanceTimersToNextTimer(); + expect(subscriber).toBeCalledTimes(0); setRecord({ stash, table: Position, key: { player: "0x4" }, value: { y: 2 } }); @@ -95,8 +97,6 @@ describe("defineQuery", () => { }); it("should notify subscribers when a new key matches", () => { - vi.useFakeTimers({ toFake: ["queueMicrotask"] }); - let lastUpdate: unknown; const subscriber = vi.fn((update: QueryUpdate) => (lastUpdate = update)); const result = subscribeQuery({ stash, query: [In(Position), In(Health)] }); @@ -126,8 +126,6 @@ describe("defineQuery", () => { }); it("should notify subscribers when a key doesn't match anymore", () => { - vi.useFakeTimers({ toFake: ["queueMicrotask"] }); - let lastUpdate: unknown; const subscriber = vi.fn((update: QueryUpdate) => (lastUpdate = update)); const result = subscribeQuery({ stash, query: [In(Position), In(Health)] }); @@ -157,8 +155,6 @@ describe("defineQuery", () => { }); it("should notify initial subscribers with initial query result", () => { - vi.useFakeTimers({ toFake: ["queueMicrotask"] }); - let lastUpdate: unknown; const subscriber = vi.fn((update: QueryUpdate) => (lastUpdate = update)); subscribeQuery({ stash, query: [In(Position), In(Health)], options: { initialSubscribers: [subscriber] } }); From 011e872093d1658fe97e822f9db566b562bf7c9d Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 27 Nov 2024 11:17:50 +0100 Subject: [PATCH 4/8] update types --- packages/stash/src/actions/applyUpdates.ts | 49 ++++++++++++-------- packages/stash/src/actions/registerTable.ts | 2 +- packages/stash/src/actions/subscribeTable.ts | 4 +- packages/stash/src/common.ts | 47 +++++++++++-------- 4 files changed, 59 insertions(+), 43 deletions(-) diff --git a/packages/stash/src/actions/applyUpdates.ts b/packages/stash/src/actions/applyUpdates.ts index 64b148462d..6ce7f688c2 100644 --- a/packages/stash/src/actions/applyUpdates.ts +++ b/packages/stash/src/actions/applyUpdates.ts @@ -1,10 +1,10 @@ import { schemaAbiTypeToDefaultValue } from "@latticexyz/schema-type/internal"; -import { Key, Stash, StoreUpdates, TableRecord } from "../common"; +import { Key, Stash, TableRecord, TableUpdates } from "../common"; import { encodeKey } from "./encodeKey"; import { Table } from "@latticexyz/config"; import { registerTable } from "./registerTable"; -export type StashUpdate
= { +export type PendingStashUpdate
= { table: table; key: Key
; value: undefined | Partial>; @@ -12,14 +12,20 @@ export type StashUpdate
= { export type ApplyUpdatesArgs = { stash: Stash; - updates: StashUpdate[]; + updates: PendingStashUpdate[]; }; -const pendingUpdates = new Map(); +type PendingUpdates = { + [namespaceLabel: string]: { + [tableLabel: string]: TableUpdates; + }; +}; + +const pendingStashUpdates = new Map(); export function applyUpdates({ stash, updates }: ApplyUpdatesArgs): void { - const storeUpdates = pendingUpdates.get(stash) ?? { config: {}, records: {} }; - if (!pendingUpdates.has(stash)) pendingUpdates.set(stash, storeUpdates); + const pendingUpdates = pendingStashUpdates.get(stash) ?? {}; + if (!pendingStashUpdates.has(stash)) pendingStashUpdates.set(stash, pendingUpdates); for (const { table, key, value } of updates) { if (stash.get().config[table.namespaceLabel]?.[table.label] == null) { @@ -28,6 +34,7 @@ export function applyUpdates({ stash, updates }: ApplyUpdatesArgs): void { const tableState = ((stash._.state.records[table.namespaceLabel] ??= {})[table.label] ??= {}); const encodedKey = encodeKey({ table, key }); const prevRecord = tableState[encodedKey]; + // create new record, preserving field order const nextRecord = value == null @@ -50,14 +57,13 @@ export function applyUpdates({ stash, updates }: ApplyUpdatesArgs): void { } // add update to pending updates for notifying subscribers - const prevUpdate = storeUpdates.records[table.namespaceLabel]?.[table.label]?.[encodedKey]; - const update = { - // preserve the initial prev state if we already have a pending update - // TODO: change subscribers to an array of updates instead of an object - prev: prevUpdate ? prevUpdate.prev : prevRecord, + const tableUpdates = ((pendingUpdates[table.namespaceLabel] ??= {})[table.label] ??= []); + tableUpdates.push({ + table, + key, + previous: prevRecord, current: nextRecord, - }; - ((storeUpdates.records[table.namespaceLabel] ??= {})[table.label] ??= {})[encodedKey] ??= update; + }); } queueMicrotask(() => { @@ -66,17 +72,20 @@ export function applyUpdates({ stash, updates }: ApplyUpdatesArgs): void { } function notifySubscribers(stash: Stash) { - const storeUpdates = pendingUpdates.get(stash); - if (!storeUpdates) return; + const pendingUpdates = pendingStashUpdates.get(stash); + if (!pendingUpdates) return; // Notify table subscribers - for (const [namespaceLabel, tableUpdates] of Object.entries(storeUpdates.records)) { - for (const [label, updates] of Object.entries(tableUpdates)) { - stash._.tableSubscribers[namespaceLabel]?.[label]?.forEach((subscriber) => subscriber(updates)); + for (const [namespaceLabel, namespaceUpdates] of Object.entries(pendingUpdates)) { + for (const [tableLabel, tableUpdates] of Object.entries(namespaceUpdates)) { + stash._.tableSubscribers[namespaceLabel]?.[tableLabel]?.forEach((subscriber) => subscriber(tableUpdates)); } } // Notify stash subscribers - stash._.storeSubscribers.forEach((subscriber) => subscriber(storeUpdates)); + const updates = Object.values(pendingUpdates) + .map((namespaceUpdates) => Object.values(namespaceUpdates)) + .flat(2); + stash._.storeSubscribers.forEach((subscriber) => subscriber({ type: "records", updates })); - pendingUpdates.delete(stash); + pendingStashUpdates.delete(stash); } diff --git a/packages/stash/src/actions/registerTable.ts b/packages/stash/src/actions/registerTable.ts index b33c582fb5..e94bda67b6 100644 --- a/packages/stash/src/actions/registerTable.ts +++ b/packages/stash/src/actions/registerTable.ts @@ -28,7 +28,7 @@ export function registerTable
({ // Notify stash subscribers const storeUpdate = { - config: { [namespaceLabel]: { [label]: { prev: undefined, current: tableConfig } } }, + config: { [namespaceLabel]: { [label]: { previous: undefined, current: tableConfig } } }, records: {}, }; stash._.storeSubscribers.forEach((subscriber) => subscriber(storeUpdate)); diff --git a/packages/stash/src/actions/subscribeTable.ts b/packages/stash/src/actions/subscribeTable.ts index 2a8cba3bea..aa55327029 100644 --- a/packages/stash/src/actions/subscribeTable.ts +++ b/packages/stash/src/actions/subscribeTable.ts @@ -21,6 +21,6 @@ export function subscribeTable
({ registerTable({ stash, table }); } - stash._.tableSubscribers[namespaceLabel]?.[label]?.add(subscriber); - return () => stash._.tableSubscribers[namespaceLabel]?.[label]?.delete(subscriber); + stash._.tableSubscribers[namespaceLabel]?.[label]?.add(subscriber as TableUpdatesSubscriber); + return () => stash._.tableSubscribers[namespaceLabel]?.[label]?.delete(subscriber as TableUpdatesSubscriber); } diff --git a/packages/stash/src/common.ts b/packages/stash/src/common.ts index ed5c09d7e9..c1adbfed23 100644 --- a/packages/stash/src/common.ts +++ b/packages/stash/src/common.ts @@ -19,6 +19,18 @@ export type getNamespaceTables< namespace extends keyof config["namespaces"], > = keyof config["namespaces"][namespace]["tables"]; +type namespacedTableLabels = keyof { + [key in keyof config["namespaces"] as `${key & string}__${keyof config["namespaces"][key]["tables"] & string}`]: null; +}; + +type namespacedTables = { + [key in namespacedTableLabels]: key extends `${infer namespaceLabel}__${infer tableLabel}` + ? config["namespaces"][namespaceLabel]["tables"][tableLabel] + : never; +}; + +export type getAllTables = namespacedTables[keyof namespacedTables]; + export type getConfig< config extends StoreConfig, namespace extends keyof config["namespaces"] | undefined, @@ -118,38 +130,33 @@ export type MutableState = { }; export type TableUpdate
= { - prev: TableRecord
| undefined; + table: table; + key: Key
; + previous: TableRecord
| undefined; current: TableRecord
| undefined; }; -export type TableUpdates
= { [key: string]: TableUpdate
}; +export type TableUpdates
= TableUpdate
[]; export type TableUpdatesSubscriber
= (updates: TableUpdates
) => void; export type TableSubscribers = { - [namespace: string]: { - [table: string]: Set; + [namespaceLabel: string]: { + [tableLabel: string]: Set; }; }; -export type ConfigUpdate = { prev: Table | undefined; current: Table }; +export type ConfigUpdate = { previous: Table | undefined; current: Table }; -export type StoreUpdates = { - config: { - [namespace: string]: { - [table: string]: ConfigUpdate; +export type StoreUpdates = + | { + type: "config"; + updates: ConfigUpdate[]; + } + | { + type: "records"; + updates: TableUpdates>; }; - }; - records: { - [namespace in getNamespaces]: { - [table in getNamespaceTables]: TableUpdates>; - }; - } & { - [namespace: string]: { - [table: string]: TableUpdates; - }; - }; -}; export type StoreUpdatesSubscriber = (updates: StoreUpdates) => void; From 8849a5547f0289f1e4e036f065e1c09f1c679d76 Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 27 Nov 2024 11:20:08 +0100 Subject: [PATCH 5/8] update registerTable --- packages/stash/src/actions/registerTable.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/stash/src/actions/registerTable.ts b/packages/stash/src/actions/registerTable.ts index e94bda67b6..68716bf7b6 100644 --- a/packages/stash/src/actions/registerTable.ts +++ b/packages/stash/src/actions/registerTable.ts @@ -27,11 +27,9 @@ export function registerTable
({ (stash._.tableSubscribers[namespaceLabel] ??= {})[label] ??= new Set(); // Notify stash subscribers - const storeUpdate = { - config: { [namespaceLabel]: { [label]: { previous: undefined, current: tableConfig } } }, - records: {}, - }; - stash._.storeSubscribers.forEach((subscriber) => subscriber(storeUpdate)); + stash._.storeSubscribers.forEach((subscriber) => + subscriber({ type: "config", updates: [{ previous: undefined, current: tableConfig }] }), + ); return getTable({ stash, table }); } From 98f83d98330eb7f3118964bf1dc960db108a55a0 Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 27 Nov 2024 12:15:17 +0100 Subject: [PATCH 6/8] fix type issues --- packages/stash/src/actions/getTable.ts | 2 +- packages/stash/src/decorators/defaultActions.ts | 4 ++-- packages/stash/tsconfig.json | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/stash/src/actions/getTable.ts b/packages/stash/src/actions/getTable.ts index 3338b69ee5..1998f8403e 100644 --- a/packages/stash/src/actions/getTable.ts +++ b/packages/stash/src/actions/getTable.ts @@ -67,7 +67,7 @@ export function getTable
({ stash, table }: GetTableArgs) => getRecords({ stash, table, ...args }), setRecord: (args: TableBoundSetRecordArgs
) => setRecord({ stash, table, ...args }), setRecords: (args: TableBoundSetRecordsArgs
) => setRecords({ stash, table, ...args }), - subscribe: (args: TableBoundSubscribeTableArgs) => subscribeTable({ stash, table, ...args }), + subscribe: (args: TableBoundSubscribeTableArgs
) => subscribeTable({ stash, table, ...args }), // TODO: dynamically add setters and getters for individual fields of the table }; diff --git a/packages/stash/src/decorators/defaultActions.ts b/packages/stash/src/decorators/defaultActions.ts index 207b605c6f..4719b54371 100644 --- a/packages/stash/src/decorators/defaultActions.ts +++ b/packages/stash/src/decorators/defaultActions.ts @@ -1,4 +1,4 @@ -import { Query, Stash, StoreConfig } from "../common"; +import { Query, Stash, StoreConfig, StoreUpdatesSubscriber } from "../common"; import { DecodeKeyArgs, DecodeKeyResult, decodeKey } from "../actions/decodeKey"; import { DeleteRecordArgs, DeleteRecordResult, deleteRecord } from "../actions/deleteRecord"; import { EncodeKeyArgs, EncodeKeyResult, encodeKey } from "../actions/encodeKey"; @@ -79,7 +79,7 @@ export function defaultActions(stash: Stash) subscribeQuery: (args: StashBoundSubscribeQueryArgs) => subscribeQuery({ stash, ...args }), subscribeStash: (args: StashBoundSubscribeStashArgs) => - subscribeStash({ stash, ...args }), + subscribeStash({ stash, subscriber: args.subscriber as StoreUpdatesSubscriber }), subscribeTable:
(args: StashBoundSubscribeTableArgs
) => subscribeTable({ stash, ...args }), }; diff --git a/packages/stash/tsconfig.json b/packages/stash/tsconfig.json index 65b4611165..7d172d5905 100644 --- a/packages/stash/tsconfig.json +++ b/packages/stash/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", - "noUncheckedIndexedAccess": true + "noUncheckedIndexedAccess": true, + "noErrorTruncation": true }, "include": ["src"] } From 88f1ccd6088fb89f0a22df726519cfc0786a441f Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 27 Nov 2024 12:37:15 +0100 Subject: [PATCH 7/8] fix subscribeStash test --- .../stash/src/actions/subscribeStash.test.ts | 128 ++++++++---------- 1 file changed, 57 insertions(+), 71 deletions(-) diff --git a/packages/stash/src/actions/subscribeStash.test.ts b/packages/stash/src/actions/subscribeStash.test.ts index 51d1f93158..d78a6512dc 100644 --- a/packages/stash/src/actions/subscribeStash.test.ts +++ b/packages/stash/src/actions/subscribeStash.test.ts @@ -39,18 +39,16 @@ describe("subscribeStash", () => { vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenCalledTimes(1); - expect(subscriber).toHaveBeenNthCalledWith(1, { - config: {}, - records: { - namespace1: { - table1: { - "0x00": { - prev: undefined, - current: { a: "0x00", b: 1n, c: 2 }, - }, - }, + expect(subscriber).toHaveBeenCalledWith({ + type: "records", + updates: [ + { + table: config.tables.namespace1__table1, + key: { a: "0x00" }, + previous: undefined, + current: { a: "0x00", b: 1n, c: 2 }, }, - }, + ], }); setRecord({ stash, table: config.tables.namespace2__table2, key: { a: "0x01" }, value: { b: 1n, c: 2 } }); @@ -58,17 +56,15 @@ describe("subscribeStash", () => { expect(subscriber).toHaveBeenCalledTimes(2); expect(subscriber).toHaveBeenNthCalledWith(2, { - config: {}, - records: { - namespace2: { - table2: { - "0x01": { - prev: undefined, - current: { a: "0x01", b: 1n, c: 2 }, - }, - }, + type: "records", + updates: [ + { + table: config.tables.namespace2__table2, + key: { a: "0x01" }, + previous: undefined, + current: { a: "0x01", b: 1n, c: 2 }, }, - }, + ], }); setRecord({ stash, table: config.tables.namespace2__table2, key: { a: "0x01" }, value: { b: 1n, c: 3 } }); @@ -76,17 +72,15 @@ describe("subscribeStash", () => { expect(subscriber).toHaveBeenCalledTimes(3); expect(subscriber).toHaveBeenNthCalledWith(3, { - config: {}, - records: { - namespace2: { - table2: { - "0x01": { - prev: { a: "0x01", b: 1n, c: 2 }, - current: { a: "0x01", b: 1n, c: 3 }, - }, - }, + type: "records", + updates: [ + { + table: config.tables.namespace2__table2, + key: { a: "0x01" }, + previous: { a: "0x01", b: 1n, c: 2 }, + current: { a: "0x01", b: 1n, c: 3 }, }, - }, + ], }); deleteRecord({ stash, table: config.tables.namespace2__table2, key: { a: "0x01" } }); @@ -94,17 +88,15 @@ describe("subscribeStash", () => { expect(subscriber).toHaveBeenCalledTimes(4); expect(subscriber).toHaveBeenNthCalledWith(4, { - config: {}, - records: { - namespace2: { - table2: { - "0x01": { - prev: { a: "0x01", b: 1n, c: 3 }, - current: undefined, - }, - }, + type: "records", + updates: [ + { + table: config.tables.namespace2__table2, + key: { a: "0x01" }, + previous: { a: "0x01", b: 1n, c: 3 }, + current: undefined, }, - }, + ], }); }); @@ -134,17 +126,15 @@ describe("subscribeStash", () => { expect(subscriber).toHaveBeenCalledTimes(1); expect(subscriber).toHaveBeenNthCalledWith(1, { - config: {}, - records: { - app: { - config: { - "": { - prev: undefined, - current: { enabled: true }, - }, - }, + type: "records", + updates: [ + { + table: config.tables.app__config, + key: {}, + previous: undefined, + current: { enabled: true }, }, - }, + ], }); setRecord({ stash, table: config.tables.app__config, key: {}, value: { enabled: false } }); @@ -152,17 +142,15 @@ describe("subscribeStash", () => { expect(subscriber).toHaveBeenCalledTimes(2); expect(subscriber).toHaveBeenNthCalledWith(2, { - config: {}, - records: { - app: { - config: { - "": { - prev: { enabled: true }, - current: { enabled: false }, - }, - }, + type: "records", + updates: [ + { + table: config.tables.app__config, + key: {}, + previous: { enabled: true }, + current: { enabled: false }, }, - }, + ], }); deleteRecord({ stash, table: config.tables.app__config, key: {} }); @@ -170,17 +158,15 @@ describe("subscribeStash", () => { expect(subscriber).toHaveBeenCalledTimes(3); expect(subscriber).toHaveBeenNthCalledWith(3, { - config: {}, - records: { - app: { - config: { - "": { - prev: { enabled: false }, - current: undefined, - }, - }, + type: "records", + updates: [ + { + table: config.tables.app__config, + key: {}, + previous: { enabled: false }, + current: undefined, }, - }, + ], }); }); }); From 910f9cdd1e934ce195de3cfb53e8b0dfb6b258d6 Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 27 Nov 2024 12:44:26 +0100 Subject: [PATCH 8/8] fix subscribeTable test --- .../stash/src/actions/subscribeTable.test.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/stash/src/actions/subscribeTable.test.ts b/packages/stash/src/actions/subscribeTable.test.ts index 1495f67d09..364c7493e3 100644 --- a/packages/stash/src/actions/subscribeTable.test.ts +++ b/packages/stash/src/actions/subscribeTable.test.ts @@ -40,12 +40,14 @@ describe("subscribeTable", () => { vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenCalledTimes(1); - expect(subscriber).toHaveBeenNthCalledWith(1, { - "0x00": { - prev: undefined, + expect(subscriber).toHaveBeenNthCalledWith(1, [ + { + table: table1, + key: { a: "0x00" }, + previous: undefined, current: { a: "0x00", b: 1n, c: 2 }, }, - }); + ]); // Expect unrelated updates to not notify subscribers setRecord({ stash, table: table2, key: { a: "0x01" }, value: { b: 1n, c: 2 } }); @@ -57,11 +59,13 @@ describe("subscribeTable", () => { vi.advanceTimersToNextTimer(); expect(subscriber).toHaveBeenCalledTimes(2); - expect(subscriber).toHaveBeenNthCalledWith(2, { - "0x00": { - prev: { a: "0x00", b: 1n, c: 2 }, + expect(subscriber).toHaveBeenNthCalledWith(2, [ + { + table: table1, + key: { a: "0x00" }, + previous: { a: "0x00", b: 1n, c: 2 }, current: { a: "0x00", b: 1n, c: 3 }, }, - }); + ]); }); });