Merge feat/value-hash-detection: add Value.Hash O(1) change detection with commitHashes (clean merge, no Value.Diff overlap)

This commit is contained in:
2026-05-18 17:40:13 +00:00
parent 6d704f59e0
commit 27c1f2671b
4 changed files with 36 additions and 50 deletions

View File

@@ -60,8 +60,8 @@ describe("signal-driven-updates", () => {
});
describe("wireSignalToFiber", () => {
it("signal change triggers prepareUpdate + commitUpdate", async () => {
const { host, prepareUpdateCalls, commitUpdateCalls } = makeTrackingHost();
it("signal change triggers commitUpdate", async () => {
const { host, commitUpdateCalls } = makeTrackingHost();
const color = signal("red");
const root = createHostRoot(host, {});
@@ -77,8 +77,7 @@ describe("signal-driven-updates", () => {
{},
);
expect(prepareUpdateCalls.length).toBe(0);
expect(commitUpdateCalls.length).toBe(0);
commitUpdateCalls.length = 0;
color.value = "blue";
@@ -86,16 +85,9 @@ describe("signal-driven-updates", () => {
queueMicrotask(() => resolve());
});
expect(prepareUpdateCalls.length).toBeGreaterThanOrEqual(1);
const lastPrepare = prepareUpdateCalls[prepareUpdateCalls.length - 1]!;
expect(lastPrepare.tag).toBe("div");
expect(lastPrepare.prevProps.color).toBe("red");
expect(lastPrepare.nextProps.color).toBe("blue");
expect(commitUpdateCalls.length).toBeGreaterThanOrEqual(1);
const lastCommit = commitUpdateCalls[commitUpdateCalls.length - 1]!;
expect(lastCommit.instance).toBe(divFiber.instance);
expect(lastCommit.payload).toEqual({ color: "blue" });
});
it("signal effect disposer stored in fiber.signalDisposers", () => {
@@ -120,7 +112,7 @@ describe("signal-driven-updates", () => {
});
it("disposing signal via signalDisposers stops updates", async () => {
const { host, prepareUpdateCalls } = makeTrackingHost();
const { host, commitUpdateCalls } = makeTrackingHost();
const color = signal("red");
const root = createHostRoot(host, {});
@@ -142,20 +134,20 @@ describe("signal-driven-updates", () => {
const disposer = divFiber.signalDisposers.pop()!;
disposer();
const callCountBefore = prepareUpdateCalls.filter((c) => c.tag === "div").length;
const callCountBefore = commitUpdateCalls.filter((c) => c.tag === "div").length;
color.value = "green";
await new Promise<void>((resolve) => {
queueMicrotask(() => resolve());
});
expect(prepareUpdateCalls.filter((c) => c.tag === "div").length).toBe(callCountBefore);
expect(commitUpdateCalls.filter((c) => c.tag === "div").length).toBe(callCountBefore);
});
});
describe("batch of signal changes", () => {
it("batch of signal changes results in single reconciliation pass", async () => {
const { host, prepareUpdateCalls, commitUpdateCalls } = makeTrackingHost();
const { host, commitUpdateCalls } = makeTrackingHost();
const color = signal("red");
const size = signal("small");
@@ -175,7 +167,6 @@ describe("signal-driven-updates", () => {
queueMicrotask(() => resolve());
});
resetUpdateQueue();
prepareUpdateCalls.length = 0;
commitUpdateCalls.length = 0;
batch(() => {
@@ -187,14 +178,13 @@ describe("signal-driven-updates", () => {
queueMicrotask(() => resolve());
});
expect(prepareUpdateCalls.filter((c) => c.tag === "div").length).toBe(1);
expect(commitUpdateCalls.filter((c) => c.tag === "div").length).toBe(1);
});
});
describe("render() re-renderable", () => {
it("second render reconciles props against existing fiber tree", () => {
const { host, prepareUpdateCalls, commitUpdateCalls, instances } = makeTrackingHost();
const { host, commitUpdateCalls, instances } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(h("div", { color: "red" }, "hello"));
@@ -205,14 +195,10 @@ describe("signal-driven-updates", () => {
expect(instances.length).toBe(instanceCountBefore);
const divPrepareCalls = prepareUpdateCalls.filter((c) => c.tag === "div");
expect(divPrepareCalls.length).toBe(1);
expect(divPrepareCalls[0]!.prevProps.color).toBe("red");
expect(divPrepareCalls[0]!.nextProps.color).toBe("blue");
const divCommitCalls = commitUpdateCalls.filter((c) => c.tag === "div");
expect(divCommitCalls.length).toBe(1);
expect(divCommitCalls[0]!.payload).toEqual({ color: "blue" });
expect(divCommitCalls[0]!.prevProps.color).toBe("red");
expect(divCommitCalls[0]!.nextProps.color).toBe("blue");
});
it("second render does not create duplicate instances", () => {
@@ -225,16 +211,16 @@ describe("signal-driven-updates", () => {
expect(instances.length).toBe(1);
});
it("prepareUpdate is called for changed props on re-render", () => {
const { host, prepareUpdateCalls } = makeTrackingHost();
it("commitUpdate is called for changed props on re-render", () => {
const { host, commitUpdateCalls } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(h("span", { label: "a" }));
root.render(h("span", { label: "b" }));
const spanPrepares = prepareUpdateCalls.filter((c) => c.tag === "span");
expect(spanPrepares.length).toBe(1);
expect(spanPrepares[0]!.prevProps.label).toBe("a");
expect(spanPrepares[0]!.nextProps.label).toBe("b");
const spanCommits = commitUpdateCalls.filter((c) => c.tag === "span");
expect(spanCommits.length).toBe(1);
expect(spanCommits[0]!.prevProps.label).toBe("a");
expect(spanCommits[0]!.nextProps.label).toBe("b");
});
});