LogoPear Docs
How ToRun on mobile & native

Type a native RPC bridge

Replace raw IPC bytes between a native shell and a Bare worklet with a typed, schema-generated RPC seam using hyperschema and bare-rpc.

Once a worklet is exchanging bytes with its host, the next problem is structure: framing messages and parsing them by hand on both sides is error-prone, and the two sides drift apart over time. This guide puts a typed RPC seam on the channel—methods generated from a single schema—as described in One core, many platforms.

The pieces: hyperschema defines the data structures and generates compact-encoding codecs; bare-rpc frames requests and replies over the IPC stream.

Install the tools

npm i hyperschema bare-rpc compact-encoding

Define a schema

Register your structures on a namespace. Because schemas are versioned and append-only, you can add optional fields later without breaking older peers:

// build-schema.js
const Hyperschema = require('hyperschema')

const schema = Hyperschema.from('./schema')
const ns = schema.namespace('app')

ns.register({
  name: 'message',
  fields: [
    { name: 'id', type: 'uint', required: true },
    { name: 'text', type: 'string' }
  ]
})

Hyperschema.toDisk(schema)

Running this writes a schema.json (for versioning) and a generated index.js of compact-encoding definitions you resolve by name and version:

const c = require('compact-encoding')
const { resolveStruct } = require('./schema')

const message = resolveStruct('@app/message', 1)
const bytes = c.encode(message, { id: 1, text: 'hello' })

To generate wire-compatible Swift types for an iOS shell, run the Swift toolchain (hyperschema-swift, compact-encoding-swift) against the same schema; for the C and Kotlin paths, see One core, many platforms.

Frame calls with bare-rpc

Give each method a unique command number. In the worklet (the core), construct an RPC over the BareKit.IPC stream and handle incoming requests:

// inside the worklet
import RPC from 'bare-rpc'
import c from 'compact-encoding'
import { resolveStruct } from './schema'

const SEND_MESSAGE = 1
const message = resolveStruct('@app/message', 1)
const { IPC } = BareKit

const rpc = new RPC(IPC, (req) => {
  if (req.command === SEND_MESSAGE) {
    const { text } = c.decode(message, req.data)
    console.log('received:', text)
    req.reply('ok')
  }
})

On the host side, construct an RPC over the worklet's IPC and send a request:

// in the React Native host
import RPC from 'bare-rpc'
import c from 'compact-encoding'

const rpc = new RPC(worklet.IPC)

const req = rpc.request(SEND_MESSAGE)
req.send(c.encode(message, { id: 1, text: 'hello' }))

const reply = await req.reply()
console.log(reply.toString()) // ok

A Swift shell does the same through the generated HRPC class and a small transport delegate that forwards bytes to and from the worklet—no hand-written parsing.

Stream when one message isn't enough

For more than request/response, bare-rpc exposes streams on a request—req.createRequestStream() and req.createResponseStream()—covering the five patterns the seam supports: unary, send-only events, response-stream, request-stream, and duplex.

const req = rpc.request(SUBSCRIBE)
const updates = req.createResponseStream()
for await (const chunk of updates) {
  // handle each streamed update
}

See also

On this page