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
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@

### Native command dispatch

On Android, imperative chart commands use `codegenNativeCommands` with the
On Android and iOS, imperative chart commands use `codegenNativeCommands` with the
current native host ref. Keep this path ref-based: converting the ref with
`findNodeHandle` and calling `UIManager.dispatchViewManagerCommand` sends Fabric
through `findShadowNodeByTag_DEPRECATED`. React Native 0.85.3's default lookup can
race native tree teardown (PRO-4970). Ref-based dispatch avoids that lookup for
chart commands without changing global React Native feature flags.

iOS retains its existing numeric command mapping. Both paths check the current
ref at call time and ignore commands after detachment. The native command names,
argument order and payloads are unchanged.
through `findShadowNodeByTag_DEPRECATED`. That lookup walks the whole shadow tree
on the JS thread and can race concurrent commits and native tree teardown
(PRO-4970); React Native only fixes it behind the
`fixFindShadowNodeByTagRaceCondition` flag, which is off by default. Ref-based
dispatch avoids the lookup for chart commands without changing global React
Native feature flags.

On iOS the view is a legacy view manager running through Fabric's interop layer,
which resolves a command by its method name and prepends the view tag itself, so
the native `RCT_EXTERN_METHOD` signatures are unchanged. Both platforms check the
current ref at call time and ignore commands after detachment. The native command
names, argument order and payloads are unchanged.

Run `yarn test` (Node 22+) for command routing, payload, detachment and independent
chart checks. These tests mock the native bridge; they do **not** prove the
intermittent Android SIGSEGV is resolved. Validate a mobile build containing this
intermittent Android or iOS SIGSEGV is resolved. Validate a mobile build containing this
package with repeated interval changes (including 3D → weekly), market and engine
switches, reconnects, background/foreground and idle runs, collecting native logs.

Expand Down
21 changes: 6 additions & 15 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import { requireNativeComponent, UIManager, findNodeHandle, Platform } from 'react-native';
import { requireNativeComponent } from 'react-native';
import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';

const NativeRNKLineView = requireNativeComponent('RNKLineView');
Expand All @@ -26,20 +26,11 @@ const RNKLineView = forwardRef((props, ref) => {
const view = nativeRef.current;
if (!view) return detachedResult;

if (Platform.OS === 'android') {
// Dispatch through the host ref. Numeric tags force Fabric through
// findShadowNodeByTag_DEPRECATED, whose RN 0.85 lookup can race teardown.
return Commands[name](view, ...args);
}

// Preserve iOS's exported numeric command mapping.
const nodeHandle = findNodeHandle(view);
if (!nodeHandle) return detachedResult;
return UIManager.dispatchViewManagerCommand(
nodeHandle,
UIManager.getViewManagerConfig('RNKLineView').Commands[name],
args
);
// Dispatch through the host ref on every platform. Numeric tags force Fabric
// through findShadowNodeByTag_DEPRECATED, whose tree walk can race concurrent
// commits and native tree teardown (PRO-4970). iOS's legacy view manager
// interop resolves these commands by method name.
return Commands[name](view, ...args);
};

useImperativeHandle(ref, () => ({
Expand Down
25 changes: 13 additions & 12 deletions tests/commands.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,12 @@ for (const platform of ['android', 'ios']) {
const payload = { time: 123, close: 42 };
const args = name.startsWith('get') ? [] : [payload];
api[name](...args);
if (platform === 'android') {
assert.equal(calls.length, 1);
assert.equal(calls[0][0], 'host');
assert.equal(calls[0][1], host);
assert.equal(calls[0][2], name);
} else {
assert.equal(calls.length, 2);
assert.equal(calls[0][0], 'lookup');
assert.equal(calls[1][0], 'legacy');
assert.equal(calls[1][1], host.tag);
assert.equal(calls[1][2], ids[name]);
}
// Both platforms must use the host ref: a numeric tag would send Fabric
// through findShadowNodeByTag_DEPRECATED.
assert.equal(calls.length, 1);
assert.equal(calls[0][0], 'host');
assert.equal(calls[0][1], host);
assert.equal(calls[0][2], name);
const sent = calls.at(-1)[3];
assert.equal(sent.length, args.length);
if (args.length) assert.equal(sent[0], payload);
Expand All @@ -95,6 +89,13 @@ for (const platform of ['android', 'ios']) {
api.updateLastCandlestick({ close: 12 });
assert.equal(calls[0][1], replacement);
});

test(`${platform}: no command resolves a numeric tag or uses the legacy dispatcher`, async () => {
const { api, calls } = await mount(platform);
for (const name of names) api[name]({ time: 123 });
assert.equal(calls.length, names.length);
assert.ok(calls.every(([kind]) => kind === 'host'));
});
}

test('independent charts dispatch to their own native refs', async () => {
Expand Down
Loading