LogoPear Docs
How ToRun on mobile & native

Handle app suspension

When a Bare process enters suspend state every active I/O handle must be stopped or unreffed—otherwise the OS will force-terminate the app without warning.

Getting suspension wrong is silent: the OS gives the app no warning before it force-terminates it. A backgrounded app that leaves an HTTP server listening, a TCP socket open, or a live timer running will be killed—usually within a few seconds on iOS and Android.

Mobile operating systems suspend apps when they move to the background. Bare models this explicitly through its lifecycle:

  1. the host calls Bare.suspend(), which emits a suspend event so your code can stop outstanding work,
  2. then the loop drains and emits idle,
  3. and finally blocks—keeping the process alive but quiet.
  4. Bare.resume() brings it back.

The problem is step 2. The loop goes idle only when it has no remaining referenced handles: no open sockets, no listening servers, no active timers, no pending file operations. If any referenced handle survives the suspend event, the loop never emits idle, the process never fully suspends, and the OS terminates it without warning when its patience runs out.

For the state machine behind this, see Inside Bare.

The two patterns

Every handle you manage falls into one of two categories.

Pattern A — unref() for handles that survive suspension

Some handles must stay open across a suspend/resume cycle—for example, the IPC channel between a bare-kit worklet and its host. Calling .unref() removes the handle from the loop's reference count without closing it, so the loop can go idle while the handle stays alive. Call .ref() on resume to recount it.

const IPC = require('bare-ipc')
const [portA] = IPC.open()
const ipc = portA.connect()

Bare.on('suspend', () => ipc.unref())
Bare.on('resume', () => ipc.ref())

Pattern B—close on suspend, recreate on resume

Most active I/O should not survive suspension. Stop it on suspend and start it again on resume. The OS connection timeout is shorter than you might expect—don't rely on connections surviving while the process is idle.

Examples

Stop a bare-tcp server

server.close() stops accepting new connections, but existing connections are still referenced. Destroy them first, or the loop will not go idle.

const net = require('bare-tcp')

let server = null

function startServer () {
  server = net.createServer((socket) => {
    socket.on('data', (data) => socket.write(data)) // echo
  })
  server.listen(3000)
}

startServer()

Bare.on('suspend', () => {
  for (const socket of server.connections) socket.destroy()
  server.close()
  server = null
})

Bare.on('resume', () => startServer())

See bare-tcp for the full server and socket API.

Stop Hyperswarm

Each connection event gives you a live socket stream. Hyperswarm tracks them internally; calling swarm.destroy() closes all connections and the underlying DHT socket in one step.

const Hyperswarm = require('hyperswarm')
const Corestore = require('corestore')

const store = new Corestore('./data')
const topic = Buffer.alloc(32) // your 32-byte topic
let swarm = null

async function startSwarm () {
  swarm = new Hyperswarm()
  swarm.on('connection', (conn) => store.replicate(conn))
  await swarm.join(topic, { server: true, client: true }).flushed()
}

await startSwarm()

Bare.on('suspend', async () => {
  await swarm.destroy() // closes connections + DHT socket
  swarm = null
  await store.suspend() // flush buffered writes to disk
})

Bare.on('resume', async () => {
  await store.resume()
  await startSwarm()
})

Destroying the swarm stops the replication streams—the only handles it holds in the loop. The store does not keep the loop referenced while idle, but it may hold buffered writes that have not yet reached disk. Call store.suspend() to flush them before the process suspends, and store.resume() on the way back—if the OS force-terminates the app before a flush, recent writes can be lost.

Unref the bare-kit IPC channel

The IPC channel between a worklet and its native host must survive suspension so the host can still send messages (for example, a resume instruction). Use .unref() / .ref() (Pattern A):

// worklet entry — runs inside Bare
const { IPC } = BareKit

Bare.on('suspend', () => IPC.unref())
Bare.on('resume', () => IPC.ref())

IPC.on('data', (data) => {
  IPC.write(Buffer.from(`echo: ${data}`))
})

See bare-ipc for the .ref() / .unref() API, and bare-kit for the host-side worklet API.

Clear timers

Active setInterval and setTimeout callbacks hold a reference in the event loop just like open sockets. Clear them on suspend and restart on resume.

let heartbeat = null

function startHeartbeat () {
  heartbeat = setInterval(() => sync(), 30_000)
}

startHeartbeat()

Bare.on('suspend', () => {
  clearInterval(heartbeat)
  heartbeat = null
})

Bare.on('resume', () => startHeartbeat())

See bare-timers for the full timer API, including .ref() / .unref() for a timer that must keep running across the cycle.

How the host triggers suspension

On mobile, the embedder calls bare_suspend() and bare_resume() from the C API when the OS fires its own app-lifecycle callbacks.

react-native-bare-kit subscribes to React Native AppState changes and calls worklet.suspend() and worklet.resume() on your behalf. You do not need to wire this up yourself; adding your own listeners is redundant but harmless.

If you need explicit control—for example, to coordinate suspension with custom host logic—you can drive the worklet directly:

// React Native host component
import { AppState } from 'react-native'
import { Worklet } from 'react-native-bare-kit'

const worklet = new Worklet()
worklet.start('/app.js', source)

AppState.addEventListener('change', (state) => {
  if (state === 'background') worklet.suspend()
  else if (state === 'active') worklet.resume()
})

Whether suspension is triggered by react-native-bare-kit or from your own host code, the JavaScript inside the worklet responds to the resulting suspend and resume events via Bare.on('suspend') and Bare.on('resume').

See Embed Bare in a React Native app for the full worklet setup.


See also

On this page