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
16 changes: 16 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Command dispatch tests

on:
pull_request:
push:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: '22.19.0'
- run: node --experimental-vm-modules --test tests/*.test.cjs
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@

## Development

### Native command dispatch

On Android, 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.

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
package with repeated interval changes (including 3D → weekly), market and engine
switches, reconnects, background/foreground and idle runs, collecting native logs.

To start the project for:

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

const NativeRNKLineView = requireNativeComponent('RNKLineView');
const Commands = codegenNativeCommands({
supportedCommands: [
'updateLastCandlestick',
'addCandlesticksAtTheEnd',
'addCandlesticksAtTheStart',
'addOrderLine',
'removeOrderLine',
'updateOrderLine',
'getOrderLines',
'addBuySellMark',
'removeBuySellMark',
'updateBuySellMark',
'getBuySellMarks',
],
});

const RNKLineView = forwardRef((props, ref) => {
const nativeRef = useRef(null);

useImperativeHandle(ref, () => ({
updateLastCandlestick: (candlestick) => {
const nodeHandle = findNodeHandle(nativeRef.current);
if (nodeHandle) {
if (Platform.OS === 'ios') {
UIManager.dispatchViewManagerCommand(
nodeHandle,
UIManager.getViewManagerConfig('RNKLineView').Commands.updateLastCandlestick,
[candlestick]
);
} else {
UIManager.dispatchViewManagerCommand(
nodeHandle,
'updateLastCandlestick',
[candlestick]
);
}
} else {
console.warn('No nodeHandle found for RNKLineView');
}
},
addCandlesticksAtTheEnd: (candlesticks) => {
const nodeHandle = findNodeHandle(nativeRef.current);
if (nodeHandle) {
if (Platform.OS === 'ios') {
UIManager.dispatchViewManagerCommand(
nodeHandle,
UIManager.getViewManagerConfig('RNKLineView').Commands.addCandlesticksAtTheEnd,
[candlesticks]
);
} else {
UIManager.dispatchViewManagerCommand(
nodeHandle,
'addCandlesticksAtTheEnd',
[candlesticks]
);
}
} else {
console.warn('No nodeHandle found for RNKLineView');
}
},
addCandlesticksAtTheStart: (candlesticks) => {
const nodeHandle = findNodeHandle(nativeRef.current);
if (nodeHandle) {
if (Platform.OS === 'ios') {
UIManager.dispatchViewManagerCommand(
nodeHandle,
UIManager.getViewManagerConfig('RNKLineView').Commands.addCandlesticksAtTheStart,
[candlesticks]
);
} else {
UIManager.dispatchViewManagerCommand(
nodeHandle,
'addCandlesticksAtTheStart',
[candlesticks]
);
}
} else {
console.warn('No nodeHandle found for RNKLineView');
}
},
addOrderLine: (orderLine) => {
const nodeHandle = findNodeHandle(nativeRef.current);
if (nodeHandle) {
if (Platform.OS === 'ios') {
UIManager.dispatchViewManagerCommand(
nodeHandle,
UIManager.getViewManagerConfig('RNKLineView').Commands.addOrderLine,
[orderLine]
);
} else {
UIManager.dispatchViewManagerCommand(
nodeHandle,
'addOrderLine',
[orderLine]
);
}
} else {
console.warn('No nodeHandle found for RNKLineView');
}
},
removeOrderLine: (orderLineId) => {
const nodeHandle = findNodeHandle(nativeRef.current);
if (nodeHandle) {
if (Platform.OS === 'ios') {
UIManager.dispatchViewManagerCommand(
nodeHandle,
UIManager.getViewManagerConfig('RNKLineView').Commands.removeOrderLine,
[orderLineId]
);
} else {
UIManager.dispatchViewManagerCommand(
nodeHandle,
'removeOrderLine',
[orderLineId]
);
}
} else {
console.warn('No nodeHandle found for RNKLineView');
}
},
updateOrderLine: (orderLine) => {
const nodeHandle = findNodeHandle(nativeRef.current);
if (nodeHandle) {
if (Platform.OS === 'ios') {
UIManager.dispatchViewManagerCommand(
nodeHandle,
UIManager.getViewManagerConfig('RNKLineView').Commands.updateOrderLine,
[orderLine]
);
} else {
UIManager.dispatchViewManagerCommand(
nodeHandle,
'updateOrderLine',
[orderLine]
);
}
} else {
console.warn('No nodeHandle found for RNKLineView');
}
},
getOrderLines: () => {
const nodeHandle = findNodeHandle(nativeRef.current);
if (nodeHandle) {
if (Platform.OS === 'ios') {
return UIManager.dispatchViewManagerCommand(
nodeHandle,
UIManager.getViewManagerConfig('RNKLineView').Commands.getOrderLines,
[]
);
} else {
return UIManager.dispatchViewManagerCommand(
nodeHandle,
'getOrderLines',
[]
);
}
} else {
console.warn('No nodeHandle found for RNKLineView');
return [];
}
},
addBuySellMark: (buySellMark) => {
const nodeHandle = findNodeHandle(nativeRef.current);
if (nodeHandle) {
if (Platform.OS === 'ios') {
UIManager.dispatchViewManagerCommand(
nodeHandle,
UIManager.getViewManagerConfig('RNKLineView').Commands.addBuySellMark,
[buySellMark]
);
} else {
UIManager.dispatchViewManagerCommand(
nodeHandle,
'addBuySellMark',
[buySellMark]
);
}
} else {
console.warn('No nodeHandle found for RNKLineView');
}
},
removeBuySellMark: (buySellMarkId) => {
const nodeHandle = findNodeHandle(nativeRef.current);
if (nodeHandle) {
if (Platform.OS === 'ios') {
UIManager.dispatchViewManagerCommand(
nodeHandle,
UIManager.getViewManagerConfig('RNKLineView').Commands.removeBuySellMark,
[buySellMarkId]
);
} else {
UIManager.dispatchViewManagerCommand(
nodeHandle,
'removeBuySellMark',
[buySellMarkId]
);
}
} else {
console.warn('No nodeHandle found for RNKLineView');
}
},
updateBuySellMark: (buySellMark) => {
const nodeHandle = findNodeHandle(nativeRef.current);
if (nodeHandle) {
if (Platform.OS === 'ios') {
UIManager.dispatchViewManagerCommand(
nodeHandle,
UIManager.getViewManagerConfig('RNKLineView').Commands.updateBuySellMark,
[buySellMark]
);
} else {
UIManager.dispatchViewManagerCommand(
nodeHandle,
'updateBuySellMark',
[buySellMark]
);
}
} else {
console.warn('No nodeHandle found for RNKLineView');
}
},
getBuySellMarks: () => {
const nodeHandle = findNodeHandle(nativeRef.current);
if (nodeHandle) {
if (Platform.OS === 'ios') {
return UIManager.dispatchViewManagerCommand(
nodeHandle,
UIManager.getViewManagerConfig('RNKLineView').Commands.getBuySellMarks,
[]
);
} else {
return UIManager.dispatchViewManagerCommand(
nodeHandle,
'getBuySellMarks',
[]
);
}
} else {
console.warn('No nodeHandle found for RNKLineView');
return [];
}
const dispatch = (name, args, detachedResult) => {
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
);
};

useImperativeHandle(ref, () => ({
updateLastCandlestick: (candlestick) => dispatch('updateLastCandlestick', [candlestick]),
addCandlesticksAtTheEnd: (candlesticks) => dispatch('addCandlesticksAtTheEnd', [candlesticks]),
addCandlesticksAtTheStart: (candlesticks) => dispatch('addCandlesticksAtTheStart', [candlesticks]),
addOrderLine: (orderLine) => dispatch('addOrderLine', [orderLine]),
removeOrderLine: (orderLineId) => dispatch('removeOrderLine', [orderLineId]),
updateOrderLine: (orderLine) => dispatch('updateOrderLine', [orderLine]),
getOrderLines: () => dispatch('getOrderLines', [], []),
addBuySellMark: (buySellMark) => dispatch('addBuySellMark', [buySellMark]),
removeBuySellMark: (buySellMarkId) => dispatch('removeBuySellMark', [buySellMarkId]),
updateBuySellMark: (buySellMark) => dispatch('updateBuySellMark', [buySellMark]),
getBuySellMarks: () => dispatch('getBuySellMarks', [], []),
}));

return <NativeRNKLineView ref={nativeRef} {...props} />;
return React.createElement(NativeRNKLineView, { ref: nativeRef, ...props });
});

export default RNKLineView;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"registry": "https://npm.pkg.github.com/@elliottech"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "node --experimental-vm-modules --test tests/*.test.cjs",
"prepublishOnly": "echo \"Ready to publish\""
},
"keywords": [
Expand Down
Loading
Loading