@fainthit/android-foreground-reserve-client
v0.1.0
Published
Android foreground reserve protocol client plugin for Capacitor
Maintainers
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 syncOverview
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.
