Patch react-native-worklets to fix web scheduleOnUI on errors
Backports software-mansion/react-native-reanimated#10167. On web, a throwing worklet used to abort the whole per-frame batch via queue.forEach, silently dropping every callback queued after it and surfacing as "can't access property 'startTime', this.frameCallbackRegistry.get(...) is undefined". The batch runner now continues past a failed callback instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
diff --git a/lib/module/threads.js b/lib/module/threads.js
|
||||
index dd3a7f1ab12e5a8030af7f17b6a7a891e0b645d2..7d48ccbf95f00724db6b083a7155f25aadd6b8f5 100644
|
||||
--- a/lib/module/threads.js
|
||||
+++ b/lib/module/threads.js
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { WorkletsError } from './debug/WorkletsError';
|
||||
import { IS_JEST } from './platformChecker';
|
||||
-import { mockedRequestAnimationFrame } from './runLoop/uiRuntime/mockedRequestAnimationFrame';
|
||||
export function scheduleOnUI(worklet, ...args) {
|
||||
enqueueUI(worklet, args);
|
||||
}
|
||||
@@ -24,38 +23,50 @@ export function scheduleOnRN(fun, ...args) {
|
||||
queueMicrotask(args.length ? () => fun(...args) : fun);
|
||||
}
|
||||
export function runOnUIAsync(worklet, ...args) {
|
||||
- return new Promise(resolve => {
|
||||
- enqueueUI(worklet, args, resolve);
|
||||
+ return new Promise((resolve, reject) => {
|
||||
+ enqueueUI(worklet, args, resolve, reject);
|
||||
});
|
||||
}
|
||||
let runOnUIQueue = [];
|
||||
-function enqueueUI(worklet, args, resolve) {
|
||||
- if (IS_JEST) {
|
||||
- mockedRequestAnimationFrame(() => {
|
||||
- const result = worklet(...args);
|
||||
- resolve?.(result);
|
||||
- });
|
||||
- } else {
|
||||
- const job = [worklet, args, resolve];
|
||||
- runOnUIQueue.push(job);
|
||||
- if (runOnUIQueue.length === 1) {
|
||||
+function enqueueUI(worklet, args, resolve, reject) {
|
||||
+ const job = [worklet, args, resolve, reject];
|
||||
+ runOnUIQueue.push(job);
|
||||
+ if (runOnUIQueue.length === 1) {
|
||||
+ if (IS_JEST) {
|
||||
flushUIQueue();
|
||||
+ } else {
|
||||
+ queueMicrotask(flushUIQueue);
|
||||
}
|
||||
}
|
||||
}
|
||||
+let offset = 0;
|
||||
function flushUIQueue() {
|
||||
- queueMicrotask(() => {
|
||||
- const queue = runOnUIQueue;
|
||||
- runOnUIQueue = [];
|
||||
- requestAnimationFrameImpl(() => {
|
||||
- queue.forEach(([workletFunction, workletArgs, jobResolve]) => {
|
||||
- const result = workletFunction(...workletArgs);
|
||||
- if (jobResolve) {
|
||||
- jobResolve(result);
|
||||
+ const queue = runOnUIQueue;
|
||||
+ runOnUIQueue = [];
|
||||
+ requestAnimationFrame(() => {
|
||||
+ offset = 0;
|
||||
+ while (queue.length > offset) {
|
||||
+ try {
|
||||
+ drainUIQueue(queue);
|
||||
+ } catch (e) {
|
||||
+ const [, , , jobReject] = queue[offset - 1];
|
||||
+ if (jobReject) {
|
||||
+ jobReject(e);
|
||||
+ } else {
|
||||
+ console.error(e);
|
||||
}
|
||||
- });
|
||||
- });
|
||||
+ }
|
||||
+ }
|
||||
});
|
||||
}
|
||||
-const requestAnimationFrameImpl = !globalThis.requestAnimationFrame ? mockedRequestAnimationFrame : globalThis.requestAnimationFrame;
|
||||
+function drainUIQueue(queue) {
|
||||
+ while (queue.length > offset) {
|
||||
+ const [workletFunction, workletArgs, jobResolve] = queue[offset];
|
||||
+ offset++;
|
||||
+ const result = workletFunction(...workletArgs);
|
||||
+ if (jobResolve) {
|
||||
+ jobResolve(result);
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
//# sourceMappingURL=threads.js.map
|
||||
@@ -0,0 +1,37 @@
|
||||
# react-native-worklets@0.8.3.patch
|
||||
|
||||
Backport of https://github.com/software-mansion/react-native-reanimated/pull/10167
|
||||
("fix(Worklets): web scheduleOnUI implementation on errors").
|
||||
|
||||
## The bug
|
||||
|
||||
On web, `scheduleOnUI`/`runOnUI` batch their callbacks per animation frame and
|
||||
run them with `queue.forEach(...)`. If any callback in the batch throws,
|
||||
`forEach` aborts immediately and every callback still queued after it is
|
||||
silently dropped - it never runs, and any `runOnUIAsync` promise for it never
|
||||
resolves or rejects.
|
||||
|
||||
Reanimated's own internals rely on those callbacks running in order (e.g. to
|
||||
populate `frameCallbackRegistry`), so a single throwing worklet in a batch can
|
||||
leave later, unrelated frame callbacks referencing state that was never set
|
||||
up, surfacing as:
|
||||
|
||||
```
|
||||
TypeError: can't access property "startTime", this.frameCallbackRegistry.get(...) is undefined
|
||||
```
|
||||
|
||||
## The fix
|
||||
|
||||
Replace the `forEach` batch runner with a `while` loop (`drainUIQueue`) that
|
||||
tracks its position via an `offset`, wrapped in a `try`/`catch`. A throw now
|
||||
only aborts the *current* callback: the loop resumes at the next queued job
|
||||
instead of abandoning the rest of the batch. Errors are routed to the
|
||||
matching `runOnUIAsync` promise's `reject` (a new second argument threaded
|
||||
through `enqueueUI`) if there is one, or `console.error`-ed otherwise, rather
|
||||
than crashing the whole frame.
|
||||
|
||||
Only `lib/module/threads.js` (the compiled web entry point actually loaded by
|
||||
the app's webpack build) is patched - `src/threads.ts` is unused here since
|
||||
this repo's web build resolves the package's `module` field, and native
|
||||
platforms use the separate `threads.native.ts` implementation untouched by
|
||||
this PR.
|
||||
Reference in New Issue
Block a user