feat: add Value.Diff granular prop payloads for commitUpdate

Value.Diff produces property-level diff payloads instead of relying
solely on prepareUpdate. Function-valued props are stripped before
diffing; ValueDiffError is caught and falls back to prepareUpdate.
Value.Equal and Value.Clone now safely handle function-valued props
with try-catch fallbacks.
This commit is contained in:
2026-05-18 17:44:27 +00:00
parent 27c1f2671b
commit 32b2e20c54
3 changed files with 315 additions and 36 deletions

View File

@@ -137,32 +137,32 @@ describe("Value.Hash O(1) change detection", () => {
});
describe("hash mismatch falls through to Value.Equal / reconciliation", () => {
it("hash mismatch with changed prop triggers prepareUpdate", () => {
const { host, prepareUpdateCalls } = makeTrackingHost();
it("hash mismatch with changed prop triggers commitUpdate", () => {
const { host, commitUpdateCalls } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(h("div", { color: "red" }));
commitHashes(root.rootFiber!);
prepareUpdateCalls.length = 0;
commitUpdateCalls.length = 0;
root.render(h("div", { color: "blue" }));
expect(prepareUpdateCalls.length).toBeGreaterThanOrEqual(1);
expect(commitUpdateCalls.length).toBeGreaterThanOrEqual(1);
});
it("hash mismatch for child triggers prepareUpdate for child only", () => {
const { host, prepareUpdateCalls } = makeTrackingHost();
it("hash mismatch for child triggers commitUpdate for child only", () => {
const { host, commitUpdateCalls } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(h("div", { color: "red" }, h("span", { label: "a" })));
commitHashes(root.rootFiber!);
prepareUpdateCalls.length = 0;
commitUpdateCalls.length = 0;
root.render(h("div", { color: "red" }, h("span", { label: "b" })));
const spanCalls = prepareUpdateCalls.filter((c) => c.tag === "span");
const spanCalls = commitUpdateCalls.filter((c) => c.tag === "span");
expect(spanCalls.length).toBe(1);
expect(spanCalls[0]!.prevProps.label).toBe("a");
expect(spanCalls[0]!.nextProps.label).toBe("b");
@@ -171,7 +171,7 @@ describe("Value.Hash O(1) change detection", () => {
describe("hash is computed outside reactive computations", () => {
it("Value.Hash is never called from within a computed or effect callback", async () => {
const { host, prepareUpdateCalls, commitUpdateCalls } = makeTrackingHost();
const { host, commitUpdateCalls } = makeTrackingHost();
const s = signal("red");
const root = createHostRoot(host, {});
@@ -187,7 +187,6 @@ describe("Value.Hash O(1) change detection", () => {
{},
);
prepareUpdateCalls.length = 0;
commitUpdateCalls.length = 0;
s.value = "blue";
@@ -196,13 +195,12 @@ describe("Value.Hash O(1) change detection", () => {
queueMicrotask(() => resolve());
});
expect(prepareUpdateCalls.length).toBeGreaterThanOrEqual(1);
expect(commitUpdateCalls.length).toBeGreaterThanOrEqual(1);
expect(divFiber.props.color).toBe("blue");
const newHash = divFiber.hash;
expect(newHash).not.toBeNull();
prepareUpdateCalls.length = 0;
commitUpdateCalls.length = 0;
s.value = "blue";
@@ -211,7 +209,6 @@ describe("Value.Hash O(1) change detection", () => {
queueMicrotask(() => resolve());
});
expect(prepareUpdateCalls.length).toBe(0);
expect(commitUpdateCalls.length).toBe(0);
});