diff --git a/apps/common-app/src/apps/reanimated/App.tsx b/apps/common-app/src/apps/reanimated/App.tsx index 2ef8af9cb099..086f53d91057 100644 --- a/apps/common-app/src/apps/reanimated/App.tsx +++ b/apps/common-app/src/apps/reanimated/App.tsx @@ -5,6 +5,7 @@ import type { StackNavigationProp } from '@react-navigation/stack'; import React, { memo } from 'react'; import { FlatList, + Platform, Pressable, ScrollView, StyleSheet, @@ -77,7 +78,7 @@ function HomeScreen({ navigation }: HomeScreenProps) { setTimeout(() => setWasClicked([...wasClicked, name]), 500); } }} - missingOnFabric={EXAMPLES[name].missingOnFabric} + disabled={EXAMPLES[name].disabledPlatforms?.includes(Platform.OS)} wasClicked={wasClicked.includes(name)} /> )} @@ -91,29 +92,22 @@ function HomeScreen({ navigation }: HomeScreenProps) { interface ItemProps { icon?: string; title: string; + disabled?: boolean; onPress: () => void; - missingOnFabric?: boolean; wasClicked?: boolean; } -function Item({ - icon, - title, - onPress, - missingOnFabric, - wasClicked, -}: ItemProps) { - const isDisabled = missingOnFabric; +function Item({ icon, title, onPress, disabled, wasClicked }: ItemProps) { const Button = IS_MACOS ? Pressable : RectButton; return ( diff --git a/apps/common-app/src/apps/reanimated/examples/index.ts b/apps/common-app/src/apps/reanimated/examples/index.ts index afaa5adccad3..885d4f5dcbf4 100644 --- a/apps/common-app/src/apps/reanimated/examples/index.ts +++ b/apps/common-app/src/apps/reanimated/examples/index.ts @@ -135,14 +135,28 @@ import WorkletExample from './WorkletExample'; import WorkletFactoryCrash from './WorkletFactoryCrashExample'; import WorkletRuntimeExample from './WorkletRuntimeExample'; -interface Example { +export const REAPlatform = { + IOS: 'ios', + ANDROID: 'android', + MACOS: 'macos', + WEB: 'web', +}; + +export interface Example { icon?: string; title: string; screen: React.FC; - missingOnFabric?: boolean; + disabledPlatforms?: (typeof REAPlatform)[keyof typeof REAPlatform][]; } export const EXAMPLES: Record = { + // About + AboutExample: { + icon: 'â„šī¸', + title: 'About', + screen: AboutExample, + }, + // Empty example for test purposes EmptyExample: { icon: 'đŸ‘ģ', @@ -163,11 +177,13 @@ export const EXAMPLES: Record = { icon: 'âš™ī¸', title: 'RuntimeTestsExample', screen: RuntimeTestsExample, + disabledPlatforms: [REAPlatform.WEB], }, Synchronizable: { icon: '🔄', title: 'Synchronizable performance', screen: SynchronizablePerformanceExample, + disabledPlatforms: [REAPlatform.WEB], }, ReactFreeze: { icon: 'â„ī¸', @@ -183,6 +199,7 @@ export const EXAMPLES: Record = { icon: 'đŸƒâ€â™‚ī¸', title: 'Worklet runtime', screen: WorkletRuntimeExample, + disabledPlatforms: [REAPlatform.WEB], }, ModifyExample: { icon: 'đŸĒ›', @@ -203,6 +220,7 @@ export const EXAMPLES: Record = { icon: 'đŸĨļ', title: 'Serializable freezing', screen: SerializableFreezingExample, + disabledPlatforms: [REAPlatform.WEB], }, InvalidReadWriteExample: { icon: '🔒', @@ -218,6 +236,7 @@ export const EXAMPLES: Record = { icon: '🔄', title: 'Copy serializable performance test', screen: CopySerializablePerformanceTest, + disabledPlatforms: [REAPlatform.WEB], }, FlatListWithLayoutAnimations: { icon: 'đŸŽģ', @@ -225,14 +244,6 @@ export const EXAMPLES: Record = { screen: FlatListWithLayoutAnimations, }, - // About - - AboutExample: { - icon: 'â„šī¸', - title: 'About', - screen: AboutExample, - }, - // Showcase BokehExample: { @@ -299,6 +310,7 @@ export const EXAMPLES: Record = { icon: 'đŸ“ē', title: 'Screen transition', screen: ScreenTransitionExample, + disabledPlatforms: [REAPlatform.WEB], }, // Basic examples @@ -496,6 +508,11 @@ export const EXAMPLES: Record = { icon: '🔌', title: 'Without Babel plugin', screen: WithoutBabelPluginExample, + disabledPlatforms: [ + REAPlatform.ANDROID, + REAPlatform.IOS, + REAPlatform.MACOS, + ], }, MatrixExample: { icon: '🧮', @@ -531,7 +548,12 @@ export const EXAMPLES: Record = { icon: '🔎', title: 'getViewProp', screen: GetViewPropExample, - missingOnFabric: true, + disabledPlatforms: [ + REAPlatform.WEB, + REAPlatform.ANDROID, + REAPlatform.IOS, + REAPlatform.MACOS, + ], }, LogExample: { icon: '⌨', @@ -582,6 +604,7 @@ export const EXAMPLES: Record = { title: 'DynamicColorIOS', screen: DynamicColorIOSExample, icon: '🌗', + disabledPlatforms: [REAPlatform.ANDROID, REAPlatform.WEB], }, // Old examples diff --git a/apps/fabric-example/ios/Podfile.lock b/apps/fabric-example/ios/Podfile.lock index 9b1ff727a948..5e3fd3c31efc 100644 --- a/apps/fabric-example/ios/Podfile.lock +++ b/apps/fabric-example/ios/Podfile.lock @@ -3190,10 +3190,10 @@ SPEC CHECKSUMS: RNReanimated: 3b47c33660454c6f9700b463e92daa282030866a RNScreens: 6ced6ae8a526512a6eef6e28c2286e1fc2d378c3 RNSVG: 287504b73fa0e90a605225aa9f852a86d5461e84 - RNWorklets: 7119ae08263033c456c80d90794a312f2f88c956 + RNWorklets: 991f94e4fa31fc20853e74d5d987426f8580cb0d SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: e80c5fabbc3e26311152fa20404cdfa14f16a11f -PODFILE CHECKSUM: 5d8c04f461eed0f22e86610877d94f2b8b838b8b +PODFILE CHECKSUM: db099f48c6dadedd8fc0a430129b75e561867ab9 COCOAPODS: 1.15.2 diff --git a/packages/react-native-reanimated/jest-setup.js b/packages/react-native-reanimated/jest-setup.js index 1c4e95c9f09a..d235392ed8eb 100644 --- a/packages/react-native-reanimated/jest-setup.js +++ b/packages/react-native-reanimated/jest-setup.js @@ -2,6 +2,10 @@ delete global.MessageChannel; require('react-native-worklets/jestSetup'); require('./src/jestUtils').setUpTests(); +jest.mock('react-native-worklets', () => + require('react-native-worklets/src/mock') +); + global.__reanimatedLoggerConfig = { logFunction: (data) => { switch (data.level) { diff --git a/packages/react-native-worklets/__tests__/API.test.ts b/packages/react-native-worklets/__tests__/API.test.ts new file mode 100644 index 000000000000..7a5d2e5240b6 --- /dev/null +++ b/packages/react-native-worklets/__tests__/API.test.ts @@ -0,0 +1,46 @@ +import * as Worklets from '../src/index'; + +describe('web API', () => { + it('should have all exports available', () => { + const WorkletAPI = [ + 'isShareableRef', + 'makeShareable', + 'makeShareableCloneOnUIRecursive', + 'makeShareableCloneRecursive', + 'shareableMappingCache', + 'getStaticFeatureFlag', + 'setDynamicFeatureFlag', + 'isSynchronizable', + 'getRuntimeKind', + 'RuntimeKind', + 'createWorkletRuntime', + 'runOnRuntime', + 'scheduleOnRuntime', + 'createSerializable', + 'isSerializableRef', + 'serializableMappingCache', + 'createSynchronizable', + 'callMicrotasks', + 'executeOnUIRuntimeSync', + 'runOnJS', + 'runOnUI', + 'runOnUIAsync', + 'runOnUISync', + 'scheduleOnRN', + 'scheduleOnUI', + 'unstable_eventLoopTask', + 'isWorkletFunction', + 'WorkletsModule', + ]; + + // Check if all exports are available. + expect(WorkletAPI.sort()).toEqual(Object.keys(Worklets).sort()); + + const definedWorklets = WorkletAPI.filter((api) => { + return Worklets[api as keyof typeof Worklets] !== undefined; + }).map((api) => api); + + // Check if all exports are defined. + expect(WorkletAPI.sort()).toEqual(definedWorklets.sort()); + }); +}); diff --git a/packages/react-native-worklets/jest.config.js b/packages/react-native-worklets/jest.config.js index 0587b08d2690..f07dd16bf53e 100644 --- a/packages/react-native-worklets/jest.config.js +++ b/packages/react-native-worklets/jest.config.js @@ -4,4 +4,14 @@ module.exports = { modulePathIgnorePatterns: ['lib'], testEnvironment: 'node', transformIgnorePatterns: [], + moduleFileExtensions: [ + 'web.ts', + 'web.tsx', + 'web.js', + 'ts', + 'tsx', + 'js', + 'jsx', + 'json', + ], }; diff --git a/packages/react-native-worklets/src/PlatformChecker/index.ts b/packages/react-native-worklets/src/PlatformChecker/index.ts index f77f7bac92f5..be1d064ef2d4 100644 --- a/packages/react-native-worklets/src/PlatformChecker/index.ts +++ b/packages/react-native-worklets/src/PlatformChecker/index.ts @@ -1,30 +1,6 @@ 'use strict'; -import { getRuntimeKind, RuntimeKind } from '../runtimeKind'; -import { - IS_JEST as RN_IS_JEST, - IS_WEB as RN_IS_WEB, - IS_WINDOWS as RN_IS_WINDOWS, - SHOULD_BE_USE_WEB as RN_SHOULD_BE_USE_WEB, -} from './PlatformChecker'; - -let IS_JEST = false; -let IS_WEB = false; -let IS_WINDOWS = false; -let SHOULD_BE_USE_WEB = false; - -if (getRuntimeKind() === RuntimeKind.ReactNative) { - IS_JEST = RN_IS_JEST; - IS_WEB = RN_IS_WEB; - IS_WINDOWS = RN_IS_WINDOWS; - SHOULD_BE_USE_WEB = RN_SHOULD_BE_USE_WEB; -} - -export { - IS_JEST, - /** @knipIgnore */ - IS_WEB, - /** @knipIgnore */ - IS_WINDOWS, - SHOULD_BE_USE_WEB, -}; +export const IS_JEST = false; +export const IS_WEB = false; +export const IS_WINDOWS = false; +export const SHOULD_BE_USE_WEB = false; diff --git a/packages/react-native-worklets/src/PlatformChecker/index.web.ts b/packages/react-native-worklets/src/PlatformChecker/index.web.ts new file mode 100644 index 000000000000..93ca38f9c264 --- /dev/null +++ b/packages/react-native-worklets/src/PlatformChecker/index.web.ts @@ -0,0 +1,8 @@ +'use strict'; + +export { + IS_JEST, + IS_WEB, + IS_WINDOWS, + SHOULD_BE_USE_WEB, +} from './PlatformChecker'; diff --git a/packages/react-native-worklets/src/WorkletsError.web.ts b/packages/react-native-worklets/src/WorkletsError.web.ts new file mode 100644 index 000000000000..32c43f3a9438 --- /dev/null +++ b/packages/react-native-worklets/src/WorkletsError.web.ts @@ -0,0 +1,12 @@ +'use strict'; + +function WorkletsErrorConstructor(message?: string) { + const prefix = '[Worklets]'; + + // eslint-disable-next-line reanimated/use-worklets-error + const errorInstance = new Error(message ? `${prefix} ${message}` : prefix); + errorInstance.name = `WorkletsError`; + return errorInstance; +} + +export const WorkletsError = WorkletsErrorConstructor; diff --git a/packages/react-native-worklets/src/WorkletsModule/JSWorklets.ts b/packages/react-native-worklets/src/WorkletsModule/JSWorklets.ts deleted file mode 100644 index 68efbd62898d..000000000000 --- a/packages/react-native-worklets/src/WorkletsModule/JSWorklets.ts +++ /dev/null @@ -1,200 +0,0 @@ -'use strict'; - -import { IS_JEST } from '../PlatformChecker'; -import { mockedRequestAnimationFrame } from '../runLoop/uiRuntime/mockedRequestAnimationFrame'; -import { WorkletsError } from '../WorkletsError'; -import type { SerializableRef } from '../workletTypes'; -import type { IWorkletsModule } from './workletsModuleProxy'; - -export function createJSWorkletsModule(): IWorkletsModule { - return new JSWorklets(); -} - -// In Node.js environments (like when static rendering with Expo Router) -// requestAnimationFrame is unavailable, so we use our mock. -// It also has to be mocked for Jest purposes (see `initializeUIRuntime`). -const requestAnimationFrameImpl = - IS_JEST || !globalThis.requestAnimationFrame - ? mockedRequestAnimationFrame - : globalThis.requestAnimationFrame; - -class JSWorklets implements IWorkletsModule { - createSerializable(): never { - throw new WorkletsError( - 'createSerializable should never be called in JSWorklets.' - ); - } - - createSerializableString(): never { - throw new WorkletsError( - 'createSerializableString should never be called in JSWorklets.' - ); - } - - createSerializableNumber(): never { - throw new WorkletsError( - 'createSerializableNumber should never be called in JSWorklets.' - ); - } - - createSerializableBoolean(): never { - throw new WorkletsError( - 'createSerializableBoolean should never be called in JSWorklets.' - ); - } - - createSerializableBigInt(): never { - throw new WorkletsError( - 'createSerializableBigInt should never be called in JSWorklets.' - ); - } - - createSerializableUndefined(): never { - throw new WorkletsError( - 'createSerializableUndefined should never be called in JSWorklets.' - ); - } - - createSerializableNull(): never { - throw new WorkletsError( - 'createSerializableNull should never be called in JSWorklets.' - ); - } - - createSerializableTurboModuleLike(): never { - throw new WorkletsError( - 'createSerializableTurboModuleLike should never be called in JSWorklets.' - ); - } - - createSerializableObject(): never { - throw new WorkletsError( - 'createSerializableObject should never be called in JSWorklets.' - ); - } - - createSerializableMap(): never { - throw new WorkletsError( - 'createSerializableMap should never be called in JSWorklets.' - ); - } - - createSerializableSet(): never { - throw new WorkletsError( - 'createSerializableSet should never be called in JSWorklets.' - ); - } - - createSerializableImport(): never { - throw new WorkletsError( - 'createSerializableImport should never be called in JSWorklets.' - ); - } - - createSerializableHostObject(): never { - throw new WorkletsError( - 'createSerializableHostObject should never be called in JSWorklets.' - ); - } - - createSerializableArray(): never { - throw new WorkletsError( - 'createSerializableArray should never be called in JSWorklets.' - ); - } - - createSerializableInitializer(): never { - throw new WorkletsError( - 'createSerializableInitializer should never be called in JSWorklets.' - ); - } - - createSerializableFunction(): never { - throw new WorkletsError( - 'createSerializableFunction should never be called in JSWorklets.' - ); - } - - createSerializableWorklet(): never { - throw new WorkletsError( - 'createSerializableWorklet should never be called in JSWorklets.' - ); - } - - scheduleOnUI(worklet: SerializableRef) { - // TODO: `requestAnimationFrame` should be used exclusively in Reanimated - - // @ts-ignore web implementation has still not been updated after the rewrite, - // this will be addressed once the web implementation updates are ready - requestAnimationFrameImpl(worklet); - } - - executeOnUIRuntimeSync(): never { - throw new WorkletsError( - '`executeOnUIRuntimeSync` is not available in JSWorklets.' - ); - } - - createWorkletRuntime(): never { - throw new WorkletsError( - 'createWorkletRuntime is not available in JSWorklets.' - ); - } - - scheduleOnRuntime(): never { - throw new WorkletsError( - 'scheduleOnRuntime is not available in JSWorklets.' - ); - } - - createSynchronizable(): never { - throw new WorkletsError( - 'createSynchronizable should never be called in JSWorklets.' - ); - } - - synchronizableGetDirty(): never { - throw new WorkletsError( - 'synchronizableGetDirty should never be called in JSWorklets.' - ); - } - - synchronizableGetBlocking(): never { - throw new WorkletsError( - 'synchronizableGetBlocking should never be called in JSWorklets.' - ); - } - - synchronizableSetBlocking(): never { - throw new WorkletsError( - 'synchronizableSetBlocking should never be called in JSWorklets.' - ); - } - - synchronizableLock(): never { - throw new WorkletsError( - 'synchronizableLock should never be called in JSWorklets.' - ); - } - - synchronizableUnlock(): never { - throw new WorkletsError( - 'synchronizableUnlock should never be called in JSWorklets.' - ); - } - - reportFatalErrorOnJS(): never { - throw new WorkletsError( - 'reportFatalErrorOnJS should never be called in JSWorklets.' - ); - } - - getStaticFeatureFlag(): boolean { - // mock implementation - return false; - } - - setDynamicFeatureFlag() { - // noop - } -} diff --git a/packages/react-native-worklets/src/WorkletsModule/index.web.ts b/packages/react-native-worklets/src/WorkletsModule/index.web.ts new file mode 100644 index 000000000000..ef9c59d23717 --- /dev/null +++ b/packages/react-native-worklets/src/WorkletsModule/index.web.ts @@ -0,0 +1,3 @@ +'use strict'; + +export const WorkletsModule = null; diff --git a/packages/react-native-worklets/src/WorkletsModule/workletsModuleInstance.ts b/packages/react-native-worklets/src/WorkletsModule/workletsModuleInstance.ts index ec07b6ff4d62..5db935842e96 100644 --- a/packages/react-native-worklets/src/WorkletsModule/workletsModuleInstance.ts +++ b/packages/react-native-worklets/src/WorkletsModule/workletsModuleInstance.ts @@ -1,9 +1,5 @@ 'use strict'; -import { SHOULD_BE_USE_WEB } from '../PlatformChecker'; -import { createJSWorkletsModule } from './JSWorklets'; import { createNativeWorkletsModule } from './NativeWorklets'; -export const WorkletsModule = SHOULD_BE_USE_WEB - ? createJSWorkletsModule() - : createNativeWorkletsModule(); +export const WorkletsModule = createNativeWorkletsModule(); diff --git a/packages/react-native-worklets/src/WorkletsModule/workletsModuleInstance.web.ts b/packages/react-native-worklets/src/WorkletsModule/workletsModuleInstance.web.ts deleted file mode 100644 index 93c11d4d1d1b..000000000000 --- a/packages/react-native-worklets/src/WorkletsModule/workletsModuleInstance.web.ts +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -import { createJSWorkletsModule } from './JSWorklets'; - -export const WorkletsModule = createJSWorkletsModule(); diff --git a/packages/react-native-worklets/src/featureFlags/index.web.ts b/packages/react-native-worklets/src/featureFlags/index.web.ts new file mode 100644 index 000000000000..270bc2b8a61e --- /dev/null +++ b/packages/react-native-worklets/src/featureFlags/index.web.ts @@ -0,0 +1,9 @@ +'use strict'; + +export function getStaticFeatureFlag() { + return false; +} + +export function setDynamicFeatureFlag() { + // no-op +} diff --git a/packages/react-native-worklets/src/initializers.ts b/packages/react-native-worklets/src/initializers.ts index fd180700c777..57c05be983fc 100644 --- a/packages/react-native-worklets/src/initializers.ts +++ b/packages/react-native-worklets/src/initializers.ts @@ -3,10 +3,8 @@ import { bundleValueUnpacker } from './bundleUnpacker'; import { setupCallGuard } from './callGuard'; import { registerReportFatalRemoteError } from './errors'; -import { IS_JEST, SHOULD_BE_USE_WEB } from './PlatformChecker'; import { setupSetImmediate } from './runLoop/common/setImmediatePolyfill'; import { setupSetInterval } from './runLoop/common/setIntervalPolyfill'; -import { mockedRequestAnimationFrame } from './runLoop/uiRuntime/mockedRequestAnimationFrame'; import { setupRequestAnimationFrame } from './runLoop/uiRuntime/requestAnimationFrame'; import { setupSetTimeout } from './runLoop/uiRuntime/setTimeoutPolyfill'; import { RuntimeKind } from './runtimeKind'; @@ -17,6 +15,12 @@ import { registerWorkletsError, WorkletsError } from './WorkletsError'; import { WorkletsModule } from './WorkletsModule'; import type { ValueUnpacker } from './workletTypes'; +if (globalThis.__RUNTIME_KIND === undefined) { + // The only runtime that doesn't have `__RUNTIME_KIND` preconfigured + // is the RN Runtime. We must set it as soon as possible. + globalThis.__RUNTIME_KIND = RuntimeKind.ReactNative; +} + let capturableConsole: typeof console; /** @@ -85,25 +89,13 @@ export function init() { } initialized = true; - if (globalThis.__RUNTIME_KIND === undefined) { - // The only runtime that doesn't have `__RUNTIME_KIND` preconfigured - // is the RN Runtime. We must set it as soon as possible. - globalThis.__RUNTIME_KIND = RuntimeKind.ReactNative; - } - initializeRuntime(); - if (SHOULD_BE_USE_WEB) { - initializeRuntimeOnWeb(); - } - if (globalThis.__RUNTIME_KIND !== RuntimeKind.ReactNative) { initializeWorkletRuntime(); } else { initializeRNRuntime(); - if (!SHOULD_BE_USE_WEB) { - installRNBindingsOnUIRuntime(); - } + installRNBindingsOnUIRuntime(); } } @@ -117,7 +109,7 @@ function initializeRuntime() { /** A function that should be run only on React Native runtime. */ function initializeRNRuntime() { - if (__DEV__ && !SHOULD_BE_USE_WEB) { + if (__DEV__) { const testWorklet = () => { 'worklet'; }; @@ -201,22 +193,6 @@ function initializeWorkletRuntime() { } } -/** A function that should be run only on RN Runtime in web implementation. */ -function initializeRuntimeOnWeb() { - globalThis._WORKLET = false; - globalThis._log = console.log; - globalThis._getAnimationTimestamp = () => performance.now(); - if (IS_JEST) { - // requestAnimationFrame react-native jest's setup is incorrect as it polyfills - // the method directly using setTimeout, therefore the callback doesn't get the - // expected timestamp as the only argument: https://github.com/facebook/react-native/blob/main/packages/react-native/jest/setup.js#L28 - // We override this setup here to make sure that callbacks get the proper timestamps - // when executed. For non-jest environments we define requestAnimationFrame in setupRequestAnimationFrame - // @ts-ignore TypeScript uses Node definition for rAF, setTimeout, etc which returns a Timeout object rather than a number - globalThis.requestAnimationFrame = mockedRequestAnimationFrame; - } -} - /** * A function that should be run on the RN Runtime to configure the UI Runtime * with callback bindings. diff --git a/packages/react-native-worklets/src/initializers.web.ts b/packages/react-native-worklets/src/initializers.web.ts new file mode 100644 index 000000000000..bb9d16763c3e --- /dev/null +++ b/packages/react-native-worklets/src/initializers.web.ts @@ -0,0 +1,21 @@ +'use strict'; + +import { IS_JEST } from './PlatformChecker'; +import { mockedRequestAnimationFrame } from './runLoop/uiRuntime/mockedRequestAnimationFrame'; +import { RuntimeKind } from './runtimeKind'; + +export function init() { + globalThis._WORKLET = false; + globalThis.__RUNTIME_KIND = RuntimeKind.ReactNative; + globalThis._log = console.log; + globalThis._getAnimationTimestamp = () => performance.now(); + if (IS_JEST) { + // requestAnimationFrame react-native jest's setup is incorrect as it polyfills + // the method directly using setTimeout, therefore the callback doesn't get the + // expected timestamp as the only argument: https://github.com/facebook/react-native/blob/main/packages/react-native/jest/setup.js#L28 + // We override this setup here to make sure that callbacks get the proper timestamps + // when executed. For non-jest environments we define requestAnimationFrame in setupRequestAnimationFrame + // @ts-ignore TypeScript uses Node definition for rAF, setTimeout, etc which returns a Timeout object rather than a number + globalThis.requestAnimationFrame = mockedRequestAnimationFrame; + } +} diff --git a/packages/react-native-worklets/src/isSynchronizable.web.ts b/packages/react-native-worklets/src/isSynchronizable.web.ts new file mode 100644 index 000000000000..7164292807c9 --- /dev/null +++ b/packages/react-native-worklets/src/isSynchronizable.web.ts @@ -0,0 +1,7 @@ +'use strict'; + +import { WorkletsError } from './WorkletsError'; + +export function isSynchronizable(): never { + throw new WorkletsError('`isSynchronizable` is not supported on web.'); +} diff --git a/packages/react-native-worklets/src/mock.ts b/packages/react-native-worklets/src/mock.ts new file mode 100644 index 000000000000..a7d18f7968e6 --- /dev/null +++ b/packages/react-native-worklets/src/mock.ts @@ -0,0 +1,107 @@ +'use strict'; + +import { mockedRequestAnimationFrame } from './runLoop/uiRuntime/mockedRequestAnimationFrame'; +import { RuntimeKind } from './runtimeKind'; +import { isWorkletFunction } from './workletFunction'; + +const NOOP = () => {}; +const NOOP_FACTORY = () => NOOP; +const ID = (value: TValue) => value; +const IMMEDIATE_CALLBACK_INVOCATION = (callback: () => TCallback) => + callback(); + +globalThis._WORKLET = false; +globalThis.__RUNTIME_KIND = RuntimeKind.ReactNative; +globalThis._log = console.log; +globalThis._getAnimationTimestamp = () => performance.now(); +// requestAnimationFrame react-native jest's setup is incorrect as it polyfills +// the method directly using setTimeout, therefore the callback doesn't get the +// expected timestamp as the only argument: https://github.com/facebook/react-native/blob/main/packages/react-native/jest/setup.js#L28 +// We override this setup here to make sure that callbacks get the proper timestamps +// when executed. For non-jest environments we define requestAnimationFrame in setupRequestAnimationFrame +// @ts-ignore TypeScript uses Node definition for rAF, setTimeout, etc which returns a Timeout object rather than a number +globalThis.requestAnimationFrame = mockedRequestAnimationFrame; + +const WorkletAPI = { + isShareableRef: () => true, + makeShareable: ID, + makeShareableCloneOnUIRecursive: ID, + makeShareableCloneRecursive: ID, + shareableMappingCache: new Map(), + getStaticFeatureFlag: () => false, + setDynamicFeatureFlag: NOOP, + isSynchronizable: () => false, + getRuntimeKind: () => RuntimeKind.ReactNative, + RuntimeKind: RuntimeKind, + createWorkletRuntime: NOOP_FACTORY, + runOnRuntime: ID, + scheduleOnRuntime: IMMEDIATE_CALLBACK_INVOCATION, + createSerializable: ID, + isSerializableRef: ID, + serializableMappingCache: new Map(), + createSynchronizable: ID, + callMicrotasks: NOOP, + executeOnUIRuntimeSync: ID, + runOnJS( + fun: (...args: Args) => ReturnValue + ): (...args: Args) => void { + return (...args) => + queueMicrotask( + args.length + ? () => (fun as (...args: Args) => ReturnValue)(...args) + : (fun as () => ReturnValue) + ); + }, + runOnUI( + worklet: (...args: Args) => ReturnValue + ): (...args: Args) => void { + return (...args) => { + // Mocking time in Jest is tricky as both requestAnimationFrame and queueMicrotask + // callbacks run on the same queue and can be interleaved. There is no way + // to flush particular queue in Jest and the only control over mocked timers + // is by using jest.advanceTimersByTime() method which advances all types + // of timers including immediate and animation callbacks. Ideally we'd like + // to have some way here to schedule work along with React updates, but + // that's not possible, and hence in Jest environment instead of using scheduling + // mechanism we just schedule the work ommiting the queue. This is ok for the + // uses that we currently have but may not be ok for future tests that we write. + mockedRequestAnimationFrame(() => { + worklet(...args); + }); + }; + }, + runOnUIAsync( + worklet: (...args: Args) => ReturnValue + ): (...args: Args) => Promise { + return (...args: Args) => { + return new Promise((resolve) => { + mockedRequestAnimationFrame(() => { + const result = worklet(...args); + resolve(result); + }); + }); + }; + }, + runOnUISync: IMMEDIATE_CALLBACK_INVOCATION, + scheduleOnRN( + fun: (...args: Args) => ReturnValue, + ...args: Args + ): void { + this.runOnJS(fun)(...args); + }, + scheduleOnUI( + worklet: (...args: Args) => ReturnValue, + ...args: Args + ): void { + this.runOnUI(worklet)(...args); + }, + // eslint-disable-next-line camelcase + unstable_eventLoopTask: NOOP_FACTORY, + isWorkletFunction: isWorkletFunction, + WorkletsModule: {}, +}; + +module.exports = { + __esModule: true, + ...WorkletAPI, +}; diff --git a/packages/react-native-worklets/src/runtimes.ts b/packages/react-native-worklets/src/runtimes.ts index fafc128647a5..bea572de8a5b 100644 --- a/packages/react-native-worklets/src/runtimes.ts +++ b/packages/react-native-worklets/src/runtimes.ts @@ -2,7 +2,6 @@ import { setupCallGuard } from './callGuard'; import { getMemorySafeCapturableConsole, setupConsole } from './initializers'; -import { SHOULD_BE_USE_WEB } from './PlatformChecker'; import { setupRunLoop } from './runLoop/workletRuntime'; import { RuntimeKind } from './runtimeKind'; import { @@ -110,7 +109,7 @@ export function runOnRuntime( worklet: WorkletFunction ): (...args: Args) => void { 'worklet'; - if (__DEV__ && !SHOULD_BE_USE_WEB && !isWorkletFunction(worklet)) { + if (__DEV__ && !isWorkletFunction(worklet)) { throw new WorkletsError( 'The function passed to `runOnRuntime` is not a worklet.' ); diff --git a/packages/react-native-worklets/src/runtimes.web.ts b/packages/react-native-worklets/src/runtimes.web.ts new file mode 100644 index 000000000000..69b2078d4fc4 --- /dev/null +++ b/packages/react-native-worklets/src/runtimes.web.ts @@ -0,0 +1,13 @@ +'use strict'; + +export function createWorkletRuntime(): never { + throw new WorkletsError('`createWorkletRuntime` is not supported on web.'); +} + +export function runOnRuntime(): never { + throw new WorkletsError('`runOnRuntime` is not supported on web.'); +} + +export function scheduleOnRuntime(): never { + throw new WorkletsError('`scheduleOnRuntime` is not supported on web.'); +} diff --git a/packages/react-native-worklets/src/serializable.ts b/packages/react-native-worklets/src/serializable.ts index d4a4400be1e4..e8c4ba07c9d7 100644 --- a/packages/react-native-worklets/src/serializable.ts +++ b/packages/react-native-worklets/src/serializable.ts @@ -2,7 +2,6 @@ import { registerWorkletStackDetails } from './errors'; import { isSynchronizable } from './isSynchronizable'; import { logger } from './logger'; -import { SHOULD_BE_USE_WEB } from './PlatformChecker'; import { serializableMappingCache, serializableMappingFlag, @@ -123,42 +122,38 @@ const DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD = 30; // We use it to check if later on the function reenters with the same object let processedObjectAtThresholdDepth: unknown; -function createSerializableWeb(value: T): SerializableRef { - return value as SerializableRef; -} - -function createSerializableNative( - value: T, +export function createSerializable( + value: TValue, shouldPersistRemote = false, depth = 0 -): SerializableRef { +): SerializableRef { detectCyclicObject(value, depth); const isObject = typeof value === 'object'; const isFunction = typeof value === 'function'; if (typeof value === 'string') { - return cloneString(value) as SerializableRef; + return cloneString(value) as SerializableRef; } if (typeof value === 'number') { - return cloneNumber(value) as SerializableRef; + return cloneNumber(value) as SerializableRef; } if (typeof value === 'boolean') { - return cloneBoolean(value) as SerializableRef; + return cloneBoolean(value) as SerializableRef; } if (typeof value === 'bigint') { - return cloneBigInt(value) as SerializableRef; + return cloneBigInt(value) as SerializableRef; } if (value === undefined) { - return cloneUndefined() as SerializableRef; + return cloneUndefined() as SerializableRef; } if (value === null) { - return cloneNull() as SerializableRef; + return cloneNull() as SerializableRef; } if ((!isObject && !isFunction) || value === null) { @@ -167,7 +162,7 @@ function createSerializableNative( const cached = getFromCache(value); if (cached !== undefined) { - return cached as SerializableRef; + return cached as SerializableRef; } if (Array.isArray(value)) { @@ -178,7 +173,7 @@ function createSerializableNative( isFunction && (value as WorkletImport).__bundleData ) { - return cloneImport(value as WorkletImport) as SerializableRef; + return cloneImport(value as WorkletImport) as SerializableRef; } if (isFunction && !isWorkletFunction(value)) { return cloneRemoteFunction(value); @@ -196,7 +191,7 @@ function createSerializableNative( value, shouldPersistRemote, depth - ) as SerializableRef; + ) as SerializableRef; } if (isPlainJSObject(value) && value.__workletContextObjectFactory) { return cloneContextObject(value); @@ -205,7 +200,7 @@ function createSerializableNative( return cloneWorklet(value, shouldPersistRemote, depth); } if (isSynchronizable(value)) { - return cloneSynchronizable(value) as SerializableRef; + return cloneSynchronizable(value) as SerializableRef; } if (isPlainJSObject(value) || isFunction) { return clonePlainJSObject(value, shouldPersistRemote, depth); @@ -234,26 +229,13 @@ function createSerializableNative( if (globalThis._WORKLETS_BUNDLE_MODE) { // TODO: Do it programatically. - createSerializableNative.__bundleData = { + createSerializable.__bundleData = { imported: 'createSerializable', // @ts-expect-error resolveWeak is defined by Metro source: require.resolveWeak('./index'), }; } -interface CreateSerializable { - (value: T): SerializableRef; - ( - value: T, - shouldPersistRemote: boolean, - depth: number - ): SerializableRef; -} - -export const createSerializable: CreateSerializable = SHOULD_BE_USE_WEB - ? createSerializableWeb - : createSerializableNative; - function detectCyclicObject(value: unknown, depth: number) { if (depth >= DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD) { // if we reach certain recursion depth we suspect that we are dealing with a cyclic object. @@ -380,11 +362,11 @@ function cloneHostObject(value: T): SerializableRef { return clone; } -function cloneWorklet( - value: T, +function cloneWorklet( + value: TValue, shouldPersistRemote: boolean, depth: number -): SerializableRef { +): SerializableRef { if (__DEV__) { const babelVersion = (value as WorkletFunction).__pluginVersion; if (babelVersion !== undefined && babelVersion !== jsVersion) { @@ -427,7 +409,7 @@ function cloneWorklet( // TODO: Check after refactor if we can remove shouldPersistRemote parameter (imho it's redundant here since worklets are always persistent) // retain all worklets true - ) as SerializableRef; + ) as SerializableRef; serializableMappingCache.set(value, clone); serializableMappingCache.set(clone); @@ -439,23 +421,25 @@ function cloneWorklet( * TurboModuleLike objects are JS objects that have a TurboModule as their * prototype. */ -function cloneTurboModuleLike( - value: T, +function cloneTurboModuleLike( + value: TValue, shouldPersistRemote: boolean, depth: number -): SerializableRef { +): SerializableRef { const proto = Object.getPrototypeOf(value); const clonedProps = cloneObjectProperties(value, shouldPersistRemote, depth); const clone = WorkletsModule.createSerializableTurboModuleLike( clonedProps, proto - ) as SerializableRef; + ) as SerializableRef; return clone; } -function cloneContextObject(value: T): SerializableRef { +function cloneContextObject( + value: TValue +): SerializableRef { const workletContextObjectFactory = (value as Record) - .__workletContextObjectFactory as () => T; + .__workletContextObjectFactory as () => TValue; const handle = cloneInitializer({ __init: () => { 'worklet'; @@ -463,14 +447,14 @@ function cloneContextObject(value: T): SerializableRef { }, }); serializableMappingCache.set(value, handle); - return handle as SerializableRef; + return handle as SerializableRef; } -function clonePlainJSObject( - value: T, +function clonePlainJSObject( + value: TValue, shouldPersistRemote: boolean, depth: number -): SerializableRef { +): SerializableRef { const clonedProps: Record = cloneObjectProperties( value, shouldPersistRemote, @@ -480,7 +464,7 @@ function clonePlainJSObject( clonedProps, shouldPersistRemote, value - ) as SerializableRef; + ) as SerializableRef; serializableMappingCache.set(value, clone); serializableMappingCache.set(clone); @@ -488,9 +472,9 @@ function clonePlainJSObject( return clone; } -function cloneMap>( - value: T -): SerializableRef { +function cloneMap>( + value: TValue +): SerializableRef { const clonedKeys: unknown[] = []; const clonedValues: unknown[] = []; for (const [key, element] of value.entries()) { @@ -500,7 +484,7 @@ function cloneMap>( const clone = WorkletsModule.createSerializableMap( clonedKeys, clonedValues - ) as SerializableRef; + ) as SerializableRef; serializableMappingCache.set(value, clone); serializableMappingCache.set(clone); @@ -508,14 +492,16 @@ function cloneMap>( return clone; } -function cloneSet>(value: T): SerializableRef { +function cloneSet>( + value: TValue +): SerializableRef { const clonedElements: unknown[] = []; for (const element of value) { clonedElements.push(createSerializable(element)); } const clone = WorkletsModule.createSerializableSet( clonedElements - ) as SerializableRef; + ) as SerializableRef; serializableMappingCache.set(value, clone); serializableMappingCache.set(clone); @@ -523,7 +509,9 @@ function cloneSet>(value: T): SerializableRef { return clone; } -function cloneRegExp(value: T): SerializableRef { +function cloneRegExp( + value: TValue +): SerializableRef { const pattern = value.source; const flags = value.flags; const handle = cloneInitializer({ @@ -531,13 +519,15 @@ function cloneRegExp(value: T): SerializableRef { 'worklet'; return new RegExp(pattern, flags); }, - }) as unknown as SerializableRef; + }) as unknown as SerializableRef; serializableMappingCache.set(value, handle); return handle; } -function cloneError(value: T): SerializableRef { +function cloneError( + value: TValue +): SerializableRef { const { name, message, stack } = value; const handle = cloneInitializer({ __init: () => { @@ -551,7 +541,7 @@ function cloneError(value: T): SerializableRef { }, }); serializableMappingCache.set(value, handle); - return handle as unknown as SerializableRef; + return handle as unknown as SerializableRef; } function cloneArrayBuffer( @@ -569,9 +559,9 @@ function cloneArrayBuffer( return clone; } -function cloneArrayBufferView( - value: T -): SerializableRef { +function cloneArrayBufferView( + value: TValue +): SerializableRef { const buffer = value.buffer; const typeName = value.constructor.name; const handle = cloneInitializer({ @@ -586,7 +576,7 @@ function cloneArrayBufferView( } return new constructor(buffer); }, - }) as unknown as SerializableRef; + }) as unknown as SerializableRef; serializableMappingCache.set(value, handle); return handle; @@ -611,7 +601,9 @@ function cloneImport( return clone as SerializableRef; } -function inaccessibleObject(value: T): SerializableRef { +function inaccessibleObject( + value: TValue +): SerializableRef { // This is reached for object types that are not of plain Object.prototype. // We don't support such objects from being transferred as serializables to // the UI runtime and hence we replace them with "inaccessible object" @@ -620,7 +612,7 @@ function inaccessibleObject(value: T): SerializableRef { // as attributes of objects being captured by worklets but should never // be used on the UI runtime regardless. If they are being accessed, the user // will get an appropriate error message. - const clone = createSerializable(INACCESSIBLE_OBJECT as T); + const clone = createSerializable(INACCESSIBLE_OBJECT as TValue); serializableMappingCache.set(value, clone); return clone; } @@ -638,13 +630,13 @@ function getWorkletCode(value: WorkletFunction) { return code; } -type RemoteFunction = { - __remoteFunction: FlatSerializableRef; +type RemoteFunction = { + __remoteFunction: FlatSerializableRef; }; -function isRemoteFunction(value: { +function isRemoteFunction(value: { __remoteFunction?: unknown; -}): value is RemoteFunction { +}): value is RemoteFunction { 'worklet'; return !!value.__remoteFunction; } @@ -663,7 +655,7 @@ function isRemoteFunction(value: { * the UI thread. If the user really wants some objects to be mutable they * should use shared values instead. */ -function freezeObjectInDev(value: T) { +function freezeObjectInDev(value: TValue) { if (!__DEV__) { return; } @@ -688,17 +680,12 @@ function freezeObjectInDev(value: T) { Object.preventExtensions(value); } -function makeShareableCloneOnUIRecursiveLEGACY( - value: T -): FlatSerializableRef { +function makeShareableCloneOnUIRecursiveLEGACY( + value: TValue +): FlatSerializableRef { 'worklet'; - if (SHOULD_BE_USE_WEB) { - // @ts-ignore web is an interesting place where we don't run a secondary VM on the UI thread - // see more details in the comment where USE_STUB_IMPLEMENTATION is defined. - return value; - } // eslint-disable-next-line @typescript-eslint/no-shadow - function cloneRecursive(value: T): FlatSerializableRef { + function cloneRecursive(value: TValue): FlatSerializableRef { if ( (typeof value === 'object' && value !== null) || typeof value === 'function' @@ -708,9 +695,9 @@ function makeShareableCloneOnUIRecursiveLEGACY( // inside SerializableJSRef. return global._createSerializableHostObject( value - ) as FlatSerializableRef; + ) as FlatSerializableRef; } - if (isRemoteFunction(value)) { + if (isRemoteFunction(value)) { // RemoteFunctions are created by us therefore they are // a Serializable out of the box and there is no need to // call `_createSerializableClone`. @@ -719,21 +706,21 @@ function makeShareableCloneOnUIRecursiveLEGACY( if (Array.isArray(value)) { return global._createSerializableArray( value.map(cloneRecursive) - ) as FlatSerializableRef; + ) as FlatSerializableRef; } if ((value as Record).__synchronizableRef) { return global._createSerializableSynchronizable( value - ) as FlatSerializableRef; + ) as FlatSerializableRef; } - const toAdapt: Record> = {}; + const toAdapt: Record> = {}; for (const [key, element] of Object.entries(value)) { toAdapt[key] = cloneRecursive(element); } return global._createSerializable( toAdapt, value - ) as FlatSerializableRef; + ) as FlatSerializableRef; } if (typeof value === 'string') { @@ -772,11 +759,14 @@ export const makeShareableCloneOnUIRecursive = ( : makeShareableCloneOnUIRecursiveLEGACY ) as typeof makeShareableCloneOnUIRecursiveLEGACY; -function makeShareableJS(value: T): T { - return value; -} - -function makeShareableNative(value: T): T { +/** + * This function creates a value on UI with persistent state - changes to it on + * the UI thread will be seen by all worklets. Use it when you want to create a + * value that is read and written only on the UI thread. + * + * @deprecated This function is no longer supported. + */ +export function makeShareable(value: TValue): TValue { if (serializableMappingCache.get(value)) { return value; } @@ -789,13 +779,3 @@ function makeShareableNative(value: T): T { serializableMappingCache.set(value, handle); return value; } - -/** - * This function creates a value on UI with persistent state - changes to it on - * the UI thread will be seen by all worklets. Use it when you want to create a - * value that is read and written only on the UI thread. - */ -/** @deprecated This function is no longer supported. */ -export const makeShareable = SHOULD_BE_USE_WEB - ? makeShareableJS - : makeShareableNative; diff --git a/packages/react-native-worklets/src/serializable.web.ts b/packages/react-native-worklets/src/serializable.web.ts new file mode 100644 index 000000000000..bb767d877e48 --- /dev/null +++ b/packages/react-native-worklets/src/serializable.web.ts @@ -0,0 +1,17 @@ +'use strict'; + +export function isSerializableRef(): boolean { + return true; +} + +export function createSerializable(value: TValue): TValue { + return value; +} + +export function makeShareableCloneOnUIRecursive(value: TValue): TValue { + return value; +} + +export function makeShareable(value: TValue): TValue { + return value; +} diff --git a/packages/react-native-worklets/src/serializableMappingCache.ts b/packages/react-native-worklets/src/serializableMappingCache.ts index 77e65aa0d051..7cc4e1c8d477 100644 --- a/packages/react-native-worklets/src/serializableMappingCache.ts +++ b/packages/react-native-worklets/src/serializableMappingCache.ts @@ -1,5 +1,5 @@ 'use strict'; -import { SHOULD_BE_USE_WEB } from './PlatformChecker'; + import type { SerializableRef } from './workletTypes'; /** @@ -21,22 +21,11 @@ During cloning we use `Object.entries` to iterate over the keys which throws an For convenience we moved this cache to a separate file so it doesn't scare us with red squiggles. */ -const cache = SHOULD_BE_USE_WEB - ? null - : new WeakMap(); +const cache = new WeakMap(); -export const serializableMappingCache = SHOULD_BE_USE_WEB - ? { - set() { - // NOOP - }, - get() { - return null; - }, - } - : { - set(serializable: object, serializableRef?: SerializableRef): void { - cache!.set(serializable, serializableRef || serializableMappingFlag); - }, - get: cache!.get.bind(cache), - }; +export const serializableMappingCache = { + set(serializable: object, serializableRef?: SerializableRef): void { + cache.set(serializable, serializableRef || serializableMappingFlag); + }, + get: cache.get.bind(cache), +}; diff --git a/packages/react-native-worklets/src/serializableMappingCache.web.ts b/packages/react-native-worklets/src/serializableMappingCache.web.ts new file mode 100644 index 000000000000..135f84a70150 --- /dev/null +++ b/packages/react-native-worklets/src/serializableMappingCache.web.ts @@ -0,0 +1,10 @@ +'use strict'; + +export const serializableMappingCache = { + set() { + // NOOP + }, + get() { + return null; + }, +}; diff --git a/packages/react-native-worklets/src/synchronizable.web.ts b/packages/react-native-worklets/src/synchronizable.web.ts new file mode 100644 index 000000000000..d55f9a33ab93 --- /dev/null +++ b/packages/react-native-worklets/src/synchronizable.web.ts @@ -0,0 +1,7 @@ +'use strict'; + +import { WorkletsError } from './WorkletsError'; + +export function createSynchronizable(): never { + throw new WorkletsError('`createSynchronizable` is not supported on web.'); +} diff --git a/packages/react-native-worklets/src/threads.ts b/packages/react-native-worklets/src/threads.ts index 7e16864d8fca..20d40921f3db 100644 --- a/packages/react-native-worklets/src/threads.ts +++ b/packages/react-native-worklets/src/threads.ts @@ -1,5 +1,5 @@ 'use strict'; -import { IS_JEST, SHOULD_BE_USE_WEB } from './PlatformChecker'; + import { RuntimeKind } from './runtimeKind'; import { createSerializable, @@ -52,11 +52,7 @@ function callMicrotasksOnUIThread() { global.__callMicrotasks(); } -export const callMicrotasks = SHOULD_BE_USE_WEB - ? () => { - // on web flushing is a noop as immediates are handled by the browser - } - : callMicrotasksOnUIThread; +export const callMicrotasks = callMicrotasksOnUIThread; /** * Lets you schedule a function to be executed on the [UI @@ -114,31 +110,12 @@ export function runOnUI( ): (...args: Args) => void { if ( __DEV__ && - !SHOULD_BE_USE_WEB && !isWorkletFunction(worklet) && !(worklet as unknown as WorkletImport).__bundleData ) { throw new WorkletsError('`runOnUI` can only be used with worklets.'); } return (...args) => { - if (IS_JEST) { - // Mocking time in Jest is tricky as both requestAnimationFrame and queueMicrotask - // callbacks run on the same queue and can be interleaved. There is no way - // to flush particular queue in Jest and the only control over mocked timers - // is by using jest.advanceTimersByTime() method which advances all types - // of timers including immediate and animation callbacks. Ideally we'd like - // to have some way here to schedule work along with React updates, but - // that's not possible, and hence in Jest environment instead of using scheduling - // mechanism we just schedule the work ommiting the queue. This is ok for the - // uses that we currently have but may not be ok for future tests that we write. - WorkletsModule.scheduleOnUI( - createSerializable(() => { - 'worklet'; - worklet(...args); - }) - ); - return; - } if (__DEV__) { // in DEV mode we call serializable conversion here because in case the object // can't be converted, we will get a meaningful stack-trace as opposed to the @@ -153,7 +130,7 @@ export function runOnUI( }; } -if (__DEV__ && !SHOULD_BE_USE_WEB) { +if (__DEV__) { function runOnUIWorklet(): void { 'worklet'; throw new WorkletsError( @@ -254,10 +231,7 @@ export function runOnJS( ): (...args: Args) => void { 'worklet'; type FunDevRemote = Extract>; - if ( - SHOULD_BE_USE_WEB || - globalThis.__RUNTIME_KIND === RuntimeKind.ReactNative - ) { + if (globalThis.__RUNTIME_KIND === RuntimeKind.ReactNative) { // if we are already on the JS thread, we just schedule the worklet on the JS queue return (...args) => queueMicrotask( @@ -354,29 +328,11 @@ export function scheduleOnRN( export function runOnUIAsync( worklet: (...args: Args) => ReturnValue ): (...args: Args) => Promise { - if (__DEV__ && !SHOULD_BE_USE_WEB && !isWorkletFunction(worklet)) { + if (__DEV__ && !isWorkletFunction(worklet)) { throw new WorkletsError('`runOnUIAsync` can only be used with worklets.'); } return (...args: Args) => { return new Promise((resolve) => { - if (IS_JEST) { - // Mocking time in Jest is tricky as both requestAnimationFrame and queueMicrotask - // callbacks run on the same queue and can be interleaved. There is no way - // to flush particular queue in Jest and the only control over mocked timers - // is by using jest.advanceTimersByTime() method which advances all types - // of timers including immediate and animation callbacks. Ideally we'd like - // to have some way here to schedule work along with React updates, but - // that's not possible, and hence in Jest environment instead of using scheduling - // mechanism we just schedule the work ommiting the queue. This is ok for the - // uses that we currently have but may not be ok for future tests that we write. - WorkletsModule.scheduleOnUI( - createSerializable(() => { - 'worklet'; - worklet(...args); - }) - ); - return; - } if (__DEV__) { // in DEV mode we call serializable conversion here because in case the object // can't be converted, we will get a meaningful stack-trace as opposed to the @@ -392,7 +348,7 @@ export function runOnUIAsync( }; } -if (__DEV__ && !SHOULD_BE_USE_WEB) { +if (__DEV__) { function runOnUIAsyncWorklet(): void { 'worklet'; throw new WorkletsError( diff --git a/packages/react-native-worklets/src/threads.web.ts b/packages/react-native-worklets/src/threads.web.ts new file mode 100644 index 000000000000..17d213c6527a --- /dev/null +++ b/packages/react-native-worklets/src/threads.web.ts @@ -0,0 +1,103 @@ +'use strict'; + +import { mockedRequestAnimationFrame } from './runLoop/uiRuntime/mockedRequestAnimationFrame'; +import { WorkletsError } from './WorkletsError'; + +export function callMicrotasks(): void { + // on web flushing is a noop as immediates are handled by the browser +} + +export function scheduleOnUI( + worklet: (...args: Args) => ReturnValue, + ...args: Args +): void { + runOnUI(worklet)(...args); +} + +export function runOnUI( + worklet: (...args: Args) => ReturnValue +): (...args: Args) => void { + return (...args) => { + enqueueUI(worklet, args); + }; +} + +export function runOnUISync(): never { + throw new WorkletsError('`runOnUISync` is not supported on web.'); +} + +export function executeOnUIRuntimeSync(): never { + throw new WorkletsError('`executeOnUIRuntimeSync` is not supported on web.'); +} + +export function runOnJS( + fun: (...args: Args) => ReturnValue +): (...args: Args) => void { + return (...args) => + queueMicrotask( + args.length + ? () => (fun as (...args: Args) => ReturnValue)(...args) + : (fun as () => ReturnValue) + ); +} + +export function scheduleOnRN( + fun: (...args: Args) => ReturnValue, + ...args: Args +): void { + runOnJS(fun)(...args); +} + +export function runOnUIAsync( + worklet: (...args: Args) => ReturnValue +): (...args: Args) => Promise { + return (...args: Args) => { + return new Promise((resolve) => { + enqueueUI(worklet, args, resolve); + }); + }; +} + +type UIJob = [ + worklet: (...args: Args) => ReturnValue, + args: Args, + resolve?: (value: ReturnValue) => void, +]; + +let runOnUIQueue: UIJob[] = []; + +function enqueueUI( + worklet: (...args: Args) => ReturnValue, + args: Args, + resolve?: (value: ReturnValue) => void +): void { + const job = [worklet, args, resolve] as UIJob; + runOnUIQueue.push(job as unknown as UIJob); + if (runOnUIQueue.length === 1) { + flushUIQueue(); + } +} + +function flushUIQueue(): void { + queueMicrotask(() => { + const queue = runOnUIQueue; + runOnUIQueue = []; + requestAnimationFrameImpl(() => { + queue.forEach(([workletFunction, workletArgs, jobResolve]) => { + const result = workletFunction(...workletArgs); + if (jobResolve) { + jobResolve(result); + } + }); + }); + }); +} + +// eslint-disable-next-line camelcase +export function unstable_eventLoopTask(): never { + throw new WorkletsError('`unstable_eventLoopTask` is not supported on web.'); +} + +const requestAnimationFrameImpl = !globalThis.requestAnimationFrame + ? mockedRequestAnimationFrame + : globalThis.requestAnimationFrame; diff --git a/packages/react-native-worklets/src/workletRuntimeEntry.ts b/packages/react-native-worklets/src/workletRuntimeEntry.ts index e9a481183c88..280c88282996 100644 --- a/packages/react-native-worklets/src/workletRuntimeEntry.ts +++ b/packages/react-native-worklets/src/workletRuntimeEntry.ts @@ -1,7 +1,6 @@ 'use strict'; import { init } from './initializers'; -import { SHOULD_BE_USE_WEB } from './PlatformChecker'; import { RuntimeKind } from './runtimeKind'; import { WorkletsError } from './WorkletsError'; @@ -18,10 +17,6 @@ import { WorkletsError } from './WorkletsError'; * `_WORKLETS_BUNDLE_MODE` flag. */ export function bundleModeInit() { - if (SHOULD_BE_USE_WEB) { - return; - } - globalThis._WORKLETS_BUNDLE_MODE = true; const runtimeKind = globalThis.__RUNTIME_KIND; diff --git a/packages/react-native-worklets/src/workletRuntimeEntry.web.ts b/packages/react-native-worklets/src/workletRuntimeEntry.web.ts new file mode 100644 index 000000000000..1fd651dae83b --- /dev/null +++ b/packages/react-native-worklets/src/workletRuntimeEntry.web.ts @@ -0,0 +1,5 @@ +'use strict'; + +export function bundleModeInit() { + // no-op +}