Skip to content

Commit c6aa672

Browse files
fix: re-subscribe external sources after a transition
A computation created during startTransition only tracked the transition-scoped external source. Once that source was disposed, later enableExternalSource updates were lost. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f55af45 commit c6aa672

3 files changed

Lines changed: 56 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"solid-js": patch
3+
---
4+
5+
Re-subscribe `enableExternalSource` computations to the ordinary source after a transition. A computation created while a transition was running only tracked the transition-scoped source; once that source was disposed, later external updates were lost.

packages/solid/src/reactive/signal.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,11 +1487,20 @@ function createComputation<Next, Init = unknown>(
14871487
const ordinary = ExternalSourceConfig.factory(sourceFn, trigger);
14881488
onCleanup(() => ordinary.dispose());
14891489
let inTransition: ExternalSource | undefined;
1490+
let trackedOrdinary = false;
14901491
const triggerInTransition: () => void = () =>
14911492
startTransition(trigger).then(() => {
14921493
if (inTransition) {
14931494
inTransition.dispose();
14941495
inTransition = undefined;
1496+
// A computation created while a transition was running only ever
1497+
// tracked the transition-scoped source, so its ordinary source has no
1498+
// recorded dependencies. Now that the transition is over, re-trigger
1499+
// once so the computation runs through `ordinary` again and
1500+
// re-subscribes; otherwise it would never receive further external
1501+
// updates. If the computation was disposed in the meantime this is a
1502+
// no-op because it is no longer an observer of `track`.
1503+
if (!trackedOrdinary) trigger();
14951504
}
14961505
});
14971506
c.fn = x => {
@@ -1501,6 +1510,7 @@ function createComputation<Next, Init = unknown>(
15011510
inTransition = ExternalSourceConfig!.factory(sourceFn, triggerInTransition);
15021511
return inTransition.track(x);
15031512
}
1513+
trackedOrdinary = true;
15041514
return ordinary.track(x);
15051515
};
15061516
}

packages/solid/test/external-source.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,47 @@ describe("external source", () => {
128128
});
129129
});
130130

131+
it("should keep receiving external updates after being created during a transition", async () => {
132+
// Initialize SuspenseContext so startTransition creates a real Transition
133+
getSuspenseContext();
134+
135+
const waitFor = async (fn: () => boolean) => {
136+
for (let i = 0; i < 50 && !fn(); i++) {
137+
await new Promise(resolve => setTimeout(resolve, 0));
138+
}
139+
};
140+
141+
await createRoot(async dispose => {
142+
const e = new ExternalSource(0);
143+
let memo: (() => number) | undefined;
144+
145+
// Create the memo while a transition is running. Its first run tracks
146+
// dependencies on the transition-scoped external source, never on the
147+
// ordinary one.
148+
await startTransition(() => {
149+
memo = createMemo(() => e.get());
150+
});
151+
expect(memo!()).toBe(0);
152+
153+
// First external update is delivered through the transition-scoped source.
154+
e.update(1);
155+
await waitFor(() => memo!() === 1);
156+
expect(memo!()).toBe(1);
157+
// Let triggerInTransition's .then() dispose the transition-scoped source
158+
// and re-subscribe on the ordinary one.
159+
await new Promise(resolve => setTimeout(resolve, 0));
160+
161+
// Second external update must still be delivered. Before the fix the
162+
// transition-scoped source had been disposed and the ordinary source was
163+
// never subscribed, so this update was silently lost.
164+
e.update(2);
165+
await waitFor(() => memo!() === 2);
166+
expect(memo!()).toBe(2);
167+
168+
dispose();
169+
});
170+
});
171+
131172
afterEach(() => {
132173
vi.resetModules();
133174
});

0 commit comments

Comments
 (0)