diff --git a/.changeset/fix-scheduler-child-queue-removal.md b/.changeset/fix-scheduler-child-queue-removal.md new file mode 100644 index 000000000..2345eaa90 --- /dev/null +++ b/.changeset/fix-scheduler-child-queue-removal.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +Keep traversing sibling effect queues when a child queue removes itself or an earlier sibling during a flush. Previously, the shifted sibling could be skipped and leave its queued effects unexecuted. diff --git a/packages/solid-signals/src/core/scheduler.ts b/packages/solid-signals/src/core/scheduler.ts index 02755bc15..e12eea0cc 100644 --- a/packages/solid-signals/src/core/scheduler.ts +++ b/packages/solid-signals/src/core/scheduler.ts @@ -295,7 +295,12 @@ export class Queue implements IQueue { this._queues[type - 1] = []; runQueue(effects, type); } - for (let i = 0; i < this._children.length; i++) (this._children[i] as any).run?.(type); + for (let i = 0; i < this._children.length; ) { + const child = this._children[i]; + (child as any).run?.(type); + // A child may remove itself or an earlier sibling while its effects run. + if (this._children[i] === child) i++; + } } enqueue(type: number, fn: QueueCallback): void { if (type) { diff --git a/packages/solid-signals/tests/scheduler-livelock.test.ts b/packages/solid-signals/tests/scheduler-livelock.test.ts index 8b7f6eb86..f65cec6d8 100644 --- a/packages/solid-signals/tests/scheduler-livelock.test.ts +++ b/packages/solid-signals/tests/scheduler-livelock.test.ts @@ -1,6 +1,7 @@ import { expect, it } from "vitest"; import { createEffect, + createLoadingBoundary, createMemo, createRenderEffect, createRoot, @@ -187,3 +188,47 @@ it("isPending(() => latest(x)) in a user effect does not loop on refetch (#2843) dispose(); flush(); }); + +it("disposing the current boundary queue does not skip its sibling", () => { + const effects: string[] = []; + const disposers: (() => void)[] = []; + + try { + createRoot(dispose => { + disposers.push(dispose); + createLoadingBoundary( + () => { + createEffect( + () => undefined, + () => { + effects.push("first"); + dispose(); + } + ); + }, + () => undefined + )(); + }); + + createRoot(dispose => { + disposers.push(dispose); + createLoadingBoundary( + () => { + createEffect( + () => undefined, + () => { + effects.push("second"); + } + ); + }, + () => undefined + )(); + }); + + flush(); + expect(effects).toEqual(["first", "second"]); + } finally { + for (const dispose of disposers) dispose(); + flush(); + } +});