@munchi_oy/printer-js-sdk
v1.0.5
Published
Munchi JavaScript printer SDK
Downloads
393
Readme
@munchi_oy/printer-js-sdk
Shared JavaScript printer SDK used by Munchi apps.
Release
Create a release with version bump, build, commit, git tag, and remote push:
pnpm release 1.0.3Create and publish a GitHub release after pushing tags:
pnpm release 1.0.3 --publishOther publish modes:
pnpm release 1.0.3 --publish=custom
pnpm release 1.0.3 --publish=fileQueue Options
PrinterQueueController
In-memory sequential queue for lightweight use-cases. Jobs are not recoverable after app restart.
import { PrinterQueueController } from '@munchi_oy/printer-js-sdk';
const queue = new PrinterQueueController();
await queue.enqueue(async () => {
await printSomething();
});DurablePrinterQueueController
Persistent queue with retries, dead-letter support, idempotency keys, and restart recovery for in_progress jobs.
import {
DurablePrinterQueueController,
type DurablePrinterQueueStorage,
} from '@munchi_oy/printer-js-sdk';
import AsyncStorage from '@react-native-async-storage/async-storage';
type PrintJobType = 'receipt' | 'kitchen' | 'refund';
type PrintPayload = { orderId: string; printerId: string };
const storage: DurablePrinterQueueStorage = {
getItem: (key) => AsyncStorage.getItem(key),
setItem: (key, value) => AsyncStorage.setItem(key, value),
};
const queue = new DurablePrinterQueueController<PrintJobType, PrintPayload>({
storage,
handlers: {
receipt: async ({ payload }) => {
await printReceipt(payload.orderId, payload.printerId);
},
kitchen: async ({ payload }) => {
await printKitchen(payload.orderId, payload.printerId);
},
refund: async ({ payload }) => {
await printRefund(payload.orderId, payload.printerId);
},
},
defaultMaxAttempts: 6,
baseRetryDelayMs: 1000,
maxRetryDelayMs: 30000,
leaseTimeoutMs: 120000,
yieldEveryJobs: 1,
yieldDelayMs: 0,
isRetryableError: (error) => {
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
return (
message.includes('timeout') ||
message.includes('network') ||
message.includes('disconnected')
);
},
onJobFailed: ({ job, errorMessage }) => {
console.warn(`Print job ${job.id} failed: ${errorMessage}`);
},
});
await queue.start();
await queue.enqueue({
type: 'receipt',
payload: { orderId: 'order_123', printerId: 'printer_1' },
idempotencyKey: 'receipt:order_123:printer_1',
});Queue responsiveness controls
For JS-thread workloads, these options help reduce burst lag:
yieldEveryJobs: yield back to event loop after N processed jobs.yieldDelayMs: delay used during each cooperative yield.
Recommended baseline for UI safety:
yieldEveryJobs: 1yieldDelayMs: 0
Persistence Model
DurablePrinterQueueController persists queue state through a pluggable storage interface:
interface DurablePrinterQueueStorage {
getItem(key: string): Promise<string | null> | string | null;
setItem(key: string, value: string): Promise<void> | void;
}What this means:
- Queue processing happens in memory at runtime.
- Job state is snapshotted to your storage adapter on each state transition.
- On startup, jobs are restored from storage.
in_progressjobs with expired lease are recovered back toqueued.
Which persistence backend should you use?
SQLite(recommended for restaurant POS): best durability and corruption resilience.MMKV: fast, good local durability, lighter than SQLite.AsyncStorage: acceptable for many cases, but less robust under heavy write volume.- In-memory map: testing only, no durability.
Recommended POS Architecture (Strategy + Adapter)
For multiple printer models and transports, use this split:
- Queue: controls retry/order/recovery.
- Strategy: builds commands from business payload (
receipt,kitchen,refund). - Adapter: sends commands to physical transport (
Star Bluetooth,Epson Network,Sunmi Bluetooth).
Queue -> Job Handler -> Strategy Registry + Adapter Registry -> Device
Multi-printer example
import {
DurablePrinterQueueController,
type DurablePrinterQueueJob,
type DurablePrinterQueueStorage,
} from '@munchi_oy/printer-js-sdk';
type PrintJobType = 'receipt' | 'kitchen' | 'refund';
type PrinterModel = 'Star' | 'Epson' | 'Sunmi';
type ConnectionType = 'Bluetooth' | 'Network';
type PrintCommands = string | Uint8Array;
type PrinterProfile = {
printerId: string;
model: PrinterModel;
connection: ConnectionType;
address: string;
};
type PrintPayload = {
orderId: string;
printerId: string;
data: unknown;
};
interface PrinterStrategy {
build(
jobType: PrintJobType,
payload: PrintPayload,
printer: PrinterProfile
): Promise<PrintCommands>;
}
interface PrinterAdapter {
send(commands: PrintCommands, printer: PrinterProfile): Promise<void>;
}
const strategyRegistry = {
get(jobType: PrintJobType, model: PrinterModel): PrinterStrategy {
return resolveStrategy(jobType, model);
},
};
const adapterRegistry = {
get(model: PrinterModel, connection: ConnectionType): PrinterAdapter {
return resolveAdapter(model, connection);
},
};
async function executePrintJob(
job: DurablePrinterQueueJob<PrintJobType, PrintPayload>
): Promise<void> {
const printer = await printerRepo.getById(job.payload.printerId);
if (!printer) throw new Error(`Printer not found: ${job.payload.printerId}`);
const strategy = strategyRegistry.get(job.type, printer.model);
const adapter = adapterRegistry.get(printer.model, printer.connection);
const commands = await strategy.build(job.type, job.payload, printer);
await adapter.send(commands, printer);
}
const storage: DurablePrinterQueueStorage = yourStorageAdapter;
const queue = new DurablePrinterQueueController<PrintJobType, PrintPayload>({
storage,
handlers: {
receipt: executePrintJob,
kitchen: executePrintJob,
refund: executePrintJob,
},
defaultMaxAttempts: 6,
baseRetryDelayMs: 1000,
maxRetryDelayMs: 30000,
leaseTimeoutMs: 120000,
});
await queue.start();SQLite Adapter Example
You can implement DurablePrinterQueueStorage with any SQLite wrapper. Example shape:
const sqliteStorage: DurablePrinterQueueStorage = {
async getItem(key) {
const [result] = await db.executeSql(
'SELECT value FROM kv_store WHERE key = ? LIMIT 1',
[key]
);
if (result.rows.length === 0) return null;
return result.rows.item(0).value as string;
},
async setItem(key, value) {
await db.executeSql(
'INSERT OR REPLACE INTO kv_store (key, value, updated_at) VALUES (?, ?, strftime("%s","now"))',
[key, value]
);
},
};Suggested table:
CREATE TABLE IF NOT EXISTS kv_store (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);Strategy and Adapter Helpers
The SDK includes typed helpers so you can standardize strategy and transport wiring across Star, Epson, and Sunmi.
Types you can import
import type {
PrinterStrategy,
PrinterAdapter,
} from '@munchi_oy/printer-js-sdk';Create a reusable job executor
import {
createPrinterJobExecutor,
type DurablePrinterQueueJob,
} from '@munchi_oy/printer-js-sdk';
type PrintJobType = 'receipt' | 'kitchen' | 'refund';
type PrinterModel = 'Star' | 'Epson' | 'Sunmi';
type ConnectionType = 'Bluetooth' | 'Network';
type PrintCommands = string | Uint8Array;
type PrinterProfile = {
printerId: string;
model: PrinterModel;
connection: ConnectionType;
address: string;
};
type PrintPayload = {
orderId: string;
printerId: string;
data: unknown;
};
const executePrintJob = createPrinterJobExecutor<
PrintJobType,
PrintPayload,
PrinterProfile,
PrintCommands
>({
findPrinter: async (payload) => {
return await printerRepo.getById(payload.printerId);
},
getStrategy: async (jobType, printer) => {
return strategyRegistry.get(jobType, printer.model);
},
getAdapter: async (printer) => {
return adapterRegistry.get(printer.model, printer.connection);
},
createPrinterNotFoundError: (payload, job) => {
return new Error(`Printer ${payload.printerId} missing for job ${job.id}`);
},
});Queue wiring with the executor
const queue = new DurablePrinterQueueController<PrintJobType, PrintPayload>({
storage: sqliteStorage,
handlers: {
receipt: executePrintJob,
kitchen: executePrintJob,
refund: executePrintJob,
},
defaultMaxAttempts: 6,
baseRetryDelayMs: 1000,
maxRetryDelayMs: 30000,
leaseTimeoutMs: 120000,
});
await queue.start();
await queue.enqueue({
type: 'kitchen',
payload: { orderId: 'o-123', printerId: 'p-1', data: orderDto },
idempotencyKey: 'kitchen:o-123:p-1:v1',
});Minimal strategy and adapter example
import type {
PrinterAdapter,
PrinterStrategy,
} from '@munchi_oy/printer-js-sdk';
const kitchenStarStrategy: PrinterStrategy<
'kitchen' | 'receipt' | 'refund',
PrintPayload,
PrinterProfile,
string
> = {
async build(jobType, payload, printer) {
if (jobType === 'kitchen') return buildStarKitchenCommands(payload.data);
if (jobType === 'receipt') return buildStarReceiptCommands(payload.data);
return buildStarRefundCommands(payload.data);
},
};
const starBluetoothAdapter: PrinterAdapter<PrinterProfile, string> = {
async send(commands, printer) {
await starSdk.sendBluetooth(printer.address, commands);
},
};Settings-driven runtime (single queue, multiple printers)
Use createPrinterRuntime when your app has one global queue and many printer settings.
import {
createPrinterRuntime,
type ManagedPrinterAdapter,
type PrinterRuntimePayload,
type PrinterRuntimeSetting,
} from '@munchi_oy/printer-js-sdk';
type PrintJobType = 'receipt' | 'kitchen' | 'refund';
type PrintPayload = PrinterRuntimePayload & {
orderId: string;
data: unknown;
};
type PrinterSetting = PrinterRuntimeSetting & {
printerModel: 'Star' | 'Epson' | 'Sunmi';
device: 'Bluetooth' | 'Network';
printerName: string;
};
const runtime = createPrinterRuntime<
PrintJobType,
PrintPayload,
PrinterSetting,
string | Uint8Array
>({
storage: sqliteStorage,
jobTypes: ['receipt', 'kitchen', 'refund'],
createAdapter: async (setting): Promise<ManagedPrinterAdapter<string | Uint8Array>> => {
return adapterFactory.create(setting);
},
getStrategy: async (jobType, setting) => {
return strategyRegistry.get(jobType, setting.printerModel);
},
queue: {
storageKey: 'printer_queue_v1',
defaultMaxAttempts: 6,
baseRetryDelayMs: 1000,
maxRetryDelayMs: 30000,
leaseTimeoutMs: 120000,
},
});
await runtime.syncSettings(printerSettingsFromStore);
await runtime.start();
await runtime.enqueue({
type: 'receipt',
payload: {
orderId: 'o-1001',
printerSettingId: 'setting-1',
data: orderDto,
},
idempotencyKey: 'receipt:o-1001:setting-1:v1',
});Vendor adapter and strategy wiring
Use the built-in vendor helpers to keep Epson/Star/Sunmi logic modular.
import {
createVendorAdapterFactory,
createVendorStrategyResolver,
createEpsonConnectionManagedAdapter,
resolveEpsonTarget,
createStarManagedAdapter,
createSunmiManagedAdapter,
} from '@munchi_oy/printer-js-sdk';
type Vendor = 'epson' | 'star' | 'sunmi';
type PrintJobType = 'receipt' | 'kitchen' | 'refund';
type PrintCommands = string | Uint8Array;
type PrinterSetting = {
settingId: string;
enabled: boolean;
vendor: Vendor;
printerId: string;
printerModel: 'Star' | 'Epson' | 'Sunmi';
device: 'Bluetooth' | 'Network';
connection:
| { kind: 'bluetooth'; address: string; name?: string }
| { kind: 'tcp'; host: string; port?: number }
| { kind: 'tcps'; host: string; port?: number }
| { kind: 'ip'; ip: string; port?: number };
};
type PrintPayload = {
orderId: string;
printerSettingId: string;
data: unknown;
};
const createAdapter = createVendorAdapterFactory<Vendor, PrinterSetting, PrintCommands>({
epson: async (setting) =>
createEpsonConnectionManagedAdapter({
setting,
drivers: {
bluetooth: {
open: async (s) => {
const target = resolveEpsonTarget(s.connection);
return epsonBridge.openBluetooth(target, s);
},
print: async (session, commands, s) =>
epsonBridge.printBluetooth(session, commands, s),
close: async (session, s) => epsonBridge.closeBluetooth(session, s),
},
tcp: {
open: async (s) => {
const target = resolveEpsonTarget(s.connection);
return epsonBridge.openTcp(target, s);
},
print: async (session, commands, s) =>
epsonBridge.printTcp(session, commands, s),
close: async (session, s) => epsonBridge.closeTcp(session, s),
},
tcps: {
open: async (s) => {
const target = resolveEpsonTarget(s.connection);
return epsonBridge.openTcp(target, s);
},
print: async (session, commands, s) =>
epsonBridge.printTcp(session, commands, s),
close: async (session, s) => epsonBridge.closeTcp(session, s),
},
ip: {
open: async (s) => {
const target = resolveEpsonTarget(s.connection);
return epsonBridge.openTcp(target, s);
},
print: async (session, commands, s) =>
epsonBridge.printTcp(session, commands, s),
close: async (session, s) => epsonBridge.closeTcp(session, s),
},
},
}),
star: async (setting) =>
createStarManagedAdapter(setting, {
subscribeConnection: async (s, listener) =>
starBridge.subscribeConnection(s, listener),
send: async (commands, s) => starBridge.send(s, commands),
}),
sunmi: async (setting) =>
createSunmiManagedAdapter(setting, {
start: async (s) => sunmiBridge.startSession(s),
send: async (commands, s) => sunmiBridge.send(s, commands),
subscribeConnection: async (s, listener) =>
sunmiBridge.subscribeConnection(s, listener),
stop: async (s) => sunmiBridge.stopSession(s),
}),
});
const getStrategy = createVendorStrategyResolver<
PrintJobType,
Vendor,
PrintPayload,
PrinterSetting,
PrintCommands
>({
receipt: {
epson: epsonReceiptStrategy,
star: starReceiptStrategy,
sunmi: sunmiReceiptStrategy,
},
kitchen: {
epson: epsonKitchenStrategy,
star: starKitchenStrategy,
sunmi: sunmiKitchenStrategy,
},
refund: {
epson: epsonRefundStrategy,
star: starRefundStrategy,
sunmi: sunmiRefundStrategy,
},
});For Epson tcp, tcps, and ip connections, port is optional. When omitted, resolveEpsonTarget returns TCP:<hostOrIp> or TCPS:<host> and lets Epson SDK use its default port behavior.
React Native / React wrapper
Import from @munchi_oy/printer-js-sdk/react for provider + hooks.
import {
PrinterRuntimeProvider,
useEnqueuePrint,
usePrinterConnection,
} from '@munchi_oy/printer-js-sdk/react';
function AppRoot() {
const settings = usePrinterSettingsStore((state) => state.printerSettings);
return (
<PrinterRuntimeProvider runtime={runtime} settings={settings}>
<YourScreens />
</PrinterRuntimeProvider>
);
}
function PrintButton() {
const enqueuePrint = useEnqueuePrint<
'receipt' | 'kitchen' | 'refund',
PrintPayload,
PrinterSetting
>();
const onPress = async () => {
await enqueuePrint({
type: 'receipt',
payload: {
orderId: 'o-1001',
printerSettingId: 'setting-1',
data: orderDto,
},
idempotencyKey: 'receipt:o-1001:setting-1:v1',
});
};
return <Button title="Print receipt" onPress={onPress} />;
}
function PrinterStatus({ settingId }: { settingId: string }) {
const connectionState = usePrinterConnection(settingId);
return <Text>{connectionState}</Text>;
}Multi-printer runtime wrapper (isolated queue per printer)
Use this when you want one runtime/queue per settingId while keeping JS implementation.
Supported behavior:
- Enqueue routes by
payload.printerSettingId. - Each printer setting has its own durable queue storage key.
- Queue retention is trimmed per printer (old completed/dead-letter jobs are pruned).
- Queue capabilities remain available per printer (
enqueue, retries, dead-letter, persistence, cold boot recovery).
import {
MultiPrinterRuntimeProvider,
createSettingScopedRuntimeFactory,
useMultiEnqueuePrint,
useMultiPrinterConnection,
} from '@munchi_oy/printer-js-sdk/react';
const createRuntime = createSettingScopedRuntimeFactory<
PrintJobType,
PrintPayload,
PrinterSetting,
string | Uint8Array
>({
storage: sqliteStorage,
storageKeyPrefix: 'printer_queue',
keepCompletedJobsPerPrinter: 10,
keepDeadLetterJobsPerPrinter: 10,
jobTypes: ['receipt', 'kitchen', 'refund'],
createAdapter: async (setting) => adapterFactory.create(setting),
getStrategy: async (jobType, setting) =>
strategyRegistry.get(jobType, setting.printerModel),
queue: {
defaultMaxAttempts: 6,
baseRetryDelayMs: 1000,
maxRetryDelayMs: 30000,
leaseTimeoutMs: 120000,
yieldEveryJobs: 1,
yieldDelayMs: 0,
},
});
function AppRoot() {
const settings = usePrinterSettingsStore((state) => state.printerSettings);
return (
<MultiPrinterRuntimeProvider settings={settings} createRuntime={createRuntime}>
<YourScreens />
</MultiPrinterRuntimeProvider>
);
}
function PrintButton() {
const enqueuePrint = useMultiEnqueuePrint<
'receipt' | 'kitchen' | 'refund',
PrintPayload,
PrinterSetting
>();
return (
<Button
title="Print receipt"
onPress={async () => {
await enqueuePrint({
type: 'receipt',
payload: {
orderId: 'o-1001',
printerSettingId: 'setting-1',
data: orderDto,
},
idempotencyKey: 'receipt:o-1001:setting-1:v1',
});
}}
/>
);
}
function PrinterStatus({ settingId }: { settingId: string }) {
const connectionState = useMultiPrinterConnection(settingId);
return <Text>{connectionState}</Text>;
}Overflow retention behavior
If queue history overflows retention limits:
- Older
completedjobs are trimmed first. - Older
dead_letterjobs are trimmed independently. - Active jobs (
queued,in_progress) are never trimmed.
This allows “new success overwrites old success history” behavior while keeping active reliability.
Current limitations (JS runtime mode)
- Queue processing still runs on JS runtime, not on a dedicated native worker thread.
- Long synchronous work in strategy/adapter code can still cause UI jank.
- Very large payloads increase memory pressure and serialization overhead.
- Cross-printer global ordering is not guaranteed when using per-printer isolated queues.
Performance roadmap (future)
- Move queue worker + transport send to native layer (Swift/Kotlin) while JS remains enqueue/orchestration.
- Keep payloads minimal (
orderId,printerSettingId) and fetch heavy data late. - Use SQLite-backed storage for durability and write performance.
- Add queue telemetry (
enqueue latency,handler duration,retry count,queue depth) and alert on thresholds.
Provider durability policy (recommended)
- Epson: native queue can be primary; keep JS durable queue for offline persistence and cross-provider consistency.
- Star: if using spool APIs with status/retry, native queue can be primary; JS queue still handles app-level persistence and routing.
- Sunmi: keep JS durable queue as primary retry/dead-letter mechanism unless native retry/status confirmation is verified.
Consumer Operating Modes (Dynamic Control)
Use queue controls based on app lifecycle and operational intent:
start/stop: full runtime lifecycle (mount/unmount app shell, logout, hard shutdown).pause/resume: keep runtime alive but temporarily stop job execution.syncSettings: always run when printer settings change.
Recommended React Native pattern:
import { AppState } from 'react-native';
const queue = runtime.getQueueController();
const subscription = AppState.addEventListener('change', async (state) => {
if (state === 'active') {
await runtime.syncSettings(latestSettings);
await runtime.start();
queue.resume();
return;
}
if (state === 'background' || state === 'inactive') {
queue.pause();
}
});Notes:
pauseis safer thanstopfor short background transitions.stopis appropriate when session/app context is no longer valid.leaseTimeoutMsmust be greater than0.0is treated as invalid and falls back to the queue default.
Dead-letter Policy (Restaurant Ready)
Recommended policy for unstable printer connectivity:
- Retry locally first. The device that owns the printer connection should execute retries.
- Use
isRetryableErrorto retry transient failures only (timeout, disconnected, busy, network jitter). - Move terminal failures to dead-letter quickly (invalid payload, unsupported format, missing mapping).
- Expose dead-letter actions in UI:
Retry,Retry all,Reroute to another printer. - Send dead-letter telemetry to backend for monitoring/support, not for backend-side printing.
- Use
onJobFailedfor immediate user feedback; it fires only when a job becomesdead_letter.
Example retry classifier:
isRetryableError: (error) => {
const message =
error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
return (
message.includes('timeout') ||
message.includes('network') ||
message.includes('disconnect') ||
message.includes('busy')
);
}Operational Defaults for Restaurants
Choose one profile based on how aggressive recovery should be.
| Profile | Recommended queue values | Use when |
| --- | --- | --- |
| Balanced | defaultMaxAttempts: 5, baseRetryDelayMs: 1000, maxRetryDelayMs: 8000, leaseTimeoutMs: 5000 | Most stores; good reliability without queue buildup |
| Fast recovery | defaultMaxAttempts: 3, baseRetryDelayMs: 500, maxRetryDelayMs: 3000, leaseTimeoutMs: 1 | Real-time kitchen flow where stale in_progress must recover immediately |
| High resilience | defaultMaxAttempts: 8, baseRetryDelayMs: 1000, maxRetryDelayMs: 30000, leaseTimeoutMs: 10000 | Unstable network/printer environments where temporary outages are common |
Also recommended for every profile:
- Use idempotency key format:
<jobType>:<orderId>:<printerId>:<version>. - Keep dead-letter jobs visible in support/settings UI.
- Prune old completed/dead-letter jobs with
keepCompletedJobsandkeepDeadLetterJobs.
