Skip to content

Commit

Permalink
feat: draft zustand state management system
Browse files Browse the repository at this point in the history
  • Loading branch information
ponderingdemocritus committed Sep 26, 2024
1 parent 69662d1 commit 2d8cb57
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 73 deletions.
10 changes: 7 additions & 3 deletions examples/example-vite-react-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
79 changes: 45 additions & 34 deletions examples/example-vite-react-sdk/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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<Schema> }) {
const [entities, setEntities] = useState<ParsedEntity<Schema>[]>([]);
const state = useGameState((state) => state);
const entities = useGameState((state) => state.entities);

useEffect(() => {
let unsubscribe: (() => void) | undefined;
Expand All @@ -28,15 +32,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 }
Expand All @@ -54,8 +50,6 @@ function App({ db }: { db: SDK<Schema> }) {
};
}, [db]);

console.log("entities:");

useEffect(() => {
const fetchEntities = async () => {
try {
Expand All @@ -76,23 +70,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);
}
}
);
Expand All @@ -104,12 +82,45 @@ function App({ db }: { db: SDK<Schema> }) {
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 (
<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:{" "}
Expand Down
118 changes: 118 additions & 0 deletions examples/example-vite-react-sdk/src/state.ts
Original file line number Diff line number Diff line change
@@ -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<string, ParsedEntity<Schema>>;
pendingTransactions: Record<string, PendingTransaction>;
setEntities: (entities: ParsedEntity<Schema>[]) => void;
updateEntity: (entity: ParsedEntity<Schema>) => void;
applyOptimisticUpdate: (
transactionId: string,
updateFn: (draft: Draft<GameState>) => void
) => void;
revertOptimisticUpdate: (transactionId: string) => void;
confirmTransaction: (transactionId: string) => void;
subscribeToEntity: (
entityId: string,
listener: (entity: ParsedEntity<Schema> | undefined) => void
) => () => void;
waitForEntityChange: (
entityId: string,
predicate: (entity: ParsedEntity<Schema> | undefined) => boolean,
timeout?: number
) => Promise<ParsedEntity<Schema> | undefined>;
}

export const useGameState = create<GameState>()(
immer((set, get) => ({
entities: {},
pendingTransactions: {},
setEntities: (entities: ParsedEntity<Schema>[]) => {
set((state) => {
entities.forEach((entity) => {
state.entities[entity.entityId] = entity;
});
});
},
updateEntity: (entity: ParsedEntity<Schema>) => {
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<ParsedEntity<Schema> | 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);
}
);
},
}))
);
Loading

0 comments on commit 2d8cb57

Please sign in to comment.