From 2d8cb57cb24cc849692937d9e45b38a78f804eaf Mon Sep 17 00:00:00 2001 From: ponderingdemocritus Date: Thu, 26 Sep 2024 11:43:40 +1000 Subject: [PATCH] feat: draft zustand state management system --- examples/example-vite-react-sdk/package.json | 10 +- examples/example-vite-react-sdk/src/App.tsx | 79 +++++++------ examples/example-vite-react-sdk/src/state.ts | 118 +++++++++++++++++++ pnpm-lock.yaml | 74 ++++++------ 4 files changed, 208 insertions(+), 73 deletions(-) create mode 100644 examples/example-vite-react-sdk/src/state.ts diff --git a/examples/example-vite-react-sdk/package.json b/examples/example-vite-react-sdk/package.json index 1127bbd6..a9d3e833 100644 --- a/examples/example-vite-react-sdk/package.json +++ b/examples/example-vite-react-sdk/package.json @@ -10,13 +10,17 @@ "preview": "vite preview" }, "dependencies": { - "@dojoengine/core": "1.0.0-alpha.12", + "@dojoengine/core": "workspace:*", "@dojoengine/sdk": "workspace:*", - "@dojoengine/torii-wasm": "1.0.0-alpha.12", + "@dojoengine/torii-wasm": "workspace:*", + "@types/uuid": "^10.0.0", + "immer": "^10.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "uuid": "^10.0.0", "vite-plugin-top-level-await": "^1.4.4", - "vite-plugin-wasm": "^3.3.0" + "vite-plugin-wasm": "^3.3.0", + "zustand": "^4.5.5" }, "devDependencies": { "@eslint/js": "^9.11.1", diff --git a/examples/example-vite-react-sdk/src/App.tsx b/examples/example-vite-react-sdk/src/App.tsx index 158d864d..d7e6151d 100644 --- a/examples/example-vite-react-sdk/src/App.tsx +++ b/examples/example-vite-react-sdk/src/App.tsx @@ -1,10 +1,14 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import "./App.css"; -import { ParsedEntity, SDK } from "@dojoengine/sdk"; +import { SDK } from "@dojoengine/sdk"; import { Schema } from "./bindings.ts"; +import { useGameState } from "./state.ts"; + +import { v4 as uuidv4 } from "uuid"; function App({ db }: { db: SDK }) { - const [entities, setEntities] = useState[]>([]); + const state = useGameState((state) => state); + const entities = useGameState((state) => state.entities); useEffect(() => { let unsubscribe: (() => void) | undefined; @@ -28,15 +32,7 @@ function App({ db }: { db: SDK }) { response.data && response.data[0].entityId !== "0x0" ) { - console.log(response.data); - setEntities((prevEntities) => { - return prevEntities.map((entity) => { - const newEntity = response.data?.find( - (e) => e.entityId === entity.entityId - ); - return newEntity ? newEntity : entity; - }); - }); + state.setEntities(response.data); } }, { logging: true } @@ -54,8 +50,6 @@ function App({ db }: { db: SDK }) { }; }, [db]); - console.log("entities:"); - useEffect(() => { const fetchEntities = async () => { try { @@ -76,23 +70,7 @@ function App({ db }: { db: SDK }) { return; } if (resp.data) { - console.log(resp.data); - setEntities((prevEntities) => { - const updatedEntities = [...prevEntities]; - resp.data?.forEach((newEntity) => { - const index = updatedEntities.findIndex( - (entity) => - entity.entityId === - newEntity.entityId - ); - if (index !== -1) { - updatedEntities[index] = newEntity; - } else { - updatedEntities.push(newEntity); - } - }); - return updatedEntities; - }); + state.setEntities(resp.data); } } ); @@ -104,12 +82,45 @@ function App({ db }: { db: SDK }) { fetchEntities(); }, [db]); + const optimisticUpdate = async () => { + const entityId = + "0x571368d35c8fe136adf81eecf96a72859c43de7efd8fdd3d6f0d17e308df984"; + + const transactionId = uuidv4(); + + state.applyOptimisticUpdate(transactionId, (draft) => { + draft.entities[entityId].models.dojo_starter.Moves!.remaining = 10; + }); + + try { + // Wait for the entity to be updated before full resolving the transaction. Reverts if the condition is not met. + const updatedEntity = await state.waitForEntityChange( + entityId, + (entity) => { + // Define your specific condition here + return entity?.models.dojo_starter.Moves?.can_move === true; + } + ); + + console.log("Entity has been updated to active:", updatedEntity); + + console.log("Updating entities..."); + } catch (error) { + console.error("Error updating entities:", error); + state.revertOptimisticUpdate(transactionId); + } finally { + console.log("Updating entities..."); + state.confirmTransaction(transactionId); + } + }; + return (

Game State

- {entities.map((entity) => ( -
-

Entity {entity.entityId}

+ + {Object.entries(entities).map(([entityId, entity]) => ( +
+

Entity {entityId}

Position

Player:{" "} diff --git a/examples/example-vite-react-sdk/src/state.ts b/examples/example-vite-react-sdk/src/state.ts new file mode 100644 index 00000000..4237b5c3 --- /dev/null +++ b/examples/example-vite-react-sdk/src/state.ts @@ -0,0 +1,118 @@ +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import { Draft, Patch, applyPatches, produceWithPatches } from "immer"; +import { ParsedEntity } from "@dojoengine/sdk"; +import { Schema } from "./bindings"; + +import { enablePatches } from "immer"; +enablePatches(); + +interface PendingTransaction { + transactionId: string; + patches: Patch[]; + inversePatches: Patch[]; +} + +interface GameState { + entities: Record>; + pendingTransactions: Record; + setEntities: (entities: ParsedEntity[]) => void; + updateEntity: (entity: ParsedEntity) => void; + applyOptimisticUpdate: ( + transactionId: string, + updateFn: (draft: Draft) => void + ) => void; + revertOptimisticUpdate: (transactionId: string) => void; + confirmTransaction: (transactionId: string) => void; + subscribeToEntity: ( + entityId: string, + listener: (entity: ParsedEntity | undefined) => void + ) => () => void; + waitForEntityChange: ( + entityId: string, + predicate: (entity: ParsedEntity | undefined) => boolean, + timeout?: number + ) => Promise | undefined>; +} + +export const useGameState = create()( + immer((set, get) => ({ + entities: {}, + pendingTransactions: {}, + setEntities: (entities: ParsedEntity[]) => { + set((state) => { + entities.forEach((entity) => { + state.entities[entity.entityId] = entity; + }); + }); + }, + updateEntity: (entity: ParsedEntity) => { + set((state) => { + state.entities[entity.entityId] = entity; + }); + }, + applyOptimisticUpdate: (transactionId, updateFn) => { + const currentState = get(); + const [nextState, patches, inversePatches] = produceWithPatches( + currentState, + (draft) => { + updateFn(draft); + } + ); + + set(() => nextState); + + set((state) => { + state.pendingTransactions[transactionId] = { + transactionId, + patches, + inversePatches, + }; + }); + }, + revertOptimisticUpdate: (transactionId) => { + const transaction = get().pendingTransactions[transactionId]; + if (transaction) { + set((state) => applyPatches(state, transaction.inversePatches)); + set((state) => { + delete state.pendingTransactions[transactionId]; + }); + } + }, + confirmTransaction: (transactionId) => { + set((state) => { + delete state.pendingTransactions[transactionId]; + }); + }, + subscribeToEntity: (entityId, listener): (() => void) => { + const unsubscribe: () => void = useGameState.subscribe((state) => { + const entity = state.entities[entityId]; + listener(entity); + }); + return unsubscribe; + }, + waitForEntityChange: (entityId, predicate, timeout = 6000) => { + return new Promise | undefined>( + (resolve, reject) => { + const unsubscribe = useGameState.subscribe((state) => { + const entity = state.entities[entityId]; + if (predicate(entity)) { + clearTimeout(timer); + unsubscribe(); + resolve(entity); + } + }); + + const timer = setTimeout(() => { + unsubscribe(); + reject( + new Error( + `waitForEntityChange: Timeout of ${timeout}ms exceeded` + ) + ); + }, timeout); + } + ); + }, + })) +); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15638b5c..5ad07536 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -344,7 +344,7 @@ importers: version: 3.3.0(vite@4.5.5(@types/node@20.16.6)(terser@5.33.0)) zustand: specifier: ^4.5.5 - version: 4.5.5(@types/react@18.3.9)(react@18.3.1) + version: 4.5.5(@types/react@18.3.9)(immer@10.1.1)(react@18.3.1) devDependencies: '@types/node': specifier: ^20.16.6 @@ -507,26 +507,38 @@ importers: examples/example-vite-react-sdk: dependencies: '@dojoengine/core': - specifier: 1.0.0-alpha.12 - version: 1.0.0-alpha.12(starknet@6.11.0(encoding@0.1.13))(typescript@5.6.2) + specifier: workspace:* + version: link:../../packages/core '@dojoengine/sdk': specifier: workspace:* version: link:../../packages/sdk '@dojoengine/torii-wasm': - specifier: 1.0.0-alpha.12 - version: 1.0.0-alpha.12 + specifier: workspace:* + version: link:../../packages/torii-wasm + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + immer: + specifier: ^10.1.1 + version: 10.1.1 react: specifier: ^18.3.1 version: 18.3.1 react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + uuid: + specifier: ^10.0.0 + version: 10.0.0 vite-plugin-top-level-await: specifier: ^1.4.4 version: 1.4.4(rollup@4.22.4)(vite@5.4.7(@types/node@22.6.1)(terser@5.33.0)) vite-plugin-wasm: specifier: ^3.3.0 version: 3.3.0(vite@5.4.7(@types/node@22.6.1)(terser@5.33.0)) + zustand: + specifier: ^4.5.5 + version: 4.5.5(@types/react@18.3.9)(immer@10.1.1)(react@18.3.1) devDependencies: '@eslint/js': specifier: ^9.11.1 @@ -614,7 +626,7 @@ importers: version: 1.1.0(@types/react@18.3.9)(react@18.3.1) '@react-three/drei': specifier: ^9.114.0 - version: 9.114.0(@react-three/fiber@8.17.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.160.1))(@types/react@18.3.9)(@types/three@0.160.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.160.1) + version: 9.114.0(@react-three/fiber@8.17.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.160.1))(@types/react@18.3.9)(@types/three@0.160.0)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.160.1) '@react-three/fiber': specifier: ^8.17.8 version: 8.17.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.160.1) @@ -695,7 +707,7 @@ importers: version: 3.3.0(vite@4.5.5(@types/node@20.16.6)(terser@5.33.0)) zustand: specifier: ^4.5.5 - version: 4.5.5(@types/react@18.3.9)(react@18.3.1) + version: 4.5.5(@types/react@18.3.9)(immer@10.1.1)(react@18.3.1) devDependencies: '@storybook/addon-essentials': specifier: ^7.6.20 @@ -1000,7 +1012,7 @@ importers: version: 2.19.0 zustand: specifier: ^4.5.5 - version: 4.5.5(@types/react@18.3.9)(react@18.3.1) + version: 4.5.5(@types/react@18.3.9)(immer@10.1.1)(react@18.3.1) devDependencies: '@babel/core': specifier: ^7.25.2 @@ -1049,7 +1061,7 @@ importers: version: 3.3.0(vite@3.2.11(@types/node@22.6.1)(terser@5.33.0)) zustand: specifier: ^4.5.5 - version: 4.5.5(@types/react@18.3.9)(react@18.3.1) + version: 4.5.5(@types/react@18.3.9)(immer@10.1.1)(react@18.3.1) devDependencies: '@vitest/coverage-v8': specifier: ^1.6.0 @@ -1092,7 +1104,7 @@ importers: version: 1.6.0(@types/node@22.6.1)(jsdom@24.1.3)(terser@5.33.0) zustand: specifier: ^4.5.5 - version: 4.5.5(@types/react@18.3.9)(react@18.3.1) + version: 4.5.5(@types/react@18.3.9)(immer@10.1.1)(react@18.3.1) devDependencies: tsup: specifier: ^8.3.0 @@ -1995,18 +2007,9 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} - '@dojoengine/core@1.0.0-alpha.12': - resolution: {integrity: sha512-KuinebMRPrsGebpQqW8oXVYRaCiUOdngjr4vN6WaWkUcyvFRfeJqzCemQdgqtOwvZZKjo6UIQNvYWPsuzjcxnA==} - hasBin: true - peerDependencies: - starknet: 6.11.0 - '@dojoengine/recs@2.0.13': resolution: {integrity: sha512-Cgz4Unlnk2FSDoFTYKrJexX/KiSYPMFMxftxQkC+9LUKS5yNGkgFQM7xu4/L1HvpDAenL7NjUmH6ynRAS7Iifw==} - '@dojoengine/torii-wasm@1.0.0-alpha.12': - resolution: {integrity: sha512-GiPlaJkSqjpCzN42xv6F0zv1UJLUcIthiwU8LQYU82DCVqKkODvd/ad0YH00PQ2pB/ILEiMvoJQUQXP108yFqQ==} - '@emnapi/core@1.2.0': resolution: {integrity: sha512-E7Vgw78I93we4ZWdYCb4DGAwRROGkMIXk7/y87UmANR+J6qsWusmC3gLt0H+O0KOt5e6O38U8oJamgbudrES/w==} @@ -4906,6 +4909,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} @@ -7409,6 +7415,9 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + immutable@3.7.6: resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==} engines: {node: '>=0.8.0'} @@ -12609,16 +12618,6 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} - '@dojoengine/core@1.0.0-alpha.12(starknet@6.11.0(encoding@0.1.13))(typescript@5.6.2)': - dependencies: - '@dojoengine/recs': 2.0.13(typescript@5.6.2)(zod@3.23.8) - starknet: 6.11.0(encoding@0.1.13) - zod: 3.23.8 - transitivePeerDependencies: - - bufferutil - - typescript - - utf-8-validate - '@dojoengine/recs@2.0.13(typescript@5.6.2)(zod@3.23.8)': dependencies: '@latticexyz/schema-type': 2.0.12(typescript@5.6.2)(zod@3.23.8) @@ -12631,8 +12630,6 @@ snapshots: - utf-8-validate - zod - '@dojoengine/torii-wasm@1.0.0-alpha.12': {} - '@emnapi/core@1.2.0': dependencies: '@emnapi/wasi-threads': 1.0.1 @@ -15062,7 +15059,7 @@ snapshots: '@react-spring/types@9.6.1': {} - '@react-three/drei@9.114.0(@react-three/fiber@8.17.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.160.1))(@types/react@18.3.9)(@types/three@0.160.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.160.1)': + '@react-three/drei@9.114.0(@react-three/fiber@8.17.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.160.1))(@types/react@18.3.9)(@types/three@0.160.0)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.160.1)': dependencies: '@babel/runtime': 7.25.6 '@mediapipe/tasks-vision': 0.10.8 @@ -15086,7 +15083,7 @@ snapshots: three-mesh-bvh: 0.7.8(three@0.160.1) three-stdlib: 2.33.0(three@0.160.1) troika-three-text: 0.49.1(three@0.160.1) - tunnel-rat: 0.1.2(@types/react@18.3.9)(react@18.3.1) + tunnel-rat: 0.1.2(@types/react@18.3.9)(immer@10.1.1)(react@18.3.1) utility-types: 3.11.0 uuid: 9.0.1 zustand: 3.7.2(react@18.3.1) @@ -16459,6 +16456,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/uuid@10.0.0': {} + '@types/uuid@9.0.8': {} '@types/web@0.0.114': {} @@ -19524,6 +19523,8 @@ snapshots: immediate@3.0.6: {} + immer@10.1.1: {} + immutable@3.7.6: {} immutable@4.3.7: {} @@ -22714,9 +22715,9 @@ snapshots: dependencies: safe-buffer: 5.2.1 - tunnel-rat@0.1.2(@types/react@18.3.9)(react@18.3.1): + tunnel-rat@0.1.2(@types/react@18.3.9)(immer@10.1.1)(react@18.3.1): dependencies: - zustand: 4.5.5(@types/react@18.3.9)(react@18.3.1) + zustand: 4.5.5(@types/react@18.3.9)(immer@10.1.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer @@ -23734,11 +23735,12 @@ snapshots: optionalDependencies: react: 18.3.1 - zustand@4.5.5(@types/react@18.3.9)(react@18.3.1): + zustand@4.5.5(@types/react@18.3.9)(immer@10.1.1)(react@18.3.1): dependencies: use-sync-external-store: 1.2.2(react@18.3.1) optionalDependencies: '@types/react': 18.3.9 + immer: 10.1.1 react: 18.3.1 zwitch@2.0.4: {}