Skip to content

Commit

Permalink
Merge pull request #404 from dojoengine/fix/zustand-merge-entities
Browse files Browse the repository at this point in the history
fix: zustand deep merge entities
  • Loading branch information
MartianGreed authored Feb 27, 2025
2 parents eb1d29f + e2a4ea5 commit 6265138
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 59 deletions.
16 changes: 16 additions & 0 deletions .changeset/sharp-cooks-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@dojoengine/sdk": patch
"template-vite-ts": patch
"@dojoengine/core": patch
"@dojoengine/create-burner": patch
"@dojoengine/create-dojo": patch
"@dojoengine/predeployed-connector": patch
"@dojoengine/react": patch
"@dojoengine/state": patch
"@dojoengine/torii-client": patch
"@dojoengine/torii-wasm": patch
"@dojoengine/utils": patch
"@dojoengine/utils-wasm": patch
---

fix: zustand updateEntity and mergeEntities deeply merge objects
14 changes: 14 additions & 0 deletions packages/sdk/src/__tests__/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ describe("createDojoStore", () => {
expect(state.entities["galaxy1"]).toEqual(initialGalaxy);
});

test("mergeEntities should merge entities to the store", () => {
useStore.getState().setEntities([initialPlayer]);
const updatedPlayer = {
entityId: "player1",
models: { world: { player: { score: 120 } }, universe: {} },
};
useStore.getState().mergeEntities([updatedPlayer]);
const state = useStore.getState();
expect(state.getEntities()).toHaveLength(1);
const player = state.entities["player1"];
expect(player.models.world?.player?.name).toEqual("Alice");
expect(player.models.world?.player?.score).toEqual(120);
});

test("updateEntity should update an existing entity", () => {
useStore.getState().setEntities([initialPlayer]);
useStore.getState().updateEntity({
Expand Down
145 changes: 86 additions & 59 deletions packages/sdk/src/state/zustand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,42 @@ type CreateStore = {
): StoreApi<T>;
};

type MergedModels<T extends SchemaType> =
ParsedEntity<T>["models"][keyof ParsedEntity<T>["models"]];

function deepMerge<T extends SchemaType>(
target: MergedModels<T>,
source: Partial<MergedModels<T>>
): MergedModels<T> {
const result = { ...target } as Record<string, any>;

for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (
source[key] !== null &&
typeof source[key] === "object" &&
!Array.isArray(source[key])
) {
// If the property is an object in both source and target, recursively merge
if (
key in target &&
typeof target[key] === "object" &&
!Array.isArray(target[key])
) {
result[key] = deepMerge(target[key], source[key]);
} else {
// If the key doesn't exist in target or isn't an object, just assign
result[key] = source[key];
}
} else {
// For non-objects (primitives, arrays, null), just assign
result[key] = source[key];
}
}
}

return result;
}
/**
* Factory function to create a Zustand store based on a given SchemaType.
*
Expand Down Expand Up @@ -49,66 +85,14 @@ export function createDojoStoreFactory<T extends SchemaType>(
const existingEntity =
state.entities[entity.entityId];

if (existingEntity) {
// Create new models object without spread
const mergedModels: typeof existingEntity.models =
Object.assign(
{},
existingEntity.models
);

// Iterate through each namespace in the new models
Object.entries(entity.models).forEach(
([namespace, namespaceModels]) => {
const typedNamespace =
namespace as keyof ParsedEntity<T>["models"];
if (
!(
typedNamespace in
mergedModels
)
) {
mergedModels[
typedNamespace as keyof typeof mergedModels
] = {} as any;
}

mergedModels[
typedNamespace as keyof typeof mergedModels
] = Object.assign(
{},
mergedModels[
typedNamespace as keyof typeof mergedModels
],
namespaceModels
);
}
);

// Update the entity
state.entities[entity.entityId] = {
...existingEntity,
...entity,
models: mergedModels,
};
} else {
if (!existingEntity) {
// Set new entity
state.entities[entity.entityId] =
entity as WritableDraft<
ParsedEntity<T>
>;
return;
}
}
});
});
},
updateEntity: (entity: Partial<ParsedEntity<T>>) => {
set((state: Draft<GameState<T>>) => {
if (entity.entityId && entity.models) {
const existingEntity =
state.entities[entity.entityId];

if (existingEntity) {
// Create new models object without spread
const mergedModels: typeof existingEntity.models =
Object.assign({}, existingEntity.models);
Expand All @@ -124,28 +108,71 @@ export function createDojoStoreFactory<T extends SchemaType>(
] = {} as any;
}

// Use deep merge instead of Object.assign
mergedModels[
typedNamespace as keyof typeof mergedModels
] = Object.assign(
{},
] = deepMerge(
mergedModels[
typedNamespace as keyof typeof mergedModels
],
] as MergedModels<T>,
namespaceModels
);
) as any;
}
);

// Update the entity
state.entities[entity.entityId] = {
...existingEntity,
...entity,
models: mergedModels,
};
} else {
}
});
});
},
updateEntity: (entity: Partial<ParsedEntity<T>>) => {
set((state: Draft<GameState<T>>) => {
if (entity.entityId && entity.models) {
const existingEntity =
state.entities[entity.entityId];

if (!existingEntity) {
// Set new entity
state.entities[entity.entityId] =
entity as WritableDraft<ParsedEntity<T>>;
return;
}
// Create new models object without spread
const mergedModels: typeof existingEntity.models =
Object.assign({}, existingEntity.models);

// Iterate through each namespace in the new models
Object.entries(entity.models).forEach(
([namespace, namespaceModels]) => {
const typedNamespace =
namespace as keyof ParsedEntity<T>["models"];
if (!(typedNamespace in mergedModels)) {
mergedModels[
typedNamespace as keyof typeof mergedModels
] = {} as any;
}

mergedModels[
typedNamespace as keyof typeof mergedModels
] = deepMerge(
mergedModels[
typedNamespace as keyof typeof mergedModels
] as MergedModels<T>,
namespaceModels
) as any;
}
);
// Update the entity
state.entities[entity.entityId] = {
...existingEntity,
...entity,
models: mergedModels,
};
}
});
},
Expand Down

0 comments on commit 6265138

Please sign in to comment.