Compare commits

..

No commits in common. "258b97eb1554918a60f0cf0609cc464ed1656f8a" and "80b01b176affa297796e2e2cf8ba0553d4e1eb12" have entirely different histories.

13 changed files with 92 additions and 447 deletions

3
.gitignore vendored
View File

@ -398,6 +398,3 @@ Temporary Items
dist dist
.webpack .webpack
.serverless/**/*.zip .serverless/**/*.zip
# Zed editor workspace
.zed/

View File

@ -37,35 +37,33 @@ Retorna a lista de impressoras instaladas no servidor onde a API está rodando.
### 2. Imprimir HTML (Etiquetas/Layouts) ### 2. Imprimir HTML (Etiquetas/Layouts)
Converte um código HTML (com suporte a Tailwind CSS) para PDF e imprime na impressora selecionada via Fila de Processamento (BullMQ). Converte um código HTML (com suporte a Tailwind CSS) para PDF e imprime na impressora selecionada. Ideal para etiquetas.
- **Método:** `POST` - **Método:** `POST`
- **Rota:** `/printer/print-html` ou `/printer/:printerName/print-html` - **Rota:** `/printer/print-html`
- **Corpo da Requisição (JSON):** - **Corpo da Requisição (JSON):**
```json ```json
{ {
"html": "<div class='text-xl font-bold'>Minha Etiqueta</div>...", "html": "<div class='text-xl font-bold'>Minha Etiqueta</div>...",
"printerName": "POS-80", // Opcional se passado na URL "printerName": "POS-80", // Nome exato da impressora (conforme retornado na listagem)
"width": "60mm", // Opcional. Largura do papel/etiqueta. Default: 80mm "width": "60mm", // Opcional. Largura do papel/etiqueta. Default: 80mm
"height": "40mm", // Opcional. Altura da etiqueta. Default: auto "height": "40mm" // Opcional. Altura da etiqueta. Default: auto
"jobId": "meu-id-unico-123" // Opcional. ID para idempotência na fila.
} }
``` ```
- **Detalhes:** - **Detalhes:**
- O backend injeta automaticamente o script do Tailwind CSS. - O backend injeta automaticamente o script do Tailwind CSS.
- O HTML é renderizado via Puppeteer (Chrome headless). - O HTML é renderizado via Puppeteer (Chrome headless).
- Tarefas são processadas de forma assíncrona via Redis/BullMQ.
--- ---
### 3. Imprimir Texto Simples (ESC/POS) ### 3. Imprimir Texto Simples (ESC/POS)
Envia comandos de texto diretamente para a impressora. Ideal para cupons simples e rápidos. Envia comandos de texto diretamente para a impressora. Ideal para cupons simples e rápidos, sem formatação complexa.
- **Método:** `POST` - **Método:** `POST`
- **Rota:** `/printer/print` ou `/printer/:printerName/print` - **Rota:** `/printer/print`
- **Corpo da Requisição (JSON):** - **Corpo da Requisição (JSON):**
```json ```json
@ -78,7 +76,7 @@ Envia comandos de texto diretamente para a impressora. Ideal para cupons simples
], ],
"alignment": "center", // "left", "center", "right". Default: left "alignment": "center", // "left", "center", "right". Default: left
"upsideDown": false, // Se true, imprime de cabeça para baixo "upsideDown": false, // Se true, imprime de cabeça para baixo
"printerInterface": "POS-80" // Opcional se passado na URL. Suporta "tcp://192.168.1.100" ou nome da impressora. "printerInterface": "POS-80" // Pode ser nome da impressora (USB) ou "tcp://192.168.1.100" (Rede)
} }
``` ```
@ -100,7 +98,6 @@ export interface PrintHtmlPayload {
printerName: string; printerName: string;
width?: string; width?: string;
height?: string; height?: string;
jobId?: string;
} }
export interface PrintTextPayload { export interface PrintTextPayload {
@ -110,7 +107,3 @@ export interface PrintTextPayload {
printerInterface?: string; printerInterface?: string;
} }
``` ```
## Swagger
A documentação interativa completa (OpenAPI) está disponível em `/api`. Lá você pode testar todos os endpoints e ver os esquemas detalhados de cada DTO.

View File

@ -1,14 +1,21 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { PrinterController } from './controller/printer.controller'; import { TypeOrmModule } from '@nestjs/typeorm';
import { PrinterService } from './services/printer.service'; import { typeOrmConfig } from './config/typeorm.config';
import { ListPrinterController } from './controller/list-printer.controller';
import { ListPrinterService } from './services/create-printer';
import { PrinterProcessor } from './services/printer.processor'; import { PrinterProcessor } from './services/printer.processor';
import { InfrastructureModule } from './infrastructure/infrastructure.module'; import { BullQueuesModule } from './config/BullModule';
@Module({ @Module({
imports: [InfrastructureModule], imports: [
controllers: [AppController, PrinterController], ConfigModule.forRoot({ isGlobal: true }),
providers: [AppService, PrinterService, PrinterProcessor], TypeOrmModule.forRoot(typeOrmConfig),
BullQueuesModule
],
controllers: [AppController, ListPrinterController],
providers: [AppService, ListPrinterService, PrinterProcessor],
}) })
export class AppModule {} export class AppModule {}

View File

@ -5,23 +5,15 @@ import { ExpressAdapter } from '@bull-board/express';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
function stripWrappingQuotes(value: string | undefined): string | undefined {
if (value == null) return undefined;
const trimmed = value.trim();
return trimmed.replace(/^['"](.*)['"]$/, '$1');
}
@Module({ @Module({
imports: [ imports: [
BullModule.forRootAsync({ BullModule.forRootAsync({
imports: [ConfigModule], imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
connection: { connection: {
host: stripWrappingQuotes(configService.get<string>('REDIS_HOST')), host: configService.get<string>('REDIS_HOST'),
port: parseInt(configService.get<string>('REDIS_PORT') ?? '6379', 10), port: configService.get<number>('REDIS_PORT') || 6379,
password: stripWrappingQuotes( password: configService.get<string>('REDIS_PASSWORD'),
configService.get<string>('REDIS_PASSWORD'),
),
}, },
}), }),
inject: [ConfigService], inject: [ConfigService],
@ -40,7 +32,7 @@ function stripWrappingQuotes(value: string | undefined): string | undefined {
type: 'exponential', type: 'exponential',
delay: 1000, delay: 1000,
}, },
removeOnComplete: false, removeOnComplete: false,
removeOnFail: { count: 500 }, removeOnFail: { count: 500 },
}, },
}), }),
@ -52,4 +44,4 @@ function stripWrappingQuotes(value: string | undefined): string | undefined {
], ],
exports: [BullModule], exports: [BullModule],
}) })
export class BullQueuesModule {} export class BullQueuesModule {}

View File

@ -1,52 +1,20 @@
import { Logger } from '@nestjs/common'; import { DataSource, DataSourceOptions } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { DataSourceOptions } from 'typeorm';
export function createTypeOrmOptions( export const typeOrmConfig: DataSourceOptions = {
configService: ConfigService, type: 'oracle',
): TypeOrmModuleOptions { username: process.env.DB_USERNAME || 'teste',
const baseOptions: DataSourceOptions = { password: process.env.DB_PASSWORD || 'teste',
type: 'oracle', connectString:
username: configService.get<string>('DB_USERNAME') || 'teste', process.env.DB_CONNECT_STRING ||
password: configService.get<string>('DB_PASSWORD') || 'teste', '(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=10.1.1.241)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=BDTESTE)))',
connectString: synchronize: false,
configService.get<string>('DB_CONNECT_STRING') || logging: true,
'(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=10.1.1.241)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=BDTESTE)))', entities: [__dirname + '/../**/*.{entity,view}.{js,ts}'],
synchronize: false, subscribers: [process.env.DB_SUBSCRIBERS_PATH || 'dist/subscriber/*.js'],
logging: true, extra: {
entities: [__dirname + '/../**/*.{entity,view}.{js,ts}'], poolSize: parseInt(process.env.DB_POOL_SIZE || '10', 10),
subscribers: [ maxRows: parseInt(process.env.DB_MAX_ROWS || '1000', 10),
configService.get<string>('DB_SUBSCRIBERS_PATH') || },
'dist/subscriber/*.js', };
],
extra: {
poolSize: parseInt(configService.get<string>('DB_POOL_SIZE') || '10', 10),
maxRows: parseInt(configService.get<string>('DB_MAX_ROWS') || '1000', 10),
},
};
const retryAttempts = parseInt( export const AppDataSource = new DataSource(typeOrmConfig);
configService.get<string>('DB_RETRY_ATTEMPTS') || '5',
10,
);
const retryDelayMs = parseInt(
configService.get<string>('DB_RETRY_DELAY_MS') || '2000',
10,
);
const logger = new Logger('TypeORM');
logger.debug(
`TypeORM configured for manual initialization (retryAttempts=${retryAttempts}, retryDelayMs=${retryDelayMs}).`,
);
return {
...baseOptions,
manualInitialization: true,
retryAttempts: 0,
retryDelay: 0,
};
}

View File

@ -1,14 +1,6 @@
import { import { Controller, Post, Body, Get, HttpCode, HttpStatus, Param } from '@nestjs/common';
Controller,
Post,
Body,
Get,
HttpCode,
HttpStatus,
Param,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { PrinterService } from '../services/printer.service'; import { ListPrinterService } from '../services/create-printer';
import { PrintDataDto } from '../dto/print-data.dto'; import { PrintDataDto } from '../dto/print-data.dto';
import { PrintHtmlDto } from '../dto/print-html.dto'; import { PrintHtmlDto } from '../dto/print-html.dto';
import { PrinterDto } from '../dto/printer.dto'; import { PrinterDto } from '../dto/printer.dto';
@ -16,18 +8,16 @@ import { getPrinters } from '../services/get-printer';
@ApiTags('Printer') @ApiTags('Printer')
@Controller('printer') @Controller('printer')
export class PrinterController { export class ListPrinterController {
constructor(private readonly printerService: PrinterService) {} constructor(private readonly printerService: ListPrinterService) {}
@Post(':printerName/print') @Post(':printerName/print')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({ @ApiOperation({ summary: 'Envia um comando de impressão de texto simples (ESC/POS)' })
summary: 'Envia um comando de impressão de texto simples (ESC/POS)',
})
@ApiResponse({ status: 200, description: 'Impressão enviada com sucesso' }) @ApiResponse({ status: 200, description: 'Impressão enviada com sucesso' })
async print( async print(
@Param('printerName') printerName: string, @Param('printerName') printerName: string,
@Body() printData: PrintDataDto, @Body() printData: PrintDataDto
) { ) {
printData.printerInterface = printerName; printData.printerInterface = printerName;
return this.printerService.printReceipt(printData); return this.printerService.printReceipt(printData);
@ -42,14 +32,11 @@ export class PrinterController {
@Post(':printerName/print-html') @Post(':printerName/print-html')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({ @ApiOperation({ summary: 'Converte HTML para PDF e imprime na impressora especificada na URL' })
summary:
'Converte HTML para PDF e imprime na impressora especificada na URL',
})
@ApiResponse({ status: 200, description: 'HTML enviado para impressão' }) @ApiResponse({ status: 200, description: 'HTML enviado para impressão' })
async printHtml( async printHtml(
@Param('printerName') printerName: string, @Param('printerName') printerName: string,
@Body() printHtmlDto: PrintHtmlDto, @Body() printHtmlDto: PrintHtmlDto
) { ) {
printHtmlDto.printerName = printerName; printHtmlDto.printerName = printerName;
return this.printerService.printHtml(printHtmlDto); return this.printerService.printHtml(printHtmlDto);
@ -64,10 +51,10 @@ export class PrinterController {
@Get('list') @Get('list')
@ApiOperation({ summary: 'Lista impressoras disponíveis no sistema' }) @ApiOperation({ summary: 'Lista impressoras disponíveis no sistema' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Lista de impressoras encontradas', description: 'Lista de impressoras encontradas',
type: [PrinterDto], type: [PrinterDto]
}) })
async listPrinters() { async listPrinters() {
return getPrinters(); return getPrinters();

View File

@ -1,31 +0,0 @@
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { createTypeOrmOptions } from '../config/typeorm.config';
import { parseBooleanEnv } from '../helpers/env.helper';
import { OracleDbInitializer } from './oracle-db.initializer';
@Module({})
export class DatabaseModule {
static register(): DynamicModule {
const dbEnabled = parseBooleanEnv(process.env.DB_ENABLED, true);
if (!dbEnabled) {
return { module: DatabaseModule };
}
return {
module: DatabaseModule,
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) =>
createTypeOrmOptions(configService),
}),
],
providers: [OracleDbInitializer],
exports: [TypeOrmModule],
};
}
}

View File

@ -1,64 +0,0 @@
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DataSource } from 'typeorm';
import { parseBooleanEnv } from '../helpers/env.helper';
@Injectable()
export class OracleDbInitializer implements OnApplicationBootstrap {
private readonly logger = new Logger(OracleDbInitializer.name);
constructor(
private readonly dataSource: DataSource,
private readonly configService: ConfigService,
) {}
onApplicationBootstrap(): void {
const enabled = parseBooleanEnv(
this.configService.get<string>('DB_ENABLED'),
true,
);
if (!enabled) {
this.logger.log('DB initialization skipped (DB_ENABLED=false).');
return;
}
if (this.dataSource.isInitialized) return;
void this.initializeWithRetry();
}
private async initializeWithRetry(): Promise<void> {
const attempts = parseInt(
this.configService.get<string>('DB_RETRY_ATTEMPTS') || '5',
10,
);
const baseDelayMs = parseInt(
this.configService.get<string>('DB_RETRY_DELAY_MS') || '2000',
10,
);
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
await this.dataSource.initialize();
this.logger.log(
`Oracle DB connected (attempt ${attempt}/${attempts}).`,
);
return;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.logger.warn(
`Oracle DB connection failed (attempt ${attempt}/${attempts}): ${message}`,
);
if (attempt < attempts) {
const delay = baseDelayMs * attempt;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
this.logger.error(
`Oracle DB unreachable after ${attempts} attempts; continuing without DB (degraded mode).`,
);
}
}

View File

@ -1,15 +0,0 @@
export function parseBooleanEnv(
value: string | undefined,
defaultValue: boolean,
): boolean {
if (value == null) return defaultValue;
const normalized = value
.trim()
.replace(/^['"]|['"]$/g, '')
.toLowerCase();
if (normalized === 'true') return true;
if (normalized === 'false') return false;
return defaultValue;
}

View File

@ -1,15 +0,0 @@
import { Global, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { BullQueuesModule } from '../config/BullModule';
import { DatabaseModule } from '../database/database.module';
@Global()
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
DatabaseModule.register(),
BullQueuesModule,
],
exports: [BullQueuesModule, DatabaseModule],
})
export class InfrastructureModule {}

View File

@ -1,21 +1,11 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { json, urlencoded } from 'express';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { initializeOracleClient } from './helpers/oracle.helper'; import { initializeOracleClient } from './helpers/oracle.helper';
function getBodyLimit(): string {
const raw = process.env.BODY_LIMIT || '2mb';
return raw.trim().replace(/^['"]|['"]$/g, '');
}
async function bootstrap() { async function bootstrap() {
initializeOracleClient(); initializeOracleClient();
const app = await NestFactory.create(AppModule, { bodyParser: false }); const app = await NestFactory.create(AppModule);
const bodyLimit = getBodyLimit();
app.use(json({ limit: bodyLimit }));
app.use(urlencoded({ extended: true, limit: bodyLimit }));
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('API Documentation') .setTitle('API Documentation')

View File

@ -6,14 +6,12 @@ import { PrintHtmlDto } from '../dto/print-html.dto';
import { ThermalPrinter, PrinterTypes } from 'node-thermal-printer'; import { ThermalPrinter, PrinterTypes } from 'node-thermal-printer';
@Injectable() @Injectable()
export class PrinterService { export class ListPrinterService {
private readonly printerType = PrinterTypes.EPSON; private readonly printerType = PrinterTypes.EPSON;
constructor(@InjectQueue('printer') private printerQueue: Queue) {} constructor(@InjectQueue('printer') private printerQueue: Queue) {}
async printReceipt( async printReceipt(data: PrintDataDto): Promise<{ success: boolean; message: string }> {
data: PrintDataDto,
): Promise<{ success: boolean; message: string }> {
if (!data.lines?.length) { if (!data.lines?.length) {
return { success: false, message: 'No lines provided' }; return { success: false, message: 'No lines provided' };
} }
@ -21,11 +19,7 @@ export class PrinterService {
try { try {
let printerInterface = data.printerInterface; let printerInterface = data.printerInterface;
if ( if (printerInterface && !printerInterface.includes('://') && !printerInterface.startsWith('printer:')) {
printerInterface &&
!printerInterface.includes('://') &&
!printerInterface.startsWith('printer:')
) {
printerInterface = `printer:${printerInterface}`; printerInterface = `printer:${printerInterface}`;
} }
@ -50,12 +44,12 @@ export class PrinterService {
printer.beep(); printer.beep();
await printer.execute(); await printer.execute();
return { success: true, message: 'Print successful!' }; return { success: true, message: 'Print successful!' };
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
message: `Print failed: ${error.message || error}`, message: `Print failed: ${error.message || error}`
}; };
} }
} }
@ -64,7 +58,7 @@ export class PrinterService {
if (alignment === 'center') { if (alignment === 'center') {
return printer.alignCenter(); return printer.alignCenter();
} }
if (alignment === 'right') { if (alignment === 'right') {
return printer.alignRight(); return printer.alignRight();
} }
@ -72,24 +66,20 @@ export class PrinterService {
return printer.alignLeft(); return printer.alignLeft();
} }
async printHtml( async printHtml(data: PrintHtmlDto): Promise<{ success: boolean; message: string; jobId?: string }> {
data: PrintHtmlDto,
): Promise<{ success: boolean; message: string; jobId?: string }> {
const jobOptions: any = {}; const jobOptions: any = {};
if (data.jobId) { if (data.jobId) {
const safeJobId = /^\d+$/.test(data.jobId) const safeJobId = /^\d+$/.test(data.jobId) ? `print-${data.jobId}` : data.jobId;
? `print-${data.jobId}`
: data.jobId;
jobOptions.jobId = safeJobId; jobOptions.jobId = safeJobId;
} }
const job = await this.printerQueue.add('print-html-job', data, jobOptions); const job = await this.printerQueue.add('print-html-job', data, jobOptions);
return { return {
success: true, success: true,
message: 'Tarefa enviada para a fila de impressão', message: 'Tarefa enviada para a fila de impressão',
jobId: job.id, jobId: job.id
}; };
} }
} }

View File

@ -1,113 +1,26 @@
import { Processor, WorkerHost } from '@nestjs/bullmq'; import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Logger, OnApplicationShutdown } from '@nestjs/common';
import { Job } from 'bullmq'; import { Job } from 'bullmq';
import { performance } from 'node:perf_hooks';
import * as puppeteer from 'puppeteer'; import * as puppeteer from 'puppeteer';
import * as pdfPrinter from 'pdf-to-printer'; import * as pdfPrinter from 'pdf-to-printer';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import { PrintHtmlDto } from '../dto/print-html.dto';
@Processor('printer') @Processor('printer')
export class PrinterProcessor export class PrinterProcessor extends WorkerHost {
extends WorkerHost
implements OnApplicationShutdown async process(job: Job<any>): Promise<any> {
{
private readonly logger = new Logger(PrinterProcessor.name);
private browser: puppeteer.Browser | undefined;
private browserLaunching: Promise<puppeteer.Browser> | undefined;
private async getBrowser(): Promise<puppeteer.Browser> {
if (this.browser?.isConnected()) return this.browser;
if (this.browserLaunching) return this.browserLaunching;
this.browserLaunching = puppeteer
.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
})
.then((browser) => {
this.browser = browser;
return browser;
})
.finally(() => {
this.browserLaunching = undefined;
});
return this.browserLaunching;
}
async onApplicationShutdown(): Promise<void> {
try {
await this.browser?.close();
} catch (err) {
this.logger.warn(`Failed to close Puppeteer browser: ${String(err)}`);
} finally {
this.browser = undefined;
}
}
private logJob(
level: 'log' | 'warn' | 'error' | 'debug',
payload: Record<string, unknown>,
error?: unknown,
): void {
const message = JSON.stringify(payload);
if (level === 'error') {
const stack = error instanceof Error ? error.stack : undefined;
this.logger.error(message, stack);
return;
}
if (level === 'warn') {
this.logger.warn(message);
return;
}
if (level === 'debug') {
this.logger.debug(message);
return;
}
this.logger.log(message);
}
async process(job: Job<PrintHtmlDto>): Promise<any> {
const data = job.data; const data = job.data;
const printerName = data?.printerName; const tempFilePath = path.join(os.tmpdir(), `print-${job.id}-${Date.now()}.pdf`);
const jobCtx = { let browser;
jobId: job.id,
jobName: job.name,
printerName,
};
const tempFilePath = path.join(
os.tmpdir(),
`print-${job.id}-${Date.now()}.pdf`,
);
let page: puppeteer.Page | undefined;
let phase: 'render' | 'pdf' | 'spool' | 'unknown' = 'unknown';
const durationsMs: {
render?: number;
pdf?: number;
spool?: number;
total?: number;
} = {};
const totalStart = performance.now();
this.logJob('log', {
event: 'print_job_start',
...jobCtx,
width: data?.width,
height: data?.height,
});
try { try {
const browser = await this.getBrowser(); browser = await puppeteer.launch({
page = await browser.newPage(); headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
const width = data.width || '80mm'; const width = data.width || '80mm';
const height = data.height; const height = data.height;
@ -127,97 +40,30 @@ export class PrinterProcessor
</html> </html>
`; `;
phase = 'render';
const renderStart = performance.now();
await page.setContent(fullHtml, { waitUntil: 'networkidle0' }); await page.setContent(fullHtml, { waitUntil: 'networkidle0' });
durationsMs.render = performance.now() - renderStart;
this.logJob('log', { const pdfOptions: any = {
event: 'print_job_phase', path: tempFilePath,
phase,
durationMs: durationsMs.render,
...jobCtx,
});
const pdfOptions: any = {
path: tempFilePath,
printBackground: true, printBackground: true,
width: width, width: width,
margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' }, margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' }
}; };
if (height) pdfOptions.height = height; if (height) pdfOptions.height = height;
phase = 'pdf';
const pdfStart = performance.now();
await page.pdf(pdfOptions); await page.pdf(pdfOptions);
durationsMs.pdf = performance.now() - pdfStart; await browser.close();
this.logJob('log', {
event: 'print_job_phase',
phase,
durationMs: durationsMs.pdf,
...jobCtx,
});
await page.close(); await pdfPrinter.print(tempFilePath, {
page = undefined;
phase = 'spool';
const spoolStart = performance.now();
await pdfPrinter.print(tempFilePath, {
printer: data.printerName, printer: data.printerName,
silent: true, silent: true
});
durationsMs.spool = performance.now() - spoolStart;
this.logJob('log', {
event: 'print_job_phase',
phase,
durationMs: durationsMs.spool,
...jobCtx,
});
durationsMs.total = performance.now() - totalStart;
this.logJob('log', {
event: 'print_job_success',
...jobCtx,
durationsMs,
}); });
return { status: 'success', jobId: job.id }; return { status: 'success', jobId: job.id };
} catch (error) { } catch (error) {
if (page) { if (browser) await browser.close();
try { console.error(`Erro no Job ${job.id}:`, error);
await page.close();
} catch {
// ignore
}
}
const message = error instanceof Error ? error.message : String(error);
durationsMs.total = performance.now() - totalStart;
this.logJob(
'error',
{
event: 'print_job_error',
...jobCtx,
phase,
durationsMs,
errorMessage: message,
errorType: error instanceof Error ? error.name : typeof error,
},
error,
);
// If the shared browser crashed/closed, reset it so next job can relaunch.
if (/Target closed|Browser closed|Protocol error/i.test(message)) {
try {
await this.browser?.close();
} catch {
// ignore
} finally {
this.browser = undefined;
}
}
throw error; throw error;
} finally { } finally {
if (fs.existsSync(tempFilePath)) { if (fs.existsSync(tempFilePath)) {
@ -225,4 +71,4 @@ export class PrinterProcessor
} }
} }
} }
} }