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
7 changes: 7 additions & 0 deletions .changeset/weak-worklet-context.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Comment thread
Yradex marked this conversation as resolved.

it('should delay and run task', () => {
const firstScreenWorklet = {
_wkltId: 'ctx1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export type ClosureValueType =
| Worklet
| WorkletRef<unknown>
| Element
| (((...args: unknown[]) => unknown) & { ctx?: ClosureValueType })
| (((...args: unknown[]) => unknown) & {
ctxRef?: WeakRef<object>;
})
| ClosureValueType_
| ClosureValueType[];

Expand Down
2 changes: 1 addition & 1 deletion packages/react/runtime/src/worklet-runtime/hydrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function hydrateCtxImpl(
);
} else {
const firstScreenValue = typeof firstScreenCtxObj[key] === 'function'
? (firstScreenCtxObj[key] as { ctx: ClosureValueType }).ctx
? (firstScreenCtxObj[key] as { ctxRef?: WeakRef<object> }).ctxRef?.deref() as ClosureValueType
: firstScreenCtxObj[key];
Comment thread
Yradex marked this conversation as resolved.
hydrateCtxImpl(ctxObj[key], firstScreenValue, execId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading