diff --git a/.changeset/weak-worklet-context.md b/.changeset/weak-worklet-context.md new file mode 100644 index 0000000000..851d9486e9 --- /dev/null +++ b/.changeset/weak-worklet-context.md @@ -0,0 +1,7 @@ +--- +"@lynx-js/react": patch +--- + +Avoid retaining transformed nested worklet contexts after worklet transformation. + +Nested worklets transformed by the worklet runtime now keep their context recovery metadata through a weak reference, preventing cached transformed worklet functions from keeping list-item worklet contexts alive. diff --git a/packages/react/runtime/__test__/worklet-runtime/runOnBackground.test.js b/packages/react/runtime/__test__/worklet-runtime/runOnBackground.test.js index 5681f2a378..f6bf610619 100644 --- a/packages/react/runtime/__test__/worklet-runtime/runOnBackground.test.js +++ b/packages/react/runtime/__test__/worklet-runtime/runOnBackground.test.js @@ -17,6 +17,57 @@ afterEach(() => { }); describe('runOnBackground', () => { + it('should not keep transformed worklet ctx through a strong ctx property', () => { + const childCtx = { + _wkltId: 'child', + }; + const parentCtx = { + _wkltId: 'parent', + child: childCtx, + }; + + globalThis.registerWorklet('main-thread', 'parent', function() { + return this.child; + }); + globalThis.registerWorklet('main-thread', 'child', function() {}); + + const childWorklet = globalThis.runWorklet(parentCtx, []); + expect(childWorklet).toHaveProperty('ctxRef'); + expect(childWorklet).not.toHaveProperty('ctx'); + expect(childWorklet.ctxRef.deref()).toBe(childCtx); + }); + + it('should hydrate nested worklet ctx from a weak ctx ref', () => { + const firstScreenChildCtx = { + _wkltId: 'child', + _jsFn: { + '_jsFn1': { '_isFirstScreen': true }, + }, + }; + const firstScreenWorklet = { + _wkltId: 'parent', + child: Object.assign(function() {}, { + ctxRef: new WeakRef(firstScreenChildCtx), + }), + }; + const worklet = { + _wkltId: 'parent', + child: { + _wkltId: 'child', + _jsFn: { + '_jsFn1': { '_jsFnId': 1 }, + }, + }, + _execId: 8, + }; + + globalThis.lynxWorkletImpl._hydrateCtx(worklet, firstScreenWorklet); + + expect(firstScreenChildCtx._jsFn._jsFn1._isFirstScreen).toBe(false); + expect(firstScreenChildCtx._jsFn._jsFn1._jsFnId).toBe(1); + expect(firstScreenChildCtx._jsFn._jsFn1._execId).toBe(8); + }); + it('should delay and run task', () => { const firstScreenWorklet = { _wkltId: 'ctx1', diff --git a/packages/react/runtime/src/worklet-runtime/bindings/types.ts b/packages/react/runtime/src/worklet-runtime/bindings/types.ts index ef364926dd..d15d1c6a18 100644 --- a/packages/react/runtime/src/worklet-runtime/bindings/types.ts +++ b/packages/react/runtime/src/worklet-runtime/bindings/types.ts @@ -34,7 +34,9 @@ export type ClosureValueType = | Worklet | WorkletRef | Element - | (((...args: unknown[]) => unknown) & { ctx?: ClosureValueType }) + | (((...args: unknown[]) => unknown) & { + ctxRef?: WeakRef; + }) | ClosureValueType_ | ClosureValueType[]; diff --git a/packages/react/runtime/src/worklet-runtime/hydrate.ts b/packages/react/runtime/src/worklet-runtime/hydrate.ts index 10957d2405..96133cbfc3 100644 --- a/packages/react/runtime/src/worklet-runtime/hydrate.ts +++ b/packages/react/runtime/src/worklet-runtime/hydrate.ts @@ -52,7 +52,7 @@ function hydrateCtxImpl( ); } else { const firstScreenValue = typeof firstScreenCtxObj[key] === 'function' - ? (firstScreenCtxObj[key] as { ctx: ClosureValueType }).ctx + ? (firstScreenCtxObj[key] as { ctxRef?: WeakRef }).ctxRef?.deref() as ClosureValueType : firstScreenCtxObj[key]; hydrateCtxImpl(ctxObj[key], firstScreenValue, execId); } diff --git a/packages/react/runtime/src/worklet-runtime/workletRuntime.ts b/packages/react/runtime/src/worklet-runtime/workletRuntime.ts index 54b30ac41b..8f51a106c0 100644 --- a/packages/react/runtime/src/worklet-runtime/workletRuntime.ts +++ b/packages/react/runtime/src/worklet-runtime/workletRuntime.ts @@ -182,7 +182,7 @@ const transformWorkletInner = ( // This would result in the value of `workletCache` referencing its key. obj[key] = lynxWorkletImpl._workletMap[(subObj as Worklet)._wkltId]! .bind({ ...subObj }); - obj[key].ctx = subObj; + obj[key].ctxRef = new WeakRef(subObj as object); continue; } const isJsFn = '_jsFnId' in subObj;