Skip to content

Commit 9877a61

Browse files
authored
feat(Worklets): docs for registerCustomSerializable (#8668)
## Summary Requires - #8640 - #8634 ## Test plan <img width="1575" height="3913" alt="image" src="https://github.com/user-attachments/assets/1a151fb2-a137-42b9-a4a1-c87d4e731979" />
1 parent e4f199f commit 9877a61

File tree

12 files changed

+1463
-114
lines changed

12 files changed

+1463
-114
lines changed

docs/docs-reanimated/docs/guides/feature-flags.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
126126

127127
### `ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS`
128128

129-
When enabled, non-layout styles will be applied using the `synchronouslyUpdateViewOnUIThread` method (which doesn't involve layout recalculation) instead of than `ShadowTree::commit` method (which requires layout recalculation). In an artifical benchmark, it can lead to up to 4x increase of frames per second. Even though we don't expect such high speedups in the production apps, there should be a visible improvements in the smoothness of some animations. However, there are some unwanted side effects that one needs to take into account and properly compensate for:
129+
When enabled, non-layout styles will be applied using the `synchronouslyUpdateViewOnUIThread` method (which doesn't involve layout recalculation) instead of than `ShadowTree::commit` method (which requires layout recalculation). In an artificial benchmark, it can lead to up to 4x increase of frames per second. Even though we don't expect such high speedups in the production apps, there should be a visible improvements in the smoothness of some animations. However, there are some unwanted side effects that one needs to take into account and properly compensate for:
130130

131131
1. The changes applied via `synchronouslyUpdateViewOnUIThread` are not respected by the touch gesture system of Fabric renderer which can lead to incorrect behavior, in particular if transforms are applied. In that case, it's advisable to use `Pressable` component from `react-native-gesture-handler` (which attaches to the underlying platform view rather than using `ShadowTree` to determine the component present at given point) rather than its original counterpart from `react-native`.
132132

docs/docs-reanimated/docusaurus.config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ const config = {
3434
hooks: {
3535
onBrokenMarkdownLinks: 'throw',
3636
},
37+
mermaid: true,
3738
},
3839

40+
themes: ['@docusaurus/theme-mermaid'],
41+
3942
onBrokenLinks: 'throw',
4043
onBrokenAnchors: 'throw',
4144

@@ -136,7 +139,7 @@ const config = {
136139
'All trademarks and copyrights belong to their respective owners.',
137140
},
138141
prism: {
139-
additionalLanguages: ['bash', 'diff', 'json'],
142+
additionalLanguages: ['bash', 'diff', 'json', 'mermaid'],
140143
theme: lightCodeTheme,
141144
darkTheme: darkCodeTheme,
142145
},

docs/docs-reanimated/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@docusaurus/core": "3.9.1",
2929
"@docusaurus/plugin-debug": "3.9.1",
3030
"@docusaurus/preset-classic": "3.9.1",
31+
"@docusaurus/theme-mermaid": "3.9.1",
3132
"@emotion/react": "11.14.0",
3233
"@emotion/styled": "11.14.0",
3334
"@mdx-js/react": "3.0.0",
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
---
2+
title: registerCustomSerializable
3+
sidebar_position: 42 # Use alphabetical order
4+
---
5+
6+
# registerCustomSerializable <AvailableFrom version="0.7.0" />
7+
8+
`registerCustomSerializable` lets you register your own pre-serialization and post-deserialization logic. This is necessary for objects with prototypes different than just `Object.prototype` or some other built-in prototypes like `Map` etc. Worklets can't handle such objects by default to convert into [Serializables](/docs/memory/serializable) hence you need to register them as **Custom Serializables**. This way you can tell Worklets how to transfer your custom data structures between different Runtimes without manually serializing and deserializing them every time.
9+
10+
List of supported types for Serialization can be found [here](/docs/memory/serializable#supported-types).
11+
12+
## Reference
13+
14+
```typescript
15+
const obj = { a: 42 };
16+
Object.setPrototypeOf(obj, console); // because why not
17+
18+
type ConsoleLike = typeof console;
19+
20+
registerCustomSerializable({
21+
name: 'ConsoleLike',
22+
determine(value: object): value is ConsoleLike {
23+
'worklet';
24+
return Object.getPrototypeOf(value) === console;
25+
},
26+
pack(value: ConsoleLike) {
27+
'worklet';
28+
return { a: value.a }; // transfer data
29+
},
30+
unpack(value: object) {
31+
'worklet';
32+
// recreate object with prototype
33+
return Object.create(console, value);
34+
},
35+
});
36+
```
37+
38+
<details>
39+
<summary>Type definitions</summary>
40+
41+
```typescript
42+
function registerCustomSerializable<
43+
TValue extends object,
44+
TPacked extends object,
45+
>(registrationData: RegistrationData<TValue, TPacked>): void;
46+
47+
type RegistrationData<TValue extends object, TPacked = unknown> = {
48+
name: string;
49+
determine: (value: object) => value is TValue;
50+
pack: (value: TValue) => TPacked;
51+
unpack: (value: TPacked) => TValue;
52+
};
53+
```
54+
55+
</details>
56+
57+
## Arguments
58+
59+
### registrationData
60+
61+
An object containing the registration data for the Custom Serializable.
62+
63+
Available properties:
64+
65+
| Name | Type | Description |
66+
| --------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
67+
| name | `string` | A unique name for the Custom Serializable. It's used to prevent duplicate registrations of the same Custom Serializable. You will get warned if you attempt to register a Custom Serializable with a name that has already been used. |
68+
| determine | `(value: object) => value is TValue` | A [worklet](/docs/fundamentals/glossary#worklet) that checks whether a given JavaScript value is of the type handled by this Custom Serializable. |
69+
| pack | `(value: TValue) => TPacked` | A [worklet](/docs/fundamentals/glossary#worklet) that packs the JavaScript value of type `TValue` into a value that can be serialized by default as [Serializable](/docs/memory/serializable). The function must return a [supported type for Serialization](/docs/memory/serializable#supported-types). |
70+
| unpack | `(value: TPacked) => TValue` | A [worklet](/docs/fundamentals/glossary#worklet) that unpacks the packed value, after it's been deserialized from it's packed form, back into the JavaScript value of type `TValue`. |
71+
72+
## Motivation
73+
74+
Custom prototypes are bound to a single Runtime and don't exist in other Runtimes. Due to that it's impossible to transfer them between runtimes directly. This is why a pre-serialization (packing) and pre-deserialization (unpacking) logic is required.
75+
76+
Consider the following examples:
77+
78+
<table>
79+
<tr style={{padding: '20px'}}>
80+
<td>
81+
82+
```typescript
83+
const obj = {};
84+
85+
// because why not
86+
Object.setPrototypeOf(obj, console);
87+
88+
scheduleOnUI(() => {
89+
// This will throw because `obj`
90+
// had a custom prototype and
91+
// it couldn't be serialized.
92+
obj.log('Hello!');
93+
});
94+
```
95+
96+
</td>
97+
<td style={{textAlign: 'center', flex: 1}}>
98+
99+
```mermaid
100+
flowchart TD
101+
A[obj] -->|automatic serialization| B[no known serialization method, serialized as a dummy value]
102+
B -->|scheduleOnUI| C[transferred to UI Runtime]
103+
C -->|automatic deserialization| D[deserialized from a dummy value]
104+
D -->|"obj.log('Hello!')"| E["<b>Error: serialization failed</b>"]
105+
style E fill:#ff000022,stroke:#990000
106+
```
107+
108+
</td>
109+
</tr>
110+
<tr>
111+
<td>
112+
113+
```typescript
114+
const obj = {};
115+
116+
// because why not
117+
Object.setPrototypeOf(obj, console);
118+
119+
type ConsoleLike = typeof console;
120+
121+
registerCustomSerializable({
122+
name: 'ConsoleLike',
123+
determine(value: object): value is ConsoleLike {
124+
'worklet';
125+
return Object.getPrototypeOf(value) === console;
126+
},
127+
pack(value: ConsoleLike) {
128+
'worklet';
129+
// We don't need to transfer any data,
130+
// so we can pack it to an empty object.
131+
return {};
132+
},
133+
unpack(value: object) {
134+
'worklet';
135+
// We can recreate the original object.
136+
return Object.create(console);
137+
},
138+
});
139+
140+
scheduleOnUI(() => {
141+
obj.log('Hello!'); // prints 'Hello!'
142+
});
143+
```
144+
145+
</td>
146+
<td style={{textAlign: 'center', flex: 1}}>
147+
148+
```mermaid
149+
flowchart TD
150+
A[obj] -->|automatic serialization| B[identified as a Custom Serializable '<i>ConsoleLike</i>']
151+
subgraph S1 [ ]
152+
B -->|pack| C[packed with Custom Serializable's pack method]
153+
end
154+
C -->|actual serialization| D[serialized packed object as a default supported type]
155+
D -->|scheduleOnUI| E[transferred to Runtime B]
156+
E -->|automatic deserialization| F[deserialized to the packed object]
157+
F -->|unpack| G
158+
subgraph S2 [ ]
159+
G[unpacked with Custom Serializable's unpack method]
160+
end
161+
G -->|"obj.log('Hello!')"| H["<b>'Hello!'</b> printed to console"]
162+
style H fill:#00ff0022,stroke:#009900
163+
```
164+
165+
</td>
166+
</tr>
167+
</table>
168+
169+
## Remarks
170+
171+
- To use Custom Serializables which require `new` keyword for instantiation, you need to [disable Worklet Classes](/docs/worklets-babel-plugin/plugin-options#disableworkletclasses-) option in Worklets Babel plugin configuration.
172+
- Custom Serializables are global and shared between all [Worklet Runtimes](/docs/fundamentals/runtimeKinds#worklet-runtime). Once you register a Custom Serializable, it will be available in all Runtimes.
173+
- You can use `registerCustomSerializable` only on the [RN Runtime](/docs/fundamentals/runtimeKinds#rn-runtime).

docs/docs-worklets/docs/memory/serializable.mdx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ Serializable is a type of shared memory that holds an immutable value that can b
1515
Serializable memory model
1616
</figcaption>
1717

18-
## Supported types
19-
20-
<SerializableSupportedTypesTable />
21-
2218
<details>
2319
<summary>Type definitions</summary>
2420

@@ -31,6 +27,12 @@ type SerializableRef<TValue = unknown> = {
3127

3228
</details>
3329

30+
## Supported types
31+
32+
<SerializableSupportedTypesTable />
33+
34+
If you want to transfer objects with custom prototypes across Runtimes, you can use [registerCustomSerializable](/docs/memory/registerCustomSerializable) to define your own serialization and deserialization logic.
35+
3436
## Remarks
3537

3638
- The JavaScript value of a Serializable is a reference wrapper that only holds the C++ side in its `jsi::NativeState`.

docs/docs-worklets/docusaurus.config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ const config = {
2929
hooks: {
3030
onBrokenMarkdownLinks: 'throw',
3131
},
32+
mermaid: true,
3233
},
3334

35+
themes: ['@docusaurus/theme-mermaid'],
36+
3437
onBrokenLinks: 'throw',
3538
onBrokenAnchors: 'throw',
3639

@@ -106,7 +109,7 @@ const config = {
106109
'All trademarks and copyrights belong to their respective owners.',
107110
},
108111
prism: {
109-
additionalLanguages: ['bash', 'diff', 'json'],
112+
additionalLanguages: ['bash', 'diff', 'json', 'mermaid'],
110113
theme: lightCodeTheme,
111114
darkTheme: darkCodeTheme,
112115
},

docs/docs-worklets/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@docusaurus/core": "3.9.1",
2929
"@docusaurus/plugin-debug": "3.9.1",
3030
"@docusaurus/preset-classic": "3.9.1",
31+
"@docusaurus/theme-mermaid": "3.9.1",
3132
"@emotion/react": "11.14.0",
3233
"@emotion/styled": "11.14.0",
3334
"@mdx-js/react": "3.0.0",

packages/react-native-worklets/src/WorkletsModule/NativeWorklets.native.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ See https://docs.swmansion.com/react-native-worklets/docs/guides/troubleshooting
153153
}
154154

155155
createCustomSerializable(
156-
data: SerializableRef<object>,
156+
data: SerializableRef<unknown>,
157157
typeId: number
158-
): SerializableRef<object> {
158+
): SerializableRef<unknown> {
159159
return this.#workletsModuleProxy.createCustomSerializable(data, typeId);
160160
}
161161

packages/react-native-worklets/src/WorkletsModule/workletsModuleProxy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ export interface WorkletsModuleProxy {
7070
): SerializableRef<object>;
7171

7272
createCustomSerializable(
73-
data: SerializableRef<object>,
73+
data: SerializableRef<unknown>,
7474
typeId: number
75-
): SerializableRef<object>;
75+
): SerializableRef<unknown>;
7676

7777
registerCustomSerializable(
7878
determine: SerializableRef<object>,

packages/react-native-worklets/src/memory/serializable.native.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,20 @@ if (!globalThis.__customSerializationRegistry) {
252252
}
253253
const customSerializationRegistry = globalThis.__customSerializationRegistry;
254254

255+
/**
256+
* `registerCustomSerializable` lets you register your own pre-serialization and
257+
* post-deserialization logic. This is necessary for objects with prototypes
258+
* different than just `Object.prototype` or some other built-in prototypes like
259+
* `Map` etc. Worklets can't handle such objects by default to convert into
260+
* [Serializables](https://docs.swmansion.com/react-native-worklets/docs/memory/serializable)
261+
* hence you need to register them as **Custom Serializables**. This way you can
262+
* tell Worklets how to transfer your custom data structures between different
263+
* Runtimes without manually serializing and deserializing them every time.
264+
*
265+
* @param registrationData - The registration data for the custom serializable -
266+
* {@link RegistrationData}
267+
* @see https://docs.swmansion.com/react-native-worklets/docs/memory/registerCustomSerializable/
268+
*/
255269
export function registerCustomSerializable<
256270
TValue extends object,
257271
TPacked extends object,
@@ -263,6 +277,10 @@ export function registerCustomSerializable<
263277
}
264278

265279
const { name, determine, pack, unpack } = registrationData;
280+
281+
if (__DEV__) {
282+
verifyRegistrationData(determine, pack, unpack);
283+
}
266284
if (customSerializationRegistry.some((data) => data.name === name)) {
267285
if (__DEV__) {
268286
console.warn(
@@ -273,7 +291,7 @@ export function registerCustomSerializable<
273291
}
274292

275293
customSerializationRegistry.push(
276-
registrationData as unknown as SerializationData<object, object>
294+
registrationData as unknown as SerializationData<object, unknown>
277295
);
278296

279297
WorkletsModule.registerCustomSerializable(
@@ -284,6 +302,28 @@ export function registerCustomSerializable<
284302
);
285303
}
286304

305+
function verifyRegistrationData(
306+
determine: unknown,
307+
pack: unknown,
308+
unpack: unknown
309+
) {
310+
if (!isWorkletFunction(determine)) {
311+
throw new WorkletsError(
312+
'The "determine" function provided to registerCustomSerializable must be a worklet.'
313+
);
314+
}
315+
if (!isWorkletFunction(pack)) {
316+
throw new WorkletsError(
317+
'The "pack" function provided to registerCustomSerializable must be a worklet.'
318+
);
319+
}
320+
if (!isWorkletFunction(unpack)) {
321+
throw new WorkletsError(
322+
'The "unpack" function provided to registerCustomSerializable must be a worklet.'
323+
);
324+
}
325+
}
326+
287327
function detectCyclicObject(value: unknown, depth: number) {
288328
if (depth >= DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD) {
289329
// if we reach certain recursion depth we suspect that we are dealing with a cyclic object.
@@ -649,7 +689,7 @@ function cloneImport<TValue extends WorkletImport>(
649689
return clone as SerializableRef<TValue>;
650690
}
651691

652-
function cloneCustom<TValue extends object, TPacked extends object>(
692+
function cloneCustom<TValue extends object, TPacked = unknown>(
653693
data: TValue,
654694
pack: (data: TValue) => TPacked,
655695
typeId: number

0 commit comments

Comments
 (0)