diff --git a/.changeset/scheduled-solid2-migration.md b/.changeset/scheduled-solid2-migration.md new file mode 100644 index 000000000..ce3785d95 --- /dev/null +++ b/.changeset/scheduled-solid2-migration.md @@ -0,0 +1,41 @@ +--- +"@solid-primitives/scheduled": major +--- + +Migrate to Solid.js v2.0 (beta.10) + +## Breaking Changes + +**Peer dependency**: `solid-js@^2.0.0-beta.10` and `@solidjs/web@^2.0.0-beta.10` are now required. + +### `isServer` import source + +`isServer` is now sourced from `@solidjs/web` instead of `solid-js/web` (handled internally — no consumer change needed). + +### `createScheduled` — `getListener` renamed to `getObserver` + +Uses `getObserver` from `solid-js` internally (Solid 2.0 rename of `getListener`). No consumer API change. + +### `createScheduled` — `ownedWrite: true` on internal signal + +The internal invalidation signal now uses `{ ownedWrite: true }` to allow synchronous writes from within reactive computation scopes. This is required when using `leading`-edge schedules, which fire the invalidation callback synchronously from inside an effect's compute phase. + +### `createScheduled` with `createEffect` — two-arg form required + +In Solid 2.0, `createEffect` requires a compute function and a separate apply function. The `scheduled()` accessor should be called in the compute phase: + +```ts +// ✅ Solid 2.0 +createEffect( + () => { const value = count(); const dirty = scheduled(); return { value, dirty }; }, + ({ value, dirty }) => { if (dirty) console.log("count", value); }, +); + +// ❌ Solid 1.x (no longer works) +createEffect(() => { + const value = count(); + if (scheduled()) console.log("count", value); +}); +``` + +`createScheduled` continues to work with `createMemo` unchanged. diff --git a/packages/scheduled/README.md b/packages/scheduled/README.md index 9b4138c2e..fd132ec68 100644 --- a/packages/scheduled/README.md +++ b/packages/scheduled/README.md @@ -157,14 +157,20 @@ const scheduled = createScheduled(fn => debounce(fn, 1000)); const [count, setCount] = createSignal(0); -createEffect(() => { - // track source signal - const value = count(); - // track the debounced signal and check if it's dirty - if (scheduled()) { - console.log("count", value); - } -}); +// In Solid 2.0, use the two-arg createEffect: compute (tracked) + apply (side effects) +createEffect( + () => { + // compute: track both source signal and the scheduled signal + const value = count(); + const dirty = scheduled(); + return { value, dirty }; + }, + ({ value, dirty }) => { + if (dirty) { + console.log("count", value); + } + }, +); // or with createMemo const debouncedCount = createMemo((p: number = 0) => { diff --git a/packages/scheduled/package.json b/packages/scheduled/package.json index 57f7bf041..baaaf5954 100644 --- a/packages/scheduled/package.json +++ b/packages/scheduled/package.json @@ -62,10 +62,12 @@ "devDependencies": { "@solid-primitives/event-listener": "workspace:^", "@solid-primitives/timer": "workspace:^", - "solid-js": "^1.9.7" + "@solidjs/web": "2.0.0-beta.10", + "solid-js": "2.0.0-beta.10" }, "peerDependencies": { - "solid-js": "^1.6.12" + "@solidjs/web": "^2.0.0-beta.10", + "solid-js": "^2.0.0-beta.10" }, "typesVersions": {} } diff --git a/packages/scheduled/src/index.ts b/packages/scheduled/src/index.ts index 3630a7e34..a01a11dac 100644 --- a/packages/scheduled/src/index.ts +++ b/packages/scheduled/src/index.ts @@ -1,5 +1,5 @@ -import { type Accessor, createSignal, getListener, getOwner, onCleanup } from "solid-js"; -import { isServer } from "solid-js/web"; +import { type Accessor, createSignal, getObserver, getOwner, onCleanup } from "solid-js"; +import { isServer } from "@solidjs/web"; export type ScheduleCallback = ( callback: (...args: Args) => void, @@ -269,14 +269,15 @@ export function leadingAndTrailing( * ```ts * const debounced = createScheduled(fn => debounce(fn, 250)); * - * createEffect(() => { - * // track source signal - * const value = count(); - * // track the debounced signal and check if it's dirty - * if (debounced()) { - * console.log('count', value); + * createEffect( + * () => count(), // compute: track source signal + * value => { + * // apply: runs after scheduled invalidation + * if (debounced()) { + * console.log('count', value); + * } * } - * }); + * ); * ``` */ @@ -287,7 +288,9 @@ export function createScheduled( ): Accessor { let listeners = 0; let isDirty = false; - const [track, dirty] = createSignal(void 0, { equals: false }); + // ownedWrite: true allows dirty() to be called synchronously from within a + // reactive computation's compute phase (e.g. when using leading edge schedules). + const [track, dirty] = createSignal(void 0, { equals: false, ownedWrite: true }); const call = schedule(() => { isDirty = true; dirty(); @@ -300,7 +303,7 @@ export function createScheduled( return true; } - if (getListener()) { + if (getObserver()) { listeners++; onCleanup(() => listeners--); } diff --git a/packages/scheduled/test/create-scheduled.test.ts b/packages/scheduled/test/create-scheduled.test.ts index ec5d26f52..f1902c25b 100644 --- a/packages/scheduled/test/create-scheduled.test.ts +++ b/packages/scheduled/test/create-scheduled.test.ts @@ -1,4 +1,4 @@ -import { createComputed, createEffect, createRoot, createSignal } from "solid-js"; +import { createEffect, createRoot, createSignal, flush } from "solid-js"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createScheduled, debounce, leading } from "../src/index.js"; @@ -33,85 +33,104 @@ describe("createScheduled", () => { invalidate = fn; return () => {}; }); - let i = 0; - const [track, trigger] = createSignal(undefined, { equals: false }); - createComputed(() => { - i++; - track(); - expect(scheduled()).toBe(i === 3); - if (i === 1) return trigger(); - if (i === 2) return invalidate(); - dispose(); - }); + const vals: boolean[] = []; + // ownedWrite: true because trigger() is called inside the createRoot scope + const [track, trigger] = createSignal(undefined, { equals: false, ownedWrite: true }); + + createEffect( + () => { + track(); + return scheduled(); + }, + val => { + vals.push(val); + }, + ); + + flush(); // run 1: subscribed, not yet dirty + expect(vals).toEqual([false]); + + trigger(); + flush(); // run 2: re-run due to user signal, call() is noop here + expect(vals).toEqual([false, false]); + + invalidate(); // sets isDirty + writes track signal + flush(); // run 3: dirty → returns true + expect(vals).toEqual([false, false, true]); + + dispose(); }); }); it("debounces", () => { const [track, trigger] = createSignal(undefined, { equals: false }); - - let i = 0; - let value: boolean | undefined; + const vals: boolean[] = []; const dispose = createRoot(dispose => { const scheduled = createScheduled(fn => debounce(fn, 20)); - createEffect(() => { - track(); - i++; - value = scheduled(); - }); + createEffect( + () => { + track(); + return scheduled(); + }, + val => { + vals.push(val); + }, + ); return dispose; }); - expect(value).toBe(false); - expect(i).toBe(1); + flush(); // initial run + expect(vals).toEqual([false]); trigger(); + flush(); // re-run, debounce timer started + expect(vals).toEqual([false, false]); - expect(value).toBe(false); - expect(i).toBe(2); - - vi.advanceTimersByTime(50); - - expect(value).toBe(true); - expect(i).toBe(3); + vi.advanceTimersByTime(50); // debounce fires → isDirty=true, dirty() + flush(); // re-run after invalidation + expect(vals).toEqual([false, false, true]); - if (i === 1) return trigger(); - if (i === 2) return; dispose(); }); it("debounces with leading", () => { const [track, trigger] = createSignal(undefined, { equals: false }); - - let i = 0; - let value: boolean | undefined; + const vals: boolean[] = []; const dispose = createRoot(dispose => { const scheduled = createScheduled(fn => leading(debounce, fn, 20)); - createEffect(() => { - track(); - i++; - value = scheduled(); - }); + createEffect( + () => { + track(); + return scheduled(); + }, + val => { + vals.push(val); + }, + ); return dispose; }); - expect(value).toBe(true); - expect(i).toBe(1); + // Leading fires fn() synchronously in Run 1: isDirty=true → returns true, isDirty=false. + // dirty() write is deferred until the next flush (Solid 2.0 prevents same-flush loops). + flush(); + expect(vals).toEqual([true]); + // trigger() causes a new flush. The effect re-runs: leading already scheduled (isScheduled=true) + // so fn is NOT re-fired → returns false. trigger(); + flush(); + expect(vals).toEqual([true, false]); - expect(value).toBe(false); - expect(i).toBe(2); - + // The debounce trailing edge resets isScheduled but does NOT call fn → no dirty() call. vi.advanceTimersByTime(50); - - expect(value).toBe(false); - expect(i).toBe(2); + flush(); + expect(vals).toEqual([true, false]); dispose(); }); @@ -123,23 +142,45 @@ describe("createScheduled", () => { invalidate = fn; return () => {}; }); - let i = 0; - const [track, trigger] = createSignal(undefined, { equals: false }); - createComputed(() => { - i++; - track(); - expect(scheduled(), `(a) run ${i}`).toBe(i === 3); - if (i === 1) return; - if (i === 2) return; - }); - let j = 0; - createComputed(() => { - j++; - track(); - expect(scheduled(), `(b) run ${j}`).toBe(j === 3); - if (j === 1) return trigger(); - if (j === 2) return invalidate(); - }); + const vals_a: boolean[] = []; + const vals_b: boolean[] = []; + // ownedWrite: true because trigger() is called inside the createRoot scope + const [track, trigger] = createSignal(undefined, { equals: false, ownedWrite: true }); + + createEffect( + () => { + track(); + return scheduled(); + }, + val => { + vals_a.push(val); + }, + ); + + createEffect( + () => { + track(); + return scheduled(); + }, + val => { + vals_b.push(val); + }, + ); + + flush(); // initial runs + expect(vals_a).toEqual([false]); + expect(vals_b).toEqual([false]); + + trigger(); + flush(); // re-runs after user trigger + expect(vals_a).toEqual([false, false]); + expect(vals_b).toEqual([false, false]); + + invalidate(); + flush(); // both re-run after invalidation + expect(vals_a).toEqual([false, false, true]); + expect(vals_b).toEqual([false, false, true]); + dispose(); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f59c63a9c..dc5ed7b98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -807,9 +807,12 @@ importers: '@solid-primitives/timer': specifier: workspace:^ version: link:../timer + '@solidjs/web': + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 packages/script-loader: devDependencies: