Skip to content
Open
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
25 changes: 25 additions & 0 deletions example/react-native/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,28 @@ nativeBGBridge.onHostMessage((message) => {
}, 3000);
}
});

// SharedBridge pull-model: drain messages from the main runtime
function drainSharedBridge() {
if (globalThis.sharedBridge) {
if (globalThis.sharedBridge.hasMessages) {
const messages = globalThis.sharedBridge.drain();
messages.forEach((raw) => {
try {
const msg = JSON.parse(raw);
console.log('[SharedBridge BG] received:', msg);
if (msg.type === 'ping') {
// Echo back with pong via SharedBridge
globalThis.sharedBridge.send(
JSON.stringify({ type: 'pong', ts: Date.now() }),
);
}
} catch (e) {
console.warn('[SharedBridge BG] parse error:', e);
}
});
}
}
setTimeout(drainSharedBridge, 16); // ~60fps check cadence
}
drainSharedBridge();
7 changes: 7 additions & 0 deletions example/react-native/ios/example/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import UIKit
import React
import React_RCTAppDelegate
import ReactAppDependencyProvider
import BackgroundThread

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
Expand Down Expand Up @@ -50,4 +51,10 @@ class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}

override func hostDidStart(_ host: RCTHost) {
super.hostDidStart(host)
// Install SharedBridge HostObject into the main runtime
BackgroundThreadManager.installSharedBridge(inMainRuntime: host)
}
}
132 changes: 121 additions & 11 deletions example/react-native/pages/BackgroundThreadTestPage.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,142 @@
import React, { useState } from 'react';
import { View, Text } from 'react-native';
import { TestPageBase, TestButton } from './TestPageBase';
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { TestPageBase, TestButton, TestResult } from './TestPageBase';
import { BackgroundThread } from '@onekeyfe/react-native-background-thread';

BackgroundThread.initBackgroundThread();

export function BackgroundThreadTestPage() {
const [result, setResult] = useState<string>('');
const [pushResult, setPushResult] = useState<string>('');
const [bridgeResult, setBridgeResult] = useState<string>('');
const [bridgeAvailable, setBridgeAvailable] = useState(false);
const drainTimer = useRef<ReturnType<typeof setInterval> | null>(null);

// Check SharedBridge availability and start draining
useEffect(() => {
const check = setInterval(() => {
if (globalThis.sharedBridge) {
setBridgeAvailable(true);
clearInterval(check);
}
}, 100);

const handlePostMessage = () => {
// Drain loop: pull messages from background via SharedBridge
drainTimer.current = setInterval(() => {
if (globalThis.sharedBridge?.hasMessages) {
const messages = globalThis.sharedBridge.drain();
messages.forEach((raw) => {
try {
const msg = JSON.parse(raw);
setBridgeResult(
(prev) =>
`${prev}\n[${new Date().toLocaleTimeString()}] Received: ${JSON.stringify(msg)}`,
);
} catch {
setBridgeResult((prev) => `${prev}\nParse error: ${raw}`);
}
});
}
}, 16);

return () => {
clearInterval(check);
if (drainTimer.current) clearInterval(drainTimer.current);
};
}, []);

// Push model: existing postBackgroundMessage / onBackgroundMessage
const handlePushMessage = () => {
const message = { type: 'test1' };
BackgroundThread.onBackgroundMessage((event) => {
setResult(`Message received from background thread: ${event}`);
setPushResult(`Received from background: ${event}`);
});
BackgroundThread.postBackgroundMessage(JSON.stringify(message));
setResult(`Message sent to background thread: ${JSON.stringify(message)}`);
setPushResult(`Sent: ${JSON.stringify(message)}`);
};

// Pull model: SharedBridge send + drain
const handleSharedBridgePing = () => {
if (!globalThis.sharedBridge) {
setBridgeResult('SharedBridge not available yet');
return;
}
const msg = { type: 'ping', ts: Date.now() };
globalThis.sharedBridge.send(JSON.stringify(msg));
setBridgeResult(
(prev) =>
`${prev}\n[${new Date().toLocaleTimeString()}] Sent: ${JSON.stringify(msg)}`,
);
};

const handleClearBridgeLog = () => {
setBridgeResult('');
};

return (
<TestPageBase title="Background Thread Test">
<View>
<Text style={{ fontSize: 16, fontWeight: '600', marginBottom: 10, color: '#333' }}>Result: {result}</Text>
{/* Push Model (existing) */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Push Model (postBackgroundMessage)</Text>
<TestButton title="Send Message" onPress={handlePushMessage} />
<TestResult result={pushResult || null} />
</View>

{/* Pull Model (SharedBridge) */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Pull Model (SharedBridge HostObject)</Text>
<Text style={styles.statusText}>
SharedBridge: {bridgeAvailable ? 'Available' : 'Waiting...'}
{bridgeAvailable &&
` | isMain: ${globalThis.sharedBridge?.isMain}`}
</Text>
<TestButton
title="Send Message"
onPress={handlePostMessage}
title="Send Ping via SharedBridge"
onPress={handleSharedBridgePing}
disabled={!bridgeAvailable}
/>
<TestButton
title="Clear Log"
onPress={handleClearBridgeLog}
style={styles.clearButton}
/>
{bridgeResult ? (
<View style={styles.logContainer}>
<Text style={styles.logText}>{bridgeResult.trim()}</Text>
</View>
) : null}
</View>
</TestPageBase>
);
}

const styles = StyleSheet.create({
section: {
marginBottom: 20,
gap: 10,
},
sectionTitle: {
fontSize: 16,
fontWeight: '700',
color: '#333',
},
statusText: {
fontSize: 13,
color: '#666',
fontFamily: 'Courier New',
},
clearButton: {
backgroundColor: '#8E8E93',
},
logContainer: {
backgroundColor: '#1a1a2e',
padding: 12,
borderRadius: 8,
maxHeight: 300,
},
logText: {
fontSize: 12,
color: '#00ff88',
fontFamily: 'Courier New',
lineHeight: 18,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ Pod::Spec.new do |s|
s.platforms = { :ios => min_ios_version_supported }
s.source = { :git => "https://github.com/OneKeyHQ/app-modules/react-native-background-thread.git", :tag => "#{s.version}" }

s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}", "cpp/**/*.{h,cpp}"
s.public_header_files = "ios/**/*.h"
s.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/cpp"' }

s.dependency 'ReactNativeNativeLogger'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.13)
project(background_thread)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_VERBOSE_MAKEFILE ON)

find_package(fbjni REQUIRED CONFIG)
find_package(ReactAndroid REQUIRED CONFIG)

add_library(${PROJECT_NAME} SHARED
src/main/cpp/cpp-adapter.cpp
../cpp/SharedBridge.cpp
)

target_include_directories(${PROJECT_NAME} PRIVATE
src/main/cpp
../cpp
)

find_library(LOG_LIB log)

target_link_libraries(${PROJECT_NAME}
${LOG_LIB}
fbjni::fbjni
ReactAndroid::jsi
ReactAndroid::reactnative
android
)
42 changes: 42 additions & 0 deletions native-modules/react-native-background-thread/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ def getExtOrIntegerDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["BackgroundThread_" + name]).toInteger()
}

def reactNativeArchitectures() {
def value = rootProject.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}

android {
namespace "com.backgroundthread"

Expand All @@ -33,10 +38,47 @@ android {
defaultConfig {
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")

externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
arguments "-DANDROID_STL=c++_shared",
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
abiFilters(*reactNativeArchitectures())
}
}
}

externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}

buildFeatures {
buildConfig true
prefab true
}

packagingOptions {
excludes = [
"META-INF",
"META-INF/**",
"**/libc++_shared.so",
"**/libfbjni.so",
"**/libjsi.so",
"**/libfolly_json.so",
"**/libfolly_runtime.so",
"**/libglog.so",
"**/libhermes.so",
"**/libhermes-executor-debug.so",
"**/libhermes_executor.so",
"**/libreactnative.so",
"**/libreactnativejni.so",
"**/libturbomodulejsijni.so",
"**/libreact_nativemodule_core.so",
"**/libjscexecutor.so"
]
}

buildTypes {
Expand Down
Loading