npm package: @rn-org/react-native-thread
Description: Run JavaScript on real background threads in React Native — no Workers, no Worklets. Uses Hermes on both iOS and Android, each on a dedicated OS-level thread. Built as a New Architecture TurboModule.
Author: sriharshamadamanchi
Latest version: 0.8.3

# @rn-org/react-native-thread

Run JavaScript on real background threads in React Native — no Workers, no Worklets. Uses **Hermes** on both iOS and Android, each on a dedicated OS-level thread. Built as a New Architecture **TurboModule**.

---

## Contents

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Babel plugin (required for Hermes)](#babel-plugin-required-for-hermes)
- [Quick start](#quick-start)
- [API reference](#api-reference)
  - [runOnJS](#runonjs)
  - [runOnNewJS](#runonewjs)
  - [createThread](#createthread)
  - [getThreads](#getthreads)
  - [destroyThread](#destroythread)
  - [onMessage](#onmessage)
  - [ThreadHandle](#threadhandle)
  - [ThreadInfo](#threadinfo)
  - [ThreadTask](#threadtask)
- [Thread globals](#thread-globals)
  - [resolveThreadMessage](#resolvethreadmessage)
  - [console](#console)
  - [`__params__`](#__params__)
- [Hermes & Babel plugin](#hermes--babel-plugin-deep-dive)
  - [Task function](#task-function-first-argument)
  - [Function params](#function-params-second-argument)
  - [Cross-module imports](#cross-module-imports)
  - [Supported call sites](#supported-call-sites)
  - [What the plugin captures](#what-the-plugin-captures-in-params)
  - [Limitations](#limitations)
- [Constraints](#constraints)
- [Contributing](#contributing)
- [License](#license)

---

## Features

- **True background threads** — each thread runs its own isolated **Hermes** runtime on a dedicated OS-level thread; the main React Native runtime is never blocked. Works on both **iOS** and **Android**.
- **Unlimited threads** — create as many threads as you need; each is isolated.
- **Shared thread** (`runOnJS`) — fire-and-forget tasks on a single persistent background thread; no teardown required.
- **Promise-based `run()`** — `thread.run(fn, params)` returns a `Promise` that resolves with the value passed to `resolve(data)` and rejects on uncaught thread errors. Chain `.then()` / `.catch()` or use `await`.
- **Parameter injection** — pass values from the main thread into the background function as `(args, resolve) => { ... }`. Supports primitives, objects, arrays, **and functions**.
- **`resolve` as second param** — the callback to send a result back is passed directly as the second argument to your task function — no globals needed.
- **Function params** — pass functions (including imported ones) alongside plain data in the params object. The Babel plugin extracts their source at compile time and captures closed-over variables transitively.
- **Cross-module function params** — functions imported from other files (e.g. `import { compute } from './math'`) are automatically resolved and inlined by the Babel plugin.
- **Named threads** — give threads friendly names; list or destroy them by name.
- **Error handling** — thread exceptions are automatically caught and forwarded to the main thread via the rejected `Promise`.
- **Full `console` support** — `console.log/info/warn/error/debug` work inside threads and appear in Logcat / Xcode logs.
- **Timer support** — `setTimeout`, `setInterval`, `clearTimeout`, and `clearInterval` work inside threads.
- **Hermes-safe** — ships a Babel plugin that extracts function source at compile time so Hermes bytecode never breaks serialisation. Required on both iOS and Android since both platforms use Hermes.
- **New Architecture only** — built on the TurboModule / Codegen pipeline.

---

## Requirements

| | Minimum |
|---|---|
| React Native | 0.76+ (New Architecture) |
| iOS | 15.1 |
| Android | API 24 |
| Node | 18+ |

---

## Installation

```sh
npm install @rn-org/react-native-thread
# or
yarn add @rn-org/react-native-thread
```

**iOS** — run pod install:

```sh
cd ios && pod install
```

The `hermes-engine` pod is declared as a dependency in the library's podspec; no extra configuration needed.

**Android** — the Hermes native dependency is declared in the library's `build.gradle`; no extra steps needed.

---

## Babel plugin (required for Hermes)

Hermes compiles your JS to bytecode at build time. That means `fn.toString()` at runtime returns a placeholder (`"function () { [bytecode] }"`) instead of source code. The library includes a Babel plugin that:

1. Extracts the **task function** (first argument) source at compile time and replaces it with a string literal.
2. Detects **function-valued properties** in the params object (second argument) and inlines their source.
3. **Captures closed-over variables** transitively — if a function references outer `const`/`let`/`var` bindings (literals or other functions), those are bundled into a self-contained IIFE.
4. **Resolves cross-module imports** — functions imported from relative paths (e.g. `import { fn } from './utils'`) are read from disk and inlined.
5. **Strips TypeScript** annotations automatically when the source file is `.ts` or `.tsx`.

Add the plugin to your app's `babel.config.js`:

```js
// babel.config.js
module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: [
    '@rn-org/react-native-thread/babel-plugin',
    // ... your other plugins
  ],
};
```

The plugin is a no-op on non-Hermes builds (V8, etc.).

> **Note:** The plugin safely handles cases where `@react-native/babel-preset` transforms `async` functions before the plugin's visitor fires. It falls back to the original source text via Babel's preserved AST positions.

---

## Quick start

```tsx
import { runOnJS, createThread, getThreads, destroyThread } from '@rn-org/react-native-thread';

// ── 1. Fire and forget on the shared background thread ──────────────────────
runOnJS((args) => {
  console.log('Limit is', args.limit);
}, { limit: 42 });

// ── 2. Create a named, persistent thread ────────────────────────────────────
const thread = createThread('MyThread');

// Send work + params — thread.run() returns a Promise
const result = await thread.run(
  (args, resolve) => {
    var sum = 0;
    for (var i = 0; i < args.limit; i++) sum += i;
    resolve({ sum });
  },
  { limit: 1_000_000 }
);
console.log(result); // { sum: 499999500000 }

// ── 2b. Pass functions as params ────────────────────────────────────────────
function multiply(a, b) {
  return a * b;
}

const product = await thread.run(
  (args, resolve) => {
    resolve(args.multiply(args.x, args.y));
  },
  { x: 6, y: 7, multiply }
);
console.log(product); // 42

// Async function params work too — the Babel plugin transforms async/await
// to generator-based code before sending to the Hermes thread runtime:
const isPrime = async (num) => {
  if (num <= 1) return 'Not Prime';
  for (let i = 2; i < num; i++) {
    if (num % i === 0) return 'Not Prime';
  }
  return 'Prime';
};

const verdict = await thread.run(
  async (args, resolve) => {
    const result = await args.isPrime(args.num);
    resolve(`${args.num} is ${result}`);
  },
  { num: 17, isPrime }
);
console.log(verdict); // '17 is Prime'

// Functions that close over outer variables work too:
const factor = 10;
const scale = (n) => n * factor;

const scaled = await thread.run(
  (args, resolve) => {
    resolve(args.scale(5)); // 50
  },
  { scale }
);

// Imported functions are also supported:
import { checkEvenOdd } from './utils';

const parity = await thread.run(
  (args, resolve) => {
    resolve(args.checkEvenOdd(args.num));
  },
  { num: 42, checkEvenOdd }
);

// Error handling — thread.run() rejects if the thread throws
try {
  const data = await thread.run((args, resolve) => {
    throw new Error('something went wrong');
  });
} catch (err) {
  console.warn(err.message); // 'something went wrong'
}

// Or with .then() / .catch()
thread
  .run(async (args, resolve) => {
    const result = await args.isPrime(args.num);
    resolve(`${args.num} is ${result}`);
  }, { num: 17, isPrime })
  .then((data) => console.log(data))
  .catch((err) => console.warn(err.message));

// ── 3. List all running threads ─────────────────────────────────────────────
console.log(getThreads());
// [{ id: 1, name: 'RNOrgThread' }, { id: 2, name: 'MyThread' }]

// ── 4. Clean up ─────────────────────────────────────────────────────────────
thread.destroy();               // by handle
destroyThread('MyThread');      // by name — same effect
destroyThread(2);               // by id   — same effect
```

---

## API reference

### `runOnJS`

```ts
function runOnJS(task: ThreadTask, params?: unknown): void
```

Runs `task` on the **shared persistent background thread** (named `"RNOrgThread"`). The thread is created on first use and lives for the lifetime of the module. Ideal for fire-and-forget work where you don't need the overhead of managing a dedicated thread.

| Parameter | Type | Description |
|---|---|---|
| `task` | `ThreadTask` | Function or code string to execute. |
| `params` | `unknown` | Optional JSON-serialisable value passed as the first argument to the function. |

---

### `runOnNewJS`

```ts
function runOnNewJS(task: ThreadTask, params?: unknown, name?: string): ThreadHandle
```

Creates a **new isolated thread**, immediately runs `task` on it, and returns a `ThreadHandle`. The thread persists until you call `handle.destroy()` or `destroyThread(...)`.

| Parameter | Type | Description |
|---|---|---|
| `task` | `ThreadTask` | Function or code string to execute. |
| `params` | `unknown` | Optional value passed as the first argument to the function. |
| `name` | `string` | Optional display name. Default: `RNThread-<id>`. |

---

### `createThread`

```ts
function createThread(name?: string): ThreadHandle
```

Creates a **new persistent named thread** without immediately running any code. Use the returned `ThreadHandle` to dispatch work at any time.

| Parameter | Type | Description |
|---|---|---|
| `name` | `string` | Optional display name. Default: `RNThread-<id>`. |

---

### `getThreads`

```ts
function getThreads(): ThreadInfo[]
```

Returns a snapshot of every live thread currently managed by the library, including the `runOnJS` shared thread once it has been started. Threads destroyed via `destroyThread` or `handle.destroy()` are removed from this list immediately.

```ts
const threads = getThreads();
// [
//   { id: 1, name: 'RNOrgThread' },
//   { id: 2, name: 'MyThread' },
// ]
```

---

### `destroyThread`

```ts
function destroyThread(idOrName: number | string): void
```

Destroys a thread and frees its resources. Accepts either the numeric thread ID or the thread's name. When a name is given, the first matching thread is destroyed.

This is the same function called by `ThreadHandle.destroy()`.

```ts
destroyThread('MyThread');   // by name
destroyThread(2);            // by id
```

In `__DEV__` mode a warning is logged if no matching thread is found.

---

### `onMessage`

```ts
// Callback — fires on every message from any thread
function onMessage(
  handler: (data: unknown, threadId: number) => void
): () => void

// Promise — resolves once on the next message from any thread
function onMessage(): Promise<{ data: unknown; threadId: number }>
```

Global listener that fires whenever **any** thread calls `resolveThreadMessage(data)`. Prefer `ThreadHandle.onMessage` if you want messages scoped to a single thread.

```ts
// Callback variant
const unsub = onMessage((data, threadId) => {
  console.log(`Thread ${threadId} sent:`, data);
});
unsub(); // remove listener

// Promise variant — awaits the next message from any thread
const { data, threadId } = await onMessage();
console.log(`Thread ${threadId} sent:`, data);
```

---

### `ThreadHandle`

Object returned by `createThread` and `runOnNewJS`.

```ts
type ThreadHandle = {
  readonly id: number;
  readonly name: string;
  /** Runs task on this thread. Resolves with the value passed to resolve(), rejects on error. */
  run(task: ThreadTask, params?: unknown): Promise<unknown>;
  destroy(): void;
};
```

| Member | Description |
|---|---|
| `id` | Numeric ID assigned by the native layer. |
| `name` | Display name provided at creation (or the default `RNThread-<id>`). |
| `run(task, params?)` | Execute `task` on this thread. Returns a `Promise` that resolves with the value passed to `resolve(data)` inside the task, or rejects if the thread throws. Can be called multiple times. |
| `destroy()` | Shut down the thread and remove it from the registry. Equivalent to calling `destroyThread(handle.id)`. |

---

### `ThreadInfo`

```ts
type ThreadInfo = {
  readonly id: number;
  readonly name: string;
};
```

Returned by `getThreads()`.

---

### `ThreadTask`

```ts
type ThreadTask =
  | ((args: any, resolve: (data: unknown) => void) => void)
  | string;
```

Either an arrow function / function expression (transformed by the Babel plugin) or a raw JS code string. When a function is used, `params` is passed as `args` (first argument) and the resolve callback as `resolve` (second argument). Call `resolve(data)` to return a value to the caller.

---

## Thread globals

These globals are available inside every thread function:

### `resolveThreadMessage`

```ts
declare function resolveThreadMessage(data: unknown): void
```

Sends `data` back to the main JS thread. The value is JSON-serialised in the background thread and JSON-parsed before reaching the `onMessage` handler. Must be JSON-serialisable (`object`, `array`, `string`, `number`, `boolean`, or `null`).

```ts
const data = await thread.run((args, resolve) => {
  resolve({ status: 'done', value: args.multiply * 2 });
}, { multiply: 21 });
console.log(data); // { status: 'done', value: 42 }
```

> `resolveThreadMessage` is also available as a global inside the thread (useful for raw code strings), and is the same function that `resolve` points to.

---

### `console`

`console.log`, `.info`, `.warn`, `.error`, and `.debug` are all available and route to:

- **iOS** — `NSLog`, visible in Xcode / Console.app; tagged `[RNThread-<id>] [Hermes]`.
- **Android** — `android.util.Log`, visible in Logcat; tagged `RNThread-<id>`.

---

### Timers

`setTimeout`, `clearTimeout`, `setInterval`, and `clearInterval` are available inside threads.

Both iOS and Android use the same Hermes-based event loop: after the initial evaluation, the thread drains all pending timers (and microtasks) before returning. The thread blocks until all timers have fired or been cleared.

```ts
thread.run(() => {
  setTimeout(() => {
    resolveThreadMessage('delayed hello');
  }, 2000);
});
```

> **Note:** The event loop blocks the thread until timers complete. Be careful with `setInterval` — the thread won't finish until the interval is cleared inside the thread.

---

### `__params__`

```ts
declare const __params__: any
```

Injected by the library when you pass a second argument to `run()`, `runOnJS()`, or `runOnNewJS()`. The value is JSON-serialised on the main thread and prepended to the code string as `var __params__ = <JSON>;`.

You can access the params value in two ways:

```ts
// Option A — args/resolve callback parameters
await thread.run(
  (args, resolve) => {
    for (var i = 0; i < args.iterations; i++) {
      // ...
    }
    resolve('done');
  },
  { iterations: 50_000 }
);

// Option B — __params__ global + resolveThreadMessage global
// (works in both functions and raw code strings)
await thread.run(
  (args, resolve) => {
    for (var i = 0; i < __params__.iterations; i++) {
      // ...
    }
    resolve('done');
  },
  { iterations: 50_000 }
);

// Option C — raw code string (__params__ and resolveThreadMessage globals)
thread.run(
  'for (var i = 0; i < __params__.iterations; i++) {} resolveThreadMessage("done")',
  { iterations: 50_000 }
);
```

---

## Hermes & Babel plugin deep dive

### The problem

The Hermes compiler converts your JS bundle to bytecode at build time. Any function whose `.toString()` is called at runtime returns something like:

```
function () { [bytecode] }
```

Because this library must *serialise* functions to strings and send them to a separate JS engine, `.toString()` alone doesn't work under Hermes.

### The solution

The included Babel plugin runs at **compile time** — before Hermes touches the code — and transforms both the task function and function-valued params.

#### Task function (first argument)

Arrow functions and function expressions (including `async`) are replaced with a string literal wrapped as an IIFE. `async`/`await` is transformed to generator-based code because Hermes' eval-mode compiler does not support async syntax. The generated call passes both `__params__` and the global `resolveThreadMessage` so they arrive as `args` and `resolve` respectively:

```js
// Input (your source)
thread.run(async (args, resolve) => {
  const result = await args.compute(args.num);
  resolve(result);
}, { num: 42, compute });

// Output (what Hermes compiles) — async transformed, compute inlined
thread.run("((function(){ ... _asyncToGenerator ... })())(__params__, resolveThreadMessage)", {
  num: 42,
  compute: { __rnThreadFn: "(function(){ ... })()" }
});
```

#### Function params (second argument)

Functions passed inside the params object are detected, extracted, and tagged with `{ __rnThreadFn: "<source>" }`. The runtime serializer emits the source directly into the thread code.

```js
// Input
const b = 3;
const add = (x) => x + b;
function multiply(a) {
  return add(a) * 2;
}
thread.run(fn, { multiply });

// Output — transitive closures are captured in a self-contained IIFE
thread.run(fn, {
  multiply: {
    __rnThreadFn: "(function(){ var b = 3;\nvar add = (x) => x + b;\nreturn function multiply(a) { return add(a) * 2; };})()" 
  }
});
```

#### Cross-module imports

Functions imported from relative paths are resolved from disk, parsed, and inlined:

```js
// utils.ts
export function checkEvenOdd(num: number): string {
  return num % 2 === 0 ? 'Even' : 'Odd';
}

// App.tsx
import { checkEvenOdd } from './utils';
thread.run(fn, { checkEvenOdd });
// → checkEvenOdd is inlined with TypeScript stripped
```

### Supported call sites

| Pattern | Task transformed | Params transformed |
|---|---|---|
| `runOnJS((args) => { ... }, { fn })` | Yes | Yes |
| `runOnNewJS((args, resolve) => { ... }, { fn })` | Yes | Yes |
| `anyHandle.run((args, resolve) => { ... }, { fn })` | Yes | Yes |
| Raw code string `run("...", { fn })` | No (already a string) | Yes |

### What the plugin captures in params

| Value type | Captured |
|---|---|
| Inline arrow / function expression | Yes |
| Inline `async` arrow / function expression | Yes (transformed to generator) |
| Reference to local `function` declaration | Yes |
| Reference to local `const fn = () => ...` | Yes |
| Reference to local `const fn = async () => ...` | Yes (transformed to generator) |
| Imported function (`import { fn } from './mod'`) | Yes (relative paths only) |
| Closed-over `const`/`let`/`var` with literal init | Yes (transitively) |
| Closed-over function references | Yes (transitively) |
| Runtime-computed values, `Map`, `Set`, classes | No — pass as plain params |

### Limitations

- **Captured closures must be statically resolvable**: only variables initialized with literals and functions (local or imported) are captured. Runtime-computed values (e.g. `const x = fetchValue()`) must be passed explicitly as params.
- **`async` functions in params are supported** — the plugin transforms them to generator-based code before extraction. `await` works inside both task functions and function params.
- **Cross-module resolution** only follows **relative imports** (e.g. `'./utils'`). Package imports (e.g. `'lodash'`) are not resolved.
- `undefined`, `Map`, `Set`, and class instances are not serialisable as param values — use plain objects, arrays, strings, numbers, booleans, or `null`.
- Thread functions run in an **isolated Hermes runtime** with no access to the React tree, native modules, or the main thread's global scope.

---

## Constraints summary

| Constraint | Reason |
|---|---|
| Thread functions run in isolation | They execute in a completely separate Hermes runtime (iOS and Android) |
| Hermes eval-mode has no `async`/`await` | The Babel plugin transforms async to generators before extraction |
| Function params must be statically resolvable | The Babel plugin extracts source at compile time |
| Non-function `params` must be JSON-serialisable | Serialised via `JSON.stringify` at runtime |
| `resolveThreadMessage` payload must be JSON-serialisable | Transported as a JSON string over the bridge |
| Cross-module resolution is relative-only | The plugin reads files from disk using the import path |
| No `fetch` / `XMLHttpRequest` | Thread runtimes have no network stack |
| New Architecture required | TurboModule / Codegen only; no bridge fallback |

---

## Contributing

- [Development workflow](CONTRIBUTING.md#development-workflow)
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
- [Code of conduct](CODE_OF_CONDUCT.md)

---

## License

MIT