Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,6 @@ test('passes the Worklets runtime holder to native during installation', () => {
expect(globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER).toBeUndefined();
});

test('installs legacy bindings when Reanimated is available without Worklets', () => {
const installUIRuntimeBindings = jest.fn(() => true);

loadReanimatedWrapper(
{ reanimated: { useSharedValue: true } },
installUIRuntimeBindings
);

expect(installUIRuntimeBindings).toHaveBeenCalledTimes(1);
expect(globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER).toBeUndefined();
});

test('does not install bindings when no runtime provider is available', () => {
const installUIRuntimeBindings = jest.fn(() => true);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ghQueueMicrotask } from '../../ghQueueMicrotask';
import { tagMessage } from '../../utils';
import { NativeProxy } from '../../v3/NativeProxy';

export function installUIRuntimeBindings(
getUIRuntimeHolder: (() => object) | undefined
) {
ghQueueMicrotask(() => {
globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER = getUIRuntimeHolder?.();
Comment thread
tjzel marked this conversation as resolved.

try {
const decorated = NativeProxy.installUIRuntimeBindings();

if (!decorated) {
console.warn(
tagMessage(
'Failed to install UI runtime bindings. Please report this at https://github.com/software-mansion/react-native-gesture-handler/issues.'
)
);
}
} finally {
globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER = undefined;
}
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function installUIRuntimeBindings(
_getUIRuntimeHolder: (() => object) | undefined
) {
// noop
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import type { ComponentClass, ComponentType } from 'react';

import { ghQueueMicrotask } from '../../ghQueueMicrotask';
import { tagMessage } from '../../utils';
import { NativeProxy } from '../../v3/NativeProxy';
import type {
GestureCallbacks,
GestureUpdateEventWithHandlerData,
SharedValue,
} from '../../v3/types';
import { installUIRuntimeBindings } from './installUIRuntimeBindings';

export type ReanimatedContext<THandlerData> = {
lastUpdateEvent: GestureUpdateEventWithHandlerData<THandlerData> | undefined;
Expand All @@ -32,8 +31,17 @@ export type ReanimatedHandler<THandlerData> = {
context: ReanimatedContext<THandlerData>;
};

type WorkletsModule = {
export type SimplifiedShareableHost<TValue = unknown> = {
value: TValue;
};

type WorkletsPackage = {
getUIRuntimeHolder?: () => object;
createShareable?: <TValue>(
hostRuntimeId: number,
initial: TValue
) => SimplifiedShareableHost<TValue>;
UIRuntimeId?: number;
Comment thread
tjzel marked this conversation as resolved.
};

export type NativeEventsManager = new (component: {
Expand All @@ -49,59 +57,56 @@ export type NativeEventsManager = new (component: {
updateEvents: (prevProps: Record<string, unknown>) => void;
};

let Reanimated:
| {
default: {
// Slightly modified definition copied from 'react-native-reanimated'
createAnimatedComponent<P extends object>(
component: ComponentType<P>,
options?: unknown
): ComponentClass<P>;
};
NativeEventsManager: NativeEventsManager;
useHandler: <THandlerData, TExtendedHandlerData extends THandlerData>(
handlers: GestureCallbacks<THandlerData, TExtendedHandlerData>
) => ReanimatedHandler<TExtendedHandlerData>;
useEvent: <T>(
callback: (event: T) => void,
events: string[],
rebuild: boolean
) => (event: unknown) => void;
useSharedValue: <T>(value: T) => SharedValue<T>;
setGestureState: (handlerTag: number, newState: number) => void;
isSharedValue: <T = unknown>(value: unknown) => value is SharedValue<T>;
isWorkletFunction<
Args extends unknown[] = unknown[],
ReturnValue = unknown,
>(
value: unknown
): value is WorkletFunction<Args, ReturnValue>;
useComposedEventHandler<T>(
handlers: (((event: T) => void) | null)[]
): (event: T) => void;
// TODO: runOnJS and runOnUI are deprecated. These should be removed in near future.
runOnJS: <A extends unknown[], R>(
fn: (...args: A) => R
) => (...args: Parameters<typeof fn>) => void;
runOnUI<A extends any[], R>(
fn: (...args: A) => R
): (...args: Parameters<typeof fn>) => void;
makeMutable<T>(value: T): { value: T };
}
| undefined;

let uiRuntimeHolder: object | undefined;
type ReanimatedPackage = {
default: {
// Slightly modified definition copied from 'react-native-reanimated'
createAnimatedComponent<P extends object>(
component: ComponentType<P>,
options?: unknown
): ComponentClass<P>;
};
NativeEventsManager: NativeEventsManager;
useHandler: <THandlerData, TExtendedHandlerData extends THandlerData>(
handlers: GestureCallbacks<THandlerData, TExtendedHandlerData>
) => ReanimatedHandler<TExtendedHandlerData>;
useEvent: <T>(
callback: (event: T) => void,
events: string[],
rebuild: boolean
) => (event: unknown) => void;
useSharedValue: <T>(value: T) => SharedValue<T>;
setGestureState: (handlerTag: number, newState: number) => void;
isSharedValue: <T = unknown>(value: unknown) => value is SharedValue<T>;
isWorkletFunction<Args extends unknown[] = unknown[], ReturnValue = unknown>(
value: unknown
): value is WorkletFunction<Args, ReturnValue>;
useComposedEventHandler<T>(
handlers: (((event: T) => void) | null)[]
): (event: T) => void;
// TODO: runOnJS and runOnUI are deprecated. These should be removed in near future.
runOnJS: <A extends unknown[], R>(
fn: (...args: A) => R
) => (...args: Parameters<typeof fn>) => void;
runOnUI<A extends any[], R>(
fn: (...args: A) => R
): (...args: Parameters<typeof fn>) => void;
makeMutable<T>(value: T): { value: T };
};

let Reanimated: ReanimatedPackage | undefined;
let Worklets: WorkletsPackage | undefined;

try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Worklets = require('react-native-worklets') as WorkletsModule;
uiRuntimeHolder = Worklets?.getUIRuntimeHolder?.();
Worklets = require('react-native-worklets') as WorkletsPackage;
} catch (e) {
// When 'react-native-worklets' is not available we want to quietly continue
Worklets = undefined;
}

try {
Reanimated = require('react-native-reanimated');
// eslint-disable-next-line @typescript-eslint/no-var-requires
Reanimated = require('react-native-reanimated') as ReanimatedPackage;
} catch (e) {
// When 'react-native-reanimated' is not available we want to quietly continue
// @ts-ignore TS demands the variable to be initialized
Expand All @@ -114,25 +119,8 @@ if (!Reanimated?.useSharedValue) {
Reanimated = undefined;
}

if (uiRuntimeHolder !== undefined || Reanimated !== undefined) {
ghQueueMicrotask(() => {
globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER = uiRuntimeHolder;

try {
const decorated = NativeProxy.installUIRuntimeBindings();

if (!decorated) {
console.warn(
tagMessage(
'Failed to install UI runtime bindings. Please report this at https://github.com/software-mansion/react-native-gesture-handler/issues.'
)
);
}
} finally {
globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER = undefined;
uiRuntimeHolder = undefined;
}
});
if (Worklets !== undefined) {
installUIRuntimeBindings(Worklets.getUIRuntimeHolder);
}

if (Reanimated !== undefined && !Reanimated.setGestureState) {
Expand All @@ -147,4 +135,4 @@ if (Reanimated !== undefined && !Reanimated.setGestureState) {
};
}

export { Reanimated };
export { Reanimated, Worklets };
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
Reanimated,
Worklets,
} from '../../../handlers/gestures/reanimatedWrapper';

export type LastUpdateEventMap = Map<number, { lastUpdateEvent: unknown }>;

export function createLastUpdateEventMap() {
if (
Worklets?.createShareable === undefined ||
Worklets.UIRuntimeId === undefined ||
Reanimated === undefined
) {
return undefined;
Comment thread
tjzel marked this conversation as resolved.
}

return Worklets.createShareable<LastUpdateEventMap>(
Worklets.UIRuntimeId,
new Map()
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { SimplifiedShareableHost } from '../../../handlers/gestures/reanimatedWrapper';

export type LastUpdateEventMap = Map<number, { lastUpdateEvent: unknown }>;

export function createLastUpdateEventMap(): SimplifiedShareableHost<LastUpdateEventMap> {
return { value: new Map() };
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
UnpackedGestureHandlerEventWithHandlerData,
} from '../../types';
import { eventHandler } from './eventHandler';
import { createLastUpdateEventMap } from './lastUpdateEventMap';

const REANIMATED_EVENT_NAMES = [
'onGestureHandlerReanimatedEvent',
Expand All @@ -24,27 +25,25 @@ const workletNOOP = () => {
// no-op
};

function createLastUpdateEventMap() {
return Reanimated?.makeMutable(new Map<number, ReanimatedContext<unknown>>());
}

// Created lazily instead of at module scope so importing this module doesn't
// call into Reanimated during module evaluation.
// call into Worklets during module evaluation.
let lastUpdateEventMap: ReturnType<typeof createLastUpdateEventMap>;

function getLastUpdateEventMap() {
lastUpdateEventMap ??= createLastUpdateEventMap();
return lastUpdateEventMap;
}

type ShareableLastUpdateEventMap = ReturnType<typeof createLastUpdateEventMap>;

// Takes the map as an argument on purpose: reading the lazy `let` from this
// module-scope worklet would snapshot its value at module evaluation — before
// the first `getLastUpdateEventMap()` call — so the UI-runtime copy would stay
// `undefined` forever and the cleanup would silently never run. `runOnUI`
// arguments are serialized fresh on every call, so they always carry the
// initialized map.
function deleteHandlerEventEntry(
map: ReturnType<typeof createLastUpdateEventMap>,
map: ShareableLastUpdateEventMap,
handlerTag: number
) {
'worklet';
Expand Down Expand Up @@ -91,7 +90,7 @@ export function useReanimatedEventHandler<
>
) => {
'worklet';
// Undefined only when Reanimated is absent — and then this callback is
// Undefined only when Worklets is absent — and then this callback is
// never registered (`Reanimated?.useEvent` below short-circuits).
if (updateEventMap === undefined) {
return;
Expand Down
Loading