diff --git a/apps/electron b/apps/electron index 5851f72ec..8b1dd7e20 160000 --- a/apps/electron +++ b/apps/electron @@ -1 +1 @@ -Subproject commit 5851f72ec8ab024de498466e94bd24f1919d9a2c +Subproject commit 8b1dd7e2016137cbbaec8c32f00f60d02619ad33 diff --git a/packages/printer/src/transport/ipc-print.electron.ts b/packages/printer/src/transport/ipc-print.electron.ts new file mode 100644 index 000000000..746e23f02 --- /dev/null +++ b/packages/printer/src/transport/ipc-print.electron.ts @@ -0,0 +1,39 @@ +interface ElectronIpc { + invoke: (channel: string, args: unknown) => Promise; +} + +function getIpc(): ElectronIpc { + const w = window as { + ipcRenderer?: ElectronIpc; + electronAPI?: { ipcRenderer?: ElectronIpc }; + }; + const ipc = w.ipcRenderer ?? w.electronAPI?.ipcRenderer; + if (!ipc) throw new Error('Electron ipcRenderer not available'); + return ipc; +} + +export const PRINT_TIMEOUT_MS = 30_000; + +export async function ipcPrintRaw( + channel: string, + args: Record, + timeoutMessage: string +): Promise { + const ipc = getIpc(); + const data = args.data; + if (!(data instanceof Uint8Array)) { + throw new Error('ipcPrintRaw expected args.data to be a Uint8Array'); + } + + let timeoutId: ReturnType | undefined; + try { + await Promise.race([ + ipc.invoke(channel, { ...args, data: Array.from(data) }), + new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), PRINT_TIMEOUT_MS); + }), + ]); + } finally { + if (timeoutId !== undefined) clearTimeout(timeoutId); + } +} diff --git a/packages/printer/src/transport/network-adapter.electron.ts b/packages/printer/src/transport/network-adapter.electron.ts index c0dc396d1..698bd6f95 100644 --- a/packages/printer/src/transport/network-adapter.electron.ts +++ b/packages/printer/src/transport/network-adapter.electron.ts @@ -1,57 +1,37 @@ -import type { PrinterTransport } from "../types"; +import { ipcPrintRaw, PRINT_TIMEOUT_MS } from './ipc-print.electron'; -interface ElectronIpc { - invoke: (channel: string, args: unknown) => Promise; -} - -function getIpc(): ElectronIpc { - const ipc = (window as any).ipcRenderer as ElectronIpc | undefined; - if (!ipc) throw new Error("Electron ipcRenderer not available"); - return ipc; -} - -const PRINT_TIMEOUT_MS = 30_000; +import type { PrinterTransport } from '../types'; /** * Electron network adapter. * Sends raw ESC/POS bytes to a printer via TCP through the main process. */ export class NetworkAdapter implements PrinterTransport { - readonly name = "network-electron"; - - constructor( - private host: string, - private port: number = 9100, - _vendor?: string, - ) {} - - async printRaw(data: Uint8Array): Promise { - const ipc = getIpc(); - // Send as regular array — Uint8Array doesn't serialize across IPC cleanly - const result = Promise.race([ - ipc.invoke("print-raw-tcp", { - host: this.host, - port: this.port, - data: Array.from(data), - }), - new Promise((_, reject) => - setTimeout( - () => - reject(new Error(`Print timed out after ${PRINT_TIMEOUT_MS}ms`)), - PRINT_TIMEOUT_MS, - ), - ), - ]); - await result; - } - - async printHtml(_html: string): Promise { - throw new Error( - "NetworkAdapter does not support HTML printing. Use printRaw instead.", - ); - } - - async disconnect(): Promise { - // TCP connections are per-request; nothing to clean up - } + readonly name = 'network-electron'; + + constructor( + private host: string, + private port: number = 9100, + _vendor?: string + ) {} + + async printRaw(data: Uint8Array): Promise { + await ipcPrintRaw( + 'print-raw-tcp', + { + host: this.host, + port: this.port, + data, + }, + `Print timed out after ${PRINT_TIMEOUT_MS}ms` + ); + } + + async printHtml(_html: string): Promise { + throw new Error('NetworkAdapter does not support HTML printing. Use printRaw instead.'); + } + + async disconnect(): Promise { + // TCP connections are per-request; nothing to clean up + } } diff --git a/packages/printer/src/transport/serial-adapter.electron.ts b/packages/printer/src/transport/serial-adapter.electron.ts index abe8ff1f7..4baf0b538 100644 --- a/packages/printer/src/transport/serial-adapter.electron.ts +++ b/packages/printer/src/transport/serial-adapter.electron.ts @@ -1,20 +1,6 @@ -import type { PrinterTransport } from '../types'; - -interface ElectronIpc { - invoke: (channel: string, args: unknown) => Promise; -} +import { ipcPrintRaw, PRINT_TIMEOUT_MS } from './ipc-print.electron'; -function getIpc(): ElectronIpc { - const w = window as { - ipcRenderer?: ElectronIpc; - electronAPI?: { ipcRenderer?: ElectronIpc }; - }; - const ipc = w.ipcRenderer ?? w.electronAPI?.ipcRenderer; - if (!ipc) throw new Error('Electron ipcRenderer not available'); - return ipc; -} - -const PRINT_TIMEOUT_MS = 30_000; +import type { PrinterTransport } from '../types'; /** Electron serial adapter — sends raw bytes to the main process, which writes to the serial device * (macOS /dev/cu.*, Linux /dev/rfcomm*) for OS-paired Bluetooth Classic printers. */ @@ -24,21 +10,11 @@ export class SerialElectronAdapter implements PrinterTransport { constructor(private deviceKey: string) {} async printRaw(data: Uint8Array): Promise { - const ipc = getIpc(); - let timeoutId: ReturnType | undefined; - try { - await Promise.race([ - ipc.invoke('print-raw-serial', { device: this.deviceKey, data: Array.from(data) }), - new Promise((_, reject) => { - timeoutId = setTimeout( - () => reject(new Error(`Serial print timed out after ${PRINT_TIMEOUT_MS}ms`)), - PRINT_TIMEOUT_MS - ); - }), - ]); - } finally { - if (timeoutId !== undefined) clearTimeout(timeoutId); - } + await ipcPrintRaw( + 'print-raw-serial', + { device: this.deviceKey, data }, + `Serial print timed out after ${PRINT_TIMEOUT_MS}ms` + ); } async printHtml(_html: string): Promise { diff --git a/packages/printer/src/transport/usb-adapter.electron.ts b/packages/printer/src/transport/usb-adapter.electron.ts index 687f45a34..79cb68049 100644 --- a/packages/printer/src/transport/usb-adapter.electron.ts +++ b/packages/printer/src/transport/usb-adapter.electron.ts @@ -1,20 +1,6 @@ -import type { PrinterTransport } from '../types'; - -interface ElectronIpc { - invoke: (channel: string, args: unknown) => Promise; -} +import { ipcPrintRaw, PRINT_TIMEOUT_MS } from './ipc-print.electron'; -function getIpc(): ElectronIpc { - const w = window as { - ipcRenderer?: ElectronIpc; - electronAPI?: { ipcRenderer?: ElectronIpc }; - }; - const ipc = w.ipcRenderer ?? w.electronAPI?.ipcRenderer; - if (!ipc) throw new Error('Electron ipcRenderer not available'); - return ipc; -} - -const PRINT_TIMEOUT_MS = 30_000; +import type { PrinterTransport } from '../types'; /** Electron USB adapter — sends raw bytes to the main process, which writes to the USB endpoint. */ export class UsbElectronAdapter implements PrinterTransport { @@ -23,16 +9,11 @@ export class UsbElectronAdapter implements PrinterTransport { constructor(private deviceKey: string) {} async printRaw(data: Uint8Array): Promise { - const ipc = getIpc(); - await Promise.race([ - ipc.invoke('print-raw-usb', { device: this.deviceKey, data: Array.from(data) }), - new Promise((_, reject) => - setTimeout( - () => reject(new Error(`USB print timed out after ${PRINT_TIMEOUT_MS}ms`)), - PRINT_TIMEOUT_MS - ) - ), - ]); + await ipcPrintRaw( + 'print-raw-usb', + { device: this.deviceKey, data }, + `USB print timed out after ${PRINT_TIMEOUT_MS}ms` + ); } async printHtml(_html: string): Promise {