npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

arnacon-controller

v1.6.1

Published

Controller SDK for building webapps that communicate with the Arnacon native app

Readme

arnacon-controller

TypeScript SDK for building web apps that run inside the Arnacon native app (iOS & Android). Communicates with the native layer through a WebView bridge, exposing a promise-based API for data fetching and an event emitter for real-time push notifications.

All native array data is automatically deserialized into typed objects -- you work with message.author and session.sessionName, never positional array indices.

Installation

npm install arnacon-controller

Quick Start

import { Controller } from 'arnacon-controller';

const controller = new Controller({
  localId: 'user-wallet-address',
  send: Controller.autoDetectBridge(),
});

// IMPORTANT: The native app calls window.top.controller.receiveData(jsonString)
// to push data into the SDK, so you must expose the instance globally.
window.top.controller = controller;

Fetching data (promise-based)

const { sessions } = await controller.getRecentSessions(50);
sessions.forEach(s => {
  console.log(s.sessionName, s.lastMessageContent, s.unreadCount);
});

const { messages } = await controller.getMessages(sessionId, 20, null, true);
messages.forEach(m => {
  console.log(m.author, m.content, m.status);
});

Sending commands (fire-and-forget)

controller.sendMessage(sessionId, 'Hello!');
controller.callSession(sessionId);

Listening for push events

controller.on('new-message', ({ message }) => {
  console.log(message.author, message.content);
});

controller.on('mute-state', ({ mute }) => {
  toggleMuteUI(mute);
});

Setup

Constructor

new Controller(options: ControllerOptions)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | localId | string | Yes | -- | The local user/device identifier (e.g. wallet address) | | send | SendFunction | Yes | -- | Transport function that delivers payloads to native | | requestTimeout | number | No | 10000 | Timeout in ms for promise-based requests | | onMessage | (data) => void | No | -- | Deprecated. Catch-all callback. Prefer controller.on() |

Bridge Detection

Controller.autoDetectBridge() returns a SendFunction by checking for:

  1. window.webkit.messageHandlers.nativeHandler (iOS)
  2. window.AndroidBridge.processAction (Android)

Throws if neither is found. You can also provide a custom transport:

const send = (payload) => myWebSocket.send(JSON.stringify(payload));
const controller = new Controller({ localId: '...', send });

URL Parameters

The Arnacon native app passes parameters to the webapp via a hash fragment, not query parameters:

https://example.com/app#screen=MAIN&localId=0x1234

Parse them from location.hash:

const params = new URLSearchParams(location.hash.substring(1));
const localId = params.get('localId');
const screen = params.get('screen');

Receiving Data from Native

The native app delivers data by calling:

window.top.controller.receiveData(jsonString);

The controller routes each incoming message automatically:

  1. If the body contains a requestId matching a pending request, the corresponding promise resolves.
  2. Otherwise, the action field is emitted as an event to listeners registered via controller.on().

Data Models

The SDK automatically deserializes native array data into typed objects.

Message

Returned by getMessages(), and emitted by new-message / message-updated events.

interface Message {
  messageId: string;
  time: number;
  author: string;
  content: string;
  sessionId: number;
  contact: string | null;
  status: number;         // 0=pending, 1=sent, 2=error, 4=delivered
  fileId: unknown | null;
  replyTo: {
    messageId: string;
    author: string;
    content: string;
  } | null;
  isEdited: boolean;
  forwardInfo: unknown | null;
}

Session

Returned by getRecentSessions().

interface Session {
  lastMessageId: string;
  lastMessageTime: number;
  lastMessageAuthor: string;
  lastMessageContent: string;
  sessionId: number;
  sessionName: string;
  unreadCount: number;
  fileData: unknown | null;
  isGroup: boolean;
}

RecentCall

Returned by getRecentCalls().

interface RecentCall {
  contact: string;
  id: string;
  timestamp: number;
  status: string;   // "incoming", "outgoing", "missed"
}

CallLogEntry

Returned by getLogs().

interface CallLogEntry {
  contact: string;
  timestamp: number;
  status: string;
}

Data Fetching (Promise-based)

These methods send a request with a unique requestId and return a Promise that resolves when native echoes back the same requestId. Response data is automatically deserialized.

| Method | Returns | Description | |--------|---------|-------------| | getRecentSessions(range) | Promise<{ sessions: Session[] }> | Fetch recent chat sessions | | getMessages(id, range, start, isSession) | Promise<{ messages: Message[] }> | Fetch messages for a session or remote user | | getRecentCalls() | Promise<{ calls: RecentCall[] }> | Fetch recent call history | | getLogs(remoteId?) | Promise<{ entries: CallLogEntry[] }> | Fetch call/message logs | | getAudioDevices() | Promise<{ audioDevices }> | List available audio output devices | | getSessionName(sessionId) | Promise<{ sessionName }> | Resolve display name for a session | | getSessionId(remoteId) | Promise<{ sessionId }> | Resolve session ID for a remote user | | contactPicker(type) | Promise<{ injectRemoteId }> | Open native contact picker |

getMessages parameters

| Parameter | Type | Description | |-----------|------|-------------| | id | string | Session ID or remote user ID | | range | number | Number of messages to fetch | | start | string \| number \| null | Message ID to start from. Pass null to fetch the latest messages. The SDK strips null values from the payload so native won't receive it. | | isSession | boolean | true if id is a session ID, false if it's a remote user ID |

const { sessions } = await controller.getRecentSessions(50);

const { messages } = await controller.getMessages(
  sessionId,  // session or remote ID
  20,          // number of messages to fetch
  null,        // null = start from latest
  true,        // true = sessionId, false = remoteId
);

Fire-and-Forget Methods

These methods send a command to native and return void. No response is expected.

Calls

| Method | Description | |--------|-------------| | callSession(sessionId) | Start audio call to a session | | callRemote(remoteId) | Start audio call to a remote user | | videoCallSession(sessionId) | Start video call to a session | | videoCallRemote(remoteId) | Start video call to a remote user | | acceptCall(callId) | Accept an incoming call | | rejectCall(callId) | Reject an incoming call | | setMute(callId, muted) | Mute/unmute microphone | | setHold(callId, held) | Place call on hold or resume | | switchCamera(callId) | Toggle front/back camera | | sendDtmf(callId, digit) | Send a DTMF tone | | transferCall(callId, remoteId) | Transfer call to another user | | changeAudioDevice(callId, deviceId) | Switch audio output device |

Messaging

| Method | Description | |--------|-------------| | sendMessage(sessionId, message) | Send a text message | | sendReplyMessage(id, message, replyToMessageId, isSession) | Reply to a message | | sendEditMessage(messageId, newContent) | Edit a sent message | | sendFileMessage(sessionId, fileId, caption) | Send a file/media message | | forwardMessage(messageId, content, messageType) | Forward a message | | deleteMessage(messageId) | Delete a message |

Sessions and Other

| Method | Description | |--------|-------------| | updateSessionTimestamp(sessionId) | Mark a session as read | | deleteSession(sessionId) | Delete a session | | createGroup(groupData) | Create a new group session | | camera() | Open the native camera | | downloadFile(messageId, fileId) | Download a file attachment |


Events

Listen for push events from native using on() and off(). These are unsolicited messages that the native app sends without the web having requested them.

Message events (new-message, message-updated) are automatically deserialized into Message objects.

controller.on('new-message', ({ message }) => {
  console.log(message.author, message.content, message.status);
});

const handler = (data) => console.log(data);
controller.on('call-ended', handler);
controller.off('call-ended', handler);

Available Events

| Event | Payload | Description | |-------|---------|-------------| | new-message | { message: Message } | A new message was sent or received | | message-updated | { message: Message } | A message status changed (sent, delivered, read) | | image-picker-result | { imagePickerResult, imageId? } | User finished picking an image | | receiving-call | { from } | Incoming call | | ringing | { to } | Outbound call is ringing | | call-started | { from, videoCall? } | Call connected | | call-ended | { from } | Call terminated | | mute-state | { mute } | Mute state changed | | local-hold-state | { localHold } | Local hold state changed | | remote-hold-state | { remoteHold } | Remote hold state changed | | active-calls | { activeCalls } | Active call list updated | | speaker-state | { speaker } | Audio route changed | | inject-remote-id | { injectRemoteId } | Contact selected from native picker | | video-frame | { streamId, frame, width, height } | Video frame data for rendering | | webrtc-disconnected | {} | WebRTC peer disconnected | | error | { e } | Error from native layer | | close-error | {} | Dismiss error UI |

Any action name the native sends that isn't matched by a pending requestId is automatically emitted as an event, so custom events work without any SDK changes.


Native Integration Guide

Outgoing Payloads (Web to Native)

Every call to the native bridge sends a JSON payload:

{
  "action": "send-message",
  "body": {
    "localId": "0x...",
    "sessionId": 1,
    "content": "Hello"
  }
}

Promise-based methods include a requestId in the body:

{
  "action": "get-recent-sessions",
  "body": {
    "localId": "0x...",
    "range": 50,
    "requestId": "m3k7f-2-a9x1"
  }
}

Note: null values are stripped from the body before sending. For example, getMessages(id, 20, null, true) will not include startMessage in the payload.

Responding to Requests (Native to Web)

For any request that contains a requestId, echo it back in the response body. The action can be anything (e.g. data-retrieved):

controller.receiveData(JSON.stringify({
  action: "data-retrieved",
  body: {
    recentSessions: [...],
    requestId: "m3k7f-2-a9x1"
  }
}));

Pushing Events (Native to Web)

For unsolicited events, use a specific action name and omit requestId:

controller.receiveData(JSON.stringify({
  action: "new-message",
  body: { message: ["id", 1234567890, "author", "Hello", 1, null, 0, null, null, 0, null] }
}));

controller.receiveData(JSON.stringify({
  action: "message-updated",
  body: { updatedMessage: ["id", 1234567890, "author", "Hello", 1, null, 4, null, null, 0, null] }
}));

controller.receiveData(JSON.stringify({
  action: "receiving-call",
  body: { from: "0x..." }
}));

Summary of Native Action Names

Request/response (echo requestId): get-recent-sessions, get-messages, get-recent-calls, get-logs, get-audio-devices, get-session-name, get-session-id, contact-picker

Push events (no requestId): new-message, message-updated, image-picker-result, receiving-call, ringing, call-started, call-ended, mute-state, local-hold-state, remote-hold-state, active-calls, speaker-state, inject-remote-id, video-frame, webrtc-disconnected, error, close-error


Building from Source

cd arnacon-controller
npm install
npm run build

Produces:

  • dist/index.js -- CommonJS
  • dist/index.mjs -- ES Module
  • dist/index.d.ts -- TypeScript declarations

License

MIT