taro-bluetooth-print 2.3.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +6 -1
  3. package/dist/index.cjs.js +1 -1
  4. package/dist/index.es.js +1 -1
  5. package/dist/index.umd.js +1 -1
  6. package/dist/types/config/PrinterConfigManager.d.ts +206 -0
  7. package/dist/types/config/index.d.ts +8 -0
  8. package/dist/types/device/MultiPrinterManager.d.ts +164 -0
  9. package/dist/types/device/index.d.ts +2 -0
  10. package/dist/types/index.d.ts +5 -1
  11. package/dist/types/services/BatchPrintManager.d.ts +205 -0
  12. package/dist/types/services/PrintHistory.d.ts +142 -0
  13. package/dist/types/services/PrintJobManager.d.ts +28 -4
  14. package/dist/types/services/PrinterStatus.d.ts +97 -0
  15. package/dist/types/services/index.d.ts +3 -0
  16. package/package.json +2 -2
  17. package/src/adapters/AlipayAdapter.ts +1 -0
  18. package/src/adapters/BaiduAdapter.ts +1 -0
  19. package/src/adapters/ByteDanceAdapter.ts +1 -0
  20. package/src/adapters/TaroAdapter.ts +1 -0
  21. package/src/adapters/WebBluetoothAdapter.ts +1 -1
  22. package/src/config/PrinterConfigManager.ts +519 -0
  23. package/src/config/index.ts +15 -0
  24. package/src/device/MultiPrinterManager.ts +470 -0
  25. package/src/device/index.ts +8 -0
  26. package/src/encoding/gbk-lite.ts +81 -76
  27. package/src/encoding/gbk-table.ts +14 -14
  28. package/src/index.ts +16 -1
  29. package/src/services/BatchPrintManager.ts +500 -0
  30. package/src/services/ConnectionManager.ts +4 -1
  31. package/src/services/PrintHistory.ts +336 -0
  32. package/src/services/PrintJobManager.ts +69 -9
  33. package/src/services/PrinterStatus.ts +267 -0
  34. package/src/services/index.ts +6 -0
  35. package/src/template/TemplateEngine.ts +4 -1
@@ -0,0 +1,500 @@
1
+ /**
2
+ * Batch Print Manager
3
+ *
4
+ * Optimizes printing multiple jobs by:
5
+ * - Merging small jobs into larger chunks
6
+ * - Reducing Bluetooth communication overhead
7
+ * - Prioritizing urgent jobs
8
+ * - Batching similar content for efficiency
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const batchManager = new BatchPrintManager();
13
+ *
14
+ * // Add print jobs
15
+ * batchManager.addJob({ data: buffer1, priority: 1 });
16
+ * batchManager.addJob({ data: buffer2, priority: 2 });
17
+ *
18
+ * // Process batch when ready
19
+ * await batchManager.processBatch();
20
+ * ```
21
+ */
22
+
23
+ import { Logger } from '@/utils/logger';
24
+ import { BluetoothPrintError, ErrorCode } from '@/errors/BluetoothError';
25
+
26
+ /**
27
+ * Batch job entry
28
+ */
29
+ export interface BatchJob {
30
+ /** Unique job ID */
31
+ id: string;
32
+ /** Print data */
33
+ data: Uint8Array;
34
+ /** Priority (higher = more urgent) */
35
+ priority: number;
36
+ /** Timestamp when added */
37
+ addedAt: number;
38
+ /** Metadata */
39
+ metadata?: Record<string, unknown>;
40
+ }
41
+
42
+ /**
43
+ * Batch configuration
44
+ */
45
+ export interface BatchConfig {
46
+ /** Maximum batch size in bytes */
47
+ maxBatchSize: number;
48
+ /** Maximum wait time before processing in ms */
49
+ maxWaitTime: number;
50
+ /** Minimum jobs before batching */
51
+ minBatchSize: number;
52
+ /** Merge similar content */
53
+ enableMerging: boolean;
54
+ /** Auto-process interval in ms (0 = disabled) */
55
+ autoProcessInterval: number;
56
+ }
57
+
58
+ /**
59
+ * Batch statistics
60
+ */
61
+ export interface BatchStats {
62
+ /** Total jobs added */
63
+ totalJobs: number;
64
+ /** Total bytes processed */
65
+ totalBytes: number;
66
+ /** Batches processed */
67
+ batchesProcessed: number;
68
+ /** Average batch size */
69
+ avgBatchSize: number;
70
+ /** Merged jobs count */
71
+ mergedJobs: number;
72
+ }
73
+
74
+ /**
75
+ * Batch events
76
+ */
77
+ export interface BatchEvents {
78
+ 'batch-ready': (data: BatchJob[]) => void;
79
+ 'batch-processed': (data: { jobCount: number; bytes: number }) => void;
80
+ 'job-added': (data: BatchJob) => void;
81
+ 'job-rejected': (data: { reason: string }) => void;
82
+ }
83
+
84
+ /**
85
+ * Event handler map type
86
+ */
87
+ type BatchEventHandlerMap = {
88
+ [K in keyof BatchEvents]: Set<BatchEvents[K]>;
89
+ };
90
+
91
+ /**
92
+ * Default batch configuration
93
+ */
94
+ const DEFAULT_CONFIG: BatchConfig = {
95
+ maxBatchSize: 1024 * 50, // 50KB max per batch
96
+ maxWaitTime: 1000, // 1 second max wait
97
+ minBatchSize: 1, // Process even single jobs
98
+ enableMerging: true, // Enable content merging
99
+ autoProcessInterval: 500, // Check every 500ms
100
+ };
101
+
102
+ /**
103
+ * Batch Print Manager
104
+ *
105
+ * Collects print jobs and processes them in optimized batches.
106
+ * Reduces Bluetooth communication overhead by combining small jobs.
107
+ */
108
+ export class BatchPrintManager {
109
+ private readonly logger = Logger.scope('BatchPrintManager');
110
+ private readonly jobs: BatchJob[] = [];
111
+ private readonly listeners: BatchEventHandlerMap = {
112
+ 'batch-ready': new Set(),
113
+ 'batch-processed': new Set(),
114
+ 'job-added': new Set(),
115
+ 'job-rejected': new Set(),
116
+ };
117
+ private config: BatchConfig;
118
+ private isProcessing = false;
119
+ private waitTimer: ReturnType<typeof setTimeout> | null = null;
120
+ private autoProcessTimer: ReturnType<typeof setInterval> | null = null;
121
+ private stats: BatchStats = {
122
+ totalJobs: 0,
123
+ totalBytes: 0,
124
+ batchesProcessed: 0,
125
+ avgBatchSize: 0,
126
+ mergedJobs: 0,
127
+ };
128
+
129
+ /**
130
+ * Creates a new BatchPrintManager instance
131
+ */
132
+ constructor(config: Partial<BatchConfig> = {}) {
133
+ this.config = { ...DEFAULT_CONFIG, ...config };
134
+ }
135
+
136
+ /**
137
+ * Register event listener
138
+ */
139
+ on<K extends keyof BatchEvents>(
140
+ event: K,
141
+ callback: BatchEvents[K]
142
+ ): void {
143
+ this.listeners[event].add(callback);
144
+ }
145
+
146
+ /**
147
+ * Remove event listener
148
+ */
149
+ off<K extends keyof BatchEvents>(
150
+ event: K,
151
+ callback: BatchEvents[K]
152
+ ): void {
153
+ this.listeners[event].delete(callback);
154
+ }
155
+
156
+ /**
157
+ * Emit an event
158
+ */
159
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
160
+ private emit<K extends keyof BatchEvents>(event: K, data: any): void {
161
+ this.listeners[event].forEach(handler => {
162
+ try {
163
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
+ (handler as any)(data);
165
+ } catch (error) {
166
+ this.logger.error(`Error in event handler for "${event}":`, error);
167
+ }
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Add a job to the batch queue
173
+ *
174
+ * @param data - Print data
175
+ * @param priority - Job priority (higher = more urgent)
176
+ * @param metadata - Optional metadata
177
+ * @returns Job ID
178
+ */
179
+ addJob(
180
+ data: Uint8Array,
181
+ priority = 1,
182
+ metadata?: Record<string, unknown>
183
+ ): string {
184
+ const id = this.generateId();
185
+ const job: BatchJob = {
186
+ id,
187
+ data,
188
+ priority,
189
+ addedAt: Date.now(),
190
+ metadata,
191
+ };
192
+
193
+ this.jobs.push(job);
194
+ this.stats.totalJobs++;
195
+
196
+ // Sort by priority (descending)
197
+ this.jobs.sort((a, b) => b.priority - a.priority);
198
+
199
+ this.emit('job-added', job);
200
+ this.logger.debug(`Job added: ${id} (priority: ${priority}, queue size: ${this.jobs.length})`);
201
+
202
+ // Start/restart wait timer
203
+ this.startWaitTimer();
204
+
205
+ // Check if we should process immediately
206
+ if (this.shouldProcessImmediately()) {
207
+ this.emit('batch-ready', [...this.jobs]);
208
+ }
209
+
210
+ // Start auto-process if enabled
211
+ this.startAutoProcess();
212
+
213
+ return id;
214
+ }
215
+
216
+ /**
217
+ * Add multiple jobs at once
218
+ */
219
+ addJobs(jobs: Array<{ data: Uint8Array; priority?: number; metadata?: Record<string, unknown> }>): string[] {
220
+ return jobs.map(job => this.addJob(job.data, job.priority, job.metadata));
221
+ }
222
+
223
+ /**
224
+ * Cancel a job by ID
225
+ */
226
+ cancelJob(id: string): boolean {
227
+ const index = this.jobs.findIndex(j => j.id === id);
228
+ if (index === -1) {
229
+ return false;
230
+ }
231
+
232
+ this.jobs.splice(index, 1);
233
+ this.logger.debug(`Job cancelled: ${id}`);
234
+ return true;
235
+ }
236
+
237
+ /**
238
+ * Cancel all jobs
239
+ */
240
+ cancelAll(): void {
241
+ const count = this.jobs.length;
242
+ this.jobs.length = 0;
243
+ this.clearTimers();
244
+ this.logger.info(`Cancelled ${count} jobs`);
245
+ }
246
+
247
+ /**
248
+ * Get pending job count
249
+ */
250
+ get pendingCount(): number {
251
+ return this.jobs.length;
252
+ }
253
+
254
+ /**
255
+ * Get pending jobs
256
+ */
257
+ getPendingJobs(): BatchJob[] {
258
+ return [...this.jobs];
259
+ }
260
+
261
+ /**
262
+ * Get current statistics
263
+ */
264
+ getStats(): BatchStats {
265
+ return { ...this.stats };
266
+ }
267
+
268
+ /**
269
+ * Update configuration
270
+ */
271
+ updateConfig(updates: Partial<BatchConfig>): void {
272
+ this.config = { ...this.config, ...updates };
273
+ this.logger.debug('Configuration updated');
274
+ }
275
+
276
+ /**
277
+ * Process the current batch
278
+ *
279
+ * @param processor - Function to send batch data to printer
280
+ * @returns Number of jobs processed
281
+ */
282
+ async processBatch(
283
+ processor: (data: Uint8Array) => Promise<void>
284
+ ): Promise<number> {
285
+ if (this.isProcessing) {
286
+ throw new BluetoothPrintError(
287
+ ErrorCode.PRINT_JOB_IN_PROGRESS,
288
+ 'Batch processing already in progress'
289
+ );
290
+ }
291
+
292
+ if (this.jobs.length === 0) {
293
+ this.logger.debug('No jobs to process');
294
+ return 0;
295
+ }
296
+
297
+ this.isProcessing = true;
298
+ this.clearTimers();
299
+
300
+ try {
301
+ // Get jobs for this batch
302
+ const batchJobs = this.prepareBatch();
303
+ const mergedData = this.mergeJobs(batchJobs);
304
+
305
+ this.logger.info(`Processing batch: ${batchJobs.length} jobs, ${mergedData.length} bytes`);
306
+
307
+ // Emit batch ready event
308
+ this.emit('batch-ready', batchJobs);
309
+
310
+ // Process the merged data
311
+ await processor(mergedData);
312
+
313
+ // Update stats
314
+ this.stats.totalBytes += mergedData.length;
315
+ this.stats.batchesProcessed++;
316
+ this.stats.avgBatchSize =
317
+ (this.stats.avgBatchSize * (this.stats.batchesProcessed - 1) + mergedData.length) /
318
+ this.stats.batchesProcessed;
319
+
320
+ // Remove processed jobs
321
+ this.jobs.splice(0, batchJobs.length);
322
+
323
+ this.emit('batch-processed', { jobCount: batchJobs.length, bytes: mergedData.length });
324
+ this.logger.info(`Batch processed: ${batchJobs.length} jobs`);
325
+
326
+ return batchJobs.length;
327
+ } finally {
328
+ this.isProcessing = false;
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Prepare batch from pending jobs
334
+ */
335
+ private prepareBatch(): BatchJob[] {
336
+ const batch: BatchJob[] = [];
337
+ let totalSize = 0;
338
+
339
+ for (const job of this.jobs) {
340
+ // Check if adding this job would exceed max batch size
341
+ if (batch.length > 0 && totalSize + job.data.length > this.config.maxBatchSize) {
342
+ // Don't add if it would exceed, and we already have some jobs
343
+ if (batch.length >= this.config.minBatchSize) {
344
+ break;
345
+ }
346
+ }
347
+
348
+ batch.push(job);
349
+ totalSize += job.data.length;
350
+ }
351
+
352
+ return batch;
353
+ }
354
+
355
+ /**
356
+ * Merge multiple jobs into a single buffer
357
+ */
358
+ private mergeJobs(jobs: BatchJob[]): Uint8Array {
359
+ if (!this.config.enableMerging || jobs.length === 1) {
360
+ return jobs[0]?.data ?? new Uint8Array(0);
361
+ }
362
+
363
+ // Calculate total size
364
+ let totalSize = 0;
365
+ for (const job of jobs) {
366
+ totalSize += job.data.length;
367
+ }
368
+
369
+ // Merge into single buffer
370
+ const result = new Uint8Array(totalSize);
371
+ let offset = 0;
372
+
373
+ for (const job of jobs) {
374
+ result.set(job.data, offset);
375
+ offset += job.data.length;
376
+ this.stats.mergedJobs++;
377
+ }
378
+
379
+ this.logger.debug(`Merged ${jobs.length} jobs into ${totalSize} bytes`);
380
+ return result;
381
+ }
382
+
383
+ /**
384
+ * Check if we should process immediately
385
+ */
386
+ private shouldProcessImmediately(): boolean {
387
+ if (this.jobs.length === 0) {
388
+ return false;
389
+ }
390
+
391
+ // Large single job
392
+ const firstJob = this.jobs[0];
393
+ if (this.jobs.length === 1 && firstJob && firstJob.data.length >= this.config.maxBatchSize * 0.8) {
394
+ return true;
395
+ }
396
+
397
+ // Queue is full
398
+ const totalSize = this.jobs.reduce((sum, j) => sum + j.data.length, 0);
399
+ if (totalSize >= this.config.maxBatchSize) {
400
+ return true;
401
+ }
402
+
403
+ return false;
404
+ }
405
+
406
+ /**
407
+ * Start the wait timer
408
+ */
409
+ private startWaitTimer(): void {
410
+ this.clearWaitTimer();
411
+
412
+ if (this.config.maxWaitTime <= 0) {
413
+ return;
414
+ }
415
+
416
+ this.waitTimer = setTimeout(() => {
417
+ this.logger.debug('Wait timer expired, batch ready');
418
+ this.emit('batch-ready', [...this.jobs]);
419
+ }, this.config.maxWaitTime);
420
+ }
421
+
422
+ /**
423
+ * Clear wait timer
424
+ */
425
+ private clearWaitTimer(): void {
426
+ if (this.waitTimer) {
427
+ clearTimeout(this.waitTimer);
428
+ this.waitTimer = null;
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Start auto-process timer
434
+ */
435
+ private startAutoProcess(): void {
436
+ if (this.autoProcessTimer || this.config.autoProcessInterval <= 0) {
437
+ return;
438
+ }
439
+
440
+ this.autoProcessTimer = setInterval(() => {
441
+ // Check if batch is ready
442
+ if (this.shouldProcessImmediately()) {
443
+ this.emit('batch-ready', [...this.jobs]);
444
+ }
445
+ }, this.config.autoProcessInterval);
446
+ }
447
+
448
+ /**
449
+ * Stop auto-process timer
450
+ */
451
+ private stopAutoProcess(): void {
452
+ if (this.autoProcessTimer) {
453
+ clearInterval(this.autoProcessTimer);
454
+ this.autoProcessTimer = null;
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Clear all timers
460
+ */
461
+ private clearTimers(): void {
462
+ this.clearWaitTimer();
463
+ this.stopAutoProcess();
464
+ }
465
+
466
+ /**
467
+ * Generate unique job ID
468
+ */
469
+ private generateId(): string {
470
+ return `batch_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
471
+ }
472
+
473
+ /**
474
+ * Reset statistics
475
+ */
476
+ resetStats(): void {
477
+ this.stats = {
478
+ totalJobs: 0,
479
+ totalBytes: 0,
480
+ batchesProcessed: 0,
481
+ avgBatchSize: 0,
482
+ mergedJobs: 0,
483
+ };
484
+ }
485
+
486
+ /**
487
+ * Destroy the manager
488
+ */
489
+ destroy(): void {
490
+ this.cancelAll();
491
+ // Clear all listeners
492
+ for (const key of Object.keys(this.listeners) as (keyof BatchEventHandlerMap)[]) {
493
+ this.listeners[key].clear();
494
+ }
495
+ this.logger.info('BatchPrintManager destroyed');
496
+ }
497
+ }
498
+
499
+ // Export singleton for convenience
500
+ export const batchPrintManager = new BatchPrintManager();
@@ -185,7 +185,10 @@ export class ConnectionManager
185
185
  this.emit('error', printError);
186
186
  throw printError;
187
187
  }
188
- this.connLogger.warn(`Connection attempt ${attempts}/${retries} failed, retrying...`, error);
188
+ this.connLogger.warn(
189
+ `Connection attempt ${attempts}/${retries} failed, retrying...`,
190
+ error
191
+ );
189
192
  await new Promise(resolve => setTimeout(resolve, 1000));
190
193
  }
191
194
  }