Skip to content

Commit 7c7279b

Browse files
committed
fix: defer effects behind dirty computed recompute
1 parent 22bf390 commit 7c7279b

2 files changed

Lines changed: 42 additions & 10 deletions

File tree

packages/rescript-signals/src/signals/Scheduler.res

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,9 @@ let flush = (): unit => {
448448
}
449449

450450
// Notify all subscribers of a signal (traverse linked list)
451-
// Marks computeds dirty transitively (lazy), queues effects for execution
451+
// Marks computeds dirty transitively.
452+
// Direct effects are queued immediately.
453+
// Effects reached through dirty computeds are deferred until parent computed recompute.
452454
let notifySubs = (subs: Core.subs): unit => {
453455
dirtyQueue->Array.push(subs)->ignore
454456

@@ -472,9 +474,14 @@ let notifySubs = (subs: Core.subs): unit => {
472474
dirtyQueue->Array.push(linkedSubs)->ignore
473475
}
474476
} else {
475-
// It's an effect - queue for execution
476-
let observer = l.observer
477-
addEffectToPending(observer)
477+
// It's an effect.
478+
// If reached via a dirty computed, defer effect until computed recompute.
479+
// This lets computed equality short-circuit downstream effect runs.
480+
if Core.isComputed(s) {
481+
addComputedToPending(s)
482+
} else {
483+
addEffectToPending(l.observer)
484+
}
478485
}
479486
link := l.nextSub
480487
| None => ()
@@ -484,7 +491,7 @@ let notifySubs = (subs: Core.subs): unit => {
484491
}
485492
clearArray(dirtyQueue)
486493

487-
if pendingEffects->Array.length > 0 && !flushing.contents {
494+
if (pendingEffects->Array.length > 0 || pendingComputedSubs->Array.length > 0) && !flushing.contents {
488495
flush()
489496
}
490497
}

packages/rescript-signals/tests/ComputedTests.res

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ let tests = Suite.make(
178178

179179
Assert.combineResults([result1, result2, result3])
180180
}),
181-
Test.make("custom equals avoids duplicate downstream effect runs", () => {
181+
Test.make("custom equals suppresses downstream effect when value is unchanged", () => {
182182
let profile = Signal.make({"id": 1, "name": "Alice"})
183183
let userId = Computed.make(
184184
() => Signal.get(profile)["id"],
@@ -197,21 +197,46 @@ let tests = Suite.make(
197197
Signal.set(profile, {"id": 1, "name": "Bob"})
198198
let result2 = Assert.equal(
199199
effectRuns.contents,
200-
2,
201-
~message="Effect should run once for the update",
200+
1,
201+
~message="Effect should not run when derived value is unchanged",
202202
)
203203

204204
// Derived id changes.
205205
Signal.set(profile, {"id": 2, "name": "Bob"})
206206
let result3 = Assert.equal(
207207
effectRuns.contents,
208-
3,
209-
~message="Effect should still run once per update when derived value changes",
208+
2,
209+
~message="Effect should run once when derived value changes",
210210
)
211211

212212
disposer.dispose()
213213
Assert.combineResults([result1, result2, result3])
214214
}),
215+
Test.make("repro: custom equals should suppress effect when derived value is unchanged", () => {
216+
let profile = Signal.make({"id": 1, "name": "Alice"})
217+
let userId = Computed.make(
218+
() => Signal.get(profile)["id"],
219+
~equals=(a, b) => a == b,
220+
)
221+
let effectRuns = ref(0)
222+
let disposer = Effect.runWithDisposer(() => {
223+
effectRuns := effectRuns.contents + 1
224+
ignore(Signal.get(userId))
225+
None
226+
})
227+
228+
// Source update does not change computed output.
229+
Signal.set(profile, {"id": 1, "name": "Bob"})
230+
231+
let result = Assert.equal(
232+
effectRuns.contents,
233+
1,
234+
~message="Expected no effect re-run when custom equals says computed output is unchanged",
235+
)
236+
237+
disposer.dispose()
238+
result
239+
}),
215240
Test.make("computed equality with array length", () => {
216241
let items = Signal.make([1, 2, 3])
217242
let recomputeCount = ref(0)

0 commit comments

Comments
 (0)