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

@fainthit/android-foreground-reserve-client

v0.1.0

Published

Android foreground reserve protocol client plugin for Capacitor

Readme

ForegroundReserve

ForegroundReserve is an Android-only Capacitor plugin. It keeps a reserve protocol connection alive in an Android foreground service even when the WebView is paused or stopped, and it can run native actions such as waking the screen, vibrating, and playing audio when channel messages arrive.

It is designed for dedicated controller devices, such as escape-room tablets and local hardware controllers, where long-lived connectivity, duplicate-safe messaging, missing-message recovery, and immediate native reactions matter more than Play Store-oriented background behavior.

Install

npm install @fainthit/android-foreground-reserve-client
npx cap sync

Overview

The native Android service owns the reserve connection. JavaScript only configures the service, registers reserve channels to forward to the WebView, registers native actions, reports WebView activity state, and performs status checks or manual emits.

When the WebView is active, watched reserve channel messages are delivered immediately through Capacitor listeners. When the WebView is inactive, events are stored in a native FIFO buffer and flushed in order when setWebviewActive() is called.

The native reserve client handles hello, helloAck, token reconnect, sequence keys, ACK retry, duplicate suppression, unsynced, reset, and ping/pong. Tokens returned by the server are persisted into the native config so service restart and boot restore can reuse the same reserve session.

reserve Event Mapping

reserve application messages use channel and data: any[].

server.send('cue', { name: 'door_open' });
server.send('multi.args', 'a', 1, true);

The plugin maps reserve channel to event. The original reserve data[] is exposed as args. For Socket.IO-style compatibility, if args has one item, that item is exposed as data; if it has multiple items, the full array is exposed as data.

{
  event: 'cue',
  data: { name: 'door_open' },
  args: [{ name: 'door_open' }],
  receivedAt: 1710000000000
}

Basic Usage

import { ForegroundReserve } from '@fainthit/android-foreground-reserve-client';

await ForegroundReserve.start({
  reserve: {
    url: 'ws://192.168.0.10:3000',
    name: 'tablet-1',
    reconnectInterval: 1000,
    retryInterval: 5000,
    pingTimeout: 5000,
  },
  listen: {
    events: ['cue', 'timer', 'room.event'],
    bufferSize: 100,
  },
  service: {
    notificationTitle: 'Room Controller',
    notificationText: 'Reserve connection is running',
    startOnBoot: false,
  },
  onConnectEmit: {
    event: 'controller.ready',
    data: { deviceId: 'tablet-1' },
  },
  actions: [
    {
      id: 'door-open',
      event: 'cue',
      match: { path: 'name', equals: 'door_open' },
      cooldownMs: 1000,
      run: [
        { type: 'wakeScreen', durationMs: 8000 },
        { type: 'vibrate', durationMs: 500 },
        { type: 'playSound', source: 'asset://sounds/door.mp3', volume: 1 },
      ],
    },
  ],
});

Listening To reserve Channels In JavaScript

Register reserve channel names with listen.events during start(), or add them later with watchEvent().

await ForegroundReserve.watchEvent({ event: 'cue' });

The plugin emits forwarded reserve messages in two listener channels:

const allHandle = await ForegroundReserve.addListener('reserveEvent', event => {
  console.log(event.event);
  console.log(event.data);
  console.log(event.args);
  console.log(event.receivedAt);
});

const cueHandle = await ForegroundReserve.addListener('cue', event => {
  console.log(event.data);
});

reserveEvent receives every watched reserve channel. An event-name listener, such as cue, receives only that specific channel.

Lifecycle events connect, connected, reconnected, disconnect, disconnected, connect_error, and pong are also forwarded.

WebView Activity State

Call these from your app lifecycle hooks:

document.addEventListener('resume', () => {
  ForegroundReserve.setWebviewActive();
});

document.addEventListener('pause', () => {
  ForegroundReserve.setWebviewInactive();
});

When inactive, forwarded events are queued. When active again, queued events are flushed in the same order they were received. The queue size is configured with listen.bufferSize; when the queue is full, the oldest events are dropped first.

Native Actions

Native actions run inside Android even if the WebView is paused. They can be registered at startup or later.

await ForegroundReserve.registerAction({
  id: 'alarm',
  event: 'room.event',
  match: {
    all: [
      { path: 'type', equals: 'alarm' },
      { path: 'room', equals: 'A' },
    ],
  },
  run: [
    { type: 'setVolume', level: 1 },
    { type: 'wakeScreen', durationMs: 10000 },
    { type: 'playSound', source: 'url://http://192.168.0.10/sounds/alarm.mp3' },
    { type: 'vibrate', pattern: [0, 300, 100, 300] },
    { type: 'emit', event: 'controller.ack', data: { action: 'alarm' } },
  ],
});

Supported action types:

| Type | Description | | --- | --- | | wakeScreen | Opens a native activity that turns the screen on for durationMs. | | vibrate | Vibrates once with durationMs, or uses a vibration pattern. | | playSound | Plays audio from asset://, file://, url://, http://, or https://. | | stopSound | Stops audio on a named channel; default channel is default. | | setVolume | Sets the music stream volume with level from 0 to 1. | | delay | Waits before running the next action step. | | emit | Sends a reserve channel message from native code. | | notifyWebview | Publishes a custom event to JavaScript using the same buffering rules. |

emit actions may use either data or args.

{ type: 'emit', event: 'controller.ack', data: { action: 'alarm' } }
{ type: 'emit', event: 'multi.args', args: ['a', 1, true] }

Match Rules

Actions can be filtered with match. Paths use dot notation and are resolved against the reserve payload data. If reserve data[] contains one value, that first value is used as the match payload. If it contains multiple values, the array is used.

{ path: 'name', equals: 'door_open' }
{ path: 'level', gt: 3 }
{ path: 'tags', contains: 'urgent' }
{ path: 'enabled', exists: true }
{ all: [{ path: 'type', equals: 'cue' }, { path: 'room', equals: 'A' }] }
{ any: [{ path: 'name', equals: 'start' }, { path: 'name', equals: 'reset' }] }

If an action has no match, it runs for every message with the matching reserve channel name. cooldownMs prevents the same action from running too frequently.

Runtime Control

await ForegroundReserve.emit({
  event: 'controller.message',
  data: { value: 1 },
});

await ForegroundReserve.emit({
  event: 'controller.multi',
  args: ['room-a', 1, true],
});

const status = await ForegroundReserve.getStatus();

await ForegroundReserve.setActionEnabled({
  id: 'alarm',
  enabled: false,
});

await ForegroundReserve.unregisterAction({ id: 'alarm' });
await ForegroundReserve.clearActions();
await ForegroundReserve.disconnect();
await ForegroundReserve.connect();
await ForegroundReserve.stop();

Event Payload Shape

Forwarded reserve listener payloads have this shape:

{
  event: string;
  data: any;
  args: any[];
  receivedAt: number;
}

Status listener payloads match ForegroundReserveStatus.

ForegroundReserve.addListener('status', status => {
  console.log(status.connected);
  console.log(status.token);
  console.log(status.pendingMessages);
  console.log(status.queuedEvents);
});

Action execution events are emitted on actionExecuted.

ForegroundReserve.addListener('actionExecuted', event => {
  console.log(event.id);
});

API Summary

| Method | Description | | --- | --- | | start(options) | Starts the foreground service and configures the reserve connection, listeners, and actions. | | stop() | Closes the reserve connection and stops the foreground service. | | restart(options?) | Restarts the service with the existing or new config. | | connect() | Manually opens the reserve connection. | | disconnect() | Manually closes the reserve connection. | | watchEvent({ event }) | Registers a reserve channel to forward to JavaScript. | | unwatchEvent({ event }) | Stops forwarding a reserve channel to JavaScript. | | emit({ event, data, args }) | Sends a reserve channel message from the native client. | | registerAction(action) | Registers a native action. | | unregisterAction({ id }) | Removes an action. | | clearActions() | Removes all actions. | | listActions() | Lists registered actions. | | setActionEnabled({ id, enabled }) | Enables or disables an action. | | setWebviewActive() | Marks the WebView active and flushes buffered events. | | setWebviewInactive() | Marks the WebView inactive. | | setOnConnectEmit({ event, data, args }) | Sets the channel message sent after reserve connection succeeds. | | getStatus() | Returns service, reserve client, and queue status. | | addListener(eventName, handler) | Registers a Capacitor listener. |

Options

reserve

| Option | Description | | --- | --- | | url | reserve WebSocket server URL. | | name | Client name used by reserve targeted sends. | | token | Existing session token. Usually stored by native code after helloAck. | | reconnectInterval | Delay before reconnect attempts. Default is 1000ms. | | retryInterval | Reliable message retry interval while waiting for ACK. Default is 5000ms. | | pingInterval | Initial local ping interval for timeout calculation. Replaced by the server value after helloAck. | | pingTimeout | Additional timeout before treating missing server pings as disconnected. Default is 5000ms. | | format | Expected preface format: json or msgpack. |

service

| Option | Description | | --- | --- | | notificationTitle | Foreground service notification title. | | notificationText | Foreground service notification body. | | startOnBoot | Starts the service after boot using the saved config. |

Notes

This plugin is Android-only. Web and iOS implementations are not provided.

Screen wake, vibration, audio playback, and long-running foreground service behavior can still be affected by device-specific Android power policies. Dedicated devices should usually disable battery optimization and configure lock-screen or auto-start policies explicitly.

reserve performs ACK and retry for reliable messaging. emit() enqueues a pending reliable message but does not return a Promise for the server ACK. The current pending count is available as getStatus().pendingMessages.