From cf82fa7868393a55865450aa33d3bbac846ce1f1 Mon Sep 17 00:00:00 2001
From: yradex <11014207+Yradex@users.noreply.github.com>
Date: Thu, 11 Sep 2025 16:11:10 +0800
Subject: [PATCH] feat(react/runtime): delay the timing of `runOnMainThread()`
---
.changeset/lemon-streets-watch.md | 9 +
.changeset/smooth-dragons-smash.md | 5 +
.../__test__/worklet/runOnMainThread.test.js | 77 -----
.../__test__/worklet/runOnMainThread.test.jsx | 323 ++++++++++++++++++
.../runtime/src/lifecycle/patch/commit.ts | 7 +
.../src/lifecycle/patch/updateMainThread.ts | 22 +-
packages/react/runtime/src/lynx/tt.ts | 4 +
.../runtime/src/worklet/runOnMainThread.ts | 26 +-
.../__test__/runOnMainThread.test.js | 47 +++
.../worklet-runtime/src/bindings/bindings.ts | 10 +
packages/react/worklet-runtime/src/global.ts | 1 +
.../react/worklet-runtime/src/listeners.ts | 12 +-
.../worklet-runtime/src/runOnMainThread.ts | 21 ++
.../worklet-runtime/src/workletRuntime.ts | 2 +
packages/rspeedy/plugin-react/package.json | 2 +-
15 files changed, 474 insertions(+), 94 deletions(-)
create mode 100644 .changeset/lemon-streets-watch.md
create mode 100644 .changeset/smooth-dragons-smash.md
delete mode 100644 packages/react/runtime/__test__/worklet/runOnMainThread.test.js
create mode 100644 packages/react/runtime/__test__/worklet/runOnMainThread.test.jsx
create mode 100644 packages/react/worklet-runtime/__test__/runOnMainThread.test.js
create mode 100644 packages/react/worklet-runtime/src/runOnMainThread.ts
diff --git a/.changeset/lemon-streets-watch.md b/.changeset/lemon-streets-watch.md
new file mode 100644
index 0000000000..3e7f6a4705
--- /dev/null
+++ b/.changeset/lemon-streets-watch.md
@@ -0,0 +1,9 @@
+---
+"@lynx-js/react": minor
+---
+
+fix: Delay execution of `runOnMainThread()` during initial render
+
+When called during the initial render, `runOnMainThread()` would execute before the `main-thread:ref` was hydrated, causing it to be incorrectly set to null.
+
+This change delays the function's execution to ensure the ref is available and correctly assigned.
diff --git a/.changeset/smooth-dragons-smash.md b/.changeset/smooth-dragons-smash.md
new file mode 100644
index 0000000000..810cea4eac
--- /dev/null
+++ b/.changeset/smooth-dragons-smash.md
@@ -0,0 +1,5 @@
+---
+"@lynx-js/react-rsbuild-plugin": patch
+---
+
+Be compat with `@lynx-js/react` v0.113.0
diff --git a/packages/react/runtime/__test__/worklet/runOnMainThread.test.js b/packages/react/runtime/__test__/worklet/runOnMainThread.test.js
deleted file mode 100644
index cfc0bf5b4e..0000000000
--- a/packages/react/runtime/__test__/worklet/runOnMainThread.test.js
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright 2024 The Lynx Authors. All rights reserved.
-// Licensed under the Apache License Version 2.0 that can be found in the
-// LICENSE file in the root directory of this source tree.
-import { afterEach, beforeEach, describe, expect, it } from 'vitest';
-
-import { WorkletEvents } from '@lynx-js/react/worklet-runtime/bindings';
-
-import { destroyWorklet } from '../../src/worklet/destroy';
-import { clearConfigCacheForTesting } from '../../src/worklet/functionality';
-import { runOnMainThread } from '../../src/worklet/runOnMainThread';
-import { globalEnvManager } from '../utils/envManager';
-
-beforeEach(() => {
- globalThis.SystemInfo.lynxSdkVersion = '2.14';
- clearConfigCacheForTesting();
-});
-
-afterEach(() => {
- destroyWorklet();
-});
-
-describe('runOnMainThread', () => {
- it('should trigger event', () => {
- globalEnvManager.switchToBackground();
- const worklet = {
- _wkltId: '835d:450ef:2',
- };
- runOnMainThread(worklet)(1, ['args']);
- expect(lynx.getCoreContext().dispatchEvent.mock.calls).toMatchInlineSnapshot(`
- [
- [
- {
- "data": "{"worklet":{"_wkltId":"835d:450ef:2"},"params":[1,["args"]],"resolveId":1}",
- "type": "Lynx.Worklet.runWorkletCtx",
- },
- ],
- ]
- `);
- });
-
- it('should get return value', async () => {
- globalEnvManager.switchToBackground();
- const promise = runOnMainThread('someWorklet')('hello');
-
- globalEnvManager.switchToMainThread();
- lynx.getJSContext().dispatchEvent({
- type: WorkletEvents.FunctionCallRet,
- data: JSON.stringify({
- resolveId: 1,
- returnValue: 'world',
- }),
- });
-
- await expect(promise).resolves.toBe('world');
- });
-
- it('should throw when on the main thread', () => {
- globalEnvManager.switchToMainThread();
- const worklet = {
- _wkltId: '835d:450ef:2',
- };
- expect(() => {
- runOnMainThread(worklet)(1, ['args']);
- }).toThrowError('runOnMainThread can only be used on the background thread.');
- });
-
- it('should not trigger event when native capabilities not fulfilled', () => {
- globalThis.SystemInfo.lynxSdkVersion = '2.13';
- globalEnvManager.switchToBackground();
- const worklet = {
- _wkltId: '835d:450ef:2',
- };
- expect(() => {
- runOnMainThread(worklet)(1, ['args']);
- }).toThrowError('runOnMainThread requires Lynx sdk version 2.14.');
- });
-});
diff --git a/packages/react/runtime/__test__/worklet/runOnMainThread.test.jsx b/packages/react/runtime/__test__/worklet/runOnMainThread.test.jsx
new file mode 100644
index 0000000000..e386f00f0f
--- /dev/null
+++ b/packages/react/runtime/__test__/worklet/runOnMainThread.test.jsx
@@ -0,0 +1,323 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+import { render } from 'preact';
+import { afterEach, beforeEach, describe, expect, it, vi, beforeAll } from 'vitest';
+
+import { WorkletEvents } from '@lynx-js/react/worklet-runtime/bindings';
+
+import { destroyWorklet } from '../../src/worklet/destroy';
+import { clearConfigCacheForTesting } from '../../src/worklet/functionality';
+import { runOnMainThread } from '../../src/worklet/runOnMainThread';
+import { globalEnvManager } from '../utils/envManager';
+import { initGlobalSnapshotPatch } from '../../src/lifecycle/patch/snapshotPatch';
+import { replaceCommitHook } from '../../src/lifecycle/patch/commit';
+import { __root } from '../../src/root';
+import { root } from '../../src/lynx-api';
+import { waitSchedule } from '../utils/nativeMethod';
+
+const App = ({ fn, attr }) => {
+ fn?.();
+ return (
+
+ hello
+
+ );
+};
+
+const MTFQueue = [];
+
+beforeAll(() => {
+ vi.stubGlobal(
+ 'runWorklet',
+ vi.fn((worklet, args) => {
+ MTFQueue.push({ api: 'runWorklet', worklet, args });
+ }),
+ );
+ vi.stubGlobal('lynxWorkletImpl', {
+ _runRunOnMainThreadTask: vi.fn((worklet, args) => {
+ MTFQueue.push({ api: '_runRunOnMainThreadTask', worklet, args });
+ }),
+ _runOnBackgroundDelayImpl: {
+ runDelayedBackgroundFunctions: vi.fn(),
+ },
+ _eomImpl: {
+ setShouldFlush: vi.fn((value) => {
+ MTFQueue.push({ api: 'setShouldFlush', value });
+ }),
+ },
+ _refImpl: {
+ clearFirstScreenWorkletRefMap: vi.fn(),
+ },
+ _eventDelayImpl: {
+ clearDelayedWorklets: vi.fn(),
+ runDelayedWorklet: vi.fn(),
+ },
+ });
+ vi.stubGlobal(
+ '__FlushElementTree',
+ vi.fn(() => {
+ MTFQueue.push({ api: '__FlushElementTree' });
+ }),
+ );
+});
+
+beforeEach(() => {
+ globalThis.SystemInfo.lynxSdkVersion = '2.14';
+ clearConfigCacheForTesting();
+ globalEnvManager.resetEnv();
+ replaceCommitHook();
+});
+
+afterEach(() => {
+ destroyWorklet();
+ vi.resetAllMocks();
+ MTFQueue.length = 0;
+});
+
+describe('runOnMainThread', () => {
+ it('should trigger event', () => {
+ globalEnvManager.switchToBackground();
+ initGlobalSnapshotPatch();
+ const worklet = {
+ _wkltId: '835d:450ef:2',
+ };
+ runOnMainThread(worklet)(1, ['args']);
+ expect(lynx.getCoreContext().dispatchEvent.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ {
+ "data": "{"worklet":{"_wkltId":"835d:450ef:2"},"params":[1,["args"]],"resolveId":1}",
+ "type": "Lynx.Worklet.runWorkletCtx",
+ },
+ ],
+ ]
+ `);
+ });
+
+ it('should get return value', async () => {
+ globalEnvManager.switchToBackground();
+ initGlobalSnapshotPatch();
+ const promise = runOnMainThread('someWorklet')('hello');
+
+ globalEnvManager.switchToMainThread();
+ lynx.getJSContext().dispatchEvent({
+ type: WorkletEvents.FunctionCallRet,
+ data: JSON.stringify({
+ resolveId: 1,
+ returnValue: 'world',
+ }),
+ });
+
+ await expect(promise).resolves.toBe('world');
+ });
+
+ it('should throw when on the main thread', () => {
+ globalEnvManager.switchToMainThread();
+ const worklet = {
+ _wkltId: '835d:450ef:2',
+ };
+ expect(() => {
+ runOnMainThread(worklet)(1, ['args']);
+ }).toThrowError('runOnMainThread can only be used on the background thread.');
+ });
+
+ it('should not trigger event when native capabilities not fulfilled', () => {
+ globalThis.SystemInfo.lynxSdkVersion = '2.13';
+ globalEnvManager.switchToBackground();
+ initGlobalSnapshotPatch();
+ const worklet = {
+ _wkltId: '835d:450ef:2',
+ };
+ expect(() => {
+ runOnMainThread(worklet)(1, ['args']);
+ }).toThrowError('runOnMainThread requires Lynx sdk version 2.14.');
+ });
+
+ it('should delay until hydration finished while initial rendering', async () => {
+ const MTF_during_render = 'MTF_during_render';
+ const MTF_after_render = 'MTF_after_render';
+
+ // 1. MTS init render
+ {
+ __root.__jsx = ;
+ renderPage();
+ }
+
+ // 2. hydration
+ {
+ globalEnvManager.switchToBackground();
+ root.render();
+ await waitSchedule();
+ runOnMainThread(MTF_after_render)();
+ }
+
+ // 3. check MTFs are not invoked
+ {
+ expect(MTFQueue).toMatchInlineSnapshot(`[]`);
+ expect(lynx.getCoreContext().dispatchEvent.mock.calls).toMatchInlineSnapshot(`[]`);
+ MTFQueue.length = 0;
+ }
+
+ // 4. hydrate
+ {
+ // LifecycleConstant.firstScreen
+ lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
+
+ // rLynxChange
+ globalEnvManager.switchToMainThread();
+ const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
+ globalThis[rLynxChange[0]](rLynxChange[1]);
+ lynx.getNativeApp().callLepusMethod.mockClear();
+ }
+
+ // 5. check MTFs are invoked
+ {
+ expect(MTFQueue).toMatchInlineSnapshot(`
+ [
+ {
+ "api": "runWorklet",
+ "args": [
+ {
+ "elementRefptr":
+
+ ,
+ },
+ ],
+ "worklet": {
+ "_unmount": undefined,
+ "_wkltId": "MTRef",
+ },
+ },
+ {
+ "api": "setShouldFlush",
+ "value": false,
+ },
+ {
+ "api": "_runRunOnMainThreadTask",
+ "args": [],
+ "worklet": "MTF_during_render",
+ },
+ {
+ "api": "_runRunOnMainThreadTask",
+ "args": [],
+ "worklet": "MTF_after_render",
+ },
+ {
+ "api": "setShouldFlush",
+ "value": true,
+ },
+ {
+ "api": "__FlushElementTree",
+ },
+ ]
+ `);
+ expect(lynx.getCoreContext().dispatchEvent.mock.calls).toMatchInlineSnapshot(`[]`);
+ }
+ });
+
+ it('should delay until patch applying finished while updating', async () => {
+ const MTF_during_render = 'MTF_during_render';
+
+ // 1. MTS init render
+ {
+ __root.__jsx = ;
+ renderPage();
+ }
+
+ // 2. hydration
+ {
+ globalEnvManager.switchToBackground();
+ root.render();
+ await waitSchedule();
+ }
+
+ // 3. hydrate
+ {
+ // LifecycleConstant.firstScreen
+ lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
+
+ // rLynxChange
+ globalEnvManager.switchToMainThread();
+ const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
+ globalThis[rLynxChange[0]](rLynxChange[1]);
+ lynx.getNativeApp().callLepusMethod.mockClear();
+ }
+
+ // 4. check MTFs are not invoked
+ {
+ expect(MTFQueue).toMatchInlineSnapshot(`
+ [
+ {
+ "api": "__FlushElementTree",
+ },
+ ]
+ `);
+ expect(lynx.getCoreContext().dispatchEvent.mock.calls).toMatchInlineSnapshot(`[]`);
+ MTFQueue.length = 0;
+ }
+
+ // 5. BTS update
+ {
+ globalEnvManager.switchToBackground();
+ render(
+ ,
+ __root,
+ );
+
+ // rLynxChange
+ globalEnvManager.switchToMainThread();
+ const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
+ globalThis[rLynxChange[0]](rLynxChange[1]);
+ lynx.getNativeApp().callLepusMethod.mockClear();
+ }
+
+ // 6. check MTFs are invoked
+ {
+ expect(MTFQueue).toMatchInlineSnapshot(`
+ [
+ {
+ "api": "runWorklet",
+ "args": [
+ {
+ "elementRefptr":
+
+ ,
+ },
+ ],
+ "worklet": {
+ "_unmount": undefined,
+ "_wkltId": "MTRef",
+ },
+ },
+ {
+ "api": "setShouldFlush",
+ "value": false,
+ },
+ {
+ "api": "_runRunOnMainThreadTask",
+ "args": [],
+ "worklet": "MTF_during_render",
+ },
+ {
+ "api": "setShouldFlush",
+ "value": true,
+ },
+ {
+ "api": "__FlushElementTree",
+ },
+ ]
+ `);
+ expect(lynx.getCoreContext().dispatchEvent.mock.calls).toMatchInlineSnapshot(`[]`);
+ }
+ });
+});
diff --git a/packages/react/runtime/src/lifecycle/patch/commit.ts b/packages/react/runtime/src/lifecycle/patch/commit.ts
index f93f908571..f87a5fca6d 100644
--- a/packages/react/runtime/src/lifecycle/patch/commit.ts
+++ b/packages/react/runtime/src/lifecycle/patch/commit.ts
@@ -21,6 +21,8 @@
import { options } from 'preact';
+import type { RunWorkletCtxData } from '@lynx-js/react/worklet-runtime/bindings';
+
import { LifecycleConstant } from '../../lifecycleConstant.js';
import { globalPipelineOptions, markTiming, markTimingLegacy, setPipeline } from '../../lynx/performance.js';
import { COMMIT } from '../../renderToOpcodes/constants.js';
@@ -32,6 +34,7 @@ import { getReloadVersion } from '../pass.js';
import type { SnapshotPatch } from './snapshotPatch.js';
import { takeGlobalSnapshotPatch } from './snapshotPatch.js';
import { profileEnd, profileStart } from '../../debug/utils.js';
+import { delayedRunOnMainThreadData, takeDelayedRunOnMainThreadData } from '../../worklet/runOnMainThread.js';
import { isRendering } from '../isRendering.js';
let globalFlushOptions: FlushOptions = {};
@@ -62,6 +65,7 @@ interface Patch {
*/
interface PatchList {
patchList: Patch[];
+ delayedRunOnMainThreadData?: RunWorkletCtxData[];
flushOptions?: FlushOptions;
}
@@ -156,6 +160,9 @@ function replaceCommitHook(): void {
if (!isEmptyObject(flushOptions)) {
patchList.flushOptions = flushOptions;
}
+ if (snapshotPatch && delayedRunOnMainThreadData.length) {
+ patchList.delayedRunOnMainThreadData = takeDelayedRunOnMainThreadData();
+ }
const obj = commitPatchUpdate(patchList, patchOptions);
// Send the update to the native layer
diff --git a/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts b/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts
index 3d8776b6da..c15ce37f1d 100644
--- a/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts
+++ b/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts
@@ -2,7 +2,12 @@
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
-import { updateWorkletRefInitValueChanges } from '@lynx-js/react/worklet-runtime/bindings';
+import type { ClosureValueType } from '@lynx-js/react/worklet-runtime/bindings';
+import {
+ runRunOnMainThreadTask,
+ setEomShouldFlushElementTree,
+ updateWorkletRefInitValueChanges,
+} from '@lynx-js/react/worklet-runtime/bindings';
import type { PatchList, PatchOptions } from './commit.js';
import { setMainThreadHydrating } from './isMainThreadHydrating.js';
@@ -12,6 +17,7 @@ import { markTiming, setPipeline } from '../../lynx/performance.js';
import { __pendingListUpdates } from '../../pendingListUpdates.js';
import { applyRefQueue } from '../../snapshot/workletRef.js';
import { __page } from '../../snapshot.js';
+import { isMtsEnabled } from '../../worklet/functionality.js';
import { getReloadVersion } from '../pass.js';
function updateMainThread(
@@ -36,7 +42,7 @@ function updateMainThread(
setPipeline(patchOptions.pipelineOptions);
markTiming('mtsRenderStart');
markTiming('parseChangesStart');
- const { patchList, flushOptions = {} } = JSON.parse(data) as PatchList;
+ const { patchList, flushOptions = {}, delayedRunOnMainThreadData } = JSON.parse(data) as PatchList;
markTiming('parseChangesEnd');
markTiming('patchChangesStart');
@@ -62,6 +68,18 @@ function updateMainThread(
}
}
applyRefQueue();
+ if (delayedRunOnMainThreadData && isMtsEnabled()) {
+ setEomShouldFlushElementTree(false);
+ for (const data of delayedRunOnMainThreadData) {
+ try {
+ runRunOnMainThreadTask(data.worklet, data.params as ClosureValueType[], data.resolveId);
+ /* v8 ignore next 3 */
+ } catch (e) {
+ lynx.reportError(e as Error);
+ }
+ }
+ setEomShouldFlushElementTree(true);
+ }
if (patchOptions.pipelineOptions) {
flushOptions.pipelineOptions = patchOptions.pipelineOptions;
}
diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts
index f29c5ad262..9a6403180e 100644
--- a/packages/react/runtime/src/lynx/tt.ts
+++ b/packages/react/runtime/src/lynx/tt.ts
@@ -22,6 +22,7 @@ import { __root } from '../root.js';
import { backgroundSnapshotInstanceManager } from '../snapshot.js';
import type { SerializedSnapshotInstance } from '../snapshot.js';
import { destroyWorklet } from '../worklet/destroy.js';
+import { delayedRunOnMainThreadData, takeDelayedRunOnMainThreadData } from '../worklet/runOnMainThread.js';
export { runWithForce };
@@ -120,6 +121,9 @@ function onLifecycleEventImpl(type: LifecycleConstant, data: unknown): void {
const patchList: PatchList = {
patchList: [{ snapshotPatch, id: commitTaskId }],
};
+ if (delayedRunOnMainThreadData.length) {
+ patchList.delayedRunOnMainThreadData = takeDelayedRunOnMainThreadData();
+ }
const obj = commitPatchUpdate(patchList, { isHydration: true });
lynx.getNativeApp().callLepusMethod(LifecycleConstant.patchUpdate, obj, () => {
diff --git a/packages/react/runtime/src/worklet/runOnMainThread.ts b/packages/react/runtime/src/worklet/runOnMainThread.ts
index 5b3154a382..17cb839bfd 100644
--- a/packages/react/runtime/src/worklet/runOnMainThread.ts
+++ b/packages/react/runtime/src/worklet/runOnMainThread.ts
@@ -7,6 +7,16 @@ import { WorkletEvents } from '@lynx-js/react/worklet-runtime/bindings';
import { onPostWorkletCtx } from './ctx.js';
import { isMtsEnabled } from './functionality.js';
import { onFunctionCall } from './functionCall.js';
+import { isRendering } from '../lifecycle/isRendering.js';
+import { __globalSnapshotPatch } from '../lifecycle/patch/snapshotPatch.js';
+
+export let delayedRunOnMainThreadData: RunWorkletCtxData[] = [];
+
+export function takeDelayedRunOnMainThreadData(): typeof delayedRunOnMainThreadData {
+ const data = delayedRunOnMainThreadData;
+ delayedRunOnMainThreadData = [];
+ return data;
+}
/**
* `runOnMainThread` allows triggering main thread functions on the main thread asynchronously.
@@ -37,13 +47,19 @@ export function runOnMainThread R>(fn: Fn): (.
return new Promise((resolve) => {
onPostWorkletCtx(fn as any as Worklet);
const resolveId = onFunctionCall(resolve);
+ const data = {
+ worklet: fn as any as Worklet,
+ params,
+ resolveId,
+ } as RunWorkletCtxData;
+ if (__globalSnapshotPatch === undefined || isRendering.value) {
+ // before hydration or is rendering
+ delayedRunOnMainThreadData.push(data);
+ return;
+ }
lynx.getCoreContext().dispatchEvent({
type: WorkletEvents.runWorkletCtx,
- data: JSON.stringify({
- worklet: fn as any as Worklet,
- params,
- resolveId,
- } as RunWorkletCtxData),
+ data: JSON.stringify(data),
});
});
};
diff --git a/packages/react/worklet-runtime/__test__/runOnMainThread.test.js b/packages/react/worklet-runtime/__test__/runOnMainThread.test.js
new file mode 100644
index 0000000000..d3402708be
--- /dev/null
+++ b/packages/react/worklet-runtime/__test__/runOnMainThread.test.js
@@ -0,0 +1,47 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { runRunOnMainThreadTask } from '../src/runOnMainThread';
+import { initWorklet } from '../src/workletRuntime';
+
+beforeEach(() => {
+ globalThis.SystemInfo = {
+ lynxSdkVersion: '2.16',
+ };
+ initWorklet();
+ const dispatchEvent = vi.fn();
+ globalThis.lynx = {
+ getJSContext: vi.fn(() => ({
+ dispatchEvent,
+ })),
+ };
+});
+
+afterEach(() => {
+ delete globalThis.lynxWorkletImpl;
+});
+
+describe('runOnMainThread', () => {
+ it('worklet should be called', () => {
+ const fn = vi.fn(() => 'ret');
+ globalThis.registerWorklet('main-thread', '1', fn);
+ let worklet = {
+ _wkltId: '1',
+ };
+
+ runRunOnMainThreadTask(worklet, [42], 10);
+ expect(fn).toBeCalledWith(42);
+ expect(globalThis.lynx.getJSContext().dispatchEvent.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ {
+ "data": "{"resolveId":10,"returnValue":"ret"}",
+ "type": "Lynx.Worklet.FunctionCallRet",
+ },
+ ],
+ ]
+ `);
+ });
+});
diff --git a/packages/react/worklet-runtime/src/bindings/bindings.ts b/packages/react/worklet-runtime/src/bindings/bindings.ts
index 005283489b..a7fca5ebb2 100644
--- a/packages/react/worklet-runtime/src/bindings/bindings.ts
+++ b/packages/react/worklet-runtime/src/bindings/bindings.ts
@@ -63,6 +63,15 @@ function setEomShouldFlushElementTree(value: boolean) {
globalThis.lynxWorkletImpl?._eomImpl.setShouldFlush(value);
}
+/**
+ * Runs a task on the main thread.
+ *
+ * @internal
+ */
+function runRunOnMainThreadTask(task: Worklet, params: ClosureValueType[], resolveId: number): void {
+ globalThis.lynxWorkletImpl?._runRunOnMainThreadTask(task, params, resolveId);
+}
+
export {
runWorkletCtx,
updateWorkletRef,
@@ -70,4 +79,5 @@ export {
registerWorklet,
delayRunOnBackground,
setEomShouldFlushElementTree,
+ runRunOnMainThreadTask,
};
diff --git a/packages/react/worklet-runtime/src/global.ts b/packages/react/worklet-runtime/src/global.ts
index c00a9f0faf..02a4caffe7 100644
--- a/packages/react/worklet-runtime/src/global.ts
+++ b/packages/react/worklet-runtime/src/global.ts
@@ -18,6 +18,7 @@ declare global {
_runOnBackgroundDelayImpl: RunOnBackgroundDelayImpl;
_hydrateCtx: (worklet: Worklet, firstScreenWorklet: Worklet) => void;
_eomImpl: EomImpl;
+ _runRunOnMainThreadTask: (task: Worklet, params: ClosureValueType[], resolveId: number) => void;
};
function runWorklet(ctx: Worklet, params: ClosureValueType[]): unknown;
diff --git a/packages/react/worklet-runtime/src/listeners.ts b/packages/react/worklet-runtime/src/listeners.ts
index 6ebe94faaf..8918a48021 100644
--- a/packages/react/worklet-runtime/src/listeners.ts
+++ b/packages/react/worklet-runtime/src/listeners.ts
@@ -2,8 +2,9 @@
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
import { WorkletEvents } from './bindings/events.js';
-import type { ReleaseWorkletRefData, RunWorkletCtxData, RunWorkletCtxRetData } from './bindings/events.js';
+import type { ReleaseWorkletRefData, RunWorkletCtxData } from './bindings/events.js';
import type { ClosureValueType } from './bindings/types.js';
+import { runRunOnMainThreadTask } from './runOnMainThread.js';
import type { Event } from './types/runtimeProxy.js';
import { removeValueFromWorkletRefMap } from './workletRef.js';
@@ -13,14 +14,7 @@ function initEventListeners(): void {
WorkletEvents.runWorkletCtx,
(event: Event) => {
const data = JSON.parse(event.data as string) as RunWorkletCtxData;
- const returnValue = runWorklet(data.worklet, data.params as ClosureValueType[]);
- jsContext.dispatchEvent({
- type: WorkletEvents.FunctionCallRet,
- data: JSON.stringify({
- resolveId: data.resolveId,
- returnValue,
- } as RunWorkletCtxRetData),
- });
+ runRunOnMainThreadTask(data.worklet, data.params as ClosureValueType[], data.resolveId);
},
);
jsContext.addEventListener(
diff --git a/packages/react/worklet-runtime/src/runOnMainThread.ts b/packages/react/worklet-runtime/src/runOnMainThread.ts
new file mode 100644
index 0000000000..b8561dd2d5
--- /dev/null
+++ b/packages/react/worklet-runtime/src/runOnMainThread.ts
@@ -0,0 +1,21 @@
+// Copyright 2025 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+import { WorkletEvents } from './bindings/index.js';
+import type { ClosureValueType, RunWorkletCtxRetData, Worklet } from './bindings/index.js';
+
+export function runRunOnMainThreadTask(task: Worklet, params: ClosureValueType[], resolveId: number): void {
+ let returnValue;
+ try {
+ returnValue = runWorklet(task, params);
+ } finally {
+ // TODO: Should be more proper to reject the promise if there is an error.
+ lynx.getJSContext().dispatchEvent({
+ type: WorkletEvents.FunctionCallRet,
+ data: JSON.stringify({
+ resolveId,
+ returnValue,
+ } as RunWorkletCtxRetData),
+ });
+ }
+}
diff --git a/packages/react/worklet-runtime/src/workletRuntime.ts b/packages/react/worklet-runtime/src/workletRuntime.ts
index 41623aa2ef..9cd3876766 100644
--- a/packages/react/worklet-runtime/src/workletRuntime.ts
+++ b/packages/react/worklet-runtime/src/workletRuntime.ts
@@ -8,6 +8,7 @@ import { delayExecUntilJsReady, initEventDelay } from './delayWorkletEvent.js';
import { initEomImpl } from './eomImpl.js';
import { hydrateCtx } from './hydrate.js';
import { JsFunctionLifecycleManager, isRunOnBackgroundEnabled } from './jsFunctionLifecycle.js';
+import { runRunOnMainThreadTask } from './runOnMainThread.js';
import { profile } from './utils/profile.js';
import { getFromWorkletRefMap, initWorkletRef } from './workletRef.js';
@@ -19,6 +20,7 @@ function initWorklet(): void {
_hydrateCtx: hydrateCtx,
_eventDelayImpl: initEventDelay(),
_eomImpl: initEomImpl(),
+ _runRunOnMainThreadTask: runRunOnMainThreadTask,
};
if (isRunOnBackgroundEnabled()) {
diff --git a/packages/rspeedy/plugin-react/package.json b/packages/rspeedy/plugin-react/package.json
index ad00b6ad1e..a312a03eb0 100644
--- a/packages/rspeedy/plugin-react/package.json
+++ b/packages/rspeedy/plugin-react/package.json
@@ -64,7 +64,7 @@
"typia-rspack-plugin": "2.2.2"
},
"peerDependencies": {
- "@lynx-js/react": "^0.103.0 || ^0.104.0 || ^0.105.0 || ^0.106.0 || ^0.107.0 || ^0.108.0 || ^0.109.0 || ^0.110.0 || ^0.111.0 || ^0.112.0"
+ "@lynx-js/react": "^0.103.0 || ^0.104.0 || ^0.105.0 || ^0.106.0 || ^0.107.0 || ^0.108.0 || ^0.109.0 || ^0.110.0 || ^0.111.0 || ^0.112.0 || ^0.113.0"
},
"peerDependenciesMeta": {
"@lynx-js/react": {