Skip to content

feat: optimistic example #293

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

Merged
merged 1 commit into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions examples/example-vite-react-sdk/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { addAddressPadding } from "starknet";
import { Models, Schema } from "./bindings.ts";
import { useDojo } from "./useDojo.tsx";
import useModel from "./useModel.tsx";
import { useSystemCalls } from "./useSystemCalls.ts";

export const useDojoStore = createDojoStore<Schema>();

Expand All @@ -17,6 +18,8 @@ function App({ db }: { db: SDK<Schema> }) {
const state = useDojoStore((state) => state);
const entities = useDojoStore((state) => state.entities);

const { spawn } = useSystemCalls();

const entityId = useMemo(
() => getEntityIdFromKeys([BigInt(account?.account.address)]),
[account?.account.address]
Expand Down Expand Up @@ -161,11 +164,7 @@ function App({ db }: { db: SDK<Schema> }) {
<div className="col-start-2">
<button
className="h-12 w-12 bg-gray-600 rounded-full shadow-md active:shadow-inner active:bg-gray-500 focus:outline-none text-2xl font-bold text-gray-200"
onClick={async () =>
await client.actions.spawn({
account: account.account,
})
}
onClick={async () => await spawn()}
>
+
</button>
Expand Down
62 changes: 62 additions & 0 deletions examples/example-vite-react-sdk/src/useSystemCalls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { getEntityIdFromKeys } from "@dojoengine/utils";
import { useDojoStore } from "./App";
import { useDojo } from "./useDojo";
import { v4 as uuidv4 } from "uuid";

export const useSystemCalls = () => {
const state = useDojoStore((state) => state);

const {
setup: { client },
account: { account },
} = useDojo();

const generateEntityId = () => {
return getEntityIdFromKeys([BigInt(account?.address)]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential 'undefined' account in generateEntityId

In the generateEntityId function, calling BigInt(account?.address) may throw an error if account or account.address is undefined, since BigInt(undefined) will result in a TypeError.

Consider adding a check to ensure that account and account.address are defined before converting to BigInt.

Suggestion:

const generateEntityId = () => {
+    if (!account?.address) {
+        throw new Error('Account address is undefined');
+    }
    return getEntityIdFromKeys([BigInt(account.address)]);
};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return getEntityIdFromKeys([BigInt(account?.address)]);
if (!account?.address) {
throw new Error('Account address is undefined');
}
return getEntityIdFromKeys([BigInt(account.address)]);

};

const spawn = async () => {
// Generate a unique entity ID
const entityId = generateEntityId();

// Generate a unique transaction ID
const transactionId = uuidv4();

// The value to update the Moves model with
const remainingMoves = 100;

// Apply an optimistic update to the state
// this uses immer drafts to update the state
state.applyOptimisticUpdate(
transactionId,
(draft) =>
(draft.entities[entityId].models.dojo_starter.Moves!.remaining =
remainingMoves)
Comment on lines +33 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid assignment within an expression

Assigning a value within an expression can be confusing and may lead to unintended side effects. It's generally recommended to separate assignments from expressions for better readability.

Refactor the code to separate the assignment from the expression.

Suggestion:

state.applyOptimisticUpdate(
    transactionId,
-    (draft) =>
-        (draft.entities[entityId].models.dojo_starter.Moves!.remaining =
-            remainingMoves)
+    (draft) => {
+        draft.entities[entityId].models.dojo_starter.Moves!.remaining = remainingMoves;
+    }
);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(draft.entities[entityId].models.dojo_starter.Moves!.remaining =
remainingMoves)
state.applyOptimisticUpdate(
transactionId,
(draft) => {
draft.entities[entityId].models.dojo_starter.Moves!.remaining = remainingMoves;
}
);
🧰 Tools
🪛 Biome

[error] 33-34: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

);

try {
// Execute the spawn action from the client
await client.actions.spawn({ account });

// Wait for the entity to be updated with the new state
await state.waitForEntityChange(entityId, (entity) => {
return (
entity?.models?.dojo_starter?.Moves?.remaining ===
remainingMoves
);
});
} catch (error) {
// Revert the optimistic update if an error occurs
state.revertOptimisticUpdate(transactionId);
console.error("Error executing spawn:", error);
throw error;
} finally {
// Confirm the transaction if successful
state.confirmTransaction(transactionId);
}
Comment on lines +54 to +56
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Confirm transaction only on successful spawn

Currently, state.confirmTransaction(transactionId); is called in the finally block, which means it executes regardless of whether the spawn action succeeded or failed. This could lead to confirming a transaction that was reverted due to an error.

Move state.confirmTransaction(transactionId); to after the successful completion of the spawn action.

Suggestion:

            }
-       } catch (error) {
+           // Confirm the transaction if successful
+           state.confirmTransaction(transactionId);
+       } catch (error) {
            // Revert the optimistic update if an error occurs
            state.revertOptimisticUpdate(transactionId);
            console.error("Error executing spawn:", error);
            throw error;
-       } finally {
-           // Confirm the transaction if successful
-           state.confirmTransaction(transactionId);
        }

Committable suggestion was skipped due to low confidence.

};
Comment on lines +18 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure account is defined before proceeding in spawn

Within the spawn function, if account is undefined, calls to generateEntityId() and client.actions.spawn({ account }) may fail, leading to runtime errors.

Add a check at the beginning of the spawn function to ensure account is defined.

Suggestion:

const spawn = async () => {
+    if (!account) {
+        throw new Error('Account is undefined');
+    }
    // Generate a unique entity ID
    const entityId = generateEntityId();
    // ...
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const spawn = async () => {
// Generate a unique entity ID
const entityId = generateEntityId();
// Generate a unique transaction ID
const transactionId = uuidv4();
// The value to update the Moves model with
const remainingMoves = 100;
// Apply an optimistic update to the state
// this uses immer drafts to update the state
state.applyOptimisticUpdate(
transactionId,
(draft) =>
(draft.entities[entityId].models.dojo_starter.Moves!.remaining =
remainingMoves)
);
try {
// Execute the spawn action from the client
await client.actions.spawn({ account });
// Wait for the entity to be updated with the new state
await state.waitForEntityChange(entityId, (entity) => {
return (
entity?.models?.dojo_starter?.Moves?.remaining ===
remainingMoves
);
});
} catch (error) {
// Revert the optimistic update if an error occurs
state.revertOptimisticUpdate(transactionId);
console.error("Error executing spawn:", error);
throw error;
} finally {
// Confirm the transaction if successful
state.confirmTransaction(transactionId);
}
};
const spawn = async () => {
if (!account) {
throw new Error('Account is undefined');
}
// Generate a unique entity ID
const entityId = generateEntityId();
// Generate a unique transaction ID
const transactionId = uuidv4();
// The value to update the Moves model with
const remainingMoves = 100;
// Apply an optimistic update to the state
// this uses immer drafts to update the state
state.applyOptimisticUpdate(
transactionId,
(draft) =>
(draft.entities[entityId].models.dojo_starter.Moves!.remaining =
remainingMoves)
);
try {
// Execute the spawn action from the client
await client.actions.spawn({ account });
// Wait for the entity to be updated with the new state
await state.waitForEntityChange(entityId, (entity) => {
return (
entity?.models?.dojo_starter?.Moves?.remaining ===
remainingMoves
);
});
} catch (error) {
// Revert the optimistic update if an error occurs
state.revertOptimisticUpdate(transactionId);
console.error("Error executing spawn:", error);
throw error;
} finally {
// Confirm the transaction if successful
state.confirmTransaction(transactionId);
}
};
🧰 Tools
🪛 Biome

[error] 33-34: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


return {
spawn,
};
};
74 changes: 73 additions & 1 deletion packages/sdk/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,79 @@ const subscription = await sdk.subscribeEntityQuery(

## Optimistic Client Rendering

<!-- TODO -->
We use [immer](https://immerjs.github.io/immer/) for efficient optimistic rendering. This allows instant client-side entity state updates while awaiting blockchain confirmation.

The process:

1. Update entity state optimistically.
2. Wait for condition (e.g., a specific state change).
3. Resolve update, providing immediate user feedback.

This ensures a responsive user experience while maintaining blockchain data integrity.

See our [example project](../../examples/example-vite-react-sdk/src/useSystemCalls.ts) for a real-world implementation.

Note: You will need to have a subscription running in order for the update to resolve.

```typescript
export const useSystemCalls = () => {
const state = useDojoStore((state) => state);

const {
setup: { client },
account: { account },
} = useDojo();

const generateEntityId = () => {
return getEntityIdFromKeys([BigInt(account?.address)]);
};

const spawn = async () => {
// Generate a unique entity ID
const entityId = generateEntityId();

// Generate a unique transaction ID
const transactionId = uuidv4();

// The value to update
const remainingMoves = 100;

// Apply an optimistic update to the state
// this uses immer drafts to update the state
state.applyOptimisticUpdate(
transactionId,
(draft) =>
(draft.entities[entityId].models.dojo_starter.Moves!.remaining =
remainingMoves)
);

try {
// Execute the spawn action
await client.actions.spawn({ account });

// Wait for the entity to be updated with the new state
await state.waitForEntityChange(entityId, (entity) => {
return (
entity?.models?.dojo_starter?.Moves?.remaining ===
remainingMoves
);
});
} catch (error) {
// Revert the optimistic update if an error occurs
state.revertOptimisticUpdate(transactionId);
console.error("Error executing spawn:", error);
throw error;
} finally {
// Confirm the transaction if successful
state.confirmTransaction(transactionId);
}
};

return {
spawn,
};
};
```

# Advanced Usage

Expand Down
18 changes: 9 additions & 9 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ If you are not familiar with Dojo, then you should read the [book](https://book.

- [Quick start in 5 minutes](#quick-start-in-5-minutes)
- [Dojo SDK](#dojo-sdk)
- [Examples](#examples)
- [Examples](#all-examples)
- [All Packages](#all-packages)

## Quick start in 5 minutes
Expand Down Expand Up @@ -70,7 +70,7 @@ pnpm i @dojoengine/sdk
```

> Basic example: [example-vite-react-sdk](./examples/example-vite-react-sdk/)
> More complex: [example-vite-react-sdk](./examples/example-vite-kitchen-sink/)
> More complex: [example-vite-kitchen-sink](./examples/example-vite-kitchen-sink/)

```js
// Create client with your parameters
Expand Down Expand Up @@ -153,13 +153,13 @@ Spin any of these examples locally
npx @dojoengine/create-dojo start
```

- [example-vite-react-sdk](./examples/example-vite-react-sdk/): A React application using Vite and the Dojo SDK
- [example-vite-react-phaser-recs](./examples/example-vite-react-phaser-recs/): A React application using Vite and the Dojo SDK
- [example-vite-react-pwa-recs](./examples/example-vite-react-pwa-recs/): A React application using Vite and the Dojo SDK
- [example-vite-react-threejs-recs](./examples/example-vite-react-threejs-recs/): A React application using Vite and the Dojo SDK
- [example-vue-app-recs](./examples/example-vue-app-recs/): A React application using Vite and the Dojo SDK
- [example-vite-kitchen-sink](./examples/example-vite-kitchen-sink/): A React application using Vite and the Dojo SDK
- [example-nodejs-bot](./examples/example-nodejs-bot/): A React application using Vite and the Dojo SDK
- [example-vite-react-sdk](./examples/example-vite-react-sdk/)
- [example-vite-react-phaser-recs](./examples/example-vite-react-phaser-recs/)
- [example-vite-react-pwa-recs](./examples/example-vite-react-pwa-recs/)
- [example-vite-react-threejs-recs](./examples/example-vite-react-threejs-recs/)
- [example-vue-app-recs](./examples/example-vue-app-recs/)
- [example-vite-kitchen-sink](./examples/example-vite-kitchen-sink/)
- [example-nodejs-bot](./examples/example-nodejs-bot/)

## Contributing to dojo.js

Expand Down
Loading