From 449946b8078caf6a516a662b1488985166a60072 Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Tue, 7 Jul 2026 14:38:54 -0700 Subject: [PATCH] fix(data): record redo op for column-removal updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core store.update mutates its values argument, deleting keys whose value is undefined so it can reuse the object as the smaller archetype's row data. The transactional store recorded the redo op from that same object after the mutation, so a column-removal update (`{ comp: undefined }`) captured an empty `{}` redo op — making redo a no-op. delete -> undo -> redo left the column in place. Snapshot the redo values before handing the object to core. Co-Authored-By: Claude Opus 4.8 --- package.json | 2 +- packages/data-ai/package.json | 2 +- packages/data-gpu-samples/package.json | 2 +- packages/data-gpu/package.json | 2 +- packages/data-lit-tictactoe/package.json | 2 +- packages/data-lit-todo/package.json | 2 +- packages/data-lit/package.json | 2 +- packages/data-p2p-tictactoe/package.json | 2 +- packages/data-persistence/package.json | 2 +- packages/data-react-hello/package.json | 2 +- packages/data-react-pixie/package.json | 2 +- packages/data-react/package.json | 2 +- packages/data-solid-dashboard/package.json | 2 +- packages/data-solid/package.json | 2 +- packages/data-sync/package.json | 2 +- packages/data/package.json | 2 +- .../create-transactional-store.test.ts | 50 ++++++++++++++++++- .../create-transactional-store.ts | 9 +++- 18 files changed, 73 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index fe35ce55..39b4509b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-monorepo", - "version": "0.9.75", + "version": "0.9.76", "private": true, "scripts": { "build": "pnpm -r run build", diff --git a/packages/data-ai/package.json b/packages/data-ai/package.json index fe15661c..c8f94a31 100644 --- a/packages/data-ai/package.json +++ b/packages/data-ai/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-ai", - "version": "0.9.75", + "version": "0.9.76", "description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).", "type": "module", "private": false, diff --git a/packages/data-gpu-samples/package.json b/packages/data-gpu-samples/package.json index 6dc4ebd6..bf1775e8 100644 --- a/packages/data-gpu-samples/package.json +++ b/packages/data-gpu-samples/package.json @@ -1,6 +1,6 @@ { "name": "data-gpu-samples", - "version": "0.9.75", + "version": "0.9.76", "description": "WebGPU samples built on @adobe/data-gpu", "type": "module", "private": true, diff --git a/packages/data-gpu/package.json b/packages/data-gpu/package.json index 9163ffe5..d7d11764 100644 --- a/packages/data-gpu/package.json +++ b/packages/data-gpu/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-gpu", - "version": "0.9.75", + "version": "0.9.76", "description": "Adobe data WebGPU plugins and types for graphics and compute", "type": "module", "private": false, diff --git a/packages/data-lit-tictactoe/package.json b/packages/data-lit-tictactoe/package.json index be1e4ca8..7e2908ce 100644 --- a/packages/data-lit-tictactoe/package.json +++ b/packages/data-lit-tictactoe/package.json @@ -1,6 +1,6 @@ { "name": "data-lit-tictactoe", - "version": "0.9.75", + "version": "0.9.76", "description": "Tic-Tac-Toe sample - Lit web components with @adobe/data-lit and AgenticService", "type": "module", "private": true, diff --git a/packages/data-lit-todo/package.json b/packages/data-lit-todo/package.json index 4854bcb1..a2c57b4e 100644 --- a/packages/data-lit-todo/package.json +++ b/packages/data-lit-todo/package.json @@ -1,6 +1,6 @@ { "name": "data-lit-todo", - "version": "0.9.75", + "version": "0.9.76", "description": "Todo sample app demonstrating @adobe/data with Lit", "type": "module", "private": true, diff --git a/packages/data-lit/package.json b/packages/data-lit/package.json index 26448ff4..0fb17e98 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-lit", - "version": "0.9.75", + "version": "0.9.76", "description": "Adobe data Lit bindings - hooks, elements, decorators", "type": "module", "private": false, diff --git a/packages/data-p2p-tictactoe/package.json b/packages/data-p2p-tictactoe/package.json index 66afda91..4c647e11 100644 --- a/packages/data-p2p-tictactoe/package.json +++ b/packages/data-p2p-tictactoe/package.json @@ -1,6 +1,6 @@ { "name": "data-p2p-tictactoe", - "version": "0.9.75", + "version": "0.9.76", "description": "Serverless P2P tic-tac-toe — WebRTC DataChannel + @adobe/data-sync", "type": "module", "private": true, diff --git a/packages/data-persistence/package.json b/packages/data-persistence/package.json index 50e46338..2601befc 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-persistence", - "version": "0.9.75", + "version": "0.9.76", "description": "Worker-based incremental persistence layer for @adobe/data ECS over OPFS (browser) and node:fs (server).", "type": "module", "sideEffects": false, diff --git a/packages/data-react-hello/package.json b/packages/data-react-hello/package.json index ed04efeb..7fc201bb 100644 --- a/packages/data-react-hello/package.json +++ b/packages/data-react-hello/package.json @@ -1,6 +1,6 @@ { "name": "data-react-hello", - "version": "0.9.75", + "version": "0.9.76", "description": "Hello World sample - click counter using @adobe/data-react", "type": "module", "private": true, diff --git a/packages/data-react-pixie/package.json b/packages/data-react-pixie/package.json index 0ecaf906..5753c6e4 100644 --- a/packages/data-react-pixie/package.json +++ b/packages/data-react-pixie/package.json @@ -1,6 +1,6 @@ { "name": "data-react-pixie", - "version": "0.9.75", + "version": "0.9.76", "description": "PixiJS React sample - ECS sprites (bunny, fox) with @adobe/data-react", "type": "module", "private": true, diff --git a/packages/data-react/package.json b/packages/data-react/package.json index e8b20ef1..bd868e59 100644 --- a/packages/data-react/package.json +++ b/packages/data-react/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-react", - "version": "0.9.75", + "version": "0.9.76", "description": "Adobe data React bindings — hooks and context for ECS database", "type": "module", "private": false, diff --git a/packages/data-solid-dashboard/package.json b/packages/data-solid-dashboard/package.json index 215ddf3f..a4ebd8ee 100644 --- a/packages/data-solid-dashboard/package.json +++ b/packages/data-solid-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "data-solid-dashboard", - "version": "0.9.75", + "version": "0.9.76", "description": "Mini dashboard sample — multiple components sharing one @adobe/data ECS database with SolidJS", "type": "module", "private": true, diff --git a/packages/data-solid/package.json b/packages/data-solid/package.json index f69305d1..6ad88bc1 100644 --- a/packages/data-solid/package.json +++ b/packages/data-solid/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-solid", - "version": "0.9.75", + "version": "0.9.76", "description": "Adobe data SolidJS bindings — context and provider for ECS database", "type": "module", "private": false, diff --git a/packages/data-sync/package.json b/packages/data-sync/package.json index cbdb6a49..d8a450b7 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-sync", - "version": "0.9.75", + "version": "0.9.76", "description": "Multi-user real-time synchronisation for @adobe/data ECS — server, client, and in-process loopback.", "type": "module", "sideEffects": false, diff --git a/packages/data/package.json b/packages/data/package.json index 95d3fc9c..09b9ad99 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data", - "version": "0.9.75", + "version": "0.9.76", "description": "Adobe data oriented programming library", "type": "module", "sideEffects": false, diff --git a/packages/data/src/ecs/database/transactional-store/create-transactional-store.test.ts b/packages/data/src/ecs/database/transactional-store/create-transactional-store.test.ts index 60f5c6af..7bfa1f4e 100644 --- a/packages/data/src/ecs/database/transactional-store/create-transactional-store.test.ts +++ b/packages/data/src/ecs/database/transactional-store/create-transactional-store.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest"; import { createTransactionalStore } from "./create-transactional-store.js"; import { coalesceTransactions } from "./coalesce-actions.js"; +import { applyOperations } from "./apply-operations.js"; import { Store } from "../../store/index.js"; import { Entity } from "../../entity/entity.js"; import { F32 } from "../../../math/f32/index.js"; @@ -766,4 +767,51 @@ describe("createTransactionalStore", () => { expect(coalesced.persistent).toBe(true); }); }); -}); \ No newline at end of file + + describe("column removal via update-with-undefined", () => { + // Removing a component with `update(entity, { comp: undefined })` migrates the + // entity to an archetype lacking that column. This is the shape used by the + // orphan-delete model (sever parent link). The recorded redo op must still + // carry the column removal so delete -> undo -> redo re-removes the column. + it("records the removal in the redo op and round-trips through undo/redo", () => { + const baseStore = Store.create({ + components: { position: positionSchema, health: healthSchema }, + resources: {}, + archetypes: {} + }); + const store = createTransactionalStore(baseStore); + + // Insert an entity that has the health column. + let entity: Entity = 0 as Entity; + store.execute(t => { + const archetype = t.ensureArchetype(["id", "position", "health"]); + entity = archetype.insert({ + position: { x: 1, y: 2, z: 3 }, + health: { current: 100, max: 100 }, + }); + }); + expect(baseStore.read(entity)?.health).toEqual({ current: 100, max: 100 }); + + // Remove the health column. + const result = store.execute(t => { + t.update(entity, { health: undefined }); + }); + expect(baseStore.read(entity)?.health).toBeUndefined(); + + // The recorded redo op must still carry the column removal, not an empty {}. + const redoUpdate = result.redo.find(op => op.type === "update"); + expect(redoUpdate?.type).toBe("update"); + if (redoUpdate?.type === "update") { + expect("health" in redoUpdate.values).toBe(true); + expect(redoUpdate.values.health).toBeUndefined(); + } + + // Full round-trip: undo re-adds the column, redo must re-remove it. + applyOperations(baseStore, result.undo); + expect(baseStore.read(entity)?.health).toEqual({ current: 100, max: 100 }); + + applyOperations(baseStore, result.redo); + expect(baseStore.read(entity)?.health).toBeUndefined(); + }); + }); +}); \ No newline at end of file diff --git a/packages/data/src/ecs/database/transactional-store/create-transactional-store.ts b/packages/data/src/ecs/database/transactional-store/create-transactional-store.ts index 617832b1..dbb2b010 100644 --- a/packages/data/src/ecs/database/transactional-store/create-transactional-store.ts +++ b/packages/data/src/ecs/database/transactional-store/create-transactional-store.ts @@ -108,6 +108,13 @@ export function createTransactionalStore< changed.archetypes.add(location.archetype.id); } + // The core store mutates `values` in place: when a value is `undefined` it + // deletes that key so it can reuse the object as the new (smaller) archetype's + // row data. Snapshot the redo values first — otherwise a column-removal update + // (`{ comp: undefined }`) records an empty redo op and redo becomes a no-op, + // so delete -> undo -> redo would leave the column in place. + const redoValues = { ...values }; + // Perform the actual update store.update(entity, values as any); @@ -118,7 +125,7 @@ export function createTransactionalStore< } // Add operations with potential combining - addUpdateOperationsMaybeCombineLast(undoOperationsInReverseOrder, redoOperations, entity, values, replacedValues); + addUpdateOperationsMaybeCombineLast(undoOperationsInReverseOrder, redoOperations, entity, redoValues, replacedValues); }; const deleteEntity = (entity: Entity) => {