-
Notifications
You must be signed in to change notification settings - Fork 52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: draft zustand state management system #280
Changes from 3 commits
2d8cb57
46b79fc
03a5689
89804b5
2398139
aa6d5df
cad1567
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,10 +1,15 @@ | ||||||||||||||||||||||||
import { useEffect, useState } from "react"; | ||||||||||||||||||||||||
import { useEffect } from "react"; | ||||||||||||||||||||||||
import "./App.css"; | ||||||||||||||||||||||||
import { ParsedEntity, SDK } from "@dojoengine/sdk"; | ||||||||||||||||||||||||
import { SDK, createDojoStore } from "@dojoengine/sdk"; | ||||||||||||||||||||||||
import { Schema } from "./bindings.ts"; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
import { v4 as uuidv4 } from "uuid"; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
export const useDojoStore = createDojoStore<Schema>(); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
function App({ db }: { db: SDK<Schema> }) { | ||||||||||||||||||||||||
const [entities, setEntities] = useState<ParsedEntity<Schema>[]>([]); | ||||||||||||||||||||||||
const state = useDojoStore((state) => state); | ||||||||||||||||||||||||
const entities = useDojoStore((state) => state.entities); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||||
let unsubscribe: (() => void) | undefined; | ||||||||||||||||||||||||
|
@@ -28,15 +33,7 @@ function App({ db }: { db: SDK<Schema> }) { | |||||||||||||||||||||||
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 +51,6 @@ function App({ db }: { db: SDK<Schema> }) { | |||||||||||||||||||||||
}; | ||||||||||||||||||||||||
}, [db]); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
console.log("entities:"); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||||
const fetchEntities = async () => { | ||||||||||||||||||||||||
try { | ||||||||||||||||||||||||
|
@@ -76,23 +71,7 @@ function App({ db }: { db: SDK<Schema> }) { | |||||||||||||||||||||||
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,20 +83,55 @@ function App({ db }: { db: SDK<Schema> }) { | |||||||||||||||||||||||
fetchEntities(); | ||||||||||||||||||||||||
}, [db]); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const optimisticUpdate = async () => { | ||||||||||||||||||||||||
const entityId = | ||||||||||||||||||||||||
"0x571368d35c8fe136adf81eecf96a72859c43de7efd8fdd3d6f0d17e308df984"; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Comment on lines
+87
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid hardcoding the Hardcoding the |
||||||||||||||||||||||||
const transactionId = uuidv4(); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
state.applyOptimisticUpdate(transactionId, (draft) => { | ||||||||||||||||||||||||
draft.entities[entityId].models.dojo_starter.Moves!.remaining = 10; | ||||||||||||||||||||||||
}); | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure entity exists before applying optimistic update When applying the optimistic update, ensure that Suggested fix: state.applyOptimisticUpdate(transactionId, (draft) => {
+ if (draft.entities[entityId]) {
draft.entities[entityId].models.dojo_starter.Moves!.remaining = 10;
+ }
}); 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
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; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
Comment on lines
+101
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Define the specific condition for The condition in Example: return entity?.models.dojo_starter.Moves?.can_move === true;
- // Define your specific condition here
+ // Condition: Checks if the 'can_move' flag is 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); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
Comment on lines
+112
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Review transaction confirmation placement Calling Suggested change: } catch (error) {
console.error("Error updating entities:", error);
state.revertOptimisticUpdate(transactionId);
- } finally {
+ } finally {
console.log("Updating entities...");
- state.confirmTransaction(transactionId);
}
+ state.confirmTransaction(transactionId); 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||
<div> | ||||||||||||||||||||||||
<h1>Game State</h1> | ||||||||||||||||||||||||
{entities.map((entity) => ( | ||||||||||||||||||||||||
<div key={entity.entityId}> | ||||||||||||||||||||||||
<h2>Entity {entity.entityId}</h2> | ||||||||||||||||||||||||
<button onClick={optimisticUpdate}>update</button> | ||||||||||||||||||||||||
{Object.entries(entities).map(([entityId, entity]) => ( | ||||||||||||||||||||||||
<div key={entityId}> | ||||||||||||||||||||||||
<h2>Entity {entityId}</h2> | ||||||||||||||||||||||||
<h3>Position</h3> | ||||||||||||||||||||||||
<p> | ||||||||||||||||||||||||
Player:{" "} | ||||||||||||||||||||||||
{entity.models.dojo_starter.Position?.player ?? "N/A"} | ||||||||||||||||||||||||
<br /> | ||||||||||||||||||||||||
X: {entity.models.dojo_starter.Position?.vec.x ?? "N/A"} | ||||||||||||||||||||||||
X:{" "} | ||||||||||||||||||||||||
{entity.models.dojo_starter.Position?.vec?.x ?? "N/A"} | ||||||||||||||||||||||||
<br /> | ||||||||||||||||||||||||
Y: {entity.models.dojo_starter.Position?.vec.y ?? "N/A"} | ||||||||||||||||||||||||
Y:{" "} | ||||||||||||||||||||||||
{entity.models.dojo_starter.Position?.vec?.y ?? "N/A"} | ||||||||||||||||||||||||
</p> | ||||||||||||||||||||||||
<h3>Moves</h3> | ||||||||||||||||||||||||
<p> | ||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ import { SchemaType, SDK, UnionOfModelData } from "./types"; | |
import { Account, Signature, StarknetDomain, TypedData } from "starknet"; | ||
|
||
export * from "./types"; | ||
export * from "./state"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Codebase verification Issue Detected: Missing The export statement
🔗 Analysis chainLGTM! Verify for potential naming conflicts. The new export statement is consistent with the PR objective of implementing a state management system. It appropriately exposes the state module's functionality to SDK consumers. To ensure there are no naming conflicts, please run the following script: 🏁 Scripts executedThe following scripts were executed for the analysis: Script: #!/bin/bash
# Description: Check for potential naming conflicts between the new state exports and existing exports
# Get existing exports
existing_exports=$(rg -o --no-filename 'export \{.*?\}' packages/sdk/src/index.ts | tr -d '{}' | tr ',' '\n' | sed 's/^ *//g')
# Get exports from the state module
state_exports=$(rg -o --no-filename 'export .*' packages/sdk/src/state.ts | sed 's/export //g')
# Check for conflicts
conflicts=$(comm -12 <(echo "$existing_exports" | sort) <(echo "$state_exports" | sort))
if [ -n "$conflicts" ]; then
echo "Potential naming conflicts detected:"
echo "$conflicts"
else
echo "No naming conflicts detected."
fi
Length of output: 543 |
||
|
||
interface SDKConfig { | ||
client: torii.ClientConfig; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./zustand"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import { create } from "zustand"; | ||
import { immer } from "zustand/middleware/immer"; | ||
import { | ||
Draft, | ||
Patch, | ||
WritableDraft, | ||
applyPatches, | ||
produceWithPatches, | ||
} from "immer"; | ||
|
||
import { enablePatches } from "immer"; | ||
import { subscribeWithSelector } from "zustand/middleware"; | ||
import { ParsedEntity, SchemaType } from "../types"; | ||
|
||
enablePatches(); | ||
|
||
interface PendingTransaction { | ||
transactionId: string; | ||
patches: Patch[]; | ||
inversePatches: Patch[]; | ||
} | ||
|
||
interface GameState<T extends SchemaType> { | ||
entities: Record<string, ParsedEntity<T>>; | ||
pendingTransactions: Record<string, PendingTransaction>; | ||
setEntities: (entities: ParsedEntity<T>[]) => void; | ||
updateEntity: (entity: Partial<ParsedEntity<T>>) => void; | ||
applyOptimisticUpdate: ( | ||
transactionId: string, | ||
updateFn: (draft: Draft<GameState<T>>) => void | ||
) => void; | ||
revertOptimisticUpdate: (transactionId: string) => void; | ||
confirmTransaction: (transactionId: string) => void; | ||
subscribeToEntity: ( | ||
entityId: string, | ||
listener: (entity: ParsedEntity<T> | undefined) => void | ||
) => () => void; | ||
waitForEntityChange: ( | ||
entityId: string, | ||
predicate: (entity: ParsedEntity<T> | undefined) => boolean, | ||
timeout?: number | ||
) => Promise<ParsedEntity<T> | undefined>; | ||
} | ||
|
||
/** | ||
* Factory function to create a Zustand store based on a given SchemaType. | ||
* | ||
* @template T - The schema type. | ||
* @returns A Zustand hook tailored to the provided schema. | ||
*/ | ||
export function createDojoStore<T extends SchemaType>() { | ||
const useStore = create<GameState<T>>()( | ||
subscribeWithSelector( | ||
immer((set, get) => ({ | ||
entities: {}, | ||
pendingTransactions: {}, | ||
setEntities: (entities: ParsedEntity<T>[]) => { | ||
set((state: Draft<GameState<T>>) => { | ||
entities.forEach((entity) => { | ||
state.entities[entity.entityId] = | ||
entity as WritableDraft<ParsedEntity<T>>; | ||
}); | ||
}); | ||
}, | ||
updateEntity: (entity: Partial<ParsedEntity<T>>) => { | ||
set((state: Draft<GameState<T>>) => { | ||
if ( | ||
entity.entityId && | ||
state.entities[entity.entityId] | ||
) { | ||
Object.assign( | ||
state.entities[entity.entityId], | ||
entity | ||
); | ||
} | ||
}); | ||
}, | ||
Comment on lines
+73
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Handle the case where an entity does not exist in the state In the |
||
applyOptimisticUpdate: (transactionId, updateFn) => { | ||
const currentState = get(); | ||
const [nextState, patches, inversePatches] = | ||
produceWithPatches( | ||
currentState, | ||
(draftState: Draft<GameState<T>>) => { | ||
updateFn(draftState); | ||
} | ||
); | ||
|
||
set(() => nextState); | ||
|
||
set((state: Draft<GameState<T>>) => { | ||
state.pendingTransactions[transactionId] = { | ||
transactionId, | ||
patches, | ||
inversePatches, | ||
}; | ||
}); | ||
}, | ||
revertOptimisticUpdate: (transactionId) => { | ||
const transaction = | ||
get().pendingTransactions[transactionId]; | ||
if (transaction) { | ||
set((state: Draft<GameState<T>>) => | ||
applyPatches(state, transaction.inversePatches) | ||
); | ||
set((state: Draft<GameState<T>>) => { | ||
delete state.pendingTransactions[transactionId]; | ||
}); | ||
} | ||
}, | ||
Comment on lines
+106
to
+117
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for missing transactions in When a transaction ID is not found in |
||
confirmTransaction: (transactionId) => { | ||
set((state: Draft<GameState<T>>) => { | ||
delete state.pendingTransactions[transactionId]; | ||
}); | ||
}, | ||
subscribeToEntity: (entityId, listener): (() => void) => { | ||
return useStore.subscribe((state) => { | ||
const entity = state.entities[entityId]; | ||
listener(entity); | ||
}); | ||
}, | ||
Comment on lines
+123
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Clarify the use of The methods |
||
waitForEntityChange: (entityId, predicate, timeout = 6000) => { | ||
return new Promise<ParsedEntity<T> | undefined>( | ||
(resolve, reject) => { | ||
const unsubscribe = useStore.subscribe( | ||
(state) => state.entities[entityId], | ||
(entity) => { | ||
if (predicate(entity)) { | ||
clearTimeout(timer); | ||
unsubscribe(); | ||
resolve(entity); | ||
} | ||
} | ||
); | ||
|
||
const timer = setTimeout(() => { | ||
unsubscribe(); | ||
reject( | ||
new Error( | ||
`waitForEntityChange: Timeout of ${timeout}ms exceeded` | ||
) | ||
); | ||
}, timeout); | ||
} | ||
); | ||
}, | ||
})) | ||
) | ||
); | ||
|
||
return useStore; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider optimizing state selection
While using
useDojoStore
for state management is a good approach, selecting the entire state on line 11 might lead to unnecessary re-renders. Consider selecting only the specific parts of the state that this component needs.Example:
This way, the component will only re-render when these specific parts of the state change.