-
Notifications
You must be signed in to change notification settings - Fork 194
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(stash): add stash, the new MUD client state library (#3040)
Co-authored-by: Kevin Ingersoll <kingersoll@gmail.com>
- Loading branch information
Showing
55 changed files
with
4,287 additions
and
82 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
--- | ||
"@latticexyz/stash": patch | ||
--- | ||
|
||
Adds `@latticexyz/stash`, a TypeScript client state library optimized for the MUD Store data model. | ||
It uses the MUD store config to define local tables, which support writing, reading and subscribing to table updates. | ||
It comes with a query engine optimized for "ECS-style" queries (similar to `@latticexyz/recs`) but with native support for composite keys. | ||
|
||
You can find usage examples in the [`@latticexyz/stash` README.md](https://github.com/latticexyz/mud/blob/main/packages/stash/README.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# @latticexyz/stash |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# Stash | ||
|
||
Stash is a client state library optimized for the MUD data model. | ||
It uses the MUD store config to define local tables, which support writing, reading and subscribing to table updates. | ||
It comes with a query engine optimized for ["ECS-style"](https://mud.dev/ecs) queries (similar to `@latticexyz/recs`) but with native support for composite keys. | ||
|
||
## Getting started | ||
|
||
### Installation | ||
|
||
```bash | ||
pnpm add @latticexyz/stash @latticexyz/store | ||
``` | ||
|
||
### Example usage | ||
|
||
```ts | ||
import { createStash } from "@latticexyz/stash"; | ||
import { defineStore } from "@latticexyz/store"; | ||
|
||
// Define the store config | ||
const config = defineStore( | ||
tables: { | ||
Position: { | ||
schema: { | ||
player: "address", | ||
x: "int32", | ||
y: "int32", | ||
}, | ||
key: ["player"], | ||
}, | ||
}, | ||
); | ||
|
||
// Initialize stash | ||
const stash = createStash(config); | ||
|
||
// Write to a table | ||
const { Position } = config.tables; | ||
const alice = "0xc0F21fa55169feF83aC5f059ad2432a16F06dD44"; | ||
stash.setRecord({ | ||
table: Position, | ||
key: { | ||
player: alice | ||
}, | ||
value: { | ||
x: 1, | ||
y: 2 | ||
} | ||
}); | ||
|
||
// Read from the table | ||
const alicePosition = stash.getRecord({ table: Position, key: { player: alice }}); | ||
// ^? { player: "0xc0F21fa55169feF83aC5f059ad2432a16F06dD44", x: 1, y: 2 } | ||
|
||
// Subscribe to table updates | ||
stash.subscribeTable({ | ||
table: Position, | ||
subscriber: (update) => { | ||
console.log("Position update", update); | ||
} | ||
}); | ||
|
||
// Query the table | ||
const players = stash.runQuery({ | ||
query: [Matches(Position, { x: 1 })], | ||
options: { | ||
includeRecords: true | ||
} | ||
}) | ||
|
||
// Subscribe to query updates | ||
const query = stash.subscribeQuery({ | ||
query: [Matches(Position, { x: 1 })] | ||
}) | ||
query.subscribe((update) => { | ||
console.log("Query update", update); | ||
}); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
{ | ||
"name": "@latticexyz/stash", | ||
"version": "2.1.0", | ||
"private": true, | ||
"description": "High performance client store and query engine for MUD", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/latticexyz/mud.git", | ||
"directory": "packages/stash" | ||
}, | ||
"license": "MIT", | ||
"type": "module", | ||
"exports": { | ||
".": "./dist/index.js", | ||
"./internal": "./dist/internal.js", | ||
"./recs": "./dist/recs.js" | ||
}, | ||
"typesVersions": { | ||
"*": { | ||
"index": [ | ||
"./dist/index.d.ts" | ||
], | ||
"internal": [ | ||
"./dist/internal.d.ts" | ||
] | ||
} | ||
}, | ||
"files": [ | ||
"dist" | ||
], | ||
"scripts": { | ||
"bench": "tsx src/bench.ts", | ||
"build": "tsup", | ||
"clean": "shx rm -rf dist", | ||
"dev": "tsup --watch", | ||
"test": "vitest typecheck --run --passWithNoTests && vitest --run --passWithNoTests", | ||
"test:ci": "pnpm run test" | ||
}, | ||
"dependencies": { | ||
"@arktype/util": "0.0.40", | ||
"@latticexyz/config": "workspace:*", | ||
"@latticexyz/protocol-parser": "workspace:*", | ||
"@latticexyz/schema-type": "workspace:*", | ||
"@latticexyz/store": "workspace:*", | ||
"react": "^18.2.0", | ||
"viem": "2.9.20" | ||
}, | ||
"devDependencies": { | ||
"@arktype/attest": "0.7.5", | ||
"@testing-library/react": "^16.0.0", | ||
"@testing-library/react-hooks": "^8.0.1", | ||
"@types/react": "18.2.22", | ||
"react-dom": "^18.2.0", | ||
"tsup": "^6.7.0", | ||
"vitest": "0.34.6" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { describe, it } from "vitest"; | ||
import { createStash } from "../createStash"; | ||
import { defineStore } from "@latticexyz/store/config/v2"; | ||
import { setRecord } from "./setRecord"; | ||
import { encodeKey } from "./encodeKey"; | ||
import { attest } from "@ark/attest"; | ||
import { decodeKey } from "./decodeKey"; | ||
|
||
describe("decodeKey", () => { | ||
it("should decode an encoded table key", () => { | ||
const config = defineStore({ | ||
namespace: "namespace1", | ||
tables: { | ||
table1: { | ||
schema: { field1: "string", field2: "uint32", field3: "uint256" }, | ||
key: ["field2", "field3"], | ||
}, | ||
}, | ||
}); | ||
const stash = createStash(config); | ||
const table = config.namespaces.namespace1.tables.table1; | ||
const key = { field2: 1, field3: 2n }; | ||
setRecord({ stash, table, key, value: { field1: "hello" } }); | ||
|
||
const encodedKey = encodeKey({ table, key }); | ||
attest<typeof key>(decodeKey({ stash, table, encodedKey })).equals({ field2: 1, field3: 2n }); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { Table } from "@latticexyz/config"; | ||
import { Key, Stash } from "../common"; | ||
|
||
export type DecodeKeyArgs<table extends Table = Table> = { | ||
stash: Stash; | ||
table: table; | ||
encodedKey: string; | ||
}; | ||
|
||
export type DecodeKeyResult<table extends Table = Table> = Key<table>; | ||
|
||
export function decodeKey<table extends Table>({ | ||
stash, | ||
table, | ||
encodedKey, | ||
}: DecodeKeyArgs<table>): DecodeKeyResult<table> { | ||
const { namespaceLabel, label, key } = table; | ||
const record = stash.get().records[namespaceLabel][label][encodedKey]; | ||
|
||
// Typecast needed because record values could be arrays, but we know they are not if they are key fields | ||
return Object.fromEntries(Object.entries(record).filter(([field]) => key.includes(field))) as never; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { attest } from "@ark/attest"; | ||
import { defineStore } from "@latticexyz/store"; | ||
import { describe, it } from "vitest"; | ||
import { createStash } from "../createStash"; | ||
import { setRecord } from "./setRecord"; | ||
import { deleteRecord } from "./deleteRecord"; | ||
|
||
describe("deleteRecord", () => { | ||
it("should delete a record from the stash", () => { | ||
const config = defineStore({ | ||
namespace: "namespace1", | ||
tables: { | ||
table1: { | ||
schema: { | ||
field1: "string", | ||
field2: "uint32", | ||
field3: "int32", | ||
}, | ||
key: ["field2", "field3"], | ||
}, | ||
}, | ||
}); | ||
|
||
const table = config.namespaces.namespace1.tables.table1; | ||
|
||
const stash = createStash(config); | ||
|
||
setRecord({ | ||
stash, | ||
table, | ||
key: { field2: 1, field3: 2 }, | ||
value: { field1: "hello" }, | ||
}); | ||
|
||
setRecord({ | ||
stash, | ||
table, | ||
key: { field2: 3, field3: 1 }, | ||
value: { field1: "world" }, | ||
}); | ||
|
||
deleteRecord({ | ||
stash, | ||
table, | ||
key: { field2: 1, field3: 2 }, | ||
}); | ||
|
||
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", () => { | ||
const config = defineStore({ | ||
namespace: "namespace1", | ||
tables: { | ||
table1: { | ||
schema: { | ||
field1: "string", | ||
field2: "uint32", | ||
field3: "int32", | ||
}, | ||
key: ["field2", "field3"], | ||
}, | ||
}, | ||
}); | ||
|
||
const table = config.namespaces.namespace1.tables.table1; | ||
|
||
const stash = createStash(config); | ||
|
||
attest(() => | ||
deleteRecord({ | ||
stash, | ||
table, | ||
// @ts-expect-error Property 'field3' is missing in type '{ field2: number; }' | ||
key: { field2: 1 }, | ||
}), | ||
).type.errors(`Property 'field3' is missing in type '{ field2: number; }'`); | ||
|
||
attest(() => | ||
deleteRecord({ | ||
stash, | ||
table, | ||
// @ts-expect-error Type 'string' is not assignable to type 'number' | ||
key: { field2: 1, field3: "invalid" }, | ||
}), | ||
).type.errors(`Type 'string' is not assignable to type 'number'`); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { Table } from "@latticexyz/config"; | ||
import { Key, Stash } from "../common"; | ||
import { encodeKey } from "./encodeKey"; | ||
import { registerTable } from "./registerTable"; | ||
|
||
export type DeleteRecordArgs<table extends Table = Table> = { | ||
stash: Stash; | ||
table: table; | ||
key: Key<table>; | ||
}; | ||
|
||
export type DeleteRecordResult = void; | ||
|
||
export function deleteRecord<table extends Table>({ stash, table, key }: DeleteRecordArgs<table>): 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)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { attest } from "@ark/attest"; | ||
import { describe, it } from "vitest"; | ||
import { encodeKey } from "./encodeKey"; | ||
import { defineTable } from "@latticexyz/store/config/v2"; | ||
|
||
describe("encodeKey", () => { | ||
it("should encode a key to a string", () => { | ||
const table = defineTable({ | ||
label: "test", | ||
schema: { field1: "uint32", field2: "uint256", field3: "string" }, | ||
key: ["field1", "field2"], | ||
}); | ||
attest(encodeKey({ table, key: { field1: 1, field2: 2n } })).snap("1|2"); | ||
}); | ||
|
||
it("should throw a type error if an invalid key is provided", () => { | ||
const table = defineTable({ | ||
label: "test", | ||
schema: { field1: "uint32", field2: "uint256", field3: "string" }, | ||
key: ["field1", "field2"], | ||
}); | ||
|
||
attest(() => | ||
encodeKey({ | ||
table, | ||
// @ts-expect-error Property 'field2' is missing in type '{ field1: number; }' | ||
key: { | ||
field1: 1, | ||
}, | ||
}), | ||
) | ||
.throws(`Provided key is missing field field2.`) | ||
.type.errors(`Property 'field2' is missing in type '{ field1: number; }'`); | ||
|
||
attest( | ||
encodeKey({ | ||
table, | ||
key: { | ||
field1: 1, | ||
// @ts-expect-error Type 'string' is not assignable to type 'bigint'. | ||
field2: "invalid", | ||
}, | ||
}), | ||
).type.errors(`Type 'string' is not assignable to type 'bigint'.`); | ||
}); | ||
}); |
Oops, something went wrong.