diff --git a/eslint.config.js b/eslint.config.js index 4c8b0c9301..0549ff93fe 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -438,4 +438,22 @@ export default tseslint.config( 'headers/header-format': 'off', }, }, + { + files: [ + 'packages/react/worklet-runtime/src/*.ts', + 'packages/react/worklet-runtime/src/*/*.ts', + 'packages/react/worklet-runtime/src/*/*/*.ts', + ], + languageOptions: { + parserOptions: { + project: './packages/react/worklet-runtime/tsconfig.eslint.json', + projectService: false, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + 'headers/header-format': 'off', + 'import/export': 'off', + }, + }, ); diff --git a/packages/react/runtime/src/utils.ts b/packages/react/runtime/src/utils.ts index 275f8794f5..518f0f577c 100644 --- a/packages/react/runtime/src/utils.ts +++ b/packages/react/runtime/src/utils.ts @@ -40,7 +40,7 @@ export function isEmptyObject(obj?: object): obj is Record { } export function isSdkVersionGt(major: number, minor: number): boolean { - const lynxSdkVersion: string = SystemInfo.lynxSdkVersion || '1.0'; + const lynxSdkVersion: string = SystemInfo.lynxSdkVersion ?? '1.0'; const version = lynxSdkVersion.split('.'); return Number(version[0]) > major || (Number(version[0]) == major && Number(version[1]) > minor); } diff --git a/packages/react/runtime/src/worklet-runtime/api/animation/animation.ts b/packages/react/runtime/src/worklet-runtime/api/animation/animation.ts new file mode 100644 index 0000000000..353535593d --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/api/animation/animation.ts @@ -0,0 +1,48 @@ +// 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 type { KeyframeEffect } from './effect.js'; + +export enum AnimationOperation { + START = 0, // Start a new animation + PLAY = 1, // Play/resume a paused animation + PAUSE = 2, // Pause an existing animation + CANCEL = 3, // Cancel an animation +} + +export class Animation { + static count = 0; + public readonly effect: KeyframeEffect; + public readonly id: string; + + constructor(effect: KeyframeEffect) { + this.effect = effect; + this.id = '__lynx-inner-js-animation-' + Animation.count++; + this.start(); + } + + public cancel(): void { + // @ts-expect-error accessing private member 'element' + return __ElementAnimate(this.effect.target.element, [AnimationOperation.CANCEL, this.id]); + } + + public pause(): void { + // @ts-expect-error accessing private member 'element' + return __ElementAnimate(this.effect.target.element, [AnimationOperation.PAUSE, this.id]); + } + + public play(): void { + // @ts-expect-error accessing private member 'element' + return __ElementAnimate(this.effect.target.element, [AnimationOperation.PLAY, this.id]); + } + + private start(): void { + // @ts-expect-error accessing private member 'element' + return __ElementAnimate(this.effect.target.element, [ + AnimationOperation.START, + this.id, + this.effect.keyframes, + this.effect.options, + ]); + } +} diff --git a/packages/react/runtime/src/worklet-runtime/api/animation/effect.ts b/packages/react/runtime/src/worklet-runtime/api/animation/effect.ts new file mode 100644 index 0000000000..60c50edbf8 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/api/animation/effect.ts @@ -0,0 +1,20 @@ +// 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 type { Element } from '../element.js'; + +export class KeyframeEffect { + public readonly target: Element; + public readonly keyframes: Record[]; + public readonly options: Record; + + constructor( + target: Element, + keyframes: Record[], + options: Record, + ) { + this.target = target; + this.keyframes = keyframes; + this.options = options; + } +} diff --git a/packages/react/runtime/src/worklet-runtime/api/element.ts b/packages/react/runtime/src/worklet-runtime/api/element.ts new file mode 100644 index 0000000000..b50ae9f54f --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/api/element.ts @@ -0,0 +1,152 @@ +// 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 { Animation } from './animation/animation.js'; +import { KeyframeEffect } from './animation/effect.js'; +import { + mainThreadFlushLoopMark, + mainThreadFlushLoopOnFlushMicrotask, + mainThreadFlushLoopReport, +} from '../utils/mainThreadFlushLoopGuard.js'; +import { isSdkVersionGt } from '../utils/version.js'; + +let willFlush = false; +let shouldFlush = true; + +export function setShouldFlush(value: boolean): void { + shouldFlush = value; +} + +export class Element { + // @ts-expect-error set in constructor + private readonly element: ElementNode; + + constructor(element: ElementNode) { + // In Lynx versions prior to and including 2.15, + // a crash occurs when printing or transferring refCounted across threads. + // Bypass this problem by hiding the element object. + Object.defineProperty(this, 'element', { + get() { + return element; + }, + }); + } + + public setAttribute(name: string, value: unknown): void { + /* v8 ignore next 3 */ + if (__DEV__) { + mainThreadFlushLoopMark(`element:setAttribute ${name}`); + } + __SetAttribute(this.element, name, value); + this.flushElementTree(); + } + + public setStyleProperty(name: string, value: string): void { + /* v8 ignore next 3 */ + if (__DEV__) { + mainThreadFlushLoopMark(`element:setStyleProperty ${name}`); + } + __AddInlineStyle(this.element, name, value); + this.flushElementTree(); + } + + public setStyleProperties(styles: Record): void { + /* v8 ignore next 5 */ + if (__DEV__) { + mainThreadFlushLoopMark( + `element:setStyleProperties keys=${Object.keys(styles).length}`, + ); + } + for (const key in styles) { + __AddInlineStyle(this.element, key, styles[key]!); + } + this.flushElementTree(); + } + + public getAttribute(attributeName: string): unknown { + return __GetAttributeByName(this.element, attributeName); + } + + public getAttributeNames(): string[] { + return __GetAttributeNames(this.element); + } + + public querySelector(selector: string): Element | null { + const ref = __QuerySelector(this.element, selector, {}); + return ref ? new Element(ref) : null; + } + + public querySelectorAll(selector: string): Element[] { + return __QuerySelectorAll(this.element, selector, {}).map((element) => { + return new Element(element); + }); + } + + public getComputedStyleProperty(key: string): string { + if (!isSdkVersionGt(3, 4)) { + throw new Error( + 'getComputedStyleProperty requires Lynx sdk version 3.5', + ); + } + + if (!key) { + throw new Error('getComputedStyleProperty: key is required'); + } + return __GetComputedStyleByKey(this.element, key); + } + + public animate( + keyframes: Record[], + options?: number | Record, + ): Animation { + const normalizedOptions = typeof options === 'number' ? { duration: options } : options ?? {}; + return new Animation(new KeyframeEffect(this, keyframes, normalizedOptions)); + } + + public invoke( + methodName: string, + params?: Record, + ): Promise { + /* v8 ignore next 3 */ + if (__DEV__) { + mainThreadFlushLoopMark(`element:invoke ${methodName}`); + } + return new Promise((resolve, reject) => { + __InvokeUIMethod( + this.element, + methodName, + params ?? {}, + (res: { code: number; data: unknown }) => { + if (res.code === 0) { + resolve(res.data); + } else { + reject(new Error('UI method invoke: ' + JSON.stringify(res))); + } + }, + ); + this.flushElementTree(); + }); + } + + private flushElementTree() { + if (willFlush || !shouldFlush) { + return; + } + willFlush = true; + void Promise.resolve().then(() => { + willFlush = false; + if (__DEV__) { + mainThreadFlushLoopMark('render'); + const error = mainThreadFlushLoopOnFlushMicrotask(); + if (error) { + // Stop scheduling further flushes so we can surface the error. + // This is DEV-only behavior guarded internally by the dev guard. + shouldFlush = false; + mainThreadFlushLoopReport(error); + return; + } + } + __FlushElementTree(); + }); + } +} diff --git a/packages/react/runtime/src/worklet-runtime/api/lepusQuerySelector.ts b/packages/react/runtime/src/worklet-runtime/api/lepusQuerySelector.ts new file mode 100644 index 0000000000..34bce80f39 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/api/lepusQuerySelector.ts @@ -0,0 +1,26 @@ +// 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 { Element } from './element.js'; + +class PageElement { + private static pageElement: ElementNode | undefined; + + static get() { + PageElement.pageElement ??= __GetPageElement(); + return PageElement.pageElement; + } +} + +export function querySelector(cssSelector: string): Element | null { + const element = __QuerySelector(PageElement.get(), cssSelector, {}); + return element ? new Element(element) : null; +} + +export function querySelectorAll(cssSelector: string): Element[] { + return __QuerySelectorAll(PageElement.get(), cssSelector, {}).map( + (element) => { + return new Element(element); + }, + ); +} diff --git a/packages/react/runtime/src/worklet-runtime/api/lynxApi.ts b/packages/react/runtime/src/worklet-runtime/api/lynxApi.ts new file mode 100644 index 0000000000..f04024aae9 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/api/lynxApi.ts @@ -0,0 +1,42 @@ +// 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 { querySelector, querySelectorAll } from './lepusQuerySelector.js'; +import { isSdkVersionGt } from '../utils/version.js'; + +function initApiEnv(): void { + // @ts-expect-error type + lynx.querySelector = querySelector; + // @ts-expect-error type + lynx.querySelectorAll = querySelectorAll; + // @ts-expect-error type + globalThis.setTimeout = lynx.setTimeout as (cb: () => void, timeout: number) => number; + // @ts-expect-error type + globalThis.setInterval = lynx.setInterval as (cb: () => void, timeout: number) => number; + // @ts-expect-error type + globalThis.clearTimeout = lynx.clearTimeout as (timeout: number) => void; + // In lynx 2.14 `clearInterval` is mistakenly spelled as `clearTimeInterval`. This is fixed in lynx 2.15. + // @ts-expect-error type + globalThis.clearInterval = (lynx.clearInterval ?? lynx.clearTimeInterval) as (timeout: number) => void; + + { + // @ts-expect-error type + const requestAnimationFrame = lynx.requestAnimationFrame as (callback: () => void) => number; + // @ts-expect-error type + lynx.requestAnimationFrame = globalThis.requestAnimationFrame = ( + callback: () => void, + ) => { + if (!isSdkVersionGt(2, 15)) { + throw new Error( + 'requestAnimationFrame in main thread script requires Lynx sdk version 2.16', + ); + } + return requestAnimationFrame(callback); + }; + } + + // @ts-expect-error type + globalThis.cancelAnimationFrame = lynx.cancelAnimationFrame as (requestId: number) => void; +} + +export { initApiEnv }; diff --git a/packages/react/runtime/src/worklet-runtime/bindings/bindings.ts b/packages/react/runtime/src/worklet-runtime/bindings/bindings.ts new file mode 100644 index 0000000000..a7fca5ebb2 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/bindings/bindings.ts @@ -0,0 +1,83 @@ +// 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 type { ClosureValueType, JsFnHandle, Worklet, WorkletRefImpl } from './types.js'; +import type { Element } from '../api/element.js'; + +/** + * Executes the worklet ctx. + * @param worklet - The Worklet ctx to run. + * @param params - An array as parameters of the worklet run. + */ +function runWorkletCtx(worklet: Worklet, params: ClosureValueType[]): unknown { + return globalThis.runWorklet?.(worklet, params); +} + +/** + * Save an element to a `WorkletRef`. + * + * @param workletRef - The `WorkletRef` to be updated. + * @param element - The element. + * @internal + */ +function updateWorkletRef(workletRef: WorkletRefImpl, element: ElementNode | null): void { + globalThis.lynxWorkletImpl?._refImpl.updateWorkletRef(workletRef, element); +} + +/** + * Update the initial value of the `WorkletRef`. + * + * @param patch - An array containing the index and new value of the worklet value. + */ +function updateWorkletRefInitValueChanges(patch?: [number, unknown][]): void { + if (patch) { + globalThis.lynxWorkletImpl?._refImpl.updateWorkletRefInitValueChanges(patch); + } +} + +/** + * Register a worklet. + * + * @internal + */ +function registerWorklet(type: string, id: string, worklet: (...args: unknown[]) => unknown): void { + globalThis.registerWorklet(type, id, worklet); +} + +/** + * Delay a runOnBackground after hydration. + * + * @internal + */ +function delayRunOnBackground(fnObj: JsFnHandle, fn: (fnId: number, execId: number) => void): void { + globalThis.lynxWorkletImpl?._runOnBackgroundDelayImpl.delayRunOnBackground(fnObj, fn); +} + +/** + * Set whether EOM operations should flush the element tree. + * + * @internal + */ +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, + updateWorkletRefInitValueChanges, + registerWorklet, + delayRunOnBackground, + setEomShouldFlushElementTree, + runRunOnMainThreadTask, +}; diff --git a/packages/react/runtime/src/worklet-runtime/bindings/events.ts b/packages/react/runtime/src/worklet-runtime/bindings/events.ts new file mode 100644 index 0000000000..fa840cf343 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/bindings/events.ts @@ -0,0 +1,29 @@ +// 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 type { Worklet } from './types.js'; + +const enum WorkletEvents { + runWorkletCtx = 'Lynx.Worklet.runWorkletCtx', + runOnBackground = 'Lynx.Worklet.runOnBackground', + FunctionCallRet = 'Lynx.Worklet.FunctionCallRet', + releaseBackgroundWorkletCtx = 'Lynx.Worklet.releaseBackgroundWorkletCtx', + releaseWorkletRef = 'Lynx.Worklet.releaseWorkletRef', +} + +interface RunWorkletCtxData { + resolveId: number; + worklet: Worklet; + params: unknown[]; +} + +interface RunWorkletCtxRetData { + resolveId: number; + returnValue: unknown; +} + +interface ReleaseWorkletRefData { + id: number; +} + +export { WorkletEvents, type RunWorkletCtxData, type RunWorkletCtxRetData, type ReleaseWorkletRefData }; diff --git a/packages/react/runtime/src/worklet-runtime/bindings/index.ts b/packages/react/runtime/src/worklet-runtime/bindings/index.ts new file mode 100644 index 0000000000..a1e6e0a83e --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/bindings/index.ts @@ -0,0 +1,12 @@ +// 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. +export { loadWorkletRuntime } from './loadRuntime.js'; + +export * from './bindings.js'; + +export * from './observers.js'; + +export type * from './types.js'; + +export { WorkletEvents, type RunWorkletCtxData, type RunWorkletCtxRetData } from './events.js'; diff --git a/packages/react/runtime/src/worklet-runtime/bindings/loadRuntime.ts b/packages/react/runtime/src/worklet-runtime/bindings/loadRuntime.ts new file mode 100644 index 0000000000..325e6618c4 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/bindings/loadRuntime.ts @@ -0,0 +1,25 @@ +// 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 '../global.js'; + +/** + * Loads and initializes the Lepus chunk in the main thread. + * @param __schema - The dynamic component entry for loading the Lepus chunk. + * @returns A boolean indicating whether the Lepus chunk was loaded and initialized successfully. + */ +function loadWorkletRuntime(__schema?: string): boolean { + if (typeof __LoadLepusChunk === 'undefined') { + return false; + } + if (globalThis.lynxWorkletImpl) { + return true; + } + return __LoadLepusChunk('worklet-runtime', { + dynamicComponentEntry: __schema, + chunkType: 0, + }); +} + +export { loadWorkletRuntime }; diff --git a/packages/react/runtime/src/worklet-runtime/bindings/observers.ts b/packages/react/runtime/src/worklet-runtime/bindings/observers.ts new file mode 100644 index 0000000000..7f39ea9f30 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/bindings/observers.ts @@ -0,0 +1,41 @@ +// 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 type { Worklet } from './types.js'; + +/** + * This function must be called when a worklet context is updated. + * + * @param worklet - The worklet to be updated + * @param oldWorklet - The old worklet context + * @param isFirstScreen - Whether it is before the hydration is finished + * @param element - The element + */ +export function onWorkletCtxUpdate( + worklet: Worklet, + oldWorklet: Worklet | null | undefined, + isFirstScreen: boolean, + element: ElementNode, +): void { + if (worklet._execId !== undefined) { + globalThis.lynxWorkletImpl?._jsFunctionLifecycleManager?.addRef(worklet._execId, worklet); + } + if (isFirstScreen && oldWorklet) { + globalThis.lynxWorkletImpl?._hydrateCtx(worklet, oldWorklet); + } + // For old version dynamic component compatibility. + if (isFirstScreen) { + globalThis.lynxWorkletImpl?._eventDelayImpl.runDelayedWorklet(worklet, element); + } +} + +/** + * This must be called when the hydration is finished. + */ +export function onHydrationFinished(): void { + globalThis.lynxWorkletImpl?._runOnBackgroundDelayImpl.runDelayedBackgroundFunctions(); + globalThis.lynxWorkletImpl?._refImpl.clearFirstScreenWorkletRefMap(); + // For old version dynamic component compatibility. + globalThis.lynxWorkletImpl?._eventDelayImpl.clearDelayedWorklets(); +} diff --git a/packages/react/runtime/src/worklet-runtime/bindings/types.ts b/packages/react/runtime/src/worklet-runtime/bindings/types.ts new file mode 100644 index 0000000000..ef364926dd --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/bindings/types.ts @@ -0,0 +1,82 @@ +// 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 type { Element } from '../api/element.js'; + +export type { Element }; + +export type WorkletRefId = number; + +export interface WorkletRefImpl { + _wvid: WorkletRefId; + _initValue: T; + _type: string; + _lifecycleObserver?: unknown; + current?: T; +} + +export interface WorkletRef { + _wvid: WorkletRefId; + current: T; + + [key: string]: unknown; +} + +interface ClosureValueType_ extends Record {} + +export type ClosureValueType = + | null + | undefined + | string + | boolean + | number + | Worklet + | WorkletRef + | Element + | (((...args: unknown[]) => unknown) & { ctx?: ClosureValueType }) + | ClosureValueType_ + | ClosureValueType[]; + +export interface Worklet { + _wkltId: string; + _workletType?: string; + _c?: Record; + _execId?: number; + _jsFn?: Record; + _unmount?: () => void; + [key: string]: ClosureValueType; + + // for pre-0.99 compatibility + _lepusWorkletHash?: string; +} + +/** + * @public + */ +export interface JsFnHandle { + _jsFnId?: number; + _fn?: (...args: unknown[]) => unknown; + _execId?: number; + _error?: string; + _isFirstScreen?: boolean; + /** + * Stores an array of indexes of runOnBackground tasks that should be processed with a delay. + * This is used before hydration. + */ + _delayIndices?: number[]; +} + +export interface EventCtx { + _eventReturnResult?: number; +} + +export enum RunWorkletSource { + NONE = 0, + EVENT = 1, + GESTURE = 2, +} + +export interface RunWorkletOptions { + source: RunWorkletSource; +} diff --git a/packages/react/runtime/src/worklet-runtime/delayRunOnBackground.ts b/packages/react/runtime/src/worklet-runtime/delayRunOnBackground.ts new file mode 100644 index 0000000000..2f2963e9b6 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/delayRunOnBackground.ts @@ -0,0 +1,45 @@ +// 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 type { JsFnHandle } from './bindings/types.js'; + +interface Details { + task: (fnId: number, execId: number) => void; + + // This comes from the background thread, inserted here during ctx hydration. + jsFnHandle?: JsFnHandle; +} + +interface RunOnBackgroundDelayImpl { + // Elements should keep the order being called by the user. + delayedBackgroundFunctionArray: Details[]; + delayRunOnBackground(fnObj: JsFnHandle, fn: (fnId: number, execId: number) => void): void; + runDelayedBackgroundFunctions(): void; +} + +let impl: RunOnBackgroundDelayImpl | undefined; + +function initRunOnBackgroundDelay(): RunOnBackgroundDelayImpl { + return (impl = { + delayedBackgroundFunctionArray: [], + delayRunOnBackground, + runDelayedBackgroundFunctions, + }); +} + +function delayRunOnBackground(fnObj: JsFnHandle, task: (fnId: number, execId: number) => void) { + impl!.delayedBackgroundFunctionArray.push({ task }); + const delayIndices = fnObj._delayIndices ??= []; + delayIndices.push(impl!.delayedBackgroundFunctionArray.length - 1); +} + +function runDelayedBackgroundFunctions(): void { + for (const details of impl!.delayedBackgroundFunctionArray) { + if (details.jsFnHandle) { + details.task(details.jsFnHandle._jsFnId!, details.jsFnHandle._execId!); + } + } + impl!.delayedBackgroundFunctionArray.length = 0; +} + +export { type RunOnBackgroundDelayImpl, initRunOnBackgroundDelay }; diff --git a/packages/react/runtime/src/worklet-runtime/delayWorkletEvent.ts b/packages/react/runtime/src/worklet-runtime/delayWorkletEvent.ts new file mode 100644 index 0000000000..77291771cb --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/delayWorkletEvent.ts @@ -0,0 +1,70 @@ +// 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 type { ClosureValueType, Worklet } from './bindings/types.js'; +import { profile } from './utils/profile.js'; + +interface EventDelayImpl { + _delayedWorkletParamsMap: Map; + runDelayedWorklet(worklet: Worklet, element: ElementNode): void; + clearDelayedWorklets(): void; +} + +let impl: EventDelayImpl | undefined; + +function initEventDelay(): EventDelayImpl { + return (impl = { + _delayedWorkletParamsMap: new Map(), + runDelayedWorklet, + clearDelayedWorklets, + }); +} + +function delayExecUntilJsReady( + hash: string, + params: ClosureValueType[], +): void { + profile('delayExecUntilJsReady: ' + hash, () => { + const map = impl!._delayedWorkletParamsMap; + const paramVec = map.get(hash); + if (paramVec) { + paramVec.push(params); + } else { + map.set(hash, [params]); + } + }); +} + +function runDelayedWorklet(worklet: Worklet, element: ElementNode): void { + profile('commitDelayedWorklet', () => { + const paramsVec = impl!._delayedWorkletParamsMap.get( + worklet._wkltId, + ); + if (paramsVec === undefined) { + return; + } + const leftParamsVec: ClosureValueType[][] = []; + paramsVec.forEach((params) => { + const firstParam = params[0] as { currentTarget?: { elementRefptr?: ElementNode } } | undefined; + if (firstParam?.currentTarget?.elementRefptr === element) { + setTimeout(() => { + profile('runDelayedWorklet', () => { + runWorklet(worklet, params); + }); + }, 0); + } else { + leftParamsVec.push(params); + } + }); + impl!._delayedWorkletParamsMap.set( + worklet._wkltId, + leftParamsVec, + ); + }); +} + +function clearDelayedWorklets(): void { + impl!._delayedWorkletParamsMap.clear(); +} + +export { type EventDelayImpl, initEventDelay, delayExecUntilJsReady, runDelayedWorklet, clearDelayedWorklets }; diff --git a/packages/react/runtime/src/worklet-runtime/eomImpl.ts b/packages/react/runtime/src/worklet-runtime/eomImpl.ts new file mode 100644 index 0000000000..0e7363ae9f --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/eomImpl.ts @@ -0,0 +1,14 @@ +// 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 { setShouldFlush } from './api/element.js'; + +export interface EomImpl { + setShouldFlush(value: boolean): void; +} + +export function initEomImpl(): EomImpl { + return { + setShouldFlush, + }; +} diff --git a/packages/react/runtime/src/worklet-runtime/eventPropagation.ts b/packages/react/runtime/src/worklet-runtime/eventPropagation.ts new file mode 100644 index 0000000000..1621792ec4 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/eventPropagation.ts @@ -0,0 +1,56 @@ +// 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 type { ClosureValueType, EventCtx, RunWorkletOptions } from './bindings/types.js'; +import { RunWorkletSource } from './bindings/types.js'; + +// EventResult enum values +export const EventResult = { + kDefault: 0x0, + kStopPropagationMask: 0x1, + kStopImmediatePropagationMask: 0x2, +} as const; + +type EventLike = Record; + +export function isEventObject( + ctx: ClosureValueType[], + options?: RunWorkletOptions, +): ctx is [EventLike, ...ClosureValueType[]] { + if (!Array.isArray(ctx) || typeof ctx[0] !== 'object' || ctx[0] === null) { + return false; + } + if (options && options.source === RunWorkletSource.EVENT) { + return true; + } + return false; +} + +/** + * Add event methods to an event object if needed + * @param ctx The event object to enhance + * @param options The run worklet options + * @returns A tuple of boolean and the event return result + */ +export function addEventMethodsIfNeeded(ctx: ClosureValueType[], options?: RunWorkletOptions): [boolean, EventCtx] { + if (!isEventObject(ctx, options)) { + return [false, {}]; + } + const eventCtx: EventCtx = {}; + const eventObj = ctx[0]; + + // Add stopPropagation method + eventObj['stopPropagation'] = function() { + eventCtx._eventReturnResult = (eventCtx._eventReturnResult ?? EventResult.kDefault) + | EventResult.kStopPropagationMask; + }; + + // Add stopImmediatePropagation method + eventObj['stopImmediatePropagation'] = function() { + eventCtx._eventReturnResult = (eventCtx._eventReturnResult ?? EventResult.kDefault) + | EventResult.kStopImmediatePropagationMask; + }; + + return [true, eventCtx]; +} diff --git a/packages/react/runtime/src/worklet-runtime/global.ts b/packages/react/runtime/src/worklet-runtime/global.ts new file mode 100644 index 0000000000..02a4caffe7 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/global.ts @@ -0,0 +1,28 @@ +// 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 type { ClosureValueType, Worklet } from './bindings/types.js'; +import type { RunOnBackgroundDelayImpl } from './delayRunOnBackground.js'; +import type { EventDelayImpl } from './delayWorkletEvent.js'; +import type { EomImpl } from './eomImpl.js'; +import type { JsFunctionLifecycleManager } from './jsFunctionLifecycle.js'; +import type { RefImpl } from './workletRef.js'; + +declare global { + var lynxWorkletImpl: { + _workletMap: Record unknown>; + _jsFunctionLifecycleManager?: JsFunctionLifecycleManager; + // for pre-0.99 compatibility + _eventDelayImpl: EventDelayImpl; + _refImpl: RefImpl; + _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; + + function registerWorklet(type: string, id: string, worklet: (...args: unknown[]) => unknown): void; + function registerWorkletInternal(type: string, id: string, worklet: (...args: unknown[]) => unknown): void; +} diff --git a/packages/react/runtime/src/worklet-runtime/hydrate.ts b/packages/react/runtime/src/worklet-runtime/hydrate.ts new file mode 100644 index 0000000000..10957d2405 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/hydrate.ts @@ -0,0 +1,110 @@ +// 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 type { ClosureValueType, JsFnHandle, Worklet, WorkletRefId, WorkletRefImpl } from './bindings/index.js'; +import { profile } from './utils/profile.js'; + +/** + * Hydrates a Worklet context with data from a first-screen Worklet context. + * This process is typically used to run all delayed `runOnBackground` functions + * and initialize `WorkletRef` values modified before hydration. + * + * @param ctx The target Worklet context to be hydrated. + * @param firstScreenCtx The Worklet context from the first screen rendering, + * containing the data to hydrate with. + */ +export function hydrateCtx(ctx: Worklet, firstScreenCtx: Worklet): void { + profile('hydrateCtx', () => { + hydrateCtxImpl(ctx, firstScreenCtx, ctx._execId!); + }); +} + +function hydrateCtxImpl( + ctx: ClosureValueType, + firstScreenCtx: ClosureValueType, + execId: number, +): void { + if ( + !ctx || typeof ctx !== 'object' || !firstScreenCtx + || typeof firstScreenCtx !== 'object' + ) return; + + const ctxObj = ctx as Record; + const firstScreenCtxObj = firstScreenCtx as Record; + + if (ctxObj['_wkltId'] && ctxObj['_wkltId'] !== firstScreenCtxObj['_wkltId']) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-for-in-array + for (const key in ctx) { + if (key === '_wvid') { + hydrateMainThreadRef( + ctxObj[key] as WorkletRefId, + firstScreenCtxObj as unknown as WorkletRefImpl, + ); + } else if (key === '_jsFn') { + hydrateDelayRunOnBackgroundTasks( + ctxObj[key] as Record, + firstScreenCtxObj[key] as Record, + execId, + ); + } else { + const firstScreenValue = typeof firstScreenCtxObj[key] === 'function' + ? (firstScreenCtxObj[key] as { ctx: ClosureValueType }).ctx + : firstScreenCtxObj[key]; + hydrateCtxImpl(ctxObj[key], firstScreenValue, execId); + } + } +} + +/** + * Hydrates a WorkletRef on the main thread. + * This is used to update the WorkletRef's background initial value based on changes + * that occurred in the first-screen Worklet context before hydration. + * + * @param refId The ID of the WorkletRef to hydrate. + * @param value The new value for the WorkletRef. + */ +function hydrateMainThreadRef( + refId: WorkletRefId, + value: WorkletRefImpl, +) { + if ('_initValue' in value) { + // The ref has not been accessed yet. + return; + } + lynxWorkletImpl!._refImpl._workletRefMap[refId] = value; +} + +/** + * Hydrates delayed `runOnBackground` tasks. + * This function ensures that any `runOnBackground` calls that were delayed + * during the first-screen rendering are correctly associated with their + * respective JavaScript function handles in the hydrated Worklet context. + */ +function hydrateDelayRunOnBackgroundTasks( + fnObjs: Record, // example: {"_jsFn1":{"_jsFnId":1}} + firstScreenFnObjs: Record, // example: {"_jsFn1":{"_isFirstScreen":true,"_delayIndices":[0]}} + execId: number, +) { + for (const fnName in fnObjs) { + const fnObj = fnObjs[fnName]!; + const firstScreenFnObj: JsFnHandle | undefined = firstScreenFnObjs[fnName]; + if (!firstScreenFnObj?._delayIndices) { + if (firstScreenFnObj) { + firstScreenFnObj._isFirstScreen = false; + firstScreenFnObj._execId = execId; + Object.assign(firstScreenFnObj, fnObj); + } + continue; + } + for (const index of firstScreenFnObj._delayIndices) { + const details = lynxWorkletImpl!._runOnBackgroundDelayImpl + .delayedBackgroundFunctionArray[index]!; + fnObj._execId = execId; + details.jsFnHandle = fnObj; + } + } +} diff --git a/packages/react/runtime/src/worklet-runtime/index.ts b/packages/react/runtime/src/worklet-runtime/index.ts new file mode 100644 index 0000000000..4627374eeb --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/index.ts @@ -0,0 +1,12 @@ +// 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 { initApiEnv } from './api/lynxApi.js'; +import { initEventListeners } from './listeners.js'; +import { initWorklet } from './workletRuntime.js'; + +if (globalThis.lynxWorkletImpl === undefined) { + initWorklet(); + initApiEnv(); + initEventListeners(); +} diff --git a/packages/react/runtime/src/worklet-runtime/jsFunctionLifecycle.ts b/packages/react/runtime/src/worklet-runtime/jsFunctionLifecycle.ts new file mode 100644 index 0000000000..b4e50f9742 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/jsFunctionLifecycle.ts @@ -0,0 +1,64 @@ +// 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 { WorkletEvents } from './bindings/events.js'; +import { profile } from './utils/profile.js'; +import { isSdkVersionGt } from './utils/version.js'; + +/** + * `JsFunctionLifecycleManager` monitors references to JS function handles to be called by `runOnBackground()`. + * In JS context, functions to be called by `runOnBackground()` is referenced by `JsFnHandle`s and finally by `execId`. + * When all `JsFnHandle`s in lepus are released, an event will be sent to JS context to de-ref the `execId`, + * resulting a de-ref to the js function in JS context. + */ +class JsFunctionLifecycleManager { + private execIdRefCount = new Map(); + private execIdSetToFire = new Set(); + private willFire = false; + private registry?: FinalizationRegistry = undefined; + + constructor() { + this.registry = new FinalizationRegistry(this.removeRef.bind(this)); + } + + addRef(execId: number, objToRef: object): void { + this.execIdRefCount.set( + execId, + (this.execIdRefCount.get(execId) ?? 0) + 1, + ); + this.registry!.register(objToRef, execId); + } + + removeRef(execId: number): void { + const rc = this.execIdRefCount.get(execId)!; + if (rc > 1) { + this.execIdRefCount.set(execId, rc - 1); + return; + } + this.execIdRefCount.delete(execId); + this.execIdSetToFire.add(execId); + if (!this.willFire) { + this.willFire = true; + void Promise.resolve().then(() => { + this.fire(); + }); + } + } + + fire(): void { + profile('JsFunctionLifecycleManager.fire', () => { + lynx.getJSContext().dispatchEvent({ + type: WorkletEvents.releaseBackgroundWorkletCtx, + data: Array.from(this.execIdSetToFire), + }); + this.execIdSetToFire.clear(); + this.willFire = false; + }); + } +} + +function isRunOnBackgroundEnabled(): boolean { + return isSdkVersionGt(2, 15); +} + +export { JsFunctionLifecycleManager, isRunOnBackgroundEnabled }; diff --git a/packages/react/runtime/src/worklet-runtime/listeners.ts b/packages/react/runtime/src/worklet-runtime/listeners.ts new file mode 100644 index 0000000000..8918a48021 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/listeners.ts @@ -0,0 +1,28 @@ +// 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 { WorkletEvents } 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'; + +function initEventListeners(): void { + const jsContext = lynx.getJSContext(); + jsContext.addEventListener( + WorkletEvents.runWorkletCtx, + (event: Event) => { + const data = JSON.parse(event.data as string) as RunWorkletCtxData; + runRunOnMainThreadTask(data.worklet, data.params as ClosureValueType[], data.resolveId); + }, + ); + jsContext.addEventListener( + WorkletEvents.releaseWorkletRef, + (event: Event) => { + removeValueFromWorkletRefMap((event.data as ReleaseWorkletRefData).id); + }, + ); +} + +export { initEventListeners }; diff --git a/packages/react/runtime/src/worklet-runtime/runOnMainThread.ts b/packages/react/runtime/src/worklet-runtime/runOnMainThread.ts new file mode 100644 index 0000000000..b8561dd2d5 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/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/runtime/src/worklet-runtime/types/dev.d.ts b/packages/react/runtime/src/worklet-runtime/types/dev.d.ts new file mode 100644 index 0000000000..8f8bd5e107 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/types/dev.d.ts @@ -0,0 +1,4 @@ +// 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. +declare let __DEV__; diff --git a/packages/react/runtime/src/worklet-runtime/types/elementApi.d.ts b/packages/react/runtime/src/worklet-runtime/types/elementApi.d.ts new file mode 100644 index 0000000000..e8b59c419c --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/types/elementApi.d.ts @@ -0,0 +1,97 @@ +// 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. +declare class ElementNode {} + +declare function __AddInlineStyle( + e: ElementNode, + key: number | string, + value: string, +): void; + +declare function __FlushElementTree(element?: ElementNode): void; + +declare function __GetAttributeByName(e: ElementNode, name: string): undefined | string; + +declare function __GetAttributeNames(e: ElementNode): string[]; + +declare function __GetPageElement(): ElementNode; + +declare function __GetComputedStyleByKey(e: ElementNode, key: string): string; + +declare function __InvokeUIMethod( + e: ElementNode, + method: string, + params: Record, + callback: (res: { code: number; data: unknown }) => void, +): ElementNode[]; + +declare function __LoadLepusChunk( + name: string, + cfg: { chunkType: number; dynamicComponentEntry?: string | undefined }, +): boolean; + +declare function __QuerySelector( + e: ElementNode, + cssSelector: string, + params: { + onlyCurrentComponent?: boolean; + }, +): ElementNode | undefined; + +declare function __QuerySelectorAll( + e: ElementNode, + cssSelector: string, + params: { + onlyCurrentComponent?: boolean; + }, +): ElementNode[]; + +declare function __SetAttribute(e: ElementNode, key: string, value: unknown): void; + +/** + * Animation operation types for ElementAnimate function + */ +declare enum AnimationOperation { + START = 0, // Start a new animation + PLAY = 1, // Play/resume a paused animation + PAUSE = 2, // Pause an existing animation + CANCEL = 3, // Cancel an animation +} + +/** + * Animation timing options configuration + */ +interface AnimationTimingOptions { + name?: string; // Animation name (optional, auto-generated if not provided) + duration?: number | string; // Animation duration + delay?: number | string; // Animation delay + iterationCount?: number | string; // Number of iterations (can be 'infinite') + fillMode?: string; // Animation fill mode + timingFunction?: string; // Animation timing function + direction?: string; // Animation direction +} + +/** + * Keyframe definition for animation + */ +type Keyframe = Record; + +/** + * ElementAnimate function - controls animations on DOM elements + * @param element - The DOM element to animate (FiberElement reference) + * @param args - Animation configuration array + * @returns undefined + */ +declare function __ElementAnimate( + element: ElementNode, + args: [ + operation: AnimationOperation, // Animation operation type + name: string, // Animation name + keyframes: Keyframe[], // Array of keyframes + options?: AnimationTimingOptions, // Timing and configuration options + ] | [ + operation: AnimationOperation.PAUSE | AnimationOperation.PLAY | AnimationOperation.CANCEL, + name: string, // Animation name to pause/play + ], +): void; diff --git a/packages/react/runtime/src/worklet-runtime/types/runtimeProxy.d.ts b/packages/react/runtime/src/worklet-runtime/types/runtimeProxy.d.ts new file mode 100644 index 0000000000..1c53e8e339 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/types/runtimeProxy.d.ts @@ -0,0 +1,32 @@ +// 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 type { LynxApi } from '@lynx-js/types'; + +export interface Event { + type: string; + data: unknown; + origin?: string; +} + +export class RuntimeProxy { + dispatchEvent(event: RuntimeProxy.Event): void; + + postMessage(message: unknown); + + addEventListener(type: string, callback: (event: RuntimeProxy.Event) => void); + + removeEventListener( + type: string, + callback: (event: RuntimeProxy.Event) => void, + ); + + onTriggerEvent(callback: (event: RuntimeProxy.Event) => void); +} + +declare module '@lynx-js/types' { + interface Lynx extends LynxApi { + getJSContext(): RuntimeProxy; + } +} diff --git a/packages/react/runtime/src/worklet-runtime/types/systeminfo.d.ts b/packages/react/runtime/src/worklet-runtime/types/systeminfo.d.ts new file mode 100644 index 0000000000..58feeb4d28 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/types/systeminfo.d.ts @@ -0,0 +1,19 @@ +// 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. +interface ISystemInfo { + osVersion: string; + pixelHeight: number; + pixelRatio: number; + pixelWidth: number; + platform: string; + theme: object; + /** + * The version of the Lynx SDK. + * @since Lynx 2.4 + * @example '2.4', '2.10' + */ + lynxSdkVersion?: string; +} + +declare let SystemInfo: ISystemInfo; diff --git a/packages/react/runtime/src/worklet-runtime/utils/mainThreadFlushLoopGuard.ts b/packages/react/runtime/src/worklet-runtime/utils/mainThreadFlushLoopGuard.ts new file mode 100644 index 0000000000..5d3c6d73a4 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/utils/mainThreadFlushLoopGuard.ts @@ -0,0 +1,87 @@ +// Copyright 2026 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. + +const TRACE_LIMIT = 256; +const DEFAULT_FLUSH_LIMIT = 256; + +let trace: string[] = []; + +let flushCountInWindow = 0; +let resetScheduled = false; +let trippedError: Error | null = null; + +function pushTrace(marker: string): void { + trace.push(marker); + if (trace.length > TRACE_LIMIT) { + trace = trace.slice(trace.length - TRACE_LIMIT); + } +} + +function compressTrace(markers: string[]): string { + if (markers.length === 0) return ''; + const out: string[] = []; + // Display most-recent-first to make loops easier to read. + let prev = markers[markers.length - 1]!; + let count = 1; + + for (let i = markers.length - 2; i >= 0; i--) { + const cur = markers[i]!; + if (cur === prev) { + count++; + continue; + } + out.push(count === 1 ? prev : `${prev} x${count}`); + prev = cur; + count = 1; + } + out.push(count === 1 ? prev : `${prev} x${count}`); + return out.join(' <- '); +} + +export function mainThreadFlushLoopMark(marker: string): void { + if (__DEV__) { + pushTrace(marker); + } +} + +export function mainThreadFlushLoopOnFlushMicrotask(): Error | null { + /* v8 ignore next 1 */ + if (!__DEV__) return null; + if (trippedError) return trippedError; + + if (!resetScheduled) { + resetScheduled = true; + setTimeout(() => { + mainThreadFlushLoopReset(); + }, 0); + } + + flushCountInWindow++; + const limit = DEFAULT_FLUSH_LIMIT; + if (flushCountInWindow > limit) { + const traceText = compressTrace(trace); + trippedError = new Error( + `[ReactLynx][DEV] MainThread flush loop detected: render executed ${flushCountInWindow} times without yielding (limit=${limit}). Trace: ${traceText}`, + ); + return trippedError; + } + + return null; +} + +export function mainThreadFlushLoopReport(error: Error): void { + if (__DEV__) { + // Throw on macrotask to avoid Promise-unhandled-rejection noise. + setTimeout(() => { + throw error; + }, 0); + } +} + +export function mainThreadFlushLoopReset(): void { + trace = []; + flushCountInWindow = 0; + resetScheduled = false; + trippedError = null; +} diff --git a/packages/react/runtime/src/worklet-runtime/utils/profile.ts b/packages/react/runtime/src/worklet-runtime/utils/profile.ts new file mode 100644 index 0000000000..ccb0b75292 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/utils/profile.ts @@ -0,0 +1,20 @@ +// 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. +export function profile Ret>( + sliceName: string, + f: Fn, +): Ret { + /* v8 ignore next 9 */ + // TODO: change it to __PROFILE__ + if (__DEV__) { + console.profile?.(sliceName); + try { + return f(); + } finally { + console.profileEnd?.(); + } + } else { + return f(); + } +} diff --git a/packages/react/runtime/src/worklet-runtime/utils/version.ts b/packages/react/runtime/src/worklet-runtime/utils/version.ts new file mode 100644 index 0000000000..efb85bab49 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/utils/version.ts @@ -0,0 +1,11 @@ +// 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. +export function isSdkVersionGt(major: number, minor: number): boolean { + const lynxSdkVersion: string = SystemInfo.lynxSdkVersion ?? '1.0'; + const version = lynxSdkVersion.split('.'); + return ( + Number(version[0]) > major + || (Number(version[0]) == major && Number(version[1]) > minor) + ); +} diff --git a/packages/react/runtime/src/worklet-runtime/workletRef.ts b/packages/react/runtime/src/worklet-runtime/workletRef.ts new file mode 100644 index 0000000000..91c8cba6a4 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/workletRef.ts @@ -0,0 +1,119 @@ +// 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 { Element } from './api/element.js'; +import type { WorkletRef, WorkletRefId, WorkletRefImpl } from './bindings/types.js'; +import { mainThreadFlushLoopMark } from './utils/mainThreadFlushLoopGuard.js'; +import { profile } from './utils/profile.js'; + +interface RefImpl { + _workletRefMap: Record>; + _firstScreenWorkletRefMap: Record>; + updateWorkletRef( + refImpl: WorkletRefImpl, + element: ElementNode | null, + ): void; + updateWorkletRefInitValueChanges(patch: [number, unknown][]): void; + clearFirstScreenWorkletRefMap(): void; +} + +let impl: RefImpl | undefined; + +function initWorkletRef(): RefImpl { + return (impl = { + _workletRefMap: {}, + /** + * Map of worklet refs that are created during first screen rendering. + * These refs are created with negative IDs and need to be hydrated + * when the app starts. The map is cleared after hydration is complete + * to free up memory. + */ + _firstScreenWorkletRefMap: {}, + updateWorkletRef, + updateWorkletRefInitValueChanges, + clearFirstScreenWorkletRefMap, + }); +} + +const createWorkletRef = ( + id: WorkletRefId, + value: T, +): WorkletRef => { + return { + current: value, + _wvid: id, + }; +}; + +const getFromWorkletRefMap = ( + refImpl: WorkletRefImpl, +): WorkletRef => { + const id = refImpl._wvid; + /* v8 ignore next 3 */ + if (__DEV__) { + mainThreadFlushLoopMark(`MainThreadRef:get id=${id}`); + } + let value; + if (id < 0) { + // At the first screen rendering, the worklet ref is created with a negative ID. + // Might be called in two scenarios: + // 1. In MTS events + // 2. In `main-thread:ref` + value = impl!._firstScreenWorkletRefMap[id] as WorkletRef; + if (!value) { + value = impl!._firstScreenWorkletRefMap[id] = createWorkletRef(id, refImpl._initValue); + } + } else { + value = impl!._workletRefMap[id] as WorkletRef; + } + + /* v8 ignore next 3 */ + if (__DEV__ && value === undefined) { + throw new Error('MainThreadRef is not initialized: ' + id); + } + return value; +}; + +function removeValueFromWorkletRefMap(id: WorkletRefId): void { + delete impl!._workletRefMap[id]; +} + +/** + * Create an element instance of the given element node, then set the worklet value to it. + * This is called in `snapshotContextUpdateWorkletRef`. + * @param handle handle of the worklet value. + * @param element the element node. + */ +function updateWorkletRef( + handle: WorkletRefImpl, + element: ElementNode | null, +): void { + getFromWorkletRefMap(handle).current = element + ? new Element(element) + : null; +} + +function updateWorkletRefInitValueChanges( + patch: [WorkletRefId, unknown][], +): void { + profile('updateWorkletRefInitValueChanges', () => { + patch.forEach(([id, value]) => { + if (!impl!._workletRefMap[id]) { + impl!._workletRefMap[id] = createWorkletRef(id, value); + } + }); + }); +} + +function clearFirstScreenWorkletRefMap(): void { + impl!._firstScreenWorkletRefMap = {}; +} + +export { + type RefImpl, + createWorkletRef, + initWorkletRef, + getFromWorkletRefMap, + removeValueFromWorkletRefMap, + updateWorkletRefInitValueChanges, +}; diff --git a/packages/react/runtime/src/worklet-runtime/workletRuntime.ts b/packages/react/runtime/src/worklet-runtime/workletRuntime.ts new file mode 100644 index 0000000000..aef075afa6 --- /dev/null +++ b/packages/react/runtime/src/worklet-runtime/workletRuntime.ts @@ -0,0 +1,201 @@ +// 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 { Element } from './api/element.js'; +import type { ClosureValueType, RunWorkletOptions, Worklet, WorkletRefImpl } from './bindings/types.js'; +import { RunWorkletSource } from './bindings/types.js'; +import { initRunOnBackgroundDelay } from './delayRunOnBackground.js'; +import { delayExecUntilJsReady, initEventDelay } from './delayWorkletEvent.js'; +import { initEomImpl } from './eomImpl.js'; +import { addEventMethodsIfNeeded } from './eventPropagation.js'; +import { hydrateCtx } from './hydrate.js'; +import { JsFunctionLifecycleManager, isRunOnBackgroundEnabled } from './jsFunctionLifecycle.js'; +import { runRunOnMainThreadTask } from './runOnMainThread.js'; +import { mainThreadFlushLoopMark } from './utils/mainThreadFlushLoopGuard.js'; +import { profile } from './utils/profile.js'; +import { getFromWorkletRefMap, initWorkletRef } from './workletRef.js'; + +function initWorklet(): void { + globalThis.lynxWorkletImpl = { + _workletMap: {}, + _refImpl: initWorkletRef(), + _runOnBackgroundDelayImpl: initRunOnBackgroundDelay(), + _hydrateCtx: hydrateCtx, + _eventDelayImpl: initEventDelay(), + _eomImpl: initEomImpl(), + _runRunOnMainThreadTask: runRunOnMainThreadTask, + }; + + if (isRunOnBackgroundEnabled()) { + globalThis.lynxWorkletImpl._jsFunctionLifecycleManager = new JsFunctionLifecycleManager(); + } + + globalThis.registerWorklet = registerWorklet; + globalThis.registerWorkletInternal = registerWorklet; + globalThis.runWorklet = runWorklet; +} + +/** + * Register a worklet function, allowing it to be executed by `runWorklet()`. + * This is called in lepus.js. + * @param _type worklet type, 'main-thread' or 'ui' + * @param id worklet hash + * @param worklet worklet function + */ +function registerWorklet(_type: string, id: string, worklet: (...args: unknown[]) => unknown): void { + lynxWorkletImpl._workletMap[id] = worklet; +} + +/** + * Entrance of all worklet calls. + * Native event touch handler will call this function. + * @param ctx worklet object. + * @param params worklet params. + * @param options run worklet options. + */ +function runWorklet(ctx: Worklet, params: ClosureValueType[], options?: RunWorkletOptions): unknown { + if (!validateWorklet(ctx)) { + console.warn('MainThreadFunction: Invalid function object: ' + JSON.stringify(ctx)); + return; + } + + if (__DEV__) { + if (options?.source === RunWorkletSource.EVENT && Array.isArray(params)) { + const first = params[0]; + const t = (first as { type?: unknown }).type; + if (typeof t === 'string') { + mainThreadFlushLoopMark(`event:${t}`); + } + } + + mainThreadFlushLoopMark(`MainThreadFunction id=${String(ctx._wkltId)}`); + } + + if ('_lepusWorkletHash' in ctx) { + delayExecUntilJsReady(ctx._lepusWorkletHash, params); + return; + } + return runWorkletImpl(ctx, params, options); +} + +function runWorkletImpl(ctx: Worklet, params: ClosureValueType[], options?: RunWorkletOptions): unknown { + const worklet: (...args: unknown[]) => unknown = profile( + 'transformWorkletCtx ' + ctx._wkltId, + () => transformWorklet(ctx, true), + ); + const params_: ClosureValueType[] = profile( + 'transformWorkletParams', + () => transformWorklet(params || [], false), + ); + + const [hasEventMethods, eventCtx] = addEventMethodsIfNeeded(params_, options); + + const result = profile('runWorklet', () => worklet(...params_)); + + if (hasEventMethods) { + return { + returnValue: result, + eventReturnResult: eventCtx._eventReturnResult, + }; + } + + return result; +} + +function validateWorklet(ctx: unknown): ctx is Worklet { + return typeof ctx === 'object' && ctx !== null && ('_wkltId' in ctx || '_lepusWorkletHash' in ctx); +} + +const workletCache = new WeakMap unknown)>(); + +function transformWorklet(ctx: Worklet, isWorklet: true): (...args: unknown[]) => unknown; +function transformWorklet( + ctx: ClosureValueType[], + isWorklet: false, +): ClosureValueType[]; + +function transformWorklet( + ctx: ClosureValueType, + isWorklet: boolean, +): ClosureValueType | ((...args: unknown[]) => unknown) { + /* v8 ignore next 3 */ + if (typeof ctx !== 'object' || ctx === null) { + return ctx; + } + + if (isWorklet) { + const res = workletCache.get(ctx); + if (res) { + return res; + } + } + + const worklet = { main: ctx }; + transformWorkletInner(worklet, 0, ctx); + + if (isWorklet) { + workletCache.set(ctx, worklet.main); + } + + return worklet.main; +} + +const transformWorkletInner = ( + value: ClosureValueType, + depth: number, + ctx: unknown, +) => { + const limit = 1000; + if (++depth >= limit) { + throw new Error('Depth of value exceeds limit of ' + limit + '.'); + } + /* v8 ignore next 3 */ + if (typeof value !== 'object' || value === null) { + return; + } + const obj = value as Record; + + for (const key in obj) { + const subObj: ClosureValueType = obj[key]; + if (typeof subObj !== 'object' || subObj === null) { + continue; + } + + if (/** isEventTarget */ 'elementRefptr' in subObj) { + obj[key] = new Element(subObj['elementRefptr'] as ElementNode); + continue; + } else if (subObj instanceof Element) { + continue; + } + + transformWorkletInner(subObj, depth, ctx); + + const isWorkletRef = '_wvid' in (subObj as object); + if (isWorkletRef) { + obj[key] = getFromWorkletRefMap( + subObj as unknown as WorkletRefImpl, + ); + continue; + } + const isWorklet = '_wkltId' in subObj; + if (isWorklet) { + // `subObj` is worklet ctx. Shallow copy it to prevent the transformed worklet from referencing ctx. + // This would result in the value of `workletCache` referencing its key. + obj[key] = lynxWorkletImpl._workletMap[(subObj as Worklet)._wkltId]! + .bind({ ...subObj }); + obj[key].ctx = subObj; + continue; + } + const isJsFn = '_jsFnId' in subObj; + if (isJsFn) { + subObj['_execId'] = (ctx as Worklet)._execId; + lynxWorkletImpl._jsFunctionLifecycleManager?.addRef( + (ctx as Worklet)._execId!, + subObj, + ); + continue; + } + } +}; + +export { initWorklet }; diff --git a/packages/react/runtime/vitest.config.ts b/packages/react/runtime/vitest.config.ts index 7aa9e7857a..abbf001c1c 100644 --- a/packages/react/runtime/vitest.config.ts +++ b/packages/react/runtime/vitest.config.ts @@ -87,6 +87,7 @@ export default defineConfig({ 'src/index.ts', 'src/lynx.ts', 'src/root.ts', + 'src/worklet-runtime/**', 'src/debug/component-stack.ts', 'src/debug/debug.ts', 'src/debug/utils.ts', diff --git a/packages/react/worklet-runtime/__test__/runtimeSourceLayout.test.js b/packages/react/worklet-runtime/__test__/runtimeSourceLayout.test.js new file mode 100644 index 0000000000..3144543496 --- /dev/null +++ b/packages/react/worklet-runtime/__test__/runtimeSourceLayout.test.js @@ -0,0 +1,66 @@ +// Copyright 2026 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 fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, test } from 'vitest'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const reactPackagesDir = path.resolve(__dirname, '..', '..'); +const reactPackageJsonPath = path.join(reactPackagesDir, 'package.json'); +const shellPackageDir = path.join(reactPackagesDir, 'worklet-runtime'); +const runtimeWorkletRuntimeDir = path.join( + reactPackagesDir, + 'runtime', + 'src', + 'worklet-runtime', +); + +describe('runtime-local worklet-runtime source layout', () => { + test('stores the real implementation under runtime/src/worklet-runtime', () => { + const expectedFiles = [ + 'index.ts', + path.join('bindings', 'index.ts'), + path.join('api', 'lynxApi.ts'), + 'workletRuntime.ts', + ]; + + for (const file of expectedFiles) { + expect(fs.existsSync(path.join(runtimeWorkletRuntimeDir, file))).toBe(true); + } + }); + + test('keeps the worklet-runtime shell sources as thin re-export entrypoints', () => { + expect( + fs.readFileSync(path.join(shellPackageDir, 'src', 'index.ts'), 'utf8'), + ).toContain('export * from \'../../runtime/src/worklet-runtime/index.js\';'); + + expect( + fs.readFileSync( + path.join(shellPackageDir, 'src', 'bindings', 'index.ts'), + 'utf8', + ), + ).toContain( + 'export * from \'../../../runtime/src/worklet-runtime/bindings/index.js\';', + ); + }); + + test('preserves the public worklet-runtime export targets on @lynx-js/react', () => { + const reactPackageJson = JSON.parse(fs.readFileSync(reactPackageJsonPath, 'utf8')); + + expect(reactPackageJson.exports['./worklet-runtime']).toEqual({ + types: './worklet-runtime/lib/index.d.ts', + default: './worklet-runtime/dist/main.js', + }); + expect(reactPackageJson.exports['./worklet-dev-runtime']).toEqual({ + types: './worklet-runtime/lib/index.d.ts', + default: './worklet-runtime/dist/dev.js', + }); + expect(reactPackageJson.exports['./worklet-runtime/bindings']).toEqual({ + types: './worklet-runtime/lib/bindings/index.d.ts', + default: './worklet-runtime/lib/bindings/index.js', + }); + }); +}); diff --git a/packages/react/worklet-runtime/src/api/animation/animation.ts b/packages/react/worklet-runtime/src/api/animation/animation.ts index 353535593d..f324f9d01e 100644 --- a/packages/react/worklet-runtime/src/api/animation/animation.ts +++ b/packages/react/worklet-runtime/src/api/animation/animation.ts @@ -1,48 +1 @@ -// 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 type { KeyframeEffect } from './effect.js'; - -export enum AnimationOperation { - START = 0, // Start a new animation - PLAY = 1, // Play/resume a paused animation - PAUSE = 2, // Pause an existing animation - CANCEL = 3, // Cancel an animation -} - -export class Animation { - static count = 0; - public readonly effect: KeyframeEffect; - public readonly id: string; - - constructor(effect: KeyframeEffect) { - this.effect = effect; - this.id = '__lynx-inner-js-animation-' + Animation.count++; - this.start(); - } - - public cancel(): void { - // @ts-expect-error accessing private member 'element' - return __ElementAnimate(this.effect.target.element, [AnimationOperation.CANCEL, this.id]); - } - - public pause(): void { - // @ts-expect-error accessing private member 'element' - return __ElementAnimate(this.effect.target.element, [AnimationOperation.PAUSE, this.id]); - } - - public play(): void { - // @ts-expect-error accessing private member 'element' - return __ElementAnimate(this.effect.target.element, [AnimationOperation.PLAY, this.id]); - } - - private start(): void { - // @ts-expect-error accessing private member 'element' - return __ElementAnimate(this.effect.target.element, [ - AnimationOperation.START, - this.id, - this.effect.keyframes, - this.effect.options, - ]); - } -} +export * from '../../../../runtime/src/worklet-runtime/api/animation/animation.js'; diff --git a/packages/react/worklet-runtime/src/api/animation/effect.ts b/packages/react/worklet-runtime/src/api/animation/effect.ts index 60c50edbf8..b3b9f75c9c 100644 --- a/packages/react/worklet-runtime/src/api/animation/effect.ts +++ b/packages/react/worklet-runtime/src/api/animation/effect.ts @@ -1,20 +1 @@ -// 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 type { Element } from '../element.js'; - -export class KeyframeEffect { - public readonly target: Element; - public readonly keyframes: Record[]; - public readonly options: Record; - - constructor( - target: Element, - keyframes: Record[], - options: Record, - ) { - this.target = target; - this.keyframes = keyframes; - this.options = options; - } -} +export * from '../../../../runtime/src/worklet-runtime/api/animation/effect.js'; diff --git a/packages/react/worklet-runtime/src/api/element.ts b/packages/react/worklet-runtime/src/api/element.ts index b50ae9f54f..bd26ec1265 100644 --- a/packages/react/worklet-runtime/src/api/element.ts +++ b/packages/react/worklet-runtime/src/api/element.ts @@ -1,152 +1 @@ -// 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 { Animation } from './animation/animation.js'; -import { KeyframeEffect } from './animation/effect.js'; -import { - mainThreadFlushLoopMark, - mainThreadFlushLoopOnFlushMicrotask, - mainThreadFlushLoopReport, -} from '../utils/mainThreadFlushLoopGuard.js'; -import { isSdkVersionGt } from '../utils/version.js'; - -let willFlush = false; -let shouldFlush = true; - -export function setShouldFlush(value: boolean): void { - shouldFlush = value; -} - -export class Element { - // @ts-expect-error set in constructor - private readonly element: ElementNode; - - constructor(element: ElementNode) { - // In Lynx versions prior to and including 2.15, - // a crash occurs when printing or transferring refCounted across threads. - // Bypass this problem by hiding the element object. - Object.defineProperty(this, 'element', { - get() { - return element; - }, - }); - } - - public setAttribute(name: string, value: unknown): void { - /* v8 ignore next 3 */ - if (__DEV__) { - mainThreadFlushLoopMark(`element:setAttribute ${name}`); - } - __SetAttribute(this.element, name, value); - this.flushElementTree(); - } - - public setStyleProperty(name: string, value: string): void { - /* v8 ignore next 3 */ - if (__DEV__) { - mainThreadFlushLoopMark(`element:setStyleProperty ${name}`); - } - __AddInlineStyle(this.element, name, value); - this.flushElementTree(); - } - - public setStyleProperties(styles: Record): void { - /* v8 ignore next 5 */ - if (__DEV__) { - mainThreadFlushLoopMark( - `element:setStyleProperties keys=${Object.keys(styles).length}`, - ); - } - for (const key in styles) { - __AddInlineStyle(this.element, key, styles[key]!); - } - this.flushElementTree(); - } - - public getAttribute(attributeName: string): unknown { - return __GetAttributeByName(this.element, attributeName); - } - - public getAttributeNames(): string[] { - return __GetAttributeNames(this.element); - } - - public querySelector(selector: string): Element | null { - const ref = __QuerySelector(this.element, selector, {}); - return ref ? new Element(ref) : null; - } - - public querySelectorAll(selector: string): Element[] { - return __QuerySelectorAll(this.element, selector, {}).map((element) => { - return new Element(element); - }); - } - - public getComputedStyleProperty(key: string): string { - if (!isSdkVersionGt(3, 4)) { - throw new Error( - 'getComputedStyleProperty requires Lynx sdk version 3.5', - ); - } - - if (!key) { - throw new Error('getComputedStyleProperty: key is required'); - } - return __GetComputedStyleByKey(this.element, key); - } - - public animate( - keyframes: Record[], - options?: number | Record, - ): Animation { - const normalizedOptions = typeof options === 'number' ? { duration: options } : options ?? {}; - return new Animation(new KeyframeEffect(this, keyframes, normalizedOptions)); - } - - public invoke( - methodName: string, - params?: Record, - ): Promise { - /* v8 ignore next 3 */ - if (__DEV__) { - mainThreadFlushLoopMark(`element:invoke ${methodName}`); - } - return new Promise((resolve, reject) => { - __InvokeUIMethod( - this.element, - methodName, - params ?? {}, - (res: { code: number; data: unknown }) => { - if (res.code === 0) { - resolve(res.data); - } else { - reject(new Error('UI method invoke: ' + JSON.stringify(res))); - } - }, - ); - this.flushElementTree(); - }); - } - - private flushElementTree() { - if (willFlush || !shouldFlush) { - return; - } - willFlush = true; - void Promise.resolve().then(() => { - willFlush = false; - if (__DEV__) { - mainThreadFlushLoopMark('render'); - const error = mainThreadFlushLoopOnFlushMicrotask(); - if (error) { - // Stop scheduling further flushes so we can surface the error. - // This is DEV-only behavior guarded internally by the dev guard. - shouldFlush = false; - mainThreadFlushLoopReport(error); - return; - } - } - __FlushElementTree(); - }); - } -} +export * from '../../../runtime/src/worklet-runtime/api/element.js'; diff --git a/packages/react/worklet-runtime/src/api/lepusQuerySelector.ts b/packages/react/worklet-runtime/src/api/lepusQuerySelector.ts index 34bce80f39..0cb363da3a 100644 --- a/packages/react/worklet-runtime/src/api/lepusQuerySelector.ts +++ b/packages/react/worklet-runtime/src/api/lepusQuerySelector.ts @@ -1,26 +1 @@ -// 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 { Element } from './element.js'; - -class PageElement { - private static pageElement: ElementNode | undefined; - - static get() { - PageElement.pageElement ??= __GetPageElement(); - return PageElement.pageElement; - } -} - -export function querySelector(cssSelector: string): Element | null { - const element = __QuerySelector(PageElement.get(), cssSelector, {}); - return element ? new Element(element) : null; -} - -export function querySelectorAll(cssSelector: string): Element[] { - return __QuerySelectorAll(PageElement.get(), cssSelector, {}).map( - (element) => { - return new Element(element); - }, - ); -} +export * from '../../../runtime/src/worklet-runtime/api/lepusQuerySelector.js'; diff --git a/packages/react/worklet-runtime/src/api/lynxApi.ts b/packages/react/worklet-runtime/src/api/lynxApi.ts index f04024aae9..2eeab6bbe5 100644 --- a/packages/react/worklet-runtime/src/api/lynxApi.ts +++ b/packages/react/worklet-runtime/src/api/lynxApi.ts @@ -1,42 +1 @@ -// 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 { querySelector, querySelectorAll } from './lepusQuerySelector.js'; -import { isSdkVersionGt } from '../utils/version.js'; - -function initApiEnv(): void { - // @ts-expect-error type - lynx.querySelector = querySelector; - // @ts-expect-error type - lynx.querySelectorAll = querySelectorAll; - // @ts-expect-error type - globalThis.setTimeout = lynx.setTimeout as (cb: () => void, timeout: number) => number; - // @ts-expect-error type - globalThis.setInterval = lynx.setInterval as (cb: () => void, timeout: number) => number; - // @ts-expect-error type - globalThis.clearTimeout = lynx.clearTimeout as (timeout: number) => void; - // In lynx 2.14 `clearInterval` is mistakenly spelled as `clearTimeInterval`. This is fixed in lynx 2.15. - // @ts-expect-error type - globalThis.clearInterval = (lynx.clearInterval ?? lynx.clearTimeInterval) as (timeout: number) => void; - - { - // @ts-expect-error type - const requestAnimationFrame = lynx.requestAnimationFrame as (callback: () => void) => number; - // @ts-expect-error type - lynx.requestAnimationFrame = globalThis.requestAnimationFrame = ( - callback: () => void, - ) => { - if (!isSdkVersionGt(2, 15)) { - throw new Error( - 'requestAnimationFrame in main thread script requires Lynx sdk version 2.16', - ); - } - return requestAnimationFrame(callback); - }; - } - - // @ts-expect-error type - globalThis.cancelAnimationFrame = lynx.cancelAnimationFrame as (requestId: number) => void; -} - -export { initApiEnv }; +export * from '../../../runtime/src/worklet-runtime/api/lynxApi.js'; diff --git a/packages/react/worklet-runtime/src/bindings/bindings.ts b/packages/react/worklet-runtime/src/bindings/bindings.ts index a7fca5ebb2..611773e0cc 100644 --- a/packages/react/worklet-runtime/src/bindings/bindings.ts +++ b/packages/react/worklet-runtime/src/bindings/bindings.ts @@ -1,83 +1 @@ -// 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 type { ClosureValueType, JsFnHandle, Worklet, WorkletRefImpl } from './types.js'; -import type { Element } from '../api/element.js'; - -/** - * Executes the worklet ctx. - * @param worklet - The Worklet ctx to run. - * @param params - An array as parameters of the worklet run. - */ -function runWorkletCtx(worklet: Worklet, params: ClosureValueType[]): unknown { - return globalThis.runWorklet?.(worklet, params); -} - -/** - * Save an element to a `WorkletRef`. - * - * @param workletRef - The `WorkletRef` to be updated. - * @param element - The element. - * @internal - */ -function updateWorkletRef(workletRef: WorkletRefImpl, element: ElementNode | null): void { - globalThis.lynxWorkletImpl?._refImpl.updateWorkletRef(workletRef, element); -} - -/** - * Update the initial value of the `WorkletRef`. - * - * @param patch - An array containing the index and new value of the worklet value. - */ -function updateWorkletRefInitValueChanges(patch?: [number, unknown][]): void { - if (patch) { - globalThis.lynxWorkletImpl?._refImpl.updateWorkletRefInitValueChanges(patch); - } -} - -/** - * Register a worklet. - * - * @internal - */ -function registerWorklet(type: string, id: string, worklet: (...args: unknown[]) => unknown): void { - globalThis.registerWorklet(type, id, worklet); -} - -/** - * Delay a runOnBackground after hydration. - * - * @internal - */ -function delayRunOnBackground(fnObj: JsFnHandle, fn: (fnId: number, execId: number) => void): void { - globalThis.lynxWorkletImpl?._runOnBackgroundDelayImpl.delayRunOnBackground(fnObj, fn); -} - -/** - * Set whether EOM operations should flush the element tree. - * - * @internal - */ -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, - updateWorkletRefInitValueChanges, - registerWorklet, - delayRunOnBackground, - setEomShouldFlushElementTree, - runRunOnMainThreadTask, -}; +export * from '../../../runtime/src/worklet-runtime/bindings/bindings.js'; diff --git a/packages/react/worklet-runtime/src/bindings/events.ts b/packages/react/worklet-runtime/src/bindings/events.ts index fa840cf343..212188d3b8 100644 --- a/packages/react/worklet-runtime/src/bindings/events.ts +++ b/packages/react/worklet-runtime/src/bindings/events.ts @@ -1,29 +1 @@ -// 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 type { Worklet } from './types.js'; - -const enum WorkletEvents { - runWorkletCtx = 'Lynx.Worklet.runWorkletCtx', - runOnBackground = 'Lynx.Worklet.runOnBackground', - FunctionCallRet = 'Lynx.Worklet.FunctionCallRet', - releaseBackgroundWorkletCtx = 'Lynx.Worklet.releaseBackgroundWorkletCtx', - releaseWorkletRef = 'Lynx.Worklet.releaseWorkletRef', -} - -interface RunWorkletCtxData { - resolveId: number; - worklet: Worklet; - params: unknown[]; -} - -interface RunWorkletCtxRetData { - resolveId: number; - returnValue: unknown; -} - -interface ReleaseWorkletRefData { - id: number; -} - -export { WorkletEvents, type RunWorkletCtxData, type RunWorkletCtxRetData, type ReleaseWorkletRefData }; +export * from '../../../runtime/src/worklet-runtime/bindings/events.js'; diff --git a/packages/react/worklet-runtime/src/bindings/index.ts b/packages/react/worklet-runtime/src/bindings/index.ts index a1e6e0a83e..8607b96c9e 100644 --- a/packages/react/worklet-runtime/src/bindings/index.ts +++ b/packages/react/worklet-runtime/src/bindings/index.ts @@ -1,12 +1 @@ -// 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. -export { loadWorkletRuntime } from './loadRuntime.js'; - -export * from './bindings.js'; - -export * from './observers.js'; - -export type * from './types.js'; - -export { WorkletEvents, type RunWorkletCtxData, type RunWorkletCtxRetData } from './events.js'; +export * from '../../../runtime/src/worklet-runtime/bindings/index.js'; diff --git a/packages/react/worklet-runtime/src/bindings/loadRuntime.ts b/packages/react/worklet-runtime/src/bindings/loadRuntime.ts index 325e6618c4..ce9c453057 100644 --- a/packages/react/worklet-runtime/src/bindings/loadRuntime.ts +++ b/packages/react/worklet-runtime/src/bindings/loadRuntime.ts @@ -1,25 +1 @@ -// 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 '../global.js'; - -/** - * Loads and initializes the Lepus chunk in the main thread. - * @param __schema - The dynamic component entry for loading the Lepus chunk. - * @returns A boolean indicating whether the Lepus chunk was loaded and initialized successfully. - */ -function loadWorkletRuntime(__schema?: string): boolean { - if (typeof __LoadLepusChunk === 'undefined') { - return false; - } - if (globalThis.lynxWorkletImpl) { - return true; - } - return __LoadLepusChunk('worklet-runtime', { - dynamicComponentEntry: __schema, - chunkType: 0, - }); -} - -export { loadWorkletRuntime }; +export * from '../../../runtime/src/worklet-runtime/bindings/loadRuntime.js'; diff --git a/packages/react/worklet-runtime/src/bindings/observers.ts b/packages/react/worklet-runtime/src/bindings/observers.ts index 7f39ea9f30..8bc9e03cc9 100644 --- a/packages/react/worklet-runtime/src/bindings/observers.ts +++ b/packages/react/worklet-runtime/src/bindings/observers.ts @@ -1,41 +1 @@ -// 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 type { Worklet } from './types.js'; - -/** - * This function must be called when a worklet context is updated. - * - * @param worklet - The worklet to be updated - * @param oldWorklet - The old worklet context - * @param isFirstScreen - Whether it is before the hydration is finished - * @param element - The element - */ -export function onWorkletCtxUpdate( - worklet: Worklet, - oldWorklet: Worklet | null | undefined, - isFirstScreen: boolean, - element: ElementNode, -): void { - if (worklet._execId !== undefined) { - globalThis.lynxWorkletImpl?._jsFunctionLifecycleManager?.addRef(worklet._execId, worklet); - } - if (isFirstScreen && oldWorklet) { - globalThis.lynxWorkletImpl?._hydrateCtx(worklet, oldWorklet); - } - // For old version dynamic component compatibility. - if (isFirstScreen) { - globalThis.lynxWorkletImpl?._eventDelayImpl.runDelayedWorklet(worklet, element); - } -} - -/** - * This must be called when the hydration is finished. - */ -export function onHydrationFinished(): void { - globalThis.lynxWorkletImpl?._runOnBackgroundDelayImpl.runDelayedBackgroundFunctions(); - globalThis.lynxWorkletImpl?._refImpl.clearFirstScreenWorkletRefMap(); - // For old version dynamic component compatibility. - globalThis.lynxWorkletImpl?._eventDelayImpl.clearDelayedWorklets(); -} +export * from '../../../runtime/src/worklet-runtime/bindings/observers.js'; diff --git a/packages/react/worklet-runtime/src/bindings/types.ts b/packages/react/worklet-runtime/src/bindings/types.ts index ef364926dd..c21f1e6e17 100644 --- a/packages/react/worklet-runtime/src/bindings/types.ts +++ b/packages/react/worklet-runtime/src/bindings/types.ts @@ -1,82 +1 @@ -// 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 type { Element } from '../api/element.js'; - -export type { Element }; - -export type WorkletRefId = number; - -export interface WorkletRefImpl { - _wvid: WorkletRefId; - _initValue: T; - _type: string; - _lifecycleObserver?: unknown; - current?: T; -} - -export interface WorkletRef { - _wvid: WorkletRefId; - current: T; - - [key: string]: unknown; -} - -interface ClosureValueType_ extends Record {} - -export type ClosureValueType = - | null - | undefined - | string - | boolean - | number - | Worklet - | WorkletRef - | Element - | (((...args: unknown[]) => unknown) & { ctx?: ClosureValueType }) - | ClosureValueType_ - | ClosureValueType[]; - -export interface Worklet { - _wkltId: string; - _workletType?: string; - _c?: Record; - _execId?: number; - _jsFn?: Record; - _unmount?: () => void; - [key: string]: ClosureValueType; - - // for pre-0.99 compatibility - _lepusWorkletHash?: string; -} - -/** - * @public - */ -export interface JsFnHandle { - _jsFnId?: number; - _fn?: (...args: unknown[]) => unknown; - _execId?: number; - _error?: string; - _isFirstScreen?: boolean; - /** - * Stores an array of indexes of runOnBackground tasks that should be processed with a delay. - * This is used before hydration. - */ - _delayIndices?: number[]; -} - -export interface EventCtx { - _eventReturnResult?: number; -} - -export enum RunWorkletSource { - NONE = 0, - EVENT = 1, - GESTURE = 2, -} - -export interface RunWorkletOptions { - source: RunWorkletSource; -} +export * from '../../../runtime/src/worklet-runtime/bindings/types.js'; diff --git a/packages/react/worklet-runtime/src/delayRunOnBackground.ts b/packages/react/worklet-runtime/src/delayRunOnBackground.ts index 2f2963e9b6..e52a19e1b3 100644 --- a/packages/react/worklet-runtime/src/delayRunOnBackground.ts +++ b/packages/react/worklet-runtime/src/delayRunOnBackground.ts @@ -1,45 +1 @@ -// 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 type { JsFnHandle } from './bindings/types.js'; - -interface Details { - task: (fnId: number, execId: number) => void; - - // This comes from the background thread, inserted here during ctx hydration. - jsFnHandle?: JsFnHandle; -} - -interface RunOnBackgroundDelayImpl { - // Elements should keep the order being called by the user. - delayedBackgroundFunctionArray: Details[]; - delayRunOnBackground(fnObj: JsFnHandle, fn: (fnId: number, execId: number) => void): void; - runDelayedBackgroundFunctions(): void; -} - -let impl: RunOnBackgroundDelayImpl | undefined; - -function initRunOnBackgroundDelay(): RunOnBackgroundDelayImpl { - return (impl = { - delayedBackgroundFunctionArray: [], - delayRunOnBackground, - runDelayedBackgroundFunctions, - }); -} - -function delayRunOnBackground(fnObj: JsFnHandle, task: (fnId: number, execId: number) => void) { - impl!.delayedBackgroundFunctionArray.push({ task }); - const delayIndices = fnObj._delayIndices ??= []; - delayIndices.push(impl!.delayedBackgroundFunctionArray.length - 1); -} - -function runDelayedBackgroundFunctions(): void { - for (const details of impl!.delayedBackgroundFunctionArray) { - if (details.jsFnHandle) { - details.task(details.jsFnHandle._jsFnId!, details.jsFnHandle._execId!); - } - } - impl!.delayedBackgroundFunctionArray.length = 0; -} - -export { type RunOnBackgroundDelayImpl, initRunOnBackgroundDelay }; +export * from '../../runtime/src/worklet-runtime/delayRunOnBackground.js'; diff --git a/packages/react/worklet-runtime/src/delayWorkletEvent.ts b/packages/react/worklet-runtime/src/delayWorkletEvent.ts index 77291771cb..04dee19639 100644 --- a/packages/react/worklet-runtime/src/delayWorkletEvent.ts +++ b/packages/react/worklet-runtime/src/delayWorkletEvent.ts @@ -1,70 +1 @@ -// 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 type { ClosureValueType, Worklet } from './bindings/types.js'; -import { profile } from './utils/profile.js'; - -interface EventDelayImpl { - _delayedWorkletParamsMap: Map; - runDelayedWorklet(worklet: Worklet, element: ElementNode): void; - clearDelayedWorklets(): void; -} - -let impl: EventDelayImpl | undefined; - -function initEventDelay(): EventDelayImpl { - return (impl = { - _delayedWorkletParamsMap: new Map(), - runDelayedWorklet, - clearDelayedWorklets, - }); -} - -function delayExecUntilJsReady( - hash: string, - params: ClosureValueType[], -): void { - profile('delayExecUntilJsReady: ' + hash, () => { - const map = impl!._delayedWorkletParamsMap; - const paramVec = map.get(hash); - if (paramVec) { - paramVec.push(params); - } else { - map.set(hash, [params]); - } - }); -} - -function runDelayedWorklet(worklet: Worklet, element: ElementNode): void { - profile('commitDelayedWorklet', () => { - const paramsVec = impl!._delayedWorkletParamsMap.get( - worklet._wkltId, - ); - if (paramsVec === undefined) { - return; - } - const leftParamsVec: ClosureValueType[][] = []; - paramsVec.forEach((params) => { - const firstParam = params[0] as { currentTarget?: { elementRefptr?: ElementNode } } | undefined; - if (firstParam?.currentTarget?.elementRefptr === element) { - setTimeout(() => { - profile('runDelayedWorklet', () => { - runWorklet(worklet, params); - }); - }, 0); - } else { - leftParamsVec.push(params); - } - }); - impl!._delayedWorkletParamsMap.set( - worklet._wkltId, - leftParamsVec, - ); - }); -} - -function clearDelayedWorklets(): void { - impl!._delayedWorkletParamsMap.clear(); -} - -export { type EventDelayImpl, initEventDelay, delayExecUntilJsReady, runDelayedWorklet, clearDelayedWorklets }; +export * from '../../runtime/src/worklet-runtime/delayWorkletEvent.js'; diff --git a/packages/react/worklet-runtime/src/eomImpl.ts b/packages/react/worklet-runtime/src/eomImpl.ts index 0e7363ae9f..c997990592 100644 --- a/packages/react/worklet-runtime/src/eomImpl.ts +++ b/packages/react/worklet-runtime/src/eomImpl.ts @@ -1,14 +1 @@ -// 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 { setShouldFlush } from './api/element.js'; - -export interface EomImpl { - setShouldFlush(value: boolean): void; -} - -export function initEomImpl(): EomImpl { - return { - setShouldFlush, - }; -} +export * from '../../runtime/src/worklet-runtime/eomImpl.js'; diff --git a/packages/react/worklet-runtime/src/eventPropagation.ts b/packages/react/worklet-runtime/src/eventPropagation.ts index 1621792ec4..03e57ab9ad 100644 --- a/packages/react/worklet-runtime/src/eventPropagation.ts +++ b/packages/react/worklet-runtime/src/eventPropagation.ts @@ -1,56 +1 @@ -// 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 type { ClosureValueType, EventCtx, RunWorkletOptions } from './bindings/types.js'; -import { RunWorkletSource } from './bindings/types.js'; - -// EventResult enum values -export const EventResult = { - kDefault: 0x0, - kStopPropagationMask: 0x1, - kStopImmediatePropagationMask: 0x2, -} as const; - -type EventLike = Record; - -export function isEventObject( - ctx: ClosureValueType[], - options?: RunWorkletOptions, -): ctx is [EventLike, ...ClosureValueType[]] { - if (!Array.isArray(ctx) || typeof ctx[0] !== 'object' || ctx[0] === null) { - return false; - } - if (options && options.source === RunWorkletSource.EVENT) { - return true; - } - return false; -} - -/** - * Add event methods to an event object if needed - * @param ctx The event object to enhance - * @param options The run worklet options - * @returns A tuple of boolean and the event return result - */ -export function addEventMethodsIfNeeded(ctx: ClosureValueType[], options?: RunWorkletOptions): [boolean, EventCtx] { - if (!isEventObject(ctx, options)) { - return [false, {}]; - } - const eventCtx: EventCtx = {}; - const eventObj = ctx[0]; - - // Add stopPropagation method - eventObj['stopPropagation'] = function() { - eventCtx._eventReturnResult = (eventCtx._eventReturnResult ?? EventResult.kDefault) - | EventResult.kStopPropagationMask; - }; - - // Add stopImmediatePropagation method - eventObj['stopImmediatePropagation'] = function() { - eventCtx._eventReturnResult = (eventCtx._eventReturnResult ?? EventResult.kDefault) - | EventResult.kStopImmediatePropagationMask; - }; - - return [true, eventCtx]; -} +export * from '../../runtime/src/worklet-runtime/eventPropagation.js'; diff --git a/packages/react/worklet-runtime/src/global.ts b/packages/react/worklet-runtime/src/global.ts index 02a4caffe7..c7a3b6402c 100644 --- a/packages/react/worklet-runtime/src/global.ts +++ b/packages/react/worklet-runtime/src/global.ts @@ -1,28 +1 @@ -// 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 type { ClosureValueType, Worklet } from './bindings/types.js'; -import type { RunOnBackgroundDelayImpl } from './delayRunOnBackground.js'; -import type { EventDelayImpl } from './delayWorkletEvent.js'; -import type { EomImpl } from './eomImpl.js'; -import type { JsFunctionLifecycleManager } from './jsFunctionLifecycle.js'; -import type { RefImpl } from './workletRef.js'; - -declare global { - var lynxWorkletImpl: { - _workletMap: Record unknown>; - _jsFunctionLifecycleManager?: JsFunctionLifecycleManager; - // for pre-0.99 compatibility - _eventDelayImpl: EventDelayImpl; - _refImpl: RefImpl; - _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; - - function registerWorklet(type: string, id: string, worklet: (...args: unknown[]) => unknown): void; - function registerWorkletInternal(type: string, id: string, worklet: (...args: unknown[]) => unknown): void; -} +export * from '../../runtime/src/worklet-runtime/global.js'; diff --git a/packages/react/worklet-runtime/src/hydrate.ts b/packages/react/worklet-runtime/src/hydrate.ts index 10957d2405..847c54f320 100644 --- a/packages/react/worklet-runtime/src/hydrate.ts +++ b/packages/react/worklet-runtime/src/hydrate.ts @@ -1,110 +1 @@ -// 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 type { ClosureValueType, JsFnHandle, Worklet, WorkletRefId, WorkletRefImpl } from './bindings/index.js'; -import { profile } from './utils/profile.js'; - -/** - * Hydrates a Worklet context with data from a first-screen Worklet context. - * This process is typically used to run all delayed `runOnBackground` functions - * and initialize `WorkletRef` values modified before hydration. - * - * @param ctx The target Worklet context to be hydrated. - * @param firstScreenCtx The Worklet context from the first screen rendering, - * containing the data to hydrate with. - */ -export function hydrateCtx(ctx: Worklet, firstScreenCtx: Worklet): void { - profile('hydrateCtx', () => { - hydrateCtxImpl(ctx, firstScreenCtx, ctx._execId!); - }); -} - -function hydrateCtxImpl( - ctx: ClosureValueType, - firstScreenCtx: ClosureValueType, - execId: number, -): void { - if ( - !ctx || typeof ctx !== 'object' || !firstScreenCtx - || typeof firstScreenCtx !== 'object' - ) return; - - const ctxObj = ctx as Record; - const firstScreenCtxObj = firstScreenCtx as Record; - - if (ctxObj['_wkltId'] && ctxObj['_wkltId'] !== firstScreenCtxObj['_wkltId']) { - return; - } - - // eslint-disable-next-line @typescript-eslint/no-for-in-array - for (const key in ctx) { - if (key === '_wvid') { - hydrateMainThreadRef( - ctxObj[key] as WorkletRefId, - firstScreenCtxObj as unknown as WorkletRefImpl, - ); - } else if (key === '_jsFn') { - hydrateDelayRunOnBackgroundTasks( - ctxObj[key] as Record, - firstScreenCtxObj[key] as Record, - execId, - ); - } else { - const firstScreenValue = typeof firstScreenCtxObj[key] === 'function' - ? (firstScreenCtxObj[key] as { ctx: ClosureValueType }).ctx - : firstScreenCtxObj[key]; - hydrateCtxImpl(ctxObj[key], firstScreenValue, execId); - } - } -} - -/** - * Hydrates a WorkletRef on the main thread. - * This is used to update the WorkletRef's background initial value based on changes - * that occurred in the first-screen Worklet context before hydration. - * - * @param refId The ID of the WorkletRef to hydrate. - * @param value The new value for the WorkletRef. - */ -function hydrateMainThreadRef( - refId: WorkletRefId, - value: WorkletRefImpl, -) { - if ('_initValue' in value) { - // The ref has not been accessed yet. - return; - } - lynxWorkletImpl!._refImpl._workletRefMap[refId] = value; -} - -/** - * Hydrates delayed `runOnBackground` tasks. - * This function ensures that any `runOnBackground` calls that were delayed - * during the first-screen rendering are correctly associated with their - * respective JavaScript function handles in the hydrated Worklet context. - */ -function hydrateDelayRunOnBackgroundTasks( - fnObjs: Record, // example: {"_jsFn1":{"_jsFnId":1}} - firstScreenFnObjs: Record, // example: {"_jsFn1":{"_isFirstScreen":true,"_delayIndices":[0]}} - execId: number, -) { - for (const fnName in fnObjs) { - const fnObj = fnObjs[fnName]!; - const firstScreenFnObj: JsFnHandle | undefined = firstScreenFnObjs[fnName]; - if (!firstScreenFnObj?._delayIndices) { - if (firstScreenFnObj) { - firstScreenFnObj._isFirstScreen = false; - firstScreenFnObj._execId = execId; - Object.assign(firstScreenFnObj, fnObj); - } - continue; - } - for (const index of firstScreenFnObj._delayIndices) { - const details = lynxWorkletImpl!._runOnBackgroundDelayImpl - .delayedBackgroundFunctionArray[index]!; - fnObj._execId = execId; - details.jsFnHandle = fnObj; - } - } -} +export * from '../../runtime/src/worklet-runtime/hydrate.js'; diff --git a/packages/react/worklet-runtime/src/index.ts b/packages/react/worklet-runtime/src/index.ts index 4627374eeb..6019c7b2e3 100644 --- a/packages/react/worklet-runtime/src/index.ts +++ b/packages/react/worklet-runtime/src/index.ts @@ -1,12 +1 @@ -// 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 { initApiEnv } from './api/lynxApi.js'; -import { initEventListeners } from './listeners.js'; -import { initWorklet } from './workletRuntime.js'; - -if (globalThis.lynxWorkletImpl === undefined) { - initWorklet(); - initApiEnv(); - initEventListeners(); -} +export * from '../../runtime/src/worklet-runtime/index.js'; diff --git a/packages/react/worklet-runtime/src/jsFunctionLifecycle.ts b/packages/react/worklet-runtime/src/jsFunctionLifecycle.ts index b4e50f9742..97dd04549f 100644 --- a/packages/react/worklet-runtime/src/jsFunctionLifecycle.ts +++ b/packages/react/worklet-runtime/src/jsFunctionLifecycle.ts @@ -1,64 +1 @@ -// 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 { WorkletEvents } from './bindings/events.js'; -import { profile } from './utils/profile.js'; -import { isSdkVersionGt } from './utils/version.js'; - -/** - * `JsFunctionLifecycleManager` monitors references to JS function handles to be called by `runOnBackground()`. - * In JS context, functions to be called by `runOnBackground()` is referenced by `JsFnHandle`s and finally by `execId`. - * When all `JsFnHandle`s in lepus are released, an event will be sent to JS context to de-ref the `execId`, - * resulting a de-ref to the js function in JS context. - */ -class JsFunctionLifecycleManager { - private execIdRefCount = new Map(); - private execIdSetToFire = new Set(); - private willFire = false; - private registry?: FinalizationRegistry = undefined; - - constructor() { - this.registry = new FinalizationRegistry(this.removeRef.bind(this)); - } - - addRef(execId: number, objToRef: object): void { - this.execIdRefCount.set( - execId, - (this.execIdRefCount.get(execId) ?? 0) + 1, - ); - this.registry!.register(objToRef, execId); - } - - removeRef(execId: number): void { - const rc = this.execIdRefCount.get(execId)!; - if (rc > 1) { - this.execIdRefCount.set(execId, rc - 1); - return; - } - this.execIdRefCount.delete(execId); - this.execIdSetToFire.add(execId); - if (!this.willFire) { - this.willFire = true; - void Promise.resolve().then(() => { - this.fire(); - }); - } - } - - fire(): void { - profile('JsFunctionLifecycleManager.fire', () => { - lynx.getJSContext().dispatchEvent({ - type: WorkletEvents.releaseBackgroundWorkletCtx, - data: Array.from(this.execIdSetToFire), - }); - this.execIdSetToFire.clear(); - this.willFire = false; - }); - } -} - -function isRunOnBackgroundEnabled(): boolean { - return isSdkVersionGt(2, 15); -} - -export { JsFunctionLifecycleManager, isRunOnBackgroundEnabled }; +export * from '../../runtime/src/worklet-runtime/jsFunctionLifecycle.js'; diff --git a/packages/react/worklet-runtime/src/listeners.ts b/packages/react/worklet-runtime/src/listeners.ts index 8918a48021..ea41ab3211 100644 --- a/packages/react/worklet-runtime/src/listeners.ts +++ b/packages/react/worklet-runtime/src/listeners.ts @@ -1,28 +1 @@ -// 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 { WorkletEvents } 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'; - -function initEventListeners(): void { - const jsContext = lynx.getJSContext(); - jsContext.addEventListener( - WorkletEvents.runWorkletCtx, - (event: Event) => { - const data = JSON.parse(event.data as string) as RunWorkletCtxData; - runRunOnMainThreadTask(data.worklet, data.params as ClosureValueType[], data.resolveId); - }, - ); - jsContext.addEventListener( - WorkletEvents.releaseWorkletRef, - (event: Event) => { - removeValueFromWorkletRefMap((event.data as ReleaseWorkletRefData).id); - }, - ); -} - -export { initEventListeners }; +export * from '../../runtime/src/worklet-runtime/listeners.js'; diff --git a/packages/react/worklet-runtime/src/runOnMainThread.ts b/packages/react/worklet-runtime/src/runOnMainThread.ts index b8561dd2d5..b8554fec84 100644 --- a/packages/react/worklet-runtime/src/runOnMainThread.ts +++ b/packages/react/worklet-runtime/src/runOnMainThread.ts @@ -1,21 +1 @@ -// 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), - }); - } -} +export * from '../../runtime/src/worklet-runtime/runOnMainThread.js'; diff --git a/packages/react/worklet-runtime/src/utils/mainThreadFlushLoopGuard.ts b/packages/react/worklet-runtime/src/utils/mainThreadFlushLoopGuard.ts index 5d3c6d73a4..5ac1790ba3 100644 --- a/packages/react/worklet-runtime/src/utils/mainThreadFlushLoopGuard.ts +++ b/packages/react/worklet-runtime/src/utils/mainThreadFlushLoopGuard.ts @@ -1,87 +1 @@ -// Copyright 2026 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. - -const TRACE_LIMIT = 256; -const DEFAULT_FLUSH_LIMIT = 256; - -let trace: string[] = []; - -let flushCountInWindow = 0; -let resetScheduled = false; -let trippedError: Error | null = null; - -function pushTrace(marker: string): void { - trace.push(marker); - if (trace.length > TRACE_LIMIT) { - trace = trace.slice(trace.length - TRACE_LIMIT); - } -} - -function compressTrace(markers: string[]): string { - if (markers.length === 0) return ''; - const out: string[] = []; - // Display most-recent-first to make loops easier to read. - let prev = markers[markers.length - 1]!; - let count = 1; - - for (let i = markers.length - 2; i >= 0; i--) { - const cur = markers[i]!; - if (cur === prev) { - count++; - continue; - } - out.push(count === 1 ? prev : `${prev} x${count}`); - prev = cur; - count = 1; - } - out.push(count === 1 ? prev : `${prev} x${count}`); - return out.join(' <- '); -} - -export function mainThreadFlushLoopMark(marker: string): void { - if (__DEV__) { - pushTrace(marker); - } -} - -export function mainThreadFlushLoopOnFlushMicrotask(): Error | null { - /* v8 ignore next 1 */ - if (!__DEV__) return null; - if (trippedError) return trippedError; - - if (!resetScheduled) { - resetScheduled = true; - setTimeout(() => { - mainThreadFlushLoopReset(); - }, 0); - } - - flushCountInWindow++; - const limit = DEFAULT_FLUSH_LIMIT; - if (flushCountInWindow > limit) { - const traceText = compressTrace(trace); - trippedError = new Error( - `[ReactLynx][DEV] MainThread flush loop detected: render executed ${flushCountInWindow} times without yielding (limit=${limit}). Trace: ${traceText}`, - ); - return trippedError; - } - - return null; -} - -export function mainThreadFlushLoopReport(error: Error): void { - if (__DEV__) { - // Throw on macrotask to avoid Promise-unhandled-rejection noise. - setTimeout(() => { - throw error; - }, 0); - } -} - -export function mainThreadFlushLoopReset(): void { - trace = []; - flushCountInWindow = 0; - resetScheduled = false; - trippedError = null; -} +export * from '../../../runtime/src/worklet-runtime/utils/mainThreadFlushLoopGuard.js'; diff --git a/packages/react/worklet-runtime/src/utils/profile.ts b/packages/react/worklet-runtime/src/utils/profile.ts index ccb0b75292..e5de22d7e7 100644 --- a/packages/react/worklet-runtime/src/utils/profile.ts +++ b/packages/react/worklet-runtime/src/utils/profile.ts @@ -1,20 +1 @@ -// 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. -export function profile Ret>( - sliceName: string, - f: Fn, -): Ret { - /* v8 ignore next 9 */ - // TODO: change it to __PROFILE__ - if (__DEV__) { - console.profile?.(sliceName); - try { - return f(); - } finally { - console.profileEnd?.(); - } - } else { - return f(); - } -} +export * from '../../../runtime/src/worklet-runtime/utils/profile.js'; diff --git a/packages/react/worklet-runtime/src/utils/version.ts b/packages/react/worklet-runtime/src/utils/version.ts index efb85bab49..2149cada14 100644 --- a/packages/react/worklet-runtime/src/utils/version.ts +++ b/packages/react/worklet-runtime/src/utils/version.ts @@ -1,11 +1 @@ -// 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. -export function isSdkVersionGt(major: number, minor: number): boolean { - const lynxSdkVersion: string = SystemInfo.lynxSdkVersion ?? '1.0'; - const version = lynxSdkVersion.split('.'); - return ( - Number(version[0]) > major - || (Number(version[0]) == major && Number(version[1]) > minor) - ); -} +export * from '../../../runtime/src/worklet-runtime/utils/version.js'; diff --git a/packages/react/worklet-runtime/src/workletRef.ts b/packages/react/worklet-runtime/src/workletRef.ts index 91c8cba6a4..190f4a61d0 100644 --- a/packages/react/worklet-runtime/src/workletRef.ts +++ b/packages/react/worklet-runtime/src/workletRef.ts @@ -1,119 +1 @@ -// 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 { Element } from './api/element.js'; -import type { WorkletRef, WorkletRefId, WorkletRefImpl } from './bindings/types.js'; -import { mainThreadFlushLoopMark } from './utils/mainThreadFlushLoopGuard.js'; -import { profile } from './utils/profile.js'; - -interface RefImpl { - _workletRefMap: Record>; - _firstScreenWorkletRefMap: Record>; - updateWorkletRef( - refImpl: WorkletRefImpl, - element: ElementNode | null, - ): void; - updateWorkletRefInitValueChanges(patch: [number, unknown][]): void; - clearFirstScreenWorkletRefMap(): void; -} - -let impl: RefImpl | undefined; - -function initWorkletRef(): RefImpl { - return (impl = { - _workletRefMap: {}, - /** - * Map of worklet refs that are created during first screen rendering. - * These refs are created with negative IDs and need to be hydrated - * when the app starts. The map is cleared after hydration is complete - * to free up memory. - */ - _firstScreenWorkletRefMap: {}, - updateWorkletRef, - updateWorkletRefInitValueChanges, - clearFirstScreenWorkletRefMap, - }); -} - -const createWorkletRef = ( - id: WorkletRefId, - value: T, -): WorkletRef => { - return { - current: value, - _wvid: id, - }; -}; - -const getFromWorkletRefMap = ( - refImpl: WorkletRefImpl, -): WorkletRef => { - const id = refImpl._wvid; - /* v8 ignore next 3 */ - if (__DEV__) { - mainThreadFlushLoopMark(`MainThreadRef:get id=${id}`); - } - let value; - if (id < 0) { - // At the first screen rendering, the worklet ref is created with a negative ID. - // Might be called in two scenarios: - // 1. In MTS events - // 2. In `main-thread:ref` - value = impl!._firstScreenWorkletRefMap[id] as WorkletRef; - if (!value) { - value = impl!._firstScreenWorkletRefMap[id] = createWorkletRef(id, refImpl._initValue); - } - } else { - value = impl!._workletRefMap[id] as WorkletRef; - } - - /* v8 ignore next 3 */ - if (__DEV__ && value === undefined) { - throw new Error('MainThreadRef is not initialized: ' + id); - } - return value; -}; - -function removeValueFromWorkletRefMap(id: WorkletRefId): void { - delete impl!._workletRefMap[id]; -} - -/** - * Create an element instance of the given element node, then set the worklet value to it. - * This is called in `snapshotContextUpdateWorkletRef`. - * @param handle handle of the worklet value. - * @param element the element node. - */ -function updateWorkletRef( - handle: WorkletRefImpl, - element: ElementNode | null, -): void { - getFromWorkletRefMap(handle).current = element - ? new Element(element) - : null; -} - -function updateWorkletRefInitValueChanges( - patch: [WorkletRefId, unknown][], -): void { - profile('updateWorkletRefInitValueChanges', () => { - patch.forEach(([id, value]) => { - if (!impl!._workletRefMap[id]) { - impl!._workletRefMap[id] = createWorkletRef(id, value); - } - }); - }); -} - -function clearFirstScreenWorkletRefMap(): void { - impl!._firstScreenWorkletRefMap = {}; -} - -export { - type RefImpl, - createWorkletRef, - initWorkletRef, - getFromWorkletRefMap, - removeValueFromWorkletRefMap, - updateWorkletRefInitValueChanges, -}; +export * from '../../runtime/src/worklet-runtime/workletRef.js'; diff --git a/packages/react/worklet-runtime/src/workletRuntime.ts b/packages/react/worklet-runtime/src/workletRuntime.ts index aef075afa6..2e9c834ac0 100644 --- a/packages/react/worklet-runtime/src/workletRuntime.ts +++ b/packages/react/worklet-runtime/src/workletRuntime.ts @@ -1,201 +1 @@ -// 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 { Element } from './api/element.js'; -import type { ClosureValueType, RunWorkletOptions, Worklet, WorkletRefImpl } from './bindings/types.js'; -import { RunWorkletSource } from './bindings/types.js'; -import { initRunOnBackgroundDelay } from './delayRunOnBackground.js'; -import { delayExecUntilJsReady, initEventDelay } from './delayWorkletEvent.js'; -import { initEomImpl } from './eomImpl.js'; -import { addEventMethodsIfNeeded } from './eventPropagation.js'; -import { hydrateCtx } from './hydrate.js'; -import { JsFunctionLifecycleManager, isRunOnBackgroundEnabled } from './jsFunctionLifecycle.js'; -import { runRunOnMainThreadTask } from './runOnMainThread.js'; -import { mainThreadFlushLoopMark } from './utils/mainThreadFlushLoopGuard.js'; -import { profile } from './utils/profile.js'; -import { getFromWorkletRefMap, initWorkletRef } from './workletRef.js'; - -function initWorklet(): void { - globalThis.lynxWorkletImpl = { - _workletMap: {}, - _refImpl: initWorkletRef(), - _runOnBackgroundDelayImpl: initRunOnBackgroundDelay(), - _hydrateCtx: hydrateCtx, - _eventDelayImpl: initEventDelay(), - _eomImpl: initEomImpl(), - _runRunOnMainThreadTask: runRunOnMainThreadTask, - }; - - if (isRunOnBackgroundEnabled()) { - globalThis.lynxWorkletImpl._jsFunctionLifecycleManager = new JsFunctionLifecycleManager(); - } - - globalThis.registerWorklet = registerWorklet; - globalThis.registerWorkletInternal = registerWorklet; - globalThis.runWorklet = runWorklet; -} - -/** - * Register a worklet function, allowing it to be executed by `runWorklet()`. - * This is called in lepus.js. - * @param _type worklet type, 'main-thread' or 'ui' - * @param id worklet hash - * @param worklet worklet function - */ -function registerWorklet(_type: string, id: string, worklet: (...args: unknown[]) => unknown): void { - lynxWorkletImpl._workletMap[id] = worklet; -} - -/** - * Entrance of all worklet calls. - * Native event touch handler will call this function. - * @param ctx worklet object. - * @param params worklet params. - * @param options run worklet options. - */ -function runWorklet(ctx: Worklet, params: ClosureValueType[], options?: RunWorkletOptions): unknown { - if (!validateWorklet(ctx)) { - console.warn('MainThreadFunction: Invalid function object: ' + JSON.stringify(ctx)); - return; - } - - if (__DEV__) { - if (options?.source === RunWorkletSource.EVENT && Array.isArray(params)) { - const first = params[0]; - const t = (first as { type?: unknown }).type; - if (typeof t === 'string') { - mainThreadFlushLoopMark(`event:${t}`); - } - } - - mainThreadFlushLoopMark(`MainThreadFunction id=${String(ctx._wkltId)}`); - } - - if ('_lepusWorkletHash' in ctx) { - delayExecUntilJsReady(ctx._lepusWorkletHash, params); - return; - } - return runWorkletImpl(ctx, params, options); -} - -function runWorkletImpl(ctx: Worklet, params: ClosureValueType[], options?: RunWorkletOptions): unknown { - const worklet: (...args: unknown[]) => unknown = profile( - 'transformWorkletCtx ' + ctx._wkltId, - () => transformWorklet(ctx, true), - ); - const params_: ClosureValueType[] = profile( - 'transformWorkletParams', - () => transformWorklet(params || [], false), - ); - - const [hasEventMethods, eventCtx] = addEventMethodsIfNeeded(params_, options); - - const result = profile('runWorklet', () => worklet(...params_)); - - if (hasEventMethods) { - return { - returnValue: result, - eventReturnResult: eventCtx._eventReturnResult, - }; - } - - return result; -} - -function validateWorklet(ctx: unknown): ctx is Worklet { - return typeof ctx === 'object' && ctx !== null && ('_wkltId' in ctx || '_lepusWorkletHash' in ctx); -} - -const workletCache = new WeakMap unknown)>(); - -function transformWorklet(ctx: Worklet, isWorklet: true): (...args: unknown[]) => unknown; -function transformWorklet( - ctx: ClosureValueType[], - isWorklet: false, -): ClosureValueType[]; - -function transformWorklet( - ctx: ClosureValueType, - isWorklet: boolean, -): ClosureValueType | ((...args: unknown[]) => unknown) { - /* v8 ignore next 3 */ - if (typeof ctx !== 'object' || ctx === null) { - return ctx; - } - - if (isWorklet) { - const res = workletCache.get(ctx); - if (res) { - return res; - } - } - - const worklet = { main: ctx }; - transformWorkletInner(worklet, 0, ctx); - - if (isWorklet) { - workletCache.set(ctx, worklet.main); - } - - return worklet.main; -} - -const transformWorkletInner = ( - value: ClosureValueType, - depth: number, - ctx: unknown, -) => { - const limit = 1000; - if (++depth >= limit) { - throw new Error('Depth of value exceeds limit of ' + limit + '.'); - } - /* v8 ignore next 3 */ - if (typeof value !== 'object' || value === null) { - return; - } - const obj = value as Record; - - for (const key in obj) { - const subObj: ClosureValueType = obj[key]; - if (typeof subObj !== 'object' || subObj === null) { - continue; - } - - if (/** isEventTarget */ 'elementRefptr' in subObj) { - obj[key] = new Element(subObj['elementRefptr'] as ElementNode); - continue; - } else if (subObj instanceof Element) { - continue; - } - - transformWorkletInner(subObj, depth, ctx); - - const isWorkletRef = '_wvid' in (subObj as object); - if (isWorkletRef) { - obj[key] = getFromWorkletRefMap( - subObj as unknown as WorkletRefImpl, - ); - continue; - } - const isWorklet = '_wkltId' in subObj; - if (isWorklet) { - // `subObj` is worklet ctx. Shallow copy it to prevent the transformed worklet from referencing ctx. - // This would result in the value of `workletCache` referencing its key. - obj[key] = lynxWorkletImpl._workletMap[(subObj as Worklet)._wkltId]! - .bind({ ...subObj }); - obj[key].ctx = subObj; - continue; - } - const isJsFn = '_jsFnId' in subObj; - if (isJsFn) { - subObj['_execId'] = (ctx as Worklet)._execId; - lynxWorkletImpl._jsFunctionLifecycleManager?.addRef( - (ctx as Worklet)._execId!, - subObj, - ); - continue; - } - } -}; - -export { initWorklet }; +export * from '../../runtime/src/worklet-runtime/workletRuntime.js'; diff --git a/packages/react/worklet-runtime/tsconfig.eslint.json b/packages/react/worklet-runtime/tsconfig.eslint.json new file mode 100644 index 0000000000..78b07e580a --- /dev/null +++ b/packages/react/worklet-runtime/tsconfig.eslint.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "target": "ESNext", + "lib": ["es2021"], + "module": "Node16", + "moduleResolution": "Node16", + "resolveJsonModule": true + }, + "include": ["src/**/*", "../runtime/src/worklet-runtime/**/*"] +} diff --git a/packages/react/worklet-runtime/tsconfig.json b/packages/react/worklet-runtime/tsconfig.json index aac4b8ccfb..d138d095c8 100644 --- a/packages/react/worklet-runtime/tsconfig.json +++ b/packages/react/worklet-runtime/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "noEmit": false, "outDir": "lib", - "rootDir": "src", + "rootDir": "../runtime/src/worklet-runtime", "stripInternal": true, "target": "ESNext", "lib": ["es2021"], @@ -12,5 +12,5 @@ "resolveJsonModule": true, "composite": true, }, - "include": ["src"], + "include": ["../runtime/src/worklet-runtime/**/*"], } diff --git a/packages/react/worklet-runtime/turbo.json b/packages/react/worklet-runtime/turbo.json index 659bad068f..7be8b860da 100644 --- a/packages/react/worklet-runtime/turbo.json +++ b/packages/react/worklet-runtime/turbo.json @@ -5,6 +5,7 @@ "build": { "dependsOn": [], "inputs": [ + "../runtime/src/worklet-runtime/**", "src/**", "rslib.config.ts" ], diff --git a/packages/react/worklet-runtime/vitest.config.ts b/packages/react/worklet-runtime/vitest.config.ts index 8065e0e0d7..fa44e73ebf 100644 --- a/packages/react/worklet-runtime/vitest.config.ts +++ b/packages/react/worklet-runtime/vitest.config.ts @@ -8,17 +8,20 @@ const config: ViteUserConfig = defineConfig({ test: { name: 'react/worklet-runtime', coverage: { + allowExternal: true, + include: ['../runtime/src/worklet-runtime/**/*.ts'], exclude: [ 'dist/**', 'lib/**', + 'src/**', 'rslib.config.ts', - 'src/api/lepusQuerySelector.ts', - 'src/api/lynxApi.ts', - 'src/bindings/**', - 'src/global.ts', - 'src/index.ts', - 'src/listeners.ts', - 'src/types/**', + '../runtime/src/worklet-runtime/api/lepusQuerySelector.ts', + '../runtime/src/worklet-runtime/api/lynxApi.ts', + '../runtime/src/worklet-runtime/bindings/**', + '../runtime/src/worklet-runtime/global.ts', + '../runtime/src/worklet-runtime/index.ts', + '../runtime/src/worklet-runtime/listeners.ts', + '../runtime/src/worklet-runtime/types/**', 'vitest.config.ts', ], thresholds: { diff --git a/packages/rspeedy/plugin-react/test/config.test.ts b/packages/rspeedy/plugin-react/test/config.test.ts index e24a481da2..47210e2e08 100644 --- a/packages/rspeedy/plugin-react/test/config.test.ts +++ b/packages/rspeedy/plugin-react/test/config.test.ts @@ -2594,6 +2594,19 @@ describe('Config', () => { ) }) + test('worklet runtime bindings resolve to the shell package build output', () => { + const require = createRequire(import.meta.url) + + expect( + require.resolve('@lynx-js/react/worklet-runtime/bindings'), + ).toContain( + '/packages/react/worklet-runtime/lib/bindings/index.js'.replaceAll( + '/', + path.sep, + ), + ) + }) + describe('environment', () => { test('lynx environment', async () => { const { pluginReactLynx } = await import('../src/pluginReactLynx.js')