@rn-org/react-native-thread 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +440 -0
- package/ReactNativeThread.podspec +20 -0
- package/android/build.gradle +69 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/rnorg/reactnativethread/ReactNativeThreadModule.kt +162 -0
- package/android/src/main/java/com/rnorg/reactnativethread/ReactNativeThreadPackage.kt +31 -0
- package/ios/ReactNativeThread.h +6 -0
- package/ios/ReactNativeThread.mm +180 -0
- package/lib/module/NativeReactNativeThread.js +5 -0
- package/lib/module/NativeReactNativeThread.js.map +1 -0
- package/lib/module/babel-plugin.js +71 -0
- package/lib/module/babel-plugin.js.map +1 -0
- package/lib/module/globals.d.js +2 -0
- package/lib/module/globals.d.js.map +1 -0
- package/lib/module/index.js +166 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeReactNativeThread.d.ts +20 -0
- package/lib/typescript/src/NativeReactNativeThread.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +49 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +179 -0
- package/src/NativeReactNativeThread.ts +24 -0
- package/src/babel-plugin.js +77 -0
- package/src/globals.d.ts +34 -0
- package/src/index.tsx +211 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type TurboModule } from 'react-native';
|
|
2
|
+
export interface Spec extends TurboModule {
|
|
3
|
+
/**
|
|
4
|
+
* Create a new isolated JS thread. Returns a numeric thread ID.
|
|
5
|
+
*/
|
|
6
|
+
createThread(): number;
|
|
7
|
+
/**
|
|
8
|
+
* Execute a JS code string on an existing thread.
|
|
9
|
+
*/
|
|
10
|
+
runOnThread(threadId: number, code: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Destroy a thread and free its resources.
|
|
13
|
+
*/
|
|
14
|
+
destroyThread(threadId: number): void;
|
|
15
|
+
addListener(eventName: string): void;
|
|
16
|
+
removeListeners(count: number): void;
|
|
17
|
+
}
|
|
18
|
+
declare const _default: Spec;
|
|
19
|
+
export default _default;
|
|
20
|
+
//# sourceMappingURL=NativeReactNativeThread.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeReactNativeThread.d.ts","sourceRoot":"","sources":["../../../src/NativeReactNativeThread.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC;;OAEG;IACH,YAAY,IAAI,MAAM,CAAC;IAEvB;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAElD;;OAEG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAGtC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;;AAED,wBAA2E"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type ThreadTask = ((args?: any) => void) | string;
|
|
2
|
+
/** Snapshot of a single live thread. */
|
|
3
|
+
export type ThreadInfo = {
|
|
4
|
+
readonly id: number;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
};
|
|
7
|
+
export type ThreadHandle = {
|
|
8
|
+
readonly id: number;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
/**
|
|
11
|
+
* Run a task on this thread.
|
|
12
|
+
* @param params - Optional JSON-serialisable value passed as the first
|
|
13
|
+
* argument to the thread function.
|
|
14
|
+
*/
|
|
15
|
+
run(task: ThreadTask, params?: unknown): void;
|
|
16
|
+
/** Subscribe to messages from this thread. Returns an unsubscribe function. */
|
|
17
|
+
onMessage(handler: (data: unknown) => void): () => void;
|
|
18
|
+
/** Returns a Promise that resolves with the next message from this thread. */
|
|
19
|
+
onMessage(): Promise<unknown>;
|
|
20
|
+
destroy(): void;
|
|
21
|
+
};
|
|
22
|
+
export declare function runOnJS(task: ThreadTask, params?: unknown): void;
|
|
23
|
+
/**
|
|
24
|
+
* Runs a task on a brand-new isolated thread and returns a handle to it.
|
|
25
|
+
* @param name - Optional display name (default: `RNThread-<id>`).
|
|
26
|
+
*/
|
|
27
|
+
export declare function runOnNewJS(task: ThreadTask, params?: unknown, name?: string): ThreadHandle;
|
|
28
|
+
/**
|
|
29
|
+
* Creates a persistent thread. Optionally give it a name.
|
|
30
|
+
* @param name - Display name (default: `RNThread-<id>`).
|
|
31
|
+
*/
|
|
32
|
+
export declare function createThread(name?: string): ThreadHandle;
|
|
33
|
+
/**
|
|
34
|
+
* Returns info about every live thread currently managed by this library,
|
|
35
|
+
* including the shared `runOnJS` thread once it has been started.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getThreads(): ThreadInfo[];
|
|
38
|
+
/**
|
|
39
|
+
* Destroy a thread by its numeric ID **or** by its name.
|
|
40
|
+
* When a name is given, the first matching thread is destroyed.
|
|
41
|
+
* This is also what `ThreadHandle.destroy()` calls internally.
|
|
42
|
+
*/
|
|
43
|
+
export declare function destroyThread(idOrName: number | string): void;
|
|
44
|
+
export declare function onMessage(handler: (data: unknown, threadId: number) => void): () => void;
|
|
45
|
+
export declare function onMessage(): Promise<{
|
|
46
|
+
data: unknown;
|
|
47
|
+
threadId: number;
|
|
48
|
+
}>;
|
|
49
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC;AAEzD,wCAAwC;AACxC,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9C,+EAA+E;IAC/E,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACxD,8EAA8E;IAC9E,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,OAAO,IAAI,IAAI,CAAC;CACjB,CAAC;AAyDF,wBAAgB,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAEhE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,UAAU,EAChB,MAAM,CAAC,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,MAAM,GACZ,YAAY,CAMd;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,YAAY,CAKxD;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,UAAU,EAAE,CAEzC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA0B7D;AAED,wBAAgB,SAAS,CACvB,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GACjD,MAAM,IAAI,CAAC;AACd,wBAAgB,SAAS,IAAI,OAAO,CAAC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rn-org/react-native-thread",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Run JavaScript on real background threads in React Native — no Workers, no Worklets. Uses JavaScriptCore on iOS and Mozilla Rhino on Android. Built as a New Architecture TurboModule.",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./babel-plugin": "./src/babel-plugin.js",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"lib",
|
|
19
|
+
"android",
|
|
20
|
+
"ios",
|
|
21
|
+
"cpp",
|
|
22
|
+
"*.podspec",
|
|
23
|
+
"react-native.config.js",
|
|
24
|
+
"!ios/build",
|
|
25
|
+
"!android/build",
|
|
26
|
+
"!android/gradle",
|
|
27
|
+
"!android/gradlew",
|
|
28
|
+
"!android/gradlew.bat",
|
|
29
|
+
"!android/local.properties",
|
|
30
|
+
"!**/__tests__",
|
|
31
|
+
"!**/__fixtures__",
|
|
32
|
+
"!**/__mocks__",
|
|
33
|
+
"!**/.*"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"example": "yarn workspace @rn-org/react-native-thread-example",
|
|
37
|
+
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
38
|
+
"prepare": "bob build",
|
|
39
|
+
"typecheck": "tsc",
|
|
40
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
41
|
+
"test": "jest",
|
|
42
|
+
"release": "release-it --only-version"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"react-native",
|
|
46
|
+
"ios",
|
|
47
|
+
"android",
|
|
48
|
+
"thread",
|
|
49
|
+
"background-thread",
|
|
50
|
+
"multithreading",
|
|
51
|
+
"javascript-core",
|
|
52
|
+
"rhino",
|
|
53
|
+
"turbo-module",
|
|
54
|
+
"new-architecture",
|
|
55
|
+
"worker",
|
|
56
|
+
"postmessage"
|
|
57
|
+
],
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "git+https://github.com/rn-org/react-native-thread.git"
|
|
61
|
+
},
|
|
62
|
+
"author": "sriharshamadamanchi <sriharshamadamanchiz@gmail.com> (https://github.com/rn-org)",
|
|
63
|
+
"license": "MIT",
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/rn-org/react-native-thread/issues"
|
|
66
|
+
},
|
|
67
|
+
"homepage": "https://github.com/rn-org/react-native-thread#readme",
|
|
68
|
+
"publishConfig": {
|
|
69
|
+
"registry": "https://registry.npmjs.org/"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
73
|
+
"@eslint/compat": "^1.3.2",
|
|
74
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
75
|
+
"@eslint/js": "^9.35.0",
|
|
76
|
+
"@react-native/babel-preset": "0.83.0",
|
|
77
|
+
"@react-native/eslint-config": "0.83.0",
|
|
78
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
79
|
+
"@types/jest": "^29.5.14",
|
|
80
|
+
"@types/react": "^19.2.0",
|
|
81
|
+
"commitlint": "^19.8.1",
|
|
82
|
+
"del-cli": "^6.0.0",
|
|
83
|
+
"eslint": "^9.35.0",
|
|
84
|
+
"eslint-config-prettier": "^10.1.8",
|
|
85
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
86
|
+
"jest": "^29.7.0",
|
|
87
|
+
"lefthook": "^2.0.3",
|
|
88
|
+
"prettier": "^2.8.8",
|
|
89
|
+
"react": "19.2.0",
|
|
90
|
+
"react-native": "0.83.0",
|
|
91
|
+
"react-native-builder-bob": "^0.40.18",
|
|
92
|
+
"release-it": "^19.0.4",
|
|
93
|
+
"turbo": "^2.5.6",
|
|
94
|
+
"typescript": "^5.9.2"
|
|
95
|
+
},
|
|
96
|
+
"peerDependencies": {
|
|
97
|
+
"react": "*",
|
|
98
|
+
"react-native": "*"
|
|
99
|
+
},
|
|
100
|
+
"workspaces": [
|
|
101
|
+
"example"
|
|
102
|
+
],
|
|
103
|
+
"packageManager": "yarn@4.11.0",
|
|
104
|
+
"react-native-builder-bob": {
|
|
105
|
+
"source": "src",
|
|
106
|
+
"output": "lib",
|
|
107
|
+
"targets": [
|
|
108
|
+
[
|
|
109
|
+
"module",
|
|
110
|
+
{
|
|
111
|
+
"esm": true
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
"typescript",
|
|
116
|
+
{
|
|
117
|
+
"project": "tsconfig.build.json"
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
"codegenConfig": {
|
|
123
|
+
"name": "ReactNativeThreadSpec",
|
|
124
|
+
"type": "modules",
|
|
125
|
+
"jsSrcsDir": "src",
|
|
126
|
+
"android": {
|
|
127
|
+
"javaPackageName": "com.rnorg.reactnativethread"
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"prettier": {
|
|
131
|
+
"quoteProps": "consistent",
|
|
132
|
+
"singleQuote": true,
|
|
133
|
+
"tabWidth": 2,
|
|
134
|
+
"trailingComma": "es5",
|
|
135
|
+
"useTabs": false
|
|
136
|
+
},
|
|
137
|
+
"jest": {
|
|
138
|
+
"preset": "react-native",
|
|
139
|
+
"modulePathIgnorePatterns": [
|
|
140
|
+
"<rootDir>/example/node_modules",
|
|
141
|
+
"<rootDir>/lib/"
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
"commitlint": {
|
|
145
|
+
"extends": [
|
|
146
|
+
"@commitlint/config-conventional"
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
"release-it": {
|
|
150
|
+
"git": {
|
|
151
|
+
"commitMessage": "chore: release ${version}",
|
|
152
|
+
"tagName": "v${version}"
|
|
153
|
+
},
|
|
154
|
+
"npm": {
|
|
155
|
+
"publish": true
|
|
156
|
+
},
|
|
157
|
+
"github": {
|
|
158
|
+
"release": true
|
|
159
|
+
},
|
|
160
|
+
"plugins": {
|
|
161
|
+
"@release-it/conventional-changelog": {
|
|
162
|
+
"preset": {
|
|
163
|
+
"name": "angular"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"create-react-native-library": {
|
|
169
|
+
"type": "turbo-module",
|
|
170
|
+
"languages": "kotlin-objc",
|
|
171
|
+
"tools": [
|
|
172
|
+
"eslint",
|
|
173
|
+
"jest",
|
|
174
|
+
"lefthook",
|
|
175
|
+
"release-it"
|
|
176
|
+
],
|
|
177
|
+
"version": "0.57.2"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TurboModuleRegistry, type TurboModule } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface Spec extends TurboModule {
|
|
4
|
+
/**
|
|
5
|
+
* Create a new isolated JS thread. Returns a numeric thread ID.
|
|
6
|
+
*/
|
|
7
|
+
createThread(): number;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Execute a JS code string on an existing thread.
|
|
11
|
+
*/
|
|
12
|
+
runOnThread(threadId: number, code: string): void;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Destroy a thread and free its resources.
|
|
16
|
+
*/
|
|
17
|
+
destroyThread(threadId: number): void;
|
|
18
|
+
|
|
19
|
+
// Required by NativeEventEmitter
|
|
20
|
+
addListener(eventName: string): void;
|
|
21
|
+
removeListeners(count: number): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('ReactNativeThread');
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Babel plugin for @rn-org/react-native-thread
|
|
3
|
+
*
|
|
4
|
+
* Hermes compiles JS to bytecode at build time, so fn.toString() at runtime
|
|
5
|
+
* returns a placeholder ("bytecode") rather than the original source.
|
|
6
|
+
*
|
|
7
|
+
* This plugin runs at compile time and replaces arrow-function / function-
|
|
8
|
+
* expression arguments passed to the thread API with string literals that
|
|
9
|
+
* contain the source wrapped as an IIFE. The background thread engine
|
|
10
|
+
* (JSC on iOS, Rhino on Android) can then simply eval() the string.
|
|
11
|
+
*
|
|
12
|
+
* Transforms:
|
|
13
|
+
* runOnJS((args) => { ... }) → runOnJS("((args) => { ... })(__params__)")
|
|
14
|
+
* runOnNewJS((args) => { ... }) → runOnNewJS("((args) => { ... })(__params__)")
|
|
15
|
+
* thread.run((args) => { ... }) → thread.run("((args) => { ... })(__params__)")
|
|
16
|
+
*
|
|
17
|
+
* Add to your app's babel.config.js:
|
|
18
|
+
* plugins: ['@rn-org/react-native-thread/babel-plugin']
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
// Top-level function names that take a fn-or-code as their first argument.
|
|
24
|
+
const TOP_LEVEL_API = new Set(['runOnJS', 'runOnNewJS']);
|
|
25
|
+
|
|
26
|
+
// Method names on a ThreadHandle that take a fn-or-code as their first arg.
|
|
27
|
+
const METHOD_API = new Set(['run']);
|
|
28
|
+
|
|
29
|
+
module.exports = function reactNativeThreadPlugin({ types: t }) {
|
|
30
|
+
/**
|
|
31
|
+
* If `argNode` is an arrow function or function expression, replace the
|
|
32
|
+
* argument at `index` with a string literal `"(<source>)()"`.
|
|
33
|
+
*/
|
|
34
|
+
function maybeTransformArg(callPath, state, argNode, index) {
|
|
35
|
+
if (
|
|
36
|
+
!t.isArrowFunctionExpression(argNode) &&
|
|
37
|
+
!t.isFunctionExpression(argNode)
|
|
38
|
+
) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const originalCode = state.file.code;
|
|
43
|
+
const fnSource = originalCode.slice(argNode.start, argNode.end);
|
|
44
|
+
const iife = `(${fnSource})(__params__)`;
|
|
45
|
+
|
|
46
|
+
callPath.node.arguments[index] = t.stringLiteral(iife);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
name: 'react-native-thread',
|
|
51
|
+
visitor: {
|
|
52
|
+
CallExpression(path, state) {
|
|
53
|
+
const { callee, arguments: args } = path.node;
|
|
54
|
+
|
|
55
|
+
// runOnJS(() => {}) / runOnNewJS(() => {})
|
|
56
|
+
if (t.isIdentifier(callee) && TOP_LEVEL_API.has(callee.name)) {
|
|
57
|
+
if (args.length >= 1) {
|
|
58
|
+
maybeTransformArg(path, state, args[0], 0);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// someThread.run(() => {})
|
|
64
|
+
if (
|
|
65
|
+
t.isMemberExpression(callee) &&
|
|
66
|
+
!callee.computed &&
|
|
67
|
+
t.isIdentifier(callee.property) &&
|
|
68
|
+
METHOD_API.has(callee.property.name)
|
|
69
|
+
) {
|
|
70
|
+
if (args.length >= 1) {
|
|
71
|
+
maybeTransformArg(path, state, args[0], 0);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
};
|
package/src/globals.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global injected by react-native-thread into every background JS runtime.
|
|
3
|
+
* Call this from inside a `runOnJS`, `runOnNewJS`, or `thread.run()` callback
|
|
4
|
+
* to send data back to the main JS thread.
|
|
5
|
+
*
|
|
6
|
+
* The value is automatically JSON-serialised before being sent and
|
|
7
|
+
* JSON-parsed when received by the `onMessage` handler.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* thread.run(() => {
|
|
11
|
+
* const result = heavyWork();
|
|
12
|
+
* postMessage({ result });
|
|
13
|
+
* });
|
|
14
|
+
*/
|
|
15
|
+
declare function postMessage(data: unknown): void;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parameters passed by the caller via the optional second argument of
|
|
19
|
+
* `thread.run(fn, params)`, `runOnJS(fn, params)`, or `runOnNewJS(fn, params)`.
|
|
20
|
+
*
|
|
21
|
+
* The value is whatever was passed in — it is JSON-serialised on the main
|
|
22
|
+
* thread and parsed before the thread function runs, so it must be a
|
|
23
|
+
* JSON-compatible value.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* worker.run((args) => {
|
|
27
|
+
* console.log(args.name); // 'hello'
|
|
28
|
+
* }, { name: 'hello' });
|
|
29
|
+
*
|
|
30
|
+
* @deprecated Use the callback argument instead: `thread.run((args) => { ... }, params)`.
|
|
31
|
+
* `__params__` is still injected for raw code strings but no longer needed for
|
|
32
|
+
* function callbacks.
|
|
33
|
+
*/
|
|
34
|
+
declare const __params__: any;
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { NativeEventEmitter } from 'react-native';
|
|
2
|
+
import ReactNativeThread from './NativeReactNativeThread';
|
|
3
|
+
|
|
4
|
+
export type ThreadTask = ((args?: any) => void) | string;
|
|
5
|
+
|
|
6
|
+
/** Snapshot of a single live thread. */
|
|
7
|
+
export type ThreadInfo = {
|
|
8
|
+
readonly id: number;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ThreadHandle = {
|
|
13
|
+
readonly id: number;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
/**
|
|
16
|
+
* Run a task on this thread.
|
|
17
|
+
* @param params - Optional JSON-serialisable value passed as the first
|
|
18
|
+
* argument to the thread function.
|
|
19
|
+
*/
|
|
20
|
+
run(task: ThreadTask, params?: unknown): void;
|
|
21
|
+
/** Subscribe to messages from this thread. Returns an unsubscribe function. */
|
|
22
|
+
onMessage(handler: (data: unknown) => void): () => void;
|
|
23
|
+
/** Returns a Promise that resolves with the next message from this thread. */
|
|
24
|
+
onMessage(): Promise<unknown>;
|
|
25
|
+
destroy(): void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const _emitter = new NativeEventEmitter(ReactNativeThread);
|
|
29
|
+
const RN_THREAD_MESSAGE_EVENT = 'RNThreadMessage';
|
|
30
|
+
|
|
31
|
+
// ─── Thread registry (JS-layer; names are a JS concept) ──────────────────────
|
|
32
|
+
const _registry = new Map<number, ThreadInfo>();
|
|
33
|
+
|
|
34
|
+
function _register(id: number, name: string): void {
|
|
35
|
+
_registry.set(id, { id, name });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function _unregister(id: number): void {
|
|
39
|
+
_registry.delete(id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toCode(task: ThreadTask, params?: unknown): string {
|
|
43
|
+
if (typeof task === 'string') {
|
|
44
|
+
// Always declare __params__ so Babel-transformed strings can pass it as
|
|
45
|
+
// an argument to the function, and raw code strings that reference
|
|
46
|
+
// __params__ directly continue to work.
|
|
47
|
+
const paramsJson =
|
|
48
|
+
params !== undefined ? JSON.stringify(params) : 'undefined';
|
|
49
|
+
return `var __params__ = ${paramsJson};\n${task}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const src = task.toString();
|
|
53
|
+
if (
|
|
54
|
+
src.includes('[bytecode]') ||
|
|
55
|
+
/^\s*function\s*\(\)\s*\{\s*bytecode\s*\}/.test(src)
|
|
56
|
+
) {
|
|
57
|
+
if (__DEV__) {
|
|
58
|
+
console.warn(
|
|
59
|
+
'[react-native-thread] fn.toString() returned a Hermes bytecode placeholder. ' +
|
|
60
|
+
"Add the Babel plugin: plugins: ['@rn-org/react-native-thread/babel-plugin']"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return '/* bytecode: no-op */';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const argsStr = params !== undefined ? JSON.stringify(params) : '';
|
|
67
|
+
return `(${src})(${argsStr})`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Shared thread (runOnJS) ──────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
const SHARED_THREAD_NAME = 'RNOrgThread';
|
|
73
|
+
let _sharedThreadId: number | null = null;
|
|
74
|
+
|
|
75
|
+
function getSharedThread(): number {
|
|
76
|
+
if (_sharedThreadId === null) {
|
|
77
|
+
_sharedThreadId = ReactNativeThread.createThread();
|
|
78
|
+
_register(_sharedThreadId, SHARED_THREAD_NAME);
|
|
79
|
+
}
|
|
80
|
+
return _sharedThreadId;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function runOnJS(task: ThreadTask, params?: unknown): void {
|
|
84
|
+
ReactNativeThread.runOnThread(getSharedThread(), toCode(task, params));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Runs a task on a brand-new isolated thread and returns a handle to it.
|
|
89
|
+
* @param name - Optional display name (default: `RNThread-<id>`).
|
|
90
|
+
*/
|
|
91
|
+
export function runOnNewJS(
|
|
92
|
+
task: ThreadTask,
|
|
93
|
+
params?: unknown,
|
|
94
|
+
name?: string
|
|
95
|
+
): ThreadHandle {
|
|
96
|
+
const id = ReactNativeThread.createThread();
|
|
97
|
+
const resolvedName = name ?? `RNThread-${id}`;
|
|
98
|
+
_register(id, resolvedName);
|
|
99
|
+
ReactNativeThread.runOnThread(id, toCode(task, params));
|
|
100
|
+
return createThreadHandle(id, resolvedName);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates a persistent thread. Optionally give it a name.
|
|
105
|
+
* @param name - Display name (default: `RNThread-<id>`).
|
|
106
|
+
*/
|
|
107
|
+
export function createThread(name?: string): ThreadHandle {
|
|
108
|
+
const id = ReactNativeThread.createThread();
|
|
109
|
+
const resolvedName = name ?? `RNThread-${id}`;
|
|
110
|
+
_register(id, resolvedName);
|
|
111
|
+
return createThreadHandle(id, resolvedName);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns info about every live thread currently managed by this library,
|
|
116
|
+
* including the shared `runOnJS` thread once it has been started.
|
|
117
|
+
*/
|
|
118
|
+
export function getThreads(): ThreadInfo[] {
|
|
119
|
+
return Array.from(_registry.values());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Destroy a thread by its numeric ID **or** by its name.
|
|
124
|
+
* When a name is given, the first matching thread is destroyed.
|
|
125
|
+
* This is also what `ThreadHandle.destroy()` calls internally.
|
|
126
|
+
*/
|
|
127
|
+
export function destroyThread(idOrName: number | string): void {
|
|
128
|
+
let targetId: number | undefined;
|
|
129
|
+
|
|
130
|
+
if (typeof idOrName === 'number') {
|
|
131
|
+
if (_registry.has(idOrName)) targetId = idOrName;
|
|
132
|
+
} else {
|
|
133
|
+
for (const info of _registry.values()) {
|
|
134
|
+
if (info.name === idOrName) {
|
|
135
|
+
targetId = info.id;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (targetId === undefined) {
|
|
142
|
+
if (__DEV__) {
|
|
143
|
+
console.warn(
|
|
144
|
+
`[react-native-thread] destroyThread: no thread found for "${idOrName}"`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_unregister(targetId);
|
|
151
|
+
if (targetId === _sharedThreadId) _sharedThreadId = null;
|
|
152
|
+
ReactNativeThread.destroyThread(targetId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function onMessage(
|
|
156
|
+
handler: (data: unknown, threadId: number) => void
|
|
157
|
+
): () => void;
|
|
158
|
+
export function onMessage(): Promise<{ data: unknown; threadId: number }>;
|
|
159
|
+
export function onMessage(
|
|
160
|
+
handler?: (data: unknown, threadId: number) => void
|
|
161
|
+
): (() => void) | Promise<{ data: unknown; threadId: number }> {
|
|
162
|
+
if (handler) {
|
|
163
|
+
const sub = _emitter.addListener(RN_THREAD_MESSAGE_EVENT, (event: any) => {
|
|
164
|
+
let parsed: unknown = event.data;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(event.data);
|
|
167
|
+
} catch {}
|
|
168
|
+
handler(parsed, event.threadId as number);
|
|
169
|
+
});
|
|
170
|
+
return () => sub.remove();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return new Promise<{ data: unknown; threadId: number }>((resolve) => {
|
|
174
|
+
const sub = _emitter.addListener(RN_THREAD_MESSAGE_EVENT, (event: any) => {
|
|
175
|
+
let parsed: unknown = event.data;
|
|
176
|
+
try {
|
|
177
|
+
parsed = JSON.parse(event.data);
|
|
178
|
+
} catch {}
|
|
179
|
+
sub.remove();
|
|
180
|
+
resolve({ data: parsed, threadId: event.threadId as number });
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function createThreadHandle(id: number, name: string): ThreadHandle {
|
|
186
|
+
return {
|
|
187
|
+
id,
|
|
188
|
+
name,
|
|
189
|
+
run(task: ThreadTask, params?: unknown) {
|
|
190
|
+
ReactNativeThread.runOnThread(id, toCode(task, params));
|
|
191
|
+
},
|
|
192
|
+
onMessage: function (handler?: (data: unknown) => void) {
|
|
193
|
+
if (handler) {
|
|
194
|
+
return onMessage((data, threadId) => {
|
|
195
|
+
if (threadId === id) handler(data);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return new Promise<unknown>((resolve) => {
|
|
199
|
+
const unsub = onMessage((data, threadId) => {
|
|
200
|
+
if (threadId === id) {
|
|
201
|
+
unsub();
|
|
202
|
+
resolve(data);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
} as ThreadHandle['onMessage'],
|
|
207
|
+
destroy() {
|
|
208
|
+
destroyThread(id);
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|