Skip to content

feat(raf): add frameloop utils. - #824

Open
royeden wants to merge 7 commits into
solidjs-community:mainfrom
royeden:feat/raf/frameloop-management
Open

feat(raf): add frameloop utils.#824
royeden wants to merge 7 commits into
solidjs-community:mainfrom
royeden:feat/raf/frameloop-management

Conversation

@royeden

@royeden royeden commented Dec 3, 2025

Copy link
Copy Markdown
  • Added createCallbacksSet util to unify calling multiple void callbacks stored in a ReactiveSet.

  • Added createScheduledLoop to handle external animation loops, such as motion's frame util.

  • Added useGlobalRAF util to handle unified request animation frame calls.

  • Fixed createRAF cleanup for id 0 by using null instead.

Addresses this proposal #822.

Summary by CodeRabbit

  • New Features
    • Added new RAF utilities to batch multiple callbacks, share a singleton global RAF loop, and create scheduled RAF-style loops.
    • Updated public documentation with TypeScript signatures, examples, and guidance on when to use the global RAF primitive.
  • Bug Fixes
    • Improved RAF stopping behavior so animation frame cancellation only occurs when a frame is actually scheduled.
  • Tests
    • Expanded coverage for shared-loop semantics, callback add/remove lifecycle, and RAF scheduling/cancellation (including disposal behavior).

- Added `useFrameloop` util to use unified request animation frame
calls.
- Added `createScheduledFrameloop` to handle request animation frame
from external sources.
- Fixed `createRAF` cleanup for id `0` by using `null` instead.
@changeset-bot

changeset-bot Bot commented Dec 3, 2025

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 2c3356b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Comment thread packages/raf/src/index.ts Outdated
}

/**
* Returns an advanced primitive factory function (that has an API similar to `createRAF`) to handle multiple animation frame callbacks in a single batched `requestAnimationFrame`, avoiding the overhead of scheduling multiple animation frames outside of a batch and making them all sync on the same delta.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explanation is way too complex. "A version of createRAF that batches multiple frames within the same render cycle instead of skipping them ."

Comment thread packages/raf/src/index.ts Outdated
Comment thread packages/raf/src/index.ts Outdated
@royeden

royeden commented Dec 21, 2025

Copy link
Copy Markdown
Author

Sorry for the delay on this, the end of the year tends to be really busy. I was starting to re-visit the approach itself and maybe landed into a nicer primitive set, want to hear your opinions on this @atk (I can leave it as an update on this PR, but I don't think this is the right approach for this):

createFrameScheduler

A function with this signature that allows scheduling with external frameloop utils like motion's frame:

type CreateScheduledFrame<Callback extends (...args: any) => any> = 
  (callback: Callback) => [running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction];

createFrameScheduler<
  // NonNullable is used here so we know nothing is scheduled when the value is `null`
  RequestID extends NonNullable<unknown>,
  Callback extends (...args: any) => any
>(
  schedule: (callback: Callback) => RequestID,
  cancel: (requestID: RequestID) => void,
): CreateScheduledFrame<Callback>

createFrameloopScheduler

A function with this signature that allows scheduling with functions that need to run in a loop for the callback to re-run:

type CreateScheduledLoop<Callback extends (...args: any) => any> = 
  (callback: Callback) => [running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction];

createFrameScheduler<
  // NonNullable is used here so we know nothing is scheduled when the value is `null`
  RequestID extends NonNullable<unknown>,
  Callback extends (...args: any) => any
>(
  schedule: (callback: Callback) => RequestID,
  cancel: (requestID: RequestID) => void,
): CreateScheduledLoop<Callback>

Why?

These primitives would allow us to compose any handlers with the running, start, stop logic, so you could basically do create your own utils (not 100% sure if these should exist under the raf package anymore either).

They both work with the same implementation that is used in createRAF.

Examples:

createRAF (requestAnimationFrame):

export const createRAF = createFrameloopScheduler(
  window.requestAnimationFrame,
  window.cancelAnimationFrame
);

createRIC (requestIdleCallback):

export const createRIC = createFrameloopScheduler(
  window.requestIdleCallback,
  window.cancelIdleCallback
);

export const createRIC100ms = createFrameloopScheduler(
  (callback: IdleRequestCallback) => window.requestIdleCallback(callback, { timeout: 100 }),
  window.cancelIdleCallback
);

motion's frame utils:

import { cancelFrame, frame } from 'motion';
export const createMotionRead = createScheduledFrame(
  callback => frame.read(callback, true),
  cancelFrame
);

export const createMotionUpdate = createScheduledFrame(
  callback => frame.update(callback, true),
  cancelFrame
);

export const createMotionRender = createScheduledFrame(
  callback => frame.render(callback, true),
  cancelFrame
);

@atk atk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see my last points addressed, also, the tests seem to fail.

Comment thread packages/raf/package.json
@royeden
royeden requested a review from atk December 30, 2025 14:35
@davedbase

Copy link
Copy Markdown
Member

Hi @royeden any interest in completing out this PR? We're preparing for Solid Primitives 2.0 and would love to include these improvements.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bb7941f5-aa58-4b99-b8f5-3501df981598

📥 Commits

Reviewing files that changed from the base of the PR and between 89919df and 2c3356b.

📒 Files selected for processing (2)
  • packages/raf/src/index.ts
  • packages/raf/test/index.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/raf/test/index.test.ts
  • packages/raf/src/index.ts

📝 Walkthrough

Walkthrough

Adds createCallbacksSet, useGlobalRAF, and createScheduledLoop to packages/raf, makes RAF request tracking nullable, adds workspace references, expands lifecycle tests, and documents the new APIs.

Changes

RAF Package New Primitives

Layer / File(s) Summary
RAF scheduling primitives and exports
packages/raf/src/index.ts, packages/raf/package.json, packages/raf/tsconfig.json
createRAF uses nullable request tracking, while createCallbacksSet batches callbacks and createScheduledLoop provides scheduler/canceller-based lifecycle controls. Workspace dependencies and project references are added.
useGlobalRAF singleton loop
packages/raf/src/index.ts
A singleton RAF root manages callback membership, optional automatic starting, shared lifecycle controls, and cleanup.
RAF behavior validation
packages/raf/test/index.test.ts
Tests cover scheduling, cancellation, singleton behavior, timestamp sharing, callback membership, automatic starting, and disposal.
RAF API documentation
packages/raf/README.md
Documents the three new primitives with signatures, examples, and useGlobalRAF guidance.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Component
  participant useGlobalRAF
  participant SingletonRoot
  participant BrowserRAF

  Component->>useGlobalRAF: callback, startWhenAdded
  useGlobalRAF->>SingletonRoot: add(callback)
  SingletonRoot->>BrowserRAF: requestAnimationFrame(loop)
  BrowserRAF-->>SingletonRoot: timestamp
  SingletonRoot->>Component: invoke callback(timestamp)
  Component->>useGlobalRAF: remove(callback)
  SingletonRoot->>BrowserRAF: cancelAnimationFrame
Loading

Suggested labels: Primitive Proposal

Suggested reviewers: atk

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main change: adding new RAF/frameloop utilities.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
packages/raf/test/index.test.ts (1)

274-303: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Same un-awaited vi.waitUntil issue as above.

Line 298's vi.waitUntil(() => { expect(caf).toHaveBeenCalledTimes(1); }) is not awaited and the it callback (line 280 test, or 274 wrapping) is not async, so the cancellation-after-dispose assertion never actually gates the test outcome.

🐛 Proposed fix
-    createRoot(dispose => {
+    createRoot(async dispose => {
       ...
       dispose();
-      vi.waitUntil(() => {
-        expect(caf).toHaveBeenCalledTimes(1);
-      });
+      await vi.waitUntil(() => caf.mock.calls.length >= 1);
+      expect(caf).toHaveBeenCalledTimes(1);
     });

Note: createRoot's callback is synchronous by design, so this may require restructuring the test to run outside createRoot (e.g., capture dispose and await afterwards) for the await to work correctly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raf/test/index.test.ts` around lines 274 - 303, The frameloop
dispose test is using vi.waitUntil without awaiting it, so the
cancelAnimationFrame assertion never participates in the test result. Update the
test around useGlobalRAF/createRoot to make the outer it callback async, capture
dispose from createRoot, and await the vi.waitUntil assertion after dispose is
called so the caf expectation is properly verified.
🧹 Nitpick comments (2)
packages/raf/src/index.ts (1)

161-172: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

requestID is never reset to null after cancellation.

In stop(), once cancel(requestID) runs, requestID keeps its stale value. If stop() is invoked twice (e.g. manual stop() followed by onCleanup(stop) on dispose), cancel() is called again with the same stale id. This is harmless for cancelAnimationFrame (a browser API tolerant of stale/invalid ids), but since cancel here is an arbitrary user-supplied function (e.g. Motion's cancelFrame), redundant calls with a stale/reused reference may not be safe.

🛡️ Proposed fix
     const stop = () => {
       setRunning(false);
-      if (requestID !== null) cancel(requestID);
+      if (requestID !== null) {
+        cancel(requestID);
+        requestID = null;
+      }
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raf/src/index.ts` around lines 161 - 172, In the RAF controller’s
start/stop logic, the local requestID is left stale after canceling, so a later
stop() can cancel the same id again. Update the stop() path in the create RAF
helper to clear requestID back to null immediately after calling
cancel(requestID), and keep the existing guard in start() so a new
schedule(callback) only happens when not already running.
packages/raf/test/index.test.ts (1)

42-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Address the acknowledged test gap.

// TODO add better test — wiring createScheduledLoop directly to raw window.requestAnimationFrame only verifies a single scheduling round-trip, not repeated-loop behavior, which is the primitive's stated purpose. Want me to draft a test using a self-rescheduling schedule function (similar to createRAF's internal loop) to actually validate repeated invocation and cancellation?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raf/test/index.test.ts` around lines 42 - 63, The current
createScheduledLoop test only covers one requestAnimationFrame round-trip, so it
does not validate the repeated-loop behavior the primitive is meant to provide.
Update the test in createScheduledLoop to use a self-rescheduling scheduler
(similar to the loop used by createRAF) and assert that the callback is invoked
repeatedly after start() until stop() cancels it. Keep the existing
createScheduledLoop API usage and verify both running() state changes and
cancelAnimationFrame behavior through the named start, stop, and running
helpers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/raf/src/index.ts`:
- Around line 150-177: The Motion adapter in createScheduledLoop is using the
wrong cancellation token, so stop() cannot cancel work scheduled by
frame.render(). Update the schedule/cancel contract to return and store a stable
handle such as the original callback reference from the Motion adapter, then
pass that same handle into cancel from start/stop. Make sure the fix is applied
in createScheduledLoop and the Motion-specific scheduler implementation so the
value returned by schedule is the one later consumed by cancel.

In `@packages/raf/test/index.test.ts`:
- Around line 64-78: The test in createScheduledLoop is using the wrong
primitive and is currently duplicating the createRAF case. Update the test body
to instantiate and exercise createScheduledLoop instead of createRAF, while
keeping the same dispose/cancel assertions so the scheduled loop’s cleanup
behavior is actually covered. Use the createScheduledLoop and createRoot symbols
to locate and correct the copy-paste error.
- Around line 193-253: The automatic-start RAF singleton test is not actually
asserting the async disposal state because vi.waitUntil in the
useGlobalRAF/createRoot block is not awaited. Make the it callback async and
await the vi.waitUntil call so the running() and cancelAnimationFrame
expectations are part of the test result, then keep dispose() after the awaited
check.

---

Duplicate comments:
In `@packages/raf/test/index.test.ts`:
- Around line 274-303: The frameloop dispose test is using vi.waitUntil without
awaiting it, so the cancelAnimationFrame assertion never participates in the
test result. Update the test around useGlobalRAF/createRoot to make the outer it
callback async, capture dispose from createRoot, and await the vi.waitUntil
assertion after dispose is called so the caf expectation is properly verified.

---

Nitpick comments:
In `@packages/raf/src/index.ts`:
- Around line 161-172: In the RAF controller’s start/stop logic, the local
requestID is left stale after canceling, so a later stop() can cancel the same
id again. Update the stop() path in the create RAF helper to clear requestID
back to null immediately after calling cancel(requestID), and keep the existing
guard in start() so a new schedule(callback) only happens when not already
running.

In `@packages/raf/test/index.test.ts`:
- Around line 42-63: The current createScheduledLoop test only covers one
requestAnimationFrame round-trip, so it does not validate the repeated-loop
behavior the primitive is meant to provide. Update the test in
createScheduledLoop to use a self-rescheduling scheduler (similar to the loop
used by createRAF) and assert that the callback is invoked repeatedly after
start() until stop() cancels it. Keep the existing createScheduledLoop API usage
and verify both running() state changes and cancelAnimationFrame behavior
through the named start, stop, and running helpers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 922a2a45-0592-4713-9037-e7ecdf9780f0

📥 Commits

Reviewing files that changed from the base of the PR and between ea6dfd2 and ff3888b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • packages/raf/README.md
  • packages/raf/package.json
  • packages/raf/src/index.ts
  • packages/raf/test/index.test.ts
  • packages/raf/tsconfig.json

Comment thread packages/raf/src/index.ts
Comment thread packages/raf/test/index.test.ts
Comment on lines +193 to +253
it("(Automatic start) global RAF singleton calls rafs with the same timestamp", () => {
// Note on this test: For some reason, the raf is being called twice, once when started (but id doesn't invoke the callback for some strange reason) and once after the timers advance (and the callback is properly invoked).
const raf = vi.spyOn(window, "requestAnimationFrame");
const caf = vi.spyOn(window, "cancelAnimationFrame");
createRoot(dispose => {
const timestamps = new Set<number>();
const createGlobalRAFCallback = useGlobalRAF();
const callback1: Mock<FrameRequestCallback> = vi.fn(ts => timestamps.add(ts));
const [added1, add1, remove1, running1, start1, stop1] = createGlobalRAFCallback(
callback1,
true,
);
const callback2: Mock<FrameRequestCallback> = vi.fn(ts => timestamps.add(ts));
const [added2, add2, remove2, running2, start2, stop2] = createGlobalRAFCallback(
callback2,
true,
);

// Queue functions should not be equal
expect(added1).not.toEqual(added2);
expect(add1).not.toEqual(add2);
expect(remove1).not.toEqual(remove2);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();

// Frameloop functions should be equal because of the singleton
expect(running1).toEqual(running2);
expect(start1).toEqual(start2);
expect(stop1).toEqual(stop2);

// Aliases
const running = running1;
const stop = stop1;

expect(added1()).toBe(false);
add1();
expect(added1()).toBe(true);
expect(added2()).toBe(false);
expect(running()).toBe(true);
vi.advanceTimersToNextFrame();
expect(raf).toHaveBeenCalledTimes(2);
expect(callback1).toHaveBeenCalledTimes(1);
expect(timestamps.size).toEqual(1);
stop();
expect(running()).toBe(false);
expect(caf).toHaveBeenCalledTimes(1);
add2();
expect(added2()).toBe(true);
expect(running()).toBe(true);
vi.advanceTimersToNextFrame();
expect(raf).toHaveBeenCalledTimes(4);
expect(timestamps.size).toEqual(2);
remove1();
remove2();
vi.waitUntil(() => {
expect(running()).toBe(false);
expect(caf).toHaveBeenCalledTimes(2);
});
dispose();
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

vi.waitUntil assertions are never verified — not awaited.

vi.waitUntil returns a Promise and this call is not awaited nor is the enclosing it callback async. The assertions inside its callback (expect(running()).toBe(false), expect(caf).toHaveBeenCalledTimes(2)) will never actually gate the test result — dispose() runs immediately after, and any failures inside the un-awaited promise surface at best as an unhandled rejection, not a test failure. This test currently provides false confidence about disposal/cancellation behavior in the automatic-start scenario.

🐛 Proposed fix
-  it("(Automatic start) global RAF singleton calls rafs with the same timestamp", () => {
+  it("(Automatic start) global RAF singleton calls rafs with the same timestamp", async () => {
     ...
       remove1();
       remove2();
-      vi.waitUntil(() => {
-        expect(running()).toBe(false);
-        expect(caf).toHaveBeenCalledTimes(2);
-      });
+      await vi.waitUntil(() => !running());
+      expect(caf).toHaveBeenCalledTimes(2);
       dispose();
-    });
+    });
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("(Automatic start) global RAF singleton calls rafs with the same timestamp", () => {
// Note on this test: For some reason, the raf is being called twice, once when started (but id doesn't invoke the callback for some strange reason) and once after the timers advance (and the callback is properly invoked).
const raf = vi.spyOn(window, "requestAnimationFrame");
const caf = vi.spyOn(window, "cancelAnimationFrame");
createRoot(dispose => {
const timestamps = new Set<number>();
const createGlobalRAFCallback = useGlobalRAF();
const callback1: Mock<FrameRequestCallback> = vi.fn(ts => timestamps.add(ts));
const [added1, add1, remove1, running1, start1, stop1] = createGlobalRAFCallback(
callback1,
true,
);
const callback2: Mock<FrameRequestCallback> = vi.fn(ts => timestamps.add(ts));
const [added2, add2, remove2, running2, start2, stop2] = createGlobalRAFCallback(
callback2,
true,
);
// Queue functions should not be equal
expect(added1).not.toEqual(added2);
expect(add1).not.toEqual(add2);
expect(remove1).not.toEqual(remove2);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
// Frameloop functions should be equal because of the singleton
expect(running1).toEqual(running2);
expect(start1).toEqual(start2);
expect(stop1).toEqual(stop2);
// Aliases
const running = running1;
const stop = stop1;
expect(added1()).toBe(false);
add1();
expect(added1()).toBe(true);
expect(added2()).toBe(false);
expect(running()).toBe(true);
vi.advanceTimersToNextFrame();
expect(raf).toHaveBeenCalledTimes(2);
expect(callback1).toHaveBeenCalledTimes(1);
expect(timestamps.size).toEqual(1);
stop();
expect(running()).toBe(false);
expect(caf).toHaveBeenCalledTimes(1);
add2();
expect(added2()).toBe(true);
expect(running()).toBe(true);
vi.advanceTimersToNextFrame();
expect(raf).toHaveBeenCalledTimes(4);
expect(timestamps.size).toEqual(2);
remove1();
remove2();
vi.waitUntil(() => {
expect(running()).toBe(false);
expect(caf).toHaveBeenCalledTimes(2);
});
dispose();
});
});
it("(Automatic start) global RAF singleton calls rafs with the same timestamp", async () => {
// Note on this test: For some reason, the raf is being called twice, once when started (but id doesn't invoke the callback for some strange reason) and once after the timers advance (and the callback is properly invoked).
const raf = vi.spyOn(window, "requestAnimationFrame");
const caf = vi.spyOn(window, "cancelAnimationFrame");
createRoot(dispose => {
const timestamps = new Set<number>();
const createGlobalRAFCallback = useGlobalRAF();
const callback1: Mock<FrameRequestCallback> = vi.fn(ts => timestamps.add(ts));
const [added1, add1, remove1, running1, start1, stop1] = createGlobalRAFCallback(
callback1,
true,
);
const callback2: Mock<FrameRequestCallback> = vi.fn(ts => timestamps.add(ts));
const [added2, add2, remove2, running2, start2, stop2] = createGlobalRAFCallback(
callback2,
true,
);
// Queue functions should not be equal
expect(added1).not.toEqual(added2);
expect(add1).not.toEqual(add2);
expect(remove1).not.toEqual(remove2);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
// Frameloop functions should be equal because of the singleton
expect(running1).toEqual(running2);
expect(start1).toEqual(start2);
expect(stop1).toEqual(stop2);
// Aliases
const running = running1;
const stop = stop1;
expect(added1()).toBe(false);
add1();
expect(added1()).toBe(true);
expect(added2()).toBe(false);
expect(running()).toBe(true);
vi.advanceTimersToNextFrame();
expect(raf).toHaveBeenCalledTimes(2);
expect(callback1).toHaveBeenCalledTimes(1);
expect(timestamps.size).toEqual(1);
stop();
expect(running()).toBe(false);
expect(caf).toHaveBeenCalledTimes(1);
add2();
expect(added2()).toBe(true);
expect(running()).toBe(true);
vi.advanceTimersToNextFrame();
expect(raf).toHaveBeenCalledTimes(4);
expect(timestamps.size).toEqual(2);
remove1();
remove2();
await vi.waitUntil(() => !running());
expect(caf).toHaveBeenCalledTimes(2);
dispose();
});
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raf/test/index.test.ts` around lines 193 - 253, The automatic-start
RAF singleton test is not actually asserting the async disposal state because
vi.waitUntil in the useGlobalRAF/createRoot block is not awaited. Make the it
callback async and await the vi.waitUntil call so the running() and
cancelAnimationFrame expectations are part of the test result, then keep
dispose() after the awaited check.

@davedbase davedbase added the enhancement New feature or request label Jul 4, 2026
@royeden

royeden commented Jul 5, 2026

Copy link
Copy Markdown
Author

Hi @royeden any interest in completing out this PR? We're preparing for Solid Primitives 2.0 and would love to include these improvements.

I'll make the current suggested changes and I'd have no problem on porting this to V2, my only issue is knowing when the dependencies I consume on the project are ported. I'll send a follow up message on discord so I can communicate better on this 😄

@royeden

royeden commented Jul 26, 2026

Copy link
Copy Markdown
Author

@davedbase comments should have been addressed 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants