signals: a chain's verdict flips to false mid-hold where a diamond's stays true (graph shape is observable)

已关闭
#4,562 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
15/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
活跃
技术栈
typescript
领域
frontend

调研方向

Start with settlePendingSource in packages/signals/src/core/async.ts, tracing how a superseded flight's settle retires a pending node's state. Compare the single-arrival chain path with the second-arrival diamond path that re-marks staged errors. Use packages/signals/tests/settle-diamond-revisit.test.ts as the template for a chain pin. Done when the chain yields false:3, true:3, E:boomA, false:6.

由索引模型根据 Issue 内容生成。

描述

What happens

The same reads produce different frames depending on whether the derivation is a chain or a diamond. During a held action, a chain's isPending drops to false while the hold is still open. A diamond's stays true until the commit. Graph shape shouldn't be observable.

Found while reviewing #4560. It reproduces the same way on next (404d06a) and on #4560, so it isn't caused by that PR.

Repro

const [s, setS] = createSignal(0);
const A = createMemo(async () => {
  const v = s();
  await Promise.resolve();
  if (v === 2) throw new Error("boomA");
  return v;
});
const B = createMemo(() => A() + 1);
// chain:   C = B + 2;  D = C
// diamond: C = A + 2;  D = B + C
const view = createErrorBoundary(() => `${isPending(D)}:` + D(), e => `E:${e().message}`);
createRenderEffect(view, v => log.push(v));

Steps:

  1. Let the first load settle.
  2. setS(1); flush().
  3. Run action(function* () { setS(2); yield new Promise(r => (release = r)); })() and let it tick.
  4. Call release() and let it tick twice.
  5. setS(3); flush(), then let it tick.
Shape Frames
diamond false:3, true:3, E:boomA, false:9
chain false:3, true:3, false:3, E:boomA, false:6

The diamond's frames are pinned in packages/signals/tests/settle-diamond-revisit.test.ts (#4560).

The frames

  1. false:3: the first load committed (A = 0).
  2. true:3: s = 1 is a new question. A's flight is out, D's dependents are pending, and the screen keeps 3.
  3. The action's setS(2) re-asks A inside a hold, superseding the s = 1 flight. That flight throws boomA, and the error is staged in the hold (D-41). The hold stays open until release().
  4. Chain only, false:3: while the hold is still open, the view re-runs and reads isPending(D) === false with the committed 3. The diamond emits nothing here.
  5. E:boomA: the commit publishes the staged error.
  6. false:6 / false:9: s = 3 lands.

Ruling: the chain is wrong (F-9, with F-2)

  • F-9. isPending(x) is true "iff x's value is not the final answer to a new question: an input changed and the new answer has not been committed, a landed answer waits in a hold…". At frame 4 the input has changed (s is 1, then 2 in the hold), no new answer has been committed, and the staged answer is still waiting in the hold. Nothing has answered the s = 1 question; the hold superseded it. So isPending(D) must stay true until the commit, which is what the diamond shows.
  • F-2. "A change that needs async to be shown is held; the screen keeps what it showed." The screen showed true:3. Flipping it to false:3 mid-hold changes what is shown before the commit, with no commit behind the change.

So the expected chain frames are false:3, true:3, E:boomA, false:6.

Where to look

The difference lives in settlePendingSource (packages/signals/src/core/async.ts).

When the superseded flight's source settles, the walk retires D's pending state through the branch that carried it. In the diamond, D is reached a second time through the other branch. That second arrival takes the walk's early-return path, which re-runs a verdict reader or marks a staged error (stageMark), and that keeps D's verdict tied to the staged frame. In the chain there is only one arrival, so nothing re-marks D, and the view re-derives isPending(D) as settled while the hold is still open.

A likely fix is for the chain's single arrival to do what the diamond's second arrival does: a node that inherits the staged error, or whose verdict reads the hold, keeps that mark rather than reading as settled. Pin the chain frames above alongside the diamond pin.

Not fixed in #4560 on purpose: that PR keeps next's behaviour exactly.

主要语言
TypeScript
星标
36.2k
派生
1.1k
平均合并
9 小时 56 分钟
30 天内合并 PR
487

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

solidjs/solid 的其他 Issue

查看 solidjs/solid 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。