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": {