From 2b8c0bcc4255f3ca245d1ecf943e35181c54f185 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Wed, 7 Feb 2024 17:32:56 +0000 Subject: [PATCH] docs: "How MUD models data" guide (#2243) --- docs/components/MUDTable.module.css | 25 ++++ docs/components/MUDTable.tsx | 10 ++ docs/next.config.mjs | 5 + docs/pages/guides/_meta.js | 3 +- ...ending-world.mdx => extending-a-world.mdx} | 2 +- docs/pages/guides/hello-world.mdx | 2 +- .../guides/replicating-onchain-state.mdx | 4 +- docs/pages/store/_meta.js | 6 +- docs/pages/store/data-model.mdx | 138 ++++++++++++++++++ docs/pages/store/introduction.mdx | 9 +- docs/pages/store/table-intro.mdx | 8 - docs/pages/store/tables.mdx | 16 +- docs/pages/world/function-selectors.mdx | 2 +- docs/pages/world/introduction.mdx | 2 +- docs/tsconfig.json | 20 +-- 15 files changed, 208 insertions(+), 44 deletions(-) create mode 100644 docs/components/MUDTable.module.css create mode 100644 docs/components/MUDTable.tsx rename docs/pages/guides/{extending-world.mdx => extending-a-world.mdx} (99%) create mode 100644 docs/pages/store/data-model.mdx delete mode 100644 docs/pages/store/table-intro.mdx diff --git a/docs/components/MUDTable.module.css b/docs/components/MUDTable.module.css new file mode 100644 index 0000000000..becc677b84 --- /dev/null +++ b/docs/components/MUDTable.module.css @@ -0,0 +1,25 @@ +.table { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace; + background-color: hsl(var(--nextra-primary-hue) 100% 66%/0.1); + margin-top: 1em; + text-align: center; +} + +.table th, +.table td { + padding: 1em 1.5em; + border-bottom: 1px solid rgba(0, 0, 0, 0.2); +} + +.table th { + color: white; + background: hsl(var(--nextra-primary-hue) 100% 45%/0.5); +} +.table thead th { + background: hsl(var(--nextra-primary-hue) 100% 45%/0.75); +} + +.table tbody tr:hover th, +.table tbody tr:hover td { + backdrop-filter: brightness(1.5); +} diff --git a/docs/components/MUDTable.tsx b/docs/components/MUDTable.tsx new file mode 100644 index 0000000000..cd3ee923ca --- /dev/null +++ b/docs/components/MUDTable.tsx @@ -0,0 +1,10 @@ +import React, { ReactNode } from "react"; +import styles from "./MUDTable.module.css"; + +type Props = { + children: ReactNode; +}; + +export function MUDTable({ children }: Props) { + return {children}
; +} diff --git a/docs/next.config.mjs b/docs/next.config.mjs index 752d004fde..eb8c7b2092 100644 --- a/docs/next.config.mjs +++ b/docs/next.config.mjs @@ -216,6 +216,11 @@ export default withNextra({ destination: "/config", permanent: false, }, + { + source: "/guides/extending-world", + destination: "/guides/extending-a-world", + permanent: false, + }, ]; }, }); diff --git a/docs/pages/guides/_meta.js b/docs/pages/guides/_meta.js index 797a65eaba..22f31cb216 100644 --- a/docs/pages/guides/_meta.js +++ b/docs/pages/guides/_meta.js @@ -1,5 +1,6 @@ export default { + "replicating-onchain-state": "Replicating onchain state", "hello-world": "Hello World", - "extending-world": "Extending World", + "extending-a-world": "Extending a World", emojimon: "Emojimon", }; diff --git a/docs/pages/guides/extending-world.mdx b/docs/pages/guides/extending-a-world.mdx similarity index 99% rename from docs/pages/guides/extending-world.mdx rename to docs/pages/guides/extending-a-world.mdx index 920b284553..0f529ba70e 100644 --- a/docs/pages/guides/extending-world.mdx +++ b/docs/pages/guides/extending-a-world.mdx @@ -1,6 +1,6 @@ import { CollapseCode } from "../../components/CollapseCode"; -# Extending World +# Extending a World On this page you learn how to modify a `World` that is already deployed to the blockchain. If you want to learn how to modify a `World` before it is deployed, [see the hello world page](./hello-world). diff --git a/docs/pages/guides/hello-world.mdx b/docs/pages/guides/hello-world.mdx index 90f0538251..9df7b03fc2 100644 --- a/docs/pages/guides/hello-world.mdx +++ b/docs/pages/guides/hello-world.mdx @@ -1,7 +1,7 @@ # Hello World On this page you learn how to modify a `World` before it is deployed to the blockchain. -If you want to learn how to modify a `World` that is already deployed, [see the extending world page](./extending-world). +If you want to learn how to modify a `World` that is already deployed, see the [Extending a World](./extending-a-world) guide. The examples on this page use [the vanilla template](../templates/typescript/vanilla). [See this page for installation instructions](../templates/typescript/getting-started). diff --git a/docs/pages/guides/replicating-onchain-state.mdx b/docs/pages/guides/replicating-onchain-state.mdx index 3c48d2dc5c..7288cb075f 100644 --- a/docs/pages/guides/replicating-onchain-state.mdx +++ b/docs/pages/guides/replicating-onchain-state.mdx @@ -1,10 +1,10 @@ -# Replicating onchain state from MUD Store events +# Replicating onchain state This guide walks through how you might recreate onchain state from MUD's Store events. We use this pattern in our sync stack to hydrate a client or indexer from blockchain logs fetched from an RPC. ## Store: MUD's onchain storage -Before we get started, it's helpful to understand [how MUD stores its data on chain](/store/introduction). MUD introduces [a few new concepts](/store/tables): tables, records, key tuples, and fields. +Before we get started, it's helpful to understand [how MUD stores its data on chain](/store/introduction). MUD introduces [a few new concepts](/store/data-model): tables, records, key tuples, and fields. You can think of tables like a native Solidity `mapping`, where its keys and values are [encoded in a standardized way](/store/encoding) to 1) more easily replicate and represent data offchain, like in relational databases, and 2) require less onchain storage and gas than native Solidity. diff --git a/docs/pages/store/_meta.js b/docs/pages/store/_meta.js index 69d1233847..109874c16f 100644 --- a/docs/pages/store/_meta.js +++ b/docs/pages/store/_meta.js @@ -1,9 +1,9 @@ export default { introduction: "Introduction", + "data-model": "Data model", tables: "Tables", - "table-libraries": "Table Libraries", + "table-libraries": "Table libraries", encoding: "Encoding", - "store-hooks": "Store Hooks", + "store-hooks": "Store hooks", reference: "Reference", - "table-intro": { display: "hidden" }, // imported }; diff --git a/docs/pages/store/data-model.mdx b/docs/pages/store/data-model.mdx new file mode 100644 index 0000000000..2e2b422718 --- /dev/null +++ b/docs/pages/store/data-model.mdx @@ -0,0 +1,138 @@ +import { CollapseCode } from "../../components/CollapseCode"; +import { MUDTable } from "../../components/MUDTable"; + +# How MUD models data + +The MUD framework helps you to model your onchain state in a way that enables building applications with familiar tooling, like relational databases. + +In your MUD config, you define data as a set of tables. Like relational databases, these tables have “columns” by way of two MUD concepts: a key schema and a value schema. Each one has a set of fields that map to Solidity types. + +## Defining tables + +Let's say we're making a game where players can move around a map. The definition for a `Position` table might look something like: + + + +```tsx filename="mud.config.ts" {3-11} showLineNumbers +export default mudConfig({ + tables: { + Position: { + keySchema: { + player: "address", + }, + valueSchema: { + x: "int32", + y: "int32", + }, + }, + }, +}); +``` + + + +Translating the table definition above would look like this in a relational database: + + + + + player + x + y + + + + + 0x1234 + -5 + 1 + + + 0x5678 + 3 + 6 + + + + +Because `player` in part of the key schema, we would have a unique/primary key constraint on `player`. + +Let's add another table for terrain at a given position on the map. MUD allows for multiple keys in the key schema and, like databases, we call these composite keys. + + + +```tsx filename="mud.config.ts" {12-20} showLineNumbers +export default mudConfig({ + tables: { + Position: { + keySchema: { + player: "address", + }, + valueSchema: { + x: "int32", + y: "int32", + }, + }, + Terrain: { + keySchema: { + x: "int32", + y: "int32", + }, + valueSchema: { + terrainType: "string", + }, + }, + }, +}); +``` + + + +Similarly, the relational database representation of that table would look like: + + + + + x + y + terrainType + + + + + -1 + 0 + grass + + + 0 + 1 + tree + + + 1 + 0 + grass + + + + +Because we have a composite key of `x` and `y`, we would have a unique/primary key constraint on the tuple of `(x, y)`. + +## Tables on chain + +Solidity and the EVM have much more limited data structures, so how can we express a relational database onchain? MUD takes the concept of tables and does a few things with them: + +- [encodes each table record as a key/value pair](./encoding) before storing onchain +- [emits an event for each mutation](/guides/replicating-onchain-state) +- [provides a typed Solidity libraries](./table-libraries) to abstract this away + +## Field types + +Key schema fields can use all of Solidity's static-length primitive types like `uint256`, `address`, `bool`. + +Value schema fields can use all of Solidity's static-length primitive types, arrays of those static-length primitive types, as well as and `string` and `bytes`. + +Enums and user types are also supported and automatically map down to their Solidity primitive types. + +More complex types like structs, `string[]`, and `bytes[]` are not yet supported. We'll cover the reasons why in our encoding/decoding guide. diff --git a/docs/pages/store/introduction.mdx b/docs/pages/store/introduction.mdx index 56145e3cb6..efad032186 100644 --- a/docs/pages/store/introduction.mdx +++ b/docs/pages/store/introduction.mdx @@ -1,14 +1,13 @@ -import TableIntro from "./table-intro.mdx"; - # Introduction `Store` is an alternative to Solidity's storage engine. -It enforces a data model that can be mapped directly to a relational database, enables [automatic indexing](../services/indexer) by emitting events on each storage operation, and [packs data more tightly](./encoding) than Solidity's storage engine. +It enforces a data model that can be mapped directly to a relational database, enables [automatic indexing](../services/indexer) by emitting a common set of events for each mutation, and [packs data more tightly](./encoding) than native Solidity. It also allows external contract storage to be read onchain without being limited by existing `view` functions and [without a new opcode](https://eips.ethereum.org/EIPS/eip-2330). ## Data model - +Each piece of data in `Store` is stored as a _record_ in a _table_. +You can think of tables in two ways, either [as a relational database](./how-mud-models-data) or [as a key-value store](./tables). ## Reading and writing data @@ -43,4 +42,4 @@ This allows advanced use cases like the [`World` protocol](../world/introduction ## Automatic indexing `Store` automatically emits events on every write operation, including when a new table is registered in the `Store` at runtime. -These events allow [automatic indexers](../services/indexer) to replicate the onchain state of each table in each `Store` contract in a relational database for offchain use, without the need for custom integrations. +These events allow [automatic indexers](../services/indexer) to [replicate the onchain state](/guides/replicating-onchain-state) of each table in each `Store` contract in a relational database for offchain use, without the need for custom integrations. diff --git a/docs/pages/store/table-intro.mdx b/docs/pages/store/table-intro.mdx deleted file mode 100644 index 25a780b070..0000000000 --- a/docs/pages/store/table-intro.mdx +++ /dev/null @@ -1,8 +0,0 @@ -Each piece of data in `Store` is stored as a _record_ in a _table_. -You can think of tables in two ways, either as a relational database or as a key-value store. - -- Each table is identified by a unique `ResourceId tableId`. -- Each record in a table is identified by a unique `bytes32[] keyTuple`. - You can think of the key tuple as a composite key in a relational database, or as a nested mapping in a key-value store. -- Each table has a `ValueSchema` that defines the types of data stored in the table. - You can think of the value schema as the column types in a table in a relational database, or the type of structs stored in a key-value store. diff --git a/docs/pages/store/tables.mdx b/docs/pages/store/tables.mdx index d9f294d029..70db84ccf9 100644 --- a/docs/pages/store/tables.mdx +++ b/docs/pages/store/tables.mdx @@ -1,13 +1,19 @@ import { Callout } from "nextra/components"; -import TableIntro from "./table-intro.mdx"; # Tables - +Each piece of data in `Store` is stored as a _record_ in a _table_. +You can think of tables in two ways, either [as a relational database](./how-mud-models-data) or as a key-value store. + +- Each table is identified by a unique `ResourceId tableId`. +- Each record in a table is identified by a unique `bytes32[] keyTuple`. + You can think of the key tuple as a composite key in a relational database, or as a nested mapping in a key-value store. +- Each table has a `ValueSchema` that defines the types of data stored in the table. + You can think of the value schema as the column types in a table in a relational database, or the type of structs stored in a key-value store. Tables are registered in the `Store` contract at runtime. When a table is registered, its ID, key and value types, as well as key and field names are stored in the internal `Tables` table. -This emits an event that can be used by [offchain indexers](../services/indexer) to start replicating the state of the new table. +This emits an event that can be used by [offchain indexers](../services/indexer) to start [replicating the state](/guides/replicating-onchain-state) of the new table. The recommended way of reading from tables and writing to tables is via the [typed table libraries](./table-libraries). However, it is also possible to use the low-level [`IStore` (external)](./reference/store) or [`StoreCore` (internal)](./reference/store-core) API directly. @@ -18,7 +24,7 @@ There are two types of tables in `Store`: _Onchain tables_ and _offchain tables_ We often omit the prefix from onchain tables and just call them tables. As the name suggests, **onchain tables** store their state onchain, in the `Store` contract. -In addition, an event is emitted on every write operation, to allow [offchain indexers](../services/indexer) to replicate the onchain state. +In addition, an event is emitted on every write operation, to allow [offchain indexers](../services/indexer) to [replicate the onchain state](/guides/replicating-onchain-state). **Offchain tables** on the other hand don't store any state onchain, but only emit the events for [offchain indexers](../services/indexer). This makes them suitable for use cases where data doesn't need to be retrieved onchain, but should still be synchronized from the `Store` contract to [offchain indexers](../services/indexer). @@ -120,7 +126,7 @@ FieldLayout fieldLayout = FieldLayoutLib.encode({ ### Manually register a table -Registering a table is a prerequisite for writing to the table, and allows [offchain indexers](../services/indexer) to replicate the onchain state. +Registering a table is a prerequisite for writing to the table, and allows [offchain indexers](../services/indexer) to [replicate the onchain state](/guides/replicating-onchain-state). ```solidity import { IStore } from "@latticexyz/store/src/IStore.sol"; diff --git a/docs/pages/world/function-selectors.mdx b/docs/pages/world/function-selectors.mdx index 8adb954fa5..9dc757e91c 100644 --- a/docs/pages/world/function-selectors.mdx +++ b/docs/pages/world/function-selectors.mdx @@ -21,7 +21,7 @@ This function takes two parameters: This value encodes both the namespace and the name of the `System`. - [The signature (name and parameter types)](https://docs.soliditylang.org/en/v0.8.22/abi-spec.html#function-selector) of the function within the `System`. -For example, in the [Extending World guide](/guides/extending-world#deploy-to-the-blockchain) we create a namespace called `messaging`, within it a `System` called `message`, and that `System` includes a function called `incrementMessage`. +For example, in the [Extending a World](/guides/extending-a-world#deploy-to-the-blockchain) guide we create a namespace called `messaging`, within it a `System` called `message`, and that `System` includes a function called `incrementMessage`. This is the code we use to register the `System` and the function selector: diff --git a/docs/pages/world/introduction.mdx b/docs/pages/world/introduction.mdx index 0a8ae81feb..c07e975d9b 100644 --- a/docs/pages/world/introduction.mdx +++ b/docs/pages/world/introduction.mdx @@ -14,7 +14,7 @@ To prevent systems from being registered in the root namespace, the root namespa `Systems` in a `World` are comparable to [`Facets` in the `Diamond` pattern](https://eips.ethereum.org/EIPS/eip-2535) in many ways. The big difference is permissionless extensions. -Anybody can claim an unused [namespace](/world/namespaces-access-control), and put into it tables and `System` contracts to [extend the world](/guides/extending-world). +Anybody can claim an unused [namespace](/world/namespaces-access-control), and put into it tables and `System` contracts to [extend the world](/guides/extending-a-world). ![overall illustration](./world-intro.svg) diff --git a/docs/tsconfig.json b/docs/tsconfig.json index f96e49f43e..6c4ba7b499 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "target": "es2020", + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": false, @@ -25,14 +21,6 @@ ], "strictNullChecks": true }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - "pages/_meta.js", - ".next/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "pages/_meta.js", ".next/types/**/*.ts"], + "exclude": ["node_modules"] }