Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions packages/raf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,129 @@ function createMs(
};
```

## createCallbacksSet

A primitive for executing multiple callbacks at once, intended for usage in conjunction with primitives like `createRAF`, where you want to execute multiple callbacks in the same `window.requestAnimationFrame` (sharing the timestamp).

#### Definition

```ts
function createCallbacksSet<T extends (...args: any) => void>(...initialCallbacks: Array<T>): [callback: T, callbacksSet: ReactiveSet<T>]
```

## useGlobalRAF

A singleton root that returns a function similar to `createRAF` that batches multiple `window.requestAnimationFrame` executions within the same same timestamp (same RAF cycle) instead of skipping requests in separate frames. This is done by using a single `createRAF` in a [singleton root](https://github.com/solidjs-community/solid-primitives/tree/main/packages/rootless#createSingletonRoot) in conjuction with [`createCallbacksSet`](https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#createCallbacksSet)

Returns a factory function that works like `createRAF` with an additional parameter to start the global RAF loop when adding the callback to the callbacks set. This function return is also similar to `createRAF`, but it's first three elements of the tuple are related to the presence of the callback in the callbacks set, while the next three are the same as `createRAF`, but for the global loop that executes all the callbacks present in the callbacks set.

```ts
import { useGlobalRAF } from "@solid-primitives/raf";

const createScheduledLoop = useGlobalRAF()
const [hasAddedManual, addManual, removeManual, isRunningManual] = createScheduledLoop(
timeStamp => console.log("Time stamp is", timeStamp)
);
const [hasAddedAuto, addAuto, removeAuto, isRunningAuto] = createScheduledLoop(
timeStamp => console.log("Time stamp is", timeStamp),
true
);

hasAddedManual() // false
addManual()
hasAddedManual() // true
isRunningManual() // false

hasAddedAuto() // false
addAuto()
hasAddedAuto() // true
// Both are running on the same global loop
isRunningAuto() // true
isRunningManual() // true
```

#### Example

```ts
import { targetFPS, useGlobalRAF } from "@solid-primitives/raf";

const createScheduledLoop = useGlobalRAF()

const [hasAddedLowFramerate, addLowFramerate, removeLowFramerate] = createScheduledLoop(
targetFPS(
() => {
/* Low framerate loop, for example for video / webcam sampling where the framerate can be capped by external sources */
},
30
),
true
);
const [hasAddedHighFramerate, addHighFramerate, removeHighFramerate] = createScheduledLoop(
targetFPS(
() => {
/* High framerate loop for an animation / drawing to a canvas */
},
60
),
true
);
```

#### Definition

```ts
function useGlobalRAF(): (callback: FrameRequestCallback, startWhenAdded?: MaybeAccessor<boolean>) => [
added: Accessor<boolean>,
add: VoidFunction,
remove: VoidFunction,
running: Accessor<boolean>,
start: VoidFunction,
stop: VoidFunction
];
```

#### Warning

Only use this when you absolutely need to schedule animations on the same frame and stick to quick executions trying not to overload the amount of work performed in the current animation frame. If you need to ensure multiple things run on the same request but you also want to schedule multiple requests, you can use [`createCallbacksSet`](https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#createCallbacksSet) and a singleton `createRAF` to compose something similar to this primitive.

## createScheduledLoop

A primitive for creating reactive interactions with external frameloop related functions (for example using [motion's frame util](https://motion.dev/docs/frame)) that are automatically disposed onCleanup.

```ts
import { cancelFrame, frame } from "motion";

const createMotionFrameRender = createScheduledLoop(
callback => frame.render(callback, true),
cancelFrame,
);
const [running, start, stop] = createMotionFrameRender(
data => element.style.transform = "translateX(...)"
);

// Alternative syntax (for a single execution in place):
import { cancelFrame, frame } from "motion";

const [running, start, stop] = createScheduledLoop(
callback => frame.render(callback, true),
cancelFrame,
)(
data => element.style.transform = "translateX(...)"
);
```

#### Definition

```ts
function createScheduledLoop<
RequestID extends NonNullable<unknown>,
Callback extends (...args: Array<any>) => any,
>(
schedule: (callback: Callback) => RequestID,
cancel: (requestID: RequestID) => void,
): (callback: Callback) => [running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction]
```

## Demo

You may view a working example here: https://codesandbox.io/s/solid-primitives-raf-demo-4xvmjd?file=/src/index.tsx
Expand Down
2 changes: 2 additions & 0 deletions packages/raf/package.json
Comment thread
royeden marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
"primitives"
],
"dependencies": {
"@solid-primitives/rootless": "workspace:^",
"@solid-primitives/set": "workspace:^",
"@solid-primitives/utils": "workspace:^"
},
"peerDependencies": {
Expand Down
149 changes: 145 additions & 4 deletions packages/raf/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { type MaybeAccessor, noop } from "@solid-primitives/utils";
import { createHydratableSingletonRoot } from "@solid-primitives/rootless";
import { ReactiveSet } from "@solid-primitives/set";
import { access, type MaybeAccessor, noop } from "@solid-primitives/utils";
import { createSignal, createMemo, type Accessor, onCleanup } from "solid-js";
import { isServer } from "solid-js/web";

Expand All @@ -23,7 +25,7 @@ function createRAF(
return [() => false, noop, noop];
}
const [running, setRunning] = createSignal(false);
let requestID = 0;
let requestID: number | null = null;

const loop: FrameRequestCallback = timeStamp => {
requestID = requestAnimationFrame(loop);
Expand All @@ -36,13 +38,144 @@ function createRAF(
};
const stop = () => {
setRunning(false);
cancelAnimationFrame(requestID);
if (requestID !== null) cancelAnimationFrame(requestID);
};

onCleanup(stop);
return [running, start, stop];
}

/**
* A primitive for executing multiple callbacks at once, intended for usage in conjunction with primitives like `createRAF`.
* @param initialCallbacks
* @returns a main callback function that executes all the callbacks at once, as well as the `ReactiveSet` that contains all the callbacks
* ```ts
* [callback: T, callbacksSet: ReactiveSet<T>]
* ```
*/
function createCallbacksSet<T extends (...args: any) => void>(
...initialCallbacks: Array<T>
): [callback: T, callbacksSet: ReactiveSet<T>] {
const callbacksSet = new ReactiveSet(initialCallbacks);

return [((...args) => callbacksSet.forEach(callback => callback(...args))) as T, callbacksSet];
}

/**
* A singleton root that returns a function similar to `createRAF` that batches multiple `window.requestAnimationFrame` executions within the same same timestamp (same RAF cycle) instead of skipping requests in separate frames. This is done by using a single `createRAF` in a [singleton root](https://github.com/solidjs-community/solid-primitives/tree/main/packages/rootless#createSingletonRoot) in conjuction with [`createCallbacksSet`](https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#createCallbacksSet)
*
* @returns Returns a factory function that works like `createRAF` with an additional parameter to start the global RAF loop when adding the callback to the callbacks set. This function return is also similar to `createRAF`, but it's first three elements of the tuple are related to the presence of the callback in the callbacks set, while the next three are the same as `createRAF`, but for the global loop that executes all the callbacks present in the callbacks set.
* ```ts
* (callback: FrameRequestCallback, startWhenAdded?: boolean) => [added: Accessor<boolean>, add: VoidFunction, remove: VoidFunction, running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction]
* ```
*
* @example
* const createGlobalRAFCallback = useGlobalRAF();
*
* const [added, add, remove, running, start, stop] = createGlobalRAFCallback(() => {
* el.style.transform = "translateX(...)"
* });
*
* // Usage with targetFPS
* const [added, add, remove, running, start, stop] = createGlobalRAFCallback(targetFPS(() => {
* el.style.transform = "translateX(...)"
* }, 60));
*/
const useGlobalRAF = createHydratableSingletonRoot<
(
callback: FrameRequestCallback,
startWhenAdded?: MaybeAccessor<boolean>,
) => [
added: Accessor<boolean>,
add: VoidFunction,
remove: VoidFunction,
running: Accessor<boolean>,
start: VoidFunction,
stop: VoidFunction,
]
>(() => {
if (isServer) return () => [() => false, noop, noop, () => false, noop, noop];

const [callback, callbacksSet] = createCallbacksSet<FrameRequestCallback>();
const [running, start, stop] = createRAF(callback);

return function createGlobalRAFCallback(callback: FrameRequestCallback, startWhenAdded = false) {
const added = () => callbacksSet.has(callback);
const add = () => {
callbacksSet.add(callback);
if (access(startWhenAdded) && !running()) start();
};
const remove = () => {
callbacksSet.delete(callback);
if (running() && callbacksSet.size === 0) stop();
};

onCleanup(remove);
return [added, add, remove, running, start, stop];
};
});

/**
* A primitive for creating reactive interactions with external frameloop related functions (for example using [motion's frame util](https://motion.dev/docs/frame)) that are automatically disposed onCleanup.
*
* @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#createScheduledLoop
* @param schedule The function that receives the callback and handles it's loop scheduling, returning a requestID that is used to cancel the loop
* @param cancel The function that cancels the scheduled callback using the requestID.
* @returns Returns a function that receives a callback that's compatible with the provided scheduler and returns a signal if currently running as well as start and stop methods
* ```ts
* (callback: Callback) => [running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction]
* ```
*
* @example
* import { cancelFrame, frame } from "motion";
*
* const createMotionFrameRender = createScheduledLoop(
* callback => frame.render(callback, true),
* cancelFrame,
* );
* const [running, start, stop] = createMotionFrameRender(
* data => element.style.transform = "translateX(...)"
* );
*
* // Alternative syntax (for a single execution in place):
* import { cancelFrame, frame } from "motion";
*
* const [running, start, stop] = createScheduledLoop(
* callback => frame.render(callback, true),
* cancelFrame,
* )(
* data => element.style.transform = "translateX(...)"
* );
*/
function createScheduledLoop<
RequestID extends NonNullable<unknown>,
Callback extends (...args: Array<any>) => any,
>(
schedule: (callback: Callback) => RequestID,
cancel: (requestID: RequestID) => void,
): (callback: Callback) => [running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction] {
return (callback: Callback) => {
if (isServer) {
return [() => false, noop, noop];
}
const [running, setRunning] = createSignal(false);
let requestID: RequestID | null = null;

const start = () => {
if (running()) return;
setRunning(true);
requestID = schedule(callback);
};
const stop = () => {
setRunning(false);
if (requestID !== null) cancel(requestID);
};

onCleanup(stop);
return [running, start, stop];
};
}
Comment on lines +150 to +177

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What does motion's frame.render() function return, and does cancelFrame require the original callback reference or the return value of frame.render()?

💡 Result:

The frame.render function (part of Motion's frameloop) does not return a value—specifically, it returns undefined [1]. When using cancelFrame, you must provide the original callback reference (the function you passed to frame.render) [1][2][3]. You cannot use a return value, as the function does not provide one [1]. In practice, frame.render is used to schedule a task to run during the render phase of Motion's animation loop [4][5]. Because it is a scheduling utility rather than an execution wrapper with a return handle, cancellation relies entirely on passing the exact function reference that was initially scheduled so it can be identified and removed from the internal queue [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant source and tests.
git ls-files 'packages/raf/src/index.ts' 'test/index.test.ts' 'package.json' 'pnpm-lock.yaml' 'package-lock.json' 'yarn.lock' | sed 's/^/FILE /'

echo
echo '--- packages/raf/src/index.ts (selected range) ---'
sed -n '1,240p' packages/raf/src/index.ts | cat -n

echo
echo '--- test/index.test.ts (selected range) ---'
sed -n '1,140p' test/index.test.ts | cat -n

echo
echo '--- search for Motion usage and createScheduledLoop/createRAF ---'
rg -n "createScheduledLoop|createRAF|frame\.render|cancelFrame|requestAnimationFrame" packages test . -g '!**/dist/**' -g '!**/build/**'

Repository: solidjs-community/solid-primitives

Length of output: 11284


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- repo files matching raf tests ---'
fd -a 'index.test.ts|raf.*test.*|test.*ts' . packages test

echo
echo '--- package.json ---'
sed -n '1,240p' package.json | cat -n

echo
echo '--- package-specific package.json files ---'
fd -a 'package.json' packages | while read -r f; do
  echo "FILE $f"
  sed -n '1,220p' "$f" | rg -n '"(motion|`@motionone`|framer-motion|solid-js)"'
done

echo
echo '--- search for createScheduledLoop tests/usages ---'
rg -n "createScheduledLoop|frame\.render|cancelFrame|targetFPS|createRAF" packages . -g '!**/dist/**' -g '!**/build/**'

Repository: solidjs-community/solid-primitives

Length of output: 50390


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- packages/raf/test/index.test.ts (relevant section) ---'
sed -n '1,120p' packages/raf/test/index.test.ts | cat -n

echo
echo '--- packages/raf/README.md (createScheduledLoop section) ---'
sed -n '200,245p' packages/raf/README.md | cat -n

echo
echo '--- motion dependency presence in lockfile/package ---'
rg -n '"motion"|from "motion"|from '\''motion'\''|cancelFrame|frame\.render' package.json packages/raf/package.json pnpm-lock.yaml packages/raf/README.md packages/raf/src/index.ts

Repository: solidjs-community/solid-primitives

Length of output: 8657


Fix the Motion example’s cancellation token
frame.render() returns undefined, while cancelFrame expects the original callback reference. As written, stop() has nothing usable to pass to cancelFrame, so the scheduled work can’t be canceled. Return the callback (or another stable handle) from the Motion adapter and use that for cancel.

🤖 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 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.


/**
* A primitive for wrapping `window.requestAnimationFrame` callback function to limit the execution of the callback to specified number of FPS.
*
Expand Down Expand Up @@ -131,4 +264,12 @@ function createMs(fps: MaybeAccessor<number>, limit?: MaybeAccessor<number>): Ms
return Object.assign(ms, { reset, running, start, stop });
}

export { createMs, createRAF, createRAF as default, targetFPS };
export {
createMs,
createCallbacksSet,
createRAF,
createRAF as default,
createScheduledLoop,
targetFPS,
useGlobalRAF,
};
Loading