Skip to content

Commit 2d8cb57

Browse files
feat: draft zustand state management system
1 parent 69662d1 commit 2d8cb57

File tree

4 files changed

+208
-73
lines changed

4 files changed

+208
-73
lines changed

examples/example-vite-react-sdk/package.json

+7-3
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13-
"@dojoengine/core": "1.0.0-alpha.12",
13+
"@dojoengine/core": "workspace:*",
1414
"@dojoengine/sdk": "workspace:*",
15-
"@dojoengine/torii-wasm": "1.0.0-alpha.12",
15+
"@dojoengine/torii-wasm": "workspace:*",
16+
"@types/uuid": "^10.0.0",
17+
"immer": "^10.1.1",
1618
"react": "^18.3.1",
1719
"react-dom": "^18.3.1",
20+
"uuid": "^10.0.0",
1821
"vite-plugin-top-level-await": "^1.4.4",
19-
"vite-plugin-wasm": "^3.3.0"
22+
"vite-plugin-wasm": "^3.3.0",
23+
"zustand": "^4.5.5"
2024
},
2125
"devDependencies": {
2226
"@eslint/js": "^9.11.1",

examples/example-vite-react-sdk/src/App.tsx

+45-34
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import { useEffect, useState } from "react";
1+
import { useEffect } from "react";
22
import "./App.css";
3-
import { ParsedEntity, SDK } from "@dojoengine/sdk";
3+
import { SDK } from "@dojoengine/sdk";
44
import { Schema } from "./bindings.ts";
5+
import { useGameState } from "./state.ts";
6+
7+
import { v4 as uuidv4 } from "uuid";
58

69
function App({ db }: { db: SDK<Schema> }) {
7-
const [entities, setEntities] = useState<ParsedEntity<Schema>[]>([]);
10+
const state = useGameState((state) => state);
11+
const entities = useGameState((state) => state.entities);
812

913
useEffect(() => {
1014
let unsubscribe: (() => void) | undefined;
@@ -28,15 +32,7 @@ function App({ db }: { db: SDK<Schema> }) {
2832
response.data &&
2933
response.data[0].entityId !== "0x0"
3034
) {
31-
console.log(response.data);
32-
setEntities((prevEntities) => {
33-
return prevEntities.map((entity) => {
34-
const newEntity = response.data?.find(
35-
(e) => e.entityId === entity.entityId
36-
);
37-
return newEntity ? newEntity : entity;
38-
});
39-
});
35+
state.setEntities(response.data);
4036
}
4137
},
4238
{ logging: true }
@@ -54,8 +50,6 @@ function App({ db }: { db: SDK<Schema> }) {
5450
};
5551
}, [db]);
5652

57-
console.log("entities:");
58-
5953
useEffect(() => {
6054
const fetchEntities = async () => {
6155
try {
@@ -76,23 +70,7 @@ function App({ db }: { db: SDK<Schema> }) {
7670
return;
7771
}
7872
if (resp.data) {
79-
console.log(resp.data);
80-
setEntities((prevEntities) => {
81-
const updatedEntities = [...prevEntities];
82-
resp.data?.forEach((newEntity) => {
83-
const index = updatedEntities.findIndex(
84-
(entity) =>
85-
entity.entityId ===
86-
newEntity.entityId
87-
);
88-
if (index !== -1) {
89-
updatedEntities[index] = newEntity;
90-
} else {
91-
updatedEntities.push(newEntity);
92-
}
93-
});
94-
return updatedEntities;
95-
});
73+
state.setEntities(resp.data);
9674
}
9775
}
9876
);
@@ -104,12 +82,45 @@ function App({ db }: { db: SDK<Schema> }) {
10482
fetchEntities();
10583
}, [db]);
10684

85+
const optimisticUpdate = async () => {
86+
const entityId =
87+
"0x571368d35c8fe136adf81eecf96a72859c43de7efd8fdd3d6f0d17e308df984";
88+
89+
const transactionId = uuidv4();
90+
91+
state.applyOptimisticUpdate(transactionId, (draft) => {
92+
draft.entities[entityId].models.dojo_starter.Moves!.remaining = 10;
93+
});
94+
95+
try {
96+
// Wait for the entity to be updated before full resolving the transaction. Reverts if the condition is not met.
97+
const updatedEntity = await state.waitForEntityChange(
98+
entityId,
99+
(entity) => {
100+
// Define your specific condition here
101+
return entity?.models.dojo_starter.Moves?.can_move === true;
102+
}
103+
);
104+
105+
console.log("Entity has been updated to active:", updatedEntity);
106+
107+
console.log("Updating entities...");
108+
} catch (error) {
109+
console.error("Error updating entities:", error);
110+
state.revertOptimisticUpdate(transactionId);
111+
} finally {
112+
console.log("Updating entities...");
113+
state.confirmTransaction(transactionId);
114+
}
115+
};
116+
107117
return (
108118
<div>
109119
<h1>Game State</h1>
110-
{entities.map((entity) => (
111-
<div key={entity.entityId}>
112-
<h2>Entity {entity.entityId}</h2>
120+
<button onClick={optimisticUpdate}>update</button>
121+
{Object.entries(entities).map(([entityId, entity]) => (
122+
<div key={entityId}>
123+
<h2>Entity {entityId}</h2>
113124
<h3>Position</h3>
114125
<p>
115126
Player:{" "}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { create } from "zustand";
2+
import { immer } from "zustand/middleware/immer";
3+
import { Draft, Patch, applyPatches, produceWithPatches } from "immer";
4+
import { ParsedEntity } from "@dojoengine/sdk";
5+
import { Schema } from "./bindings";
6+
7+
import { enablePatches } from "immer";
8+
enablePatches();
9+
10+
interface PendingTransaction {
11+
transactionId: string;
12+
patches: Patch[];
13+
inversePatches: Patch[];
14+
}
15+
16+
interface GameState {
17+
entities: Record<string, ParsedEntity<Schema>>;
18+
pendingTransactions: Record<string, PendingTransaction>;
19+
setEntities: (entities: ParsedEntity<Schema>[]) => void;
20+
updateEntity: (entity: ParsedEntity<Schema>) => void;
21+
applyOptimisticUpdate: (
22+
transactionId: string,
23+
updateFn: (draft: Draft<GameState>) => void
24+
) => void;
25+
revertOptimisticUpdate: (transactionId: string) => void;
26+
confirmTransaction: (transactionId: string) => void;
27+
subscribeToEntity: (
28+
entityId: string,
29+
listener: (entity: ParsedEntity<Schema> | undefined) => void
30+
) => () => void;
31+
waitForEntityChange: (
32+
entityId: string,
33+
predicate: (entity: ParsedEntity<Schema> | undefined) => boolean,
34+
timeout?: number
35+
) => Promise<ParsedEntity<Schema> | undefined>;
36+
}
37+
38+
export const useGameState = create<GameState>()(
39+
immer((set, get) => ({
40+
entities: {},
41+
pendingTransactions: {},
42+
setEntities: (entities: ParsedEntity<Schema>[]) => {
43+
set((state) => {
44+
entities.forEach((entity) => {
45+
state.entities[entity.entityId] = entity;
46+
});
47+
});
48+
},
49+
updateEntity: (entity: ParsedEntity<Schema>) => {
50+
set((state) => {
51+
state.entities[entity.entityId] = entity;
52+
});
53+
},
54+
applyOptimisticUpdate: (transactionId, updateFn) => {
55+
const currentState = get();
56+
const [nextState, patches, inversePatches] = produceWithPatches(
57+
currentState,
58+
(draft) => {
59+
updateFn(draft);
60+
}
61+
);
62+
63+
set(() => nextState);
64+
65+
set((state) => {
66+
state.pendingTransactions[transactionId] = {
67+
transactionId,
68+
patches,
69+
inversePatches,
70+
};
71+
});
72+
},
73+
revertOptimisticUpdate: (transactionId) => {
74+
const transaction = get().pendingTransactions[transactionId];
75+
if (transaction) {
76+
set((state) => applyPatches(state, transaction.inversePatches));
77+
set((state) => {
78+
delete state.pendingTransactions[transactionId];
79+
});
80+
}
81+
},
82+
confirmTransaction: (transactionId) => {
83+
set((state) => {
84+
delete state.pendingTransactions[transactionId];
85+
});
86+
},
87+
subscribeToEntity: (entityId, listener): (() => void) => {
88+
const unsubscribe: () => void = useGameState.subscribe((state) => {
89+
const entity = state.entities[entityId];
90+
listener(entity);
91+
});
92+
return unsubscribe;
93+
},
94+
waitForEntityChange: (entityId, predicate, timeout = 6000) => {
95+
return new Promise<ParsedEntity<Schema> | undefined>(
96+
(resolve, reject) => {
97+
const unsubscribe = useGameState.subscribe((state) => {
98+
const entity = state.entities[entityId];
99+
if (predicate(entity)) {
100+
clearTimeout(timer);
101+
unsubscribe();
102+
resolve(entity);
103+
}
104+
});
105+
106+
const timer = setTimeout(() => {
107+
unsubscribe();
108+
reject(
109+
new Error(
110+
`waitForEntityChange: Timeout of ${timeout}ms exceeded`
111+
)
112+
);
113+
}, timeout);
114+
}
115+
);
116+
},
117+
}))
118+
);

0 commit comments

Comments
 (0)