Skip to content

feat: add MainThreadValue#2144

Open
f0rdream wants to merge 1 commit intolynx-family:mainfrom
f0rdream:main_thread_value
Open

feat: add MainThreadValue#2144
f0rdream wants to merge 1 commit intolynx-family:mainfrom
f0rdream:main_thread_value

Conversation

@f0rdream
Copy link
Copy Markdown
Collaborator

@f0rdream f0rdream commented Jan 27, 2026

Summary by CodeRabbit

  • New Features

    • Added MotionValue (main-thread persistent value) and a useMotionValue hook for components
    • Added MainThreadValue base class and registration support for custom main-thread value types
    • Added workletCapture helper to normalize/capture objects for worklet transforms
    • Public API surface expanded with additional re-exports for worklet/runtime helpers
  • Tests

    • Improved test setup and mocks to better simulate runtime core context and value hydration

✏️ Tip: You can customize this high-level summary in your review settings.

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).
  • Changeset added, and when a BREAKING CHANGE occurs, it needs to be clearly marked (or not required).

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Jan 27, 2026

🦋 Changeset detected

Latest commit: 90a9cc3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@lynx-js/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

Adds a MainThreadValue framework for serializable main-thread-persistent values, a workletCapture helper, SWC transform integration to emit captures, MotionValue example and hook, and hydration/registration plumbing plus tests and exports to support custom main-thread value types.

Changes

Cohort / File(s) Summary
Core MainThreadValue & Ref
packages/react/runtime/src/worklet/ref/mainThreadValue.ts, packages/react/worklet-runtime/src/Ref.ts, packages/react/worklet-runtime/src/workletRef.ts
New abstract MainThreadValue<T> with serialization/registration, MainThreadRef refactored to extend it, registry APIs (registerMainThreadValueClass, getMainThreadValueClassMap) and hydration helpers added.
Hydration & Runtime Integration
packages/react/worklet-runtime/src/hydrate.ts, packages/react/worklet-runtime/src/workletRuntime.ts, packages/react/worklet-runtime/src/workletRef.ts
Hydration now checks for current accessor shape and uses registry to instantiate registered classes; worklet runtime wires class map, event-based release handling, and transforms to hydrate persisted values.
Capture Helper & SWC Transform
packages/react/runtime/src/worklet/capture.ts, packages/react/transform/crates/swc_plugin_worklet/gen_stmt.rs, packages/react/transform/crates/swc_plugin_worklet/tests/__swc_snapshots__/*
New workletCapture(obj,...args) helper; SWC transform imports helper and wraps nested object literals into workletCapture(...) calls; many snapshot updates to reflect generated imports/uses.
Examples: MotionValue & App
examples/react/src/MotionValue.ts, examples/react/src/App.tsx
Adds MotionValue<T> extending MainThreadValue, useMotionValue hook, subscribe API, and example usage in App with main-thread bindings and subscriptions.
Exports & Bindings
packages/react/runtime/src/index.ts, packages/react/runtime/src/internal.ts, packages/react/runtime/src/lynx-api.ts, packages/react/worklet-runtime/src/index.ts, packages/react/worklet-runtime/src/bindings/index.ts
Re-exports added for workletCapture, MainThreadValue, registry APIs and testing helpers to public/internal surfaces.
Types & Globals
packages/react/worklet-runtime/src/bindings/types.ts, packages/react/worklet-runtime/src/global.ts, packages/react/runtime/src/worklet/ref/workletRefPool.ts
New MainThreadValueImpl<T> type added, union widened, global _mainThreadValueClassMap declared, and init-value patch tuples extended to optionally include type strings.
Tests & Test Setup
packages/react/worklet-runtime/__test__/setup.js, packages/react/worklet-runtime/__test__/*, packages/react/worklet-runtime/vitest.config.ts, packages/react/worklet-runtime/__test__/workletRuntime.test.js, packages/react/worklet-runtime/__test__/workletRef.test.js
New test setup mocking globalThis.lynx, test adjustments to assert hydration and current semantics, registry re-registration in tests, and updated snapshots.
Bindings API Updates
packages/react/worklet-runtime/src/bindings/bindings.ts, packages/react/runtime/src/worklet/ref/workletRef.ts, packages/react/worklet-runtime/src/workletRef.ts
Signatures widened to accept optional type in init-value patches; update functions accept [number, unknown, string?] tuples and propagate type into hydration flow.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • HuJean
  • hzy
  • gaoachao

Poem

🐰 I hopped through threads both main and small,
Made values persistent, captured them all.
SWC wraps nests with a gentle snare,
MotionValues dance in the open air.
Hooray — a rabbit’s patch, tidy and fair! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add MainThreadValue' directly and clearly summarizes the main change: introducing a new MainThreadValue feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Jan 27, 2026

❌ 15 Tests Failed:

Tests completed Failed Passed Skipped
3605 15 3590 64
View the top 3 failed test(s) by shortest run time
packages/react/runtime/__test__/worklet/workletRef.test.jsx > WorkletRef in js > should throw when getting and setting outside of main thread script
Stack Traces | 0.00114s run time
AssertionError: expected [Function] to throw an error

- Expected: 
null

+ Received: 
undefined

 ❯ __test__/worklet/workletRef.test.jsx:150:31
packages/react/runtime/__test__/lazy.test.js > Lazy Exports > export APIs from "internal"
Stack Traces | 0.00216s run time
AssertionError: expected Set{ …(34) } to strictly equal Set{ '__page', '__pageId', …(33) }

- Expected
+ Received

@@ -30,8 +30,7 @@
    "updateRef",
    "updateSpread",
    "updateWorkletEvent",
    "updateWorkletRef",
    "withInitDataInState",
-   "workletCapture",
    "wrapWithLynxComponent",
  }

 ❯ __test__/lazy.test.js:60:7
packages/react/runtime/__test__/worklet/workletRef.test.jsx > WorkletRef in js > should send init value to the main thread even after reloadTemplate
Stack Traces | 0.00217s run time
Error: Snapshot `WorkletRef in js > should send init value to the main thread even after reloadTemplate 1` mismatched

- Expected
+ Received

@@ -1,10 +1,10 @@
  [
    [
      "rLynxChangeRefInitValue",
      {
-       "data": "[[1,233]]",
+       "data": "[[1,233,"main-thread"]]",
      },
    ],
    [
      "rLynxChange",
      {

 ❯ __test__/worklet/workletRef.test.jsx:246:27
packages/react/runtime/__test__/lazy.test.js > Lazy Exports > export APIs from "react/compat"
Stack Traces | 0.00231s run time
AssertionError: expected Set{ 'Children', 'Component', …(38) } to strictly equal Set{ 'default', 'Component', …(40) }

- Expected
+ Received

@@ -3,11 +3,10 @@
    "Component",
    "Fragment",
    "InitDataConsumer",
    "InitDataProvider",
    "MainThreadRef",
-   "MainThreadValue",
    "PureComponent",
    "Suspense",
    "cloneElement",
    "createContext",
    "createElement",
@@ -38,7 +37,6 @@
    "useRef",
    "useState",
    "useSyncExternalStore",
    "useTransition",
    "withInitDataInState",
-   "workletCapture",
  }

 ❯ __test__/lazy.test.js:34:7
packages/react/runtime/__test__/worklet/workletRef.test.jsx > WorkletRef in js > should throw when getting and setting in background
Stack Traces | 0.00333s run time
AssertionError: expected [Function] to throw error including 'MainThreadRef: value of a MainThreadR…' but got 'main-thread: value cannot be accessed…'

Expected: "MainThreadRef: value of a MainThreadRef cannot be accessed in the background thread."
Received: "main-thread: value cannot be accessed on the background thread."

 ❯ __test__/worklet/workletRef.test.jsx:139:31
packages/react/runtime/__test__/snapshot/workletRef.test.jsx > WorkletRef > update
Stack Traces | 0.00378s run time
Error: Snapshot `WorkletRef > update 3` mismatched

- Expected
+ Received

@@ -1,8 +1,11 @@
  [
    [
      {
+       "__MT_PERSIST__": true,
+       "_initValue": undefined,
+       "_type": "main-thread",
        "_wvid": -1,
      },
      <view
        has-react-ref={true}
      />,

 ❯ __test__/snapshot/workletRef.test.jsx:246:80
packages/react/runtime/__test__/worklet/workletRef.test.jsx > WorkletRef in js > to json
Stack Traces | 0.00379s run time
Error: Snapshot `WorkletRef in js > to json 1` mismatched

Expected: ""{"_wvid":1}""
Received: ""{"__MT_PERSIST__":true,"_wvid":1,"_initValue":1,"_type":"main-thread"}""

 ❯ __test__/worklet/workletRef.test.jsx:77:33
packages/react/runtime/__test__/snapshot/workletRef.test.jsx > WorkletRef > insert & remove element
Stack Traces | 0.00388s run time
Error: Snapshot `WorkletRef > insert & remove element 3` mismatched

- Expected
+ Received

@@ -1,8 +1,10 @@
  [
    [
      {
+       "__MT_PERSIST__": true,
+       "_type": "main-thread",
        "_wvid": 1,
      },
      <view
        has-react-ref={true}
      />,

 ❯ __test__/snapshot/workletRef.test.jsx:518:80
packages/react/runtime/__test__/snapshot/workletRef.test.jsx > WorkletRef in spread > insert & remove
Stack Traces | 0.00662s run time
Error: Snapshot `WorkletRef in spread > insert & remove 3` mismatched

- Expected
+ Received

@@ -1,8 +1,10 @@
  [
    [
      {
+       "__MT_PERSIST__": true,
+       "_type": "main-thread",
        "_wvid": 1,
      },
      <view
        has-react-ref={true}
      />,

 ❯ __test__/snapshot/workletRef.test.jsx:789:80
packages/react/runtime/__test__/worklet/workletRef.test.jsx > WorkletRef in js > should send init value to the main thread
Stack Traces | 0.00818s run time
Error: Snapshot `WorkletRef in js > should send init value to the main thread 1` mismatched

- Expected
+ Received

@@ -1,10 +1,10 @@
  [
    [
      "rLynxChangeRefInitValue",
      {
-       "data": "[[1,233]]",
+       "data": "[[1,233,"main-thread"]]",
      },
    ],
    [
      "rLynxChange",
      {

 ❯ __test__/worklet/workletRef.test.jsx:101:27
packages/react/transform/__test__/fixture.spec.js > worklet > member expression
Stack Traces | 0.00833s run time
Error: Snapshot `worklet > member expression 1` mismatched

- Expected
+ Received

@@ -1,14 +1,12 @@
- "import { loadWorkletRuntime as __loadWorkletRuntime } from "@lynx-js/react";
+ "import { loadWorkletRuntime as __loadWorkletRuntime, workletCapture as __workletCapture } from "@lynx-js/react";
- var loadWorkletRuntime = __loadWorkletRuntime;
+ var loadWorkletRuntime = __loadWorkletRuntime, workletCapture = __workletCapture;
  export let getCurrentDelta = {
      _c: {
-         foo: {
-             bar: {
+         foo: workletCapture(foo, "bar", {
-                 baz: foo.bar.baz
+             baz: foo.bar.baz
-             }
-         }
+         })
      },
      _wkltId: "da39:75a1b:1"
  };
  loadWorkletRuntime(typeof globDynamicComponentEntry === 'undefined' ? undefined : globDynamicComponentEntry) && registerWorkletInternal("main-thread", "da39:75a1b:1", function(event) {
      const getCurrentDelta = lynxWorkletImpl._workletMap["da39:75a1b:1"].bind(this);

 ❯ __test__/fixture.spec.js:1591:22
packages/react/transform/__test__/fixture.spec.js > worklet > member expression with multiple times
Stack Traces | 0.00867s run time
Error: Snapshot `worklet > member expression with multiple times 1` mismatched

- Expected
+ Received

@@ -1,22 +1,17 @@
- "import { loadWorkletRuntime as __loadWorkletRuntime } from "@lynx-js/react";
+ "import { loadWorkletRuntime as __loadWorkletRuntime, workletCapture as __workletCapture } from "@lynx-js/react";
- var loadWorkletRuntime = __loadWorkletRuntime;
+ var loadWorkletRuntime = __loadWorkletRuntime, workletCapture = __workletCapture;
  export let foo = {
      _c: {
-         bar: {
-             baz: {
+         bar: workletCapture(bar, "baz", {
-                 'qux': bar.baz['qux']
+             'qux': bar.baz['qux']
-             },
-             qux: {
+         }, "qux", {
-                 'baz': bar.qux['baz']
+             'baz': bar.qux['baz']
-             }
-         },
+         }),
-         qux: {
-             bar: {
+         qux: workletCapture(qux, "bar", {
-                 baz: qux.bar.baz
+             baz: qux.bar.baz
-             }
-         }
+         })
      },
      _wkltId: "da39:64631:1"
  };
  loadWorkletRuntime(typeof globDynamicComponentEntry === 'undefined' ? undefined : globDynamicComponentEntry) && registerWorkletInternal("main-thread", "da39:64631:1", function(event) {
      const foo = lynxWorkletImpl._workletMap["da39:64631:1"].bind(this);

 ❯ __test__/fixture.spec.js:1684:18
packages/react/runtime/__test__/lazy.test.js > Lazy Exports > export APIs from "react"
Stack Traces | 0.0189s run time
AssertionError: expected Set{ 'Children', 'Component', …(36) } to strictly equal Set{ 'Component', …(39) }

- Expected
+ Received

@@ -3,11 +3,10 @@
    "Component",
    "Fragment",
    "InitDataConsumer",
    "InitDataProvider",
    "MainThreadRef",
-   "MainThreadValue",
    "PureComponent",
    "Suspense",
    "cloneElement",
    "createContext",
    "createElement",
@@ -36,7 +35,6 @@
    "useReducer",
    "useRef",
    "useState",
    "useSyncExternalStore",
    "withInitDataInState",
-   "workletCapture",
  }

 ❯ __test__/lazy.test.js:21:7
packages/react/testing-library/src/__tests__/worklet.test.jsx > worklet > worklet ref should work
Stack Traces | 0.0225s run time
Error: Snapshot `worklet > worklet ref should work 3` mismatched

- Expected
+ Received

@@ -1,16 +1,16 @@
  [
    [
      "rLynxChangeRefInitValue",
      {
-       "data": "[[1,null],[2,0]]",
+       "data": "[[1,null,"main-thread"],[2,0,"main-thread"]]",
      },
    ],
    [
      "rLynxChange",
      {
-       "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wvid":1},3,-2,1,{"_c":{"ref":{"_wvid":1},"num":{"_wvid":2}},"_wkltId":"a45f:test:9","_execId":1}],"id":2}]}",
+       "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"__MT_PERSIST__":true,"_wvid":1,"_initValue":null,"_type":"main-thread"},3,-2,1,{"_c":{"ref":{"__MT_PERSIST__":true,"_wvid":1,"_initValue":null,"_type":"main-thread"},"num":{"__MT_PERSIST__":true,"_wvid":2,"_initValue":0,"_type":"main-thread"}},"_wkltId":"a45f:test:9","_execId":1}],"id":2}]}",
        "patchOptions": {
          "isHydration": true,
          "pipelineOptions": {
            "dsl": "reactLynx",
            "needTimestamps": true,

 ❯ src/__tests__/worklet.test.jsx:439:34
packages/react/runtime/__test__/snapshot/workletRef.test.jsx > WorkletRef > insert & remove
Stack Traces | 0.0336s run time
Error: Snapshot `WorkletRef > insert & remove 3` mismatched

- Expected
+ Received

@@ -1,8 +1,10 @@
  [
    [
      {
+       "__MT_PERSIST__": true,
+       "_type": "main-thread",
        "_wvid": 1,
      },
      <view
        has-react-ref={true}
      />,

 ❯ __test__/snapshot/workletRef.test.jsx:119:80

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/react/worklet-runtime/__test__/workletRef.test.js (1)

6-22: Clean up __LYNX_MTV_REGISTRY__ after each test.

Line 18 introduces a global registry; without cleanup it can leak into other suites sharing the same process.

🧹 Suggested cleanup
 afterEach(() => {
   delete globalThis.lynxWorkletImpl;
+  delete globalThis.__LYNX_MTV_REGISTRY__;
 });

Also applies to: 26-28

🤖 Fix all issues with AI agents
In @.changeset/better-pigs-run.md:
- Line 7: The fenced code block in the changeset uses a plain triple-backtick
with no language hint; update that fence to include a language tag (e.g., change
``` to ```ts) so the block is annotated (satisfies MD040). Locate the
triple-backtick fence in .changeset/better-pigs-run.md and add the appropriate
language identifier after the opening backticks.

In `@packages/react/runtime/src/worklet/ref/mainThreadValue.ts`:
- Around line 81-86: The _lifecycleObserver field in mainThreadValue.ts is only
set in the __JS__ branch and fails strict property initialization; update the
declaration of protected _lifecycleObserver to be optional or include undefined
(e.g., make it protected _lifecycleObserver?: unknown or protected
_lifecycleObserver: unknown | undefined) so the compiler knows it may be unset
on the main thread, leaving the existing assignment in the __JS__ branch (where
it's used) unchanged.

In `@packages/react/transform/crates/swc_plugin_worklet/gen_stmt.rs`:
- Around line 99-128: Clippy flags useless conversions in the object property
handling: avoid converting symbols/strings to &str and back or calling
to_string() unnecessarily. In the Prop::Shorthand and Prop::KeyValue arms (refer
to context_ident, val_obj, Prop::Shorthand, Prop::KeyValue, kv.key, kv.value),
replace patterns like id.sym.as_str().into(),
s.value.to_string_lossy().to_string(), and the intermediate key_str -> .into()
with direct clones of the underlying JsWord/Str (e.g., use id.sym.clone() and
s.value.clone() or push the appropriate cloned value directly) and push
kv.value.clone() without extra deref/conversion; remove the redundant
conversions so the Str.value fields and args use the correct cloned types
directly.
🧹 Nitpick comments (7)
packages/react/worklet-runtime/src/bindings/types.ts (1)

19-24: LGTM! Well-designed interface for main-thread persistent values.

The interface correctly includes the __MT_PERSIST__: true discriminant that aligns with the runtime check in workletCapture (packages/react/runtime/src/worklet/capture.ts).

Minor consistency nit: WorkletRefImpl uses WorkletRefId type alias for _wvid (line 12), while this interface uses raw number. Consider using WorkletRefId for consistency:

 export interface MainThreadValueImpl<T> {
   __MT_PERSIST__: true;
-  _wvid: number;
+  _wvid: WorkletRefId;
   _initValue: T;
   _type: string;
 }
packages/react/runtime/src/worklet/capture.ts (1)

15-16: Guard against odd key/value args in workletCapture.

If args length is odd, args[i + 1] becomes undefined, which can silently skew captured data. Consider guarding the loop.

♻️ Suggested guard
-  for (let i = 0; i < args.length; i += 2) {
-    result[args[i]] = args[i + 1];
-  }
+  for (let i = 0; i + 1 < args.length; i += 2) {
+    result[args[i]] = args[i + 1];
+  }
examples/react/src/MotionValue.ts (1)

13-56: De-duplicate the MotionValue type string to avoid drift.

The type identifier is repeated in the constructor default and registration. Hoist it to a single constant (e.g., static type) to keep future edits in sync.

♻️ Suggested refactor
-export class MotionValue<T> extends MainThreadValue<T> {
-  private _subscribers: Set<(value: T) => void> = new Set();
-
-  constructor(initValue: T, type = '@example/motion-value') {
-    super(initValue, type);
-  }
+export class MotionValue<T> extends MainThreadValue<T> {
+  static readonly type = '@example/motion-value';
+  private _subscribers: Set<(value: T) => void> = new Set();
+
+  constructor(initValue: T, type = MotionValue.type) {
+    super(initValue, type);
+  }
@@
-MainThreadValue.register(MotionValue, '@example/motion-value');
+MainThreadValue.register(MotionValue, MotionValue.type);
packages/react/transform/crates/swc_plugin_worklet/tests/__swc_snapshots__/lib.rs/should_transform_fn_decl_js.js (1)

1-2: Unused workletCapture import in generated code.

The workletCapture function is imported and aliased but not used in this particular snapshot. Since this is an auto-generated snapshot file, if this unused import is unintentional, the fix should be applied in the SWC transform logic (e.g., conditionally emitting the import only when workletCapture is actually needed).

If the transform intentionally adds the import unconditionally for simplicity, this is acceptable but may result in slightly larger bundle sizes when tree-shaking is not available.

packages/react/worklet-runtime/__test__/api/element.test.js (1)

17-22: Consider cleaning up globalThis.lynx in afterEach.

The globalThis.lynx mock is set up in beforeEach but not cleaned up in afterEach. For test isolation consistency, consider adding delete globalThis.lynx; in the afterEach block alongside the existing cleanup of globalThis.lynxWorkletImpl.

Suggested addition to afterEach
 afterEach(() => {
   delete globalThis.lynxWorkletImpl;
+  delete globalThis.lynx;
   vi.useRealTimers();
   vi.clearAllMocks();
   setShouldFlush(true);
 });
packages/react/transform/crates/swc_plugin_worklet/tests/__swc_snapshots__/lib.rs/should_extract_member_expr_3_js.js (1)

1-2: Same pattern as other snapshots - workletCapture imported but unused.

This is consistent with other auto-generated snapshots in this PR. The import is present but not utilized in the output. As noted for the other snapshot file, if this is unintentional, the transform logic should be adjusted.

packages/react/transform/crates/swc_plugin_worklet/gen_stmt.rs (1)

260-266: Remove duplicate comment block.

The comment /* registerWorklet($type, $hash, $function); */ appears twice consecutively. This looks unintentional.

♻️ Proposed fix
-  /*
-   * registerWorklet($type, $hash, $function);
-   */
-
   /*
    * registerWorklet($type, $hash, $function);
    */
   fn gen_register_worklet_stmt(


Add support for `MainThreadValue` to enable Main Thread Persistent Data other than `MainThreadRef`, to make library developers able to create their own main thread values.

```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a language hint to the fenced code block.

This satisfies MD040 and keeps markdown linting clean.

🛠️ Minimal fix
-```
+```ts

As per coding guidelines, ...

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

7-7: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In @.changeset/better-pigs-run.md at line 7, The fenced code block in the
changeset uses a plain triple-backtick with no language hint; update that fence
to include a language tag (e.g., change ``` to ```ts) so the block is annotated
(satisfies MD040). Locate the triple-backtick fence in
.changeset/better-pigs-run.md and add the appropriate language identifier after
the opening backticks.

Comment on lines +81 to +86
/**
* @internal
* Observer for garbage collection lifecycle.
*/
protected _lifecycleObserver: unknown;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and find the target file
fd mainThreadValue.ts

Repository: lynx-family/lynx-stack

Length of output: 123


🏁 Script executed:

# Read the file to see the property definition and its usage
cat -n packages/react/runtime/src/worklet/ref/mainThreadValue.ts | head -120

Repository: lynx-family/lynx-stack

Length of output: 4297


🏁 Script executed:

# Check the tsconfig.json for strict mode settings
cat tsconfig.json

Repository: lynx-family/lynx-stack

Length of output: 12684


🏁 Script executed:

# Search for all assignments to _lifecycleObserver in the file
rg "_lifecycleObserver" packages/react/runtime/src/worklet/ref/mainThreadValue.ts -A 2

Repository: lynx-family/lynx-stack

Length of output: 309


Make _lifecycleObserver optional to satisfy strict property initialization.

The property is only assigned in the __JS__ branch (background thread), leaving it uninitialized on the main thread. With strictPropertyInitialization enabled (via strict: true in tsconfig.json), this fails type checking. Make it optional:

Suggested fix
-  protected _lifecycleObserver: unknown;
+  protected _lifecycleObserver?: unknown;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* @internal
* Observer for garbage collection lifecycle.
*/
protected _lifecycleObserver: unknown;
/**
* `@internal`
* Observer for garbage collection lifecycle.
*/
protected _lifecycleObserver?: unknown;
🤖 Prompt for AI Agents
In `@packages/react/runtime/src/worklet/ref/mainThreadValue.ts` around lines 81 -
86, The _lifecycleObserver field in mainThreadValue.ts is only set in the __JS__
branch and fails strict property initialization; update the declaration of
protected _lifecycleObserver to be optional or include undefined (e.g., make it
protected _lifecycleObserver?: unknown or protected _lifecycleObserver: unknown
| undefined) so the compiler knows it may be unset on the main thread, leaving
the existing assignment in the __JS__ branch (where it's used) unchanged.

Comment thread packages/react/transform/crates/swc_plugin_worklet/gen_stmt.rs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@examples/react/src/App.tsx`:
- Around line 22-52: The cleanup race occurs because unsubscribeHandle may be
undefined when the component unmounts; change the runOnMainThread call to an
async IIFE that awaits the result and uses a local "cancelled" flag (or keeps
the pending Promise) so cleanup can safely no-op if the subscription setup
hasn't completed: in useEffect, replace the .then/.catch pattern with an async
function that calls runOnMainThread(), assigns the returned unsubscribe function
to a local variable (e.g., unsubscribeHandle) only if not cancelled, and on
return set cancelled=true and, if unsubscribeHandle is set, call it; reference
the existing useEffect, runOnMainThread, unsubscribeHandle, opacity.subscribe,
tapCount.subscribe, and the unsubscribe/unsubscribeTapCount variables to locate
where to implement this guard and explicit error handling.

In `@packages/react/runtime/src/worklet/ref/workletRef.ts`:
- Around line 25-34: The try/catch in the current getter is redundant because
catch calls super.getValueOnMainThread() which will rethrow the same error;
either remove the try/catch and directly return this.getValueOnMainThread(), or
if a fallback is desired change the catch to return the stored fallback
(this._initValue) instead of calling super.getValueOnMainThread(); update the
current getter accordingly and keep references to getValueOnMainThread,
super.getValueOnMainThread, current, and _initValue to locate the change.
🧹 Nitpick comments (3)
packages/react/transform/crates/swc_plugin_worklet/gen_stmt.rs (1)

260-266: Remove duplicate comment block.

Lines 260-262 duplicate the comment block at lines 264-266. This appears to be an accidental leftover.

🔧 Proposed fix
   }

-  /*
-   * registerWorklet($type, $hash, $function);
-   */
-
   /*
    * registerWorklet($type, $hash, $function);
    */
   fn gen_register_worklet_stmt(
packages/react/worklet-runtime/__test__/workletRuntime.test.js (1)

133-136: Consider adding _type to arg2 for consistency.

The arg2 worklet ref at lines 133-135 lacks the _type field, unlike other refs in this file. If this is intentional to test backward compatibility (fallback behavior when _type is missing), consider adding a comment to clarify.

packages/react/worklet-runtime/src/workletRef.ts (1)

21-29: Remove duplicate comment block.

The comment "Registry for custom MainThreadValue types..." appears twice (lines 21-24 and 25-28).

🔧 Proposed fix
 /**
  * Registry for custom MainThreadValue types.
  * Maps type string (e.g., 'main-thread', 'motion-value') to constructor.
  */
-/**
- * Registry for custom MainThreadValue types.
- * Maps type string (e.g., 'main-thread', 'motion-value') to constructor.
- */
 const typeRegistry: Record<string, new(initValue: unknown, type: string) => unknown> = {};

Comment on lines 22 to +52
useEffect(() => {
console.info('Hello, ReactLynx');

let unsubscribeHandle: (() => void) | undefined;

runOnMainThread(() => {
'main thread';

// Demo: Subscribe to value changes (main thread only)
const unsubscribe = opacity.subscribe((newValue) => {
console.info('Opacity changed to:', newValue);
});

// Demo: Subscribe to value changes (main thread only)
const unsubscribeTapCount = tapCount.subscribe((newValue) => {
console.info('TapCount changed to:', newValue);
});

return () => {
unsubscribe();
unsubscribeTapCount();
};
})().then((res) => {
unsubscribeHandle = res as () => void;
}).catch(_err => {
// omitted
});

return () => {
void runOnMainThread(unsubscribeHandle!)();
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the actual code in App.tsx at the specified lines
cat -n examples/react/src/App.tsx | sed -n '1,70p'

Repository: lynx-family/lynx-stack

Length of output: 2332


🏁 Script executed:

# Find where runOnMainThread is defined
fd -t f '*.ts' -o '*.tsx' -o '*.js' | xargs rg -l 'runOnMainThread' | head -20

Repository: lynx-family/lynx-stack

Length of output: 1352


🏁 Script executed:

# Get the definition of runOnMainThread
rg -A 10 'export.*runOnMainThread|function runOnMainThread' --type ts --type tsx

Repository: lynx-family/lynx-stack

Length of output: 93


🏁 Script executed:

# Check the type definitions or interface for runOnMainThread
rg -B 2 -A 5 'runOnMainThread' examples/react/src/App.tsx

Repository: lynx-family/lynx-stack

Length of output: 592


🏁 Script executed:

# Find the runOnMainThread implementation
cat packages/react/runtime/src/worklet/call/runOnMainThread.ts

Repository: lynx-family/lynx-stack

Length of output: 2409


🏁 Script executed:

# Check the test files for runOnMainThread to understand expected behavior
cat packages/react/runtime/__test__/worklet/runOnMainThread.test.jsx

Repository: lynx-family/lynx-stack

Length of output: 8942


🏁 Script executed:

# Also check the basic test
cat packages/react/worklet-runtime/__test__/runOnMainThread.test.js

Repository: lynx-family/lynx-stack

Length of output: 1316


🏁 Script executed:

# Check the web platform test to understand what runOnMainThread does
cat packages/web-platform/web-core-wasm-e2e/tests/reactlynx/basic-mts-run-on-main-thread/index.jsx | head -80

Repository: lynx-family/lynx-stack

Length of output: 915


🏁 Script executed:

# Look for similar subscription patterns with cleanup
rg -B 3 -A 5 '\.subscribe\(' packages/motion/src/ --type ts --type tsx

Repository: lynx-family/lynx-stack

Length of output: 93


🏁 Script executed:

# Search for other useEffect + runOnMainThread patterns
rg -B 2 -A 8 'useEffect.*runOnMainThread|runOnMainThread.*\.then' --type ts --type tsx

Repository: lynx-family/lynx-stack

Length of output: 93


🏁 Script executed:

# Check if there are other examples in examples directory
find examples -name "*.tsx" -o -name "*.ts" | xargs rg -l "useEffect" | head -5

Repository: lynx-family/lynx-stack

Length of output: 237


🏁 Script executed:

# Search for subscription patterns without type specification
rg '\.subscribe\(' packages/motion/src/ -A 5

Repository: lynx-family/lynx-stack

Length of output: 126


🏁 Script executed:

# Check other example apps for comparison
for file in examples/tailwindcss/src/App.tsx examples/react-lazy-bundle/src/App.tsx examples/react-externals/src/App.tsx; do
  echo "=== $file ==="; 
  cat -n "$file" | head -60;
done

Repository: lynx-family/lynx-stack

Length of output: 4904


🏁 Script executed:

# Look for async patterns with runOnMainThread
rg 'async.*runOnMainThread|runOnMainThread.*async' -B 2 -A 5

Repository: lynx-family/lynx-stack

Length of output: 3317


Guard main-thread subscription cleanup against race conditions.
The component can unmount before the .then() promise resolves, leaving unsubscribeHandle undefined. The non-null assertion at line 51 masks this type safety issue and cleanup will fail silently. Use async/await with a cancellation guard and explicit error handling instead.

🔧 Proposed fix
   useEffect(() => {
     console.info('Hello, ReactLynx');

-    let unsubscribeHandle: (() => void) | undefined;
-
-    runOnMainThread(() => {
-      'main thread';
-
-      // Demo: Subscribe to value changes (main thread only)
-      const unsubscribe = opacity.subscribe((newValue) => {
-        console.info('Opacity changed to:', newValue);
-      });
-
-      // Demo: Subscribe to value changes (main thread only)
-      const unsubscribeTapCount = tapCount.subscribe((newValue) => {
-        console.info('TapCount changed to:', newValue);
-      });
-
-      return () => {
-        unsubscribe();
-        unsubscribeTapCount();
-      };
-    })().then((res) => {
-      unsubscribeHandle = res as () => void;
-    }).catch(_err => {
-      // omitted
-    });
-
-    return () => {
-      void runOnMainThread(unsubscribeHandle!)();
-    };
+    let unsubscribeHandle: (() => void) | undefined;
+    let cancelled = false;
+
+    const init = async () => {
+      try {
+        const res = await runOnMainThread(() => {
+          'main thread';
+
+          // Demo: Subscribe to value changes (main thread only)
+          const unsubscribe = opacity.subscribe((newValue) => {
+            console.info('Opacity changed to:', newValue);
+          });
+
+          // Demo: Subscribe to value changes (main thread only)
+          const unsubscribeTapCount = tapCount.subscribe((newValue) => {
+            console.info('TapCount changed to:', newValue);
+          });
+
+          return () => {
+            unsubscribe();
+            unsubscribeTapCount();
+          };
+        })();
+
+        if (typeof res !== 'function') {
+          return;
+        }
+
+        if (cancelled) {
+          await runOnMainThread(res)();
+          return;
+        }
+
+        unsubscribeHandle = res;
+      } catch (err) {
+        console.error('Main thread subscription failed', err);
+      }
+    };
+
+    void init();
+
+    return () => {
+      cancelled = true;
+      if (unsubscribeHandle) {
+        void runOnMainThread(unsubscribeHandle)().catch((err) => {
+          console.error('Main thread cleanup failed', err);
+        });
+      }
+    };
   }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
console.info('Hello, ReactLynx');
let unsubscribeHandle: (() => void) | undefined;
runOnMainThread(() => {
'main thread';
// Demo: Subscribe to value changes (main thread only)
const unsubscribe = opacity.subscribe((newValue) => {
console.info('Opacity changed to:', newValue);
});
// Demo: Subscribe to value changes (main thread only)
const unsubscribeTapCount = tapCount.subscribe((newValue) => {
console.info('TapCount changed to:', newValue);
});
return () => {
unsubscribe();
unsubscribeTapCount();
};
})().then((res) => {
unsubscribeHandle = res as () => void;
}).catch(_err => {
// omitted
});
return () => {
void runOnMainThread(unsubscribeHandle!)();
};
useEffect(() => {
console.info('Hello, ReactLynx');
let unsubscribeHandle: (() => void) | undefined;
let cancelled = false;
const init = async () => {
try {
const res = await runOnMainThread(() => {
'main thread';
// Demo: Subscribe to value changes (main thread only)
const unsubscribe = opacity.subscribe((newValue) => {
console.info('Opacity changed to:', newValue);
});
// Demo: Subscribe to value changes (main thread only)
const unsubscribeTapCount = tapCount.subscribe((newValue) => {
console.info('TapCount changed to:', newValue);
});
return () => {
unsubscribe();
unsubscribeTapCount();
};
})();
if (typeof res !== 'function') {
return;
}
if (cancelled) {
await runOnMainThread(res)();
return;
}
unsubscribeHandle = res;
} catch (err) {
console.error('Main thread subscription failed', err);
}
};
void init();
return () => {
cancelled = true;
if (unsubscribeHandle) {
void runOnMainThread(unsubscribeHandle)().catch((err) => {
console.error('Main thread cleanup failed', err);
});
}
};
}, []);
🤖 Prompt for AI Agents
In `@examples/react/src/App.tsx` around lines 22 - 52, The cleanup race occurs
because unsubscribeHandle may be undefined when the component unmounts; change
the runOnMainThread call to an async IIFE that awaits the result and uses a
local "cancelled" flag (or keeps the pending Promise) so cleanup can safely
no-op if the subscription setup hasn't completed: in useEffect, replace the
.then/.catch pattern with an async function that calls runOnMainThread(),
assigns the returned unsubscribe function to a local variable (e.g.,
unsubscribeHandle) only if not cancelled, and on return set cancelled=true and,
if unsubscribeHandle is set, call it; reference the existing useEffect,
runOnMainThread, unsubscribeHandle, opacity.subscribe, tapCount.subscribe, and
the unsubscribe/unsubscribeTapCount variables to locate where to implement this
guard and explicit error handling.

Comment on lines +25 to 34
get current(): T {
try {
return this.getValueOnMainThread();
} catch {
// For backward compatibility / safety in background thread (though it throws in base)
// The base class throws if on BG.
// The original MainThreadRef threw on BG in DEV.
// Base class throws Error.
return super.getValueOnMainThread();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redundant try/catch in current getter.

The catch block calls super.getValueOnMainThread(), which is the same method that just threw. This will throw again, making the try/catch ineffective. If the intent is to provide a fallback, it should return this._initValue directly or handle the error differently.

🔧 Proposed fix
   get current(): T {
-    try {
-      return this.getValueOnMainThread();
-    } catch {
-      // For backward compatibility / safety in background thread (though it throws in base)
-      // The base class throws if on BG.
-      // The original MainThreadRef threw on BG in DEV.
-      // Base class throws Error.
-      return super.getValueOnMainThread();
-    }
+    return this.getValueOnMainThread();
   }

Or if a fallback to _initValue is intended:

   get current(): T {
-    try {
-      return this.getValueOnMainThread();
-    } catch {
-      // For backward compatibility / safety in background thread (though it throws in base)
-      // The base class throws if on BG.
-      // The original MainThreadRef threw on BG in DEV.
-      // Base class throws Error.
-      return super.getValueOnMainThread();
-    }
+    return this.getValueOnMainThread();
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
get current(): T {
try {
return this.getValueOnMainThread();
} catch {
// For backward compatibility / safety in background thread (though it throws in base)
// The base class throws if on BG.
// The original MainThreadRef threw on BG in DEV.
// Base class throws Error.
return super.getValueOnMainThread();
}
get current(): T {
return this.getValueOnMainThread();
}
🤖 Prompt for AI Agents
In `@packages/react/runtime/src/worklet/ref/workletRef.ts` around lines 25 - 34,
The try/catch in the current getter is redundant because catch calls
super.getValueOnMainThread() which will rethrow the same error; either remove
the try/catch and directly return this.getValueOnMainThread(), or if a fallback
is desired change the catch to return the stored fallback (this._initValue)
instead of calling super.getValueOnMainThread(); update the current getter
accordingly and keep references to getValueOnMainThread,
super.getValueOnMainThread, current, and _initValue to locate the change.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Jan 27, 2026

CodSpeed Performance Report

Merging this PR will degrade performance by 38.04%

Comparing f0rdream:main_thread_value (90a9cc3) with main (5e7e43c)

Summary

⚡ 1 improved benchmark
❌ 3 regressed benchmarks
✅ 59 untouched benchmarks
⏩ 3 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
004-various-update__main-thread-setAttribute__Spread 382.8 µs 617.8 µs -38.04%
004-various-update/main-thread.js_LoadScript 653.1 µs 715.1 µs -8.68%
basic-performance-small-css 7.8 ms 8.2 ms -5.63%
003-hello-list__main-thread-componentAtIndex__reuse 3.1 ms 2.2 ms +38.63%

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@relativeci
Copy link
Copy Markdown

relativeci bot commented Jan 27, 2026

Web Explorer

#7380 Bundle Size — 384.24KiB (-0.13%).

90a9cc3(current) vs 43fc7e7 main#7379(baseline)

Bundle metrics  Change 2 changes Improvement 1 improvement
                 Current
#7380
     Baseline
#7379
Improvement  Initial JS 154.23KiB(-0.32%) 154.71KiB
No change  Initial CSS 35.05KiB 35.05KiB
Change  Cache Invalidation 40.21% 40.14%
No change  Chunks 8 8
No change  Assets 8 8
No change  Modules 238 238
No change  Duplicate Modules 16 16
No change  Duplicate Code 2.99% 2.99%
No change  Packages 4 4
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Improvement 1 improvement
                 Current
#7380
     Baseline
#7379
Improvement  JS 252.17KiB (-0.19%) 252.66KiB
No change  Other 97.02KiB 97.02KiB
No change  CSS 35.05KiB 35.05KiB

Bundle analysis reportBranch f0rdream:main_thread_valueProject dashboard


Generated by RelativeCIDocumentationReport issue

@github-actions
Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had recent activity. If this pull request is still relevant, please leave any comment (for example, "bump").

@github-actions github-actions bot added the stale Inactive for 30 days. Will be closed by bots. label Mar 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

framework:React stale Inactive for 30 days. Will be closed by bots.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant