taro-bluetooth-print 2.2.1 → 2.3.1

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 (56) hide show
  1. package/CHANGELOG.md +57 -165
  2. package/README.md +142 -285
  3. package/dist/index.cjs.js +1 -1
  4. package/dist/index.es.js +1 -81644
  5. package/dist/index.umd.js +1 -1
  6. package/dist/types/adapters/AdapterFactory.d.ts +0 -1
  7. package/dist/types/adapters/AlipayAdapter.d.ts +0 -1
  8. package/dist/types/adapters/BaiduAdapter.d.ts +0 -1
  9. package/dist/types/adapters/BaseAdapter.d.ts +0 -1
  10. package/dist/types/adapters/ByteDanceAdapter.d.ts +0 -1
  11. package/dist/types/adapters/TaroAdapter.d.ts +0 -1
  12. package/dist/types/adapters/WebBluetoothAdapter.d.ts +0 -1
  13. package/dist/types/config/PrinterConfig.d.ts +0 -1
  14. package/dist/types/core/BluetoothPrinter.d.ts +1 -2
  15. package/dist/types/core/EventEmitter.d.ts +6 -26
  16. package/dist/types/core/index.d.ts +6 -0
  17. package/dist/types/drivers/CpclDriver.d.ts +304 -0
  18. package/dist/types/drivers/EscPos.d.ts +0 -1
  19. package/dist/types/drivers/GPrinterDriver.d.ts +63 -0
  20. package/dist/types/drivers/TsplDriver.d.ts +251 -0
  21. package/dist/types/drivers/ZplDriver.d.ts +325 -0
  22. package/dist/types/drivers/index.d.ts +9 -0
  23. package/dist/types/encoding/gbk-lite.d.ts +8 -0
  24. package/dist/types/encoding/gbk-table.d.ts +8 -30
  25. package/dist/types/index.d.ts +10 -5
  26. package/dist/types/plugins/PluginManager.d.ts +87 -0
  27. package/dist/types/plugins/builtin/LoggingPlugin.d.ts +14 -0
  28. package/dist/types/plugins/builtin/RetryPlugin.d.ts +18 -0
  29. package/dist/types/plugins/index.d.ts +7 -0
  30. package/dist/types/plugins/types.d.ts +97 -0
  31. package/dist/types/services/CommandBuilder.d.ts +0 -1
  32. package/dist/types/services/ConnectionManager.d.ts +1 -2
  33. package/dist/types/services/PrintJobManager.d.ts +0 -1
  34. package/dist/types/services/index.d.ts +8 -0
  35. package/dist/types/services/interfaces/index.d.ts +0 -1
  36. package/dist/types/template/TemplateEngine.d.ts +0 -1
  37. package/package.json +36 -20
  38. package/src/adapters/BaseAdapter.ts +6 -8
  39. package/src/core/BluetoothPrinter.ts +15 -15
  40. package/src/core/EventEmitter.ts +15 -15
  41. package/src/core/index.ts +7 -0
  42. package/src/drivers/CpclDriver.ts +549 -0
  43. package/src/drivers/GPrinterDriver.ts +115 -0
  44. package/src/drivers/TsplDriver.ts +405 -0
  45. package/src/drivers/ZplDriver.ts +543 -0
  46. package/src/drivers/index.ts +37 -0
  47. package/src/encoding/gbk-lite.ts +108 -0
  48. package/src/encoding/gbk-table.ts +80 -58
  49. package/src/index.ts +27 -23
  50. package/src/plugins/PluginManager.ts +195 -0
  51. package/src/plugins/builtin/LoggingPlugin.ts +99 -0
  52. package/src/plugins/builtin/RetryPlugin.ts +103 -0
  53. package/src/plugins/index.ts +10 -0
  54. package/src/plugins/types.ts +119 -0
  55. package/src/services/ConnectionManager.ts +22 -22
  56. package/src/services/index.ts +16 -0
@@ -1,98 +1,120 @@
1
1
  /**
2
- * GBK Encoding Table
2
+ * GBK Encoding Table - 懒加载版本
3
3
  *
4
- * This module provides character mapping tables for GBK, GB2312, and Big5 encodings.
5
- * GBK is a superset of GB2312 and covers most Chinese characters used in simplified Chinese.
6
- * Big5 is used for traditional Chinese characters.
4
+ * 优化策略:
5
+ * 1. 默认使用精简版编码表 (gbk-lite.ts,约 3500 常用字)
6
+ * 2. 遇到非常用字时动态加载完整编码表
7
+ * 3. 二分查找代替 Map,大幅减少内存占用
7
8
  *
8
- * GBK encoding uses double-byte encoding for Chinese characters:
9
- * - First byte: 0x81-0xFE
10
- * - Second byte: 0x40-0xFE (excluding 0x7F)
11
- *
12
- * 映射数据存储在 gbk-data.ts 中,运行时解码为 Map。
13
9
  * GBK: 23940 个字符映射
14
10
  * Big5: 13911 个字符映射
15
11
  */
16
12
 
17
- import { GBK_DATA, BIG5_DATA } from './gbk-data';
13
+ import { binarySearchGbk, isInCommonRange } from './gbk-lite';
18
14
 
19
- /**
20
- * Unicode to GBK mapping table
21
- * Maps Unicode code points to GBK byte pairs
22
- */
15
+ // 懒加载完整编码数据
16
+ let GBK_DATA: number[] | null = null;
17
+ let BIG5_DATA: number[] | null = null;
18
+
19
+ function loadFullData() {
20
+ if (!GBK_DATA) {
21
+ const data = require('./gbk-data');
22
+ GBK_DATA = data.GBK_DATA;
23
+ BIG5_DATA = data.BIG5_DATA;
24
+ }
25
+ return { GBK_DATA: GBK_DATA!, BIG5_DATA: BIG5_DATA! };
26
+ }
27
+
28
+ // Unicode to GBK mapping table
23
29
  export const unicodeToGbk: Map<number, number> = new Map();
24
30
 
25
- /**
26
- * GBK to Unicode mapping table
27
- * Maps GBK byte pairs to Unicode code points
28
- */
31
+ // GBK to Unicode mapping table
29
32
  export const gbkToUnicode: Map<number, number> = new Map();
30
33
 
31
- /**
32
- * Unicode to Big5 mapping table
33
- */
34
+ // Unicode to Big5 mapping table
34
35
  export const unicodeToBig5: Map<number, number> = new Map();
35
36
 
36
- /**
37
- * Big5 to Unicode mapping table
38
- */
37
+ // Big5 to Unicode mapping table
39
38
  export const big5ToUnicode: Map<number, number> = new Map();
40
39
 
41
- /**
42
- * Decode flat array mapping data into forward and reverse maps.
43
- * Array format: [unicode1, encoded1, unicode2, encoded2, ...]
44
- */
45
- function decodeMappings(
46
- data: number[],
47
- forwardMap: Map<number, number>,
48
- reverseMap: Map<number, number>
49
- ): void {
50
- for (let i = 0; i < data.length; i += 2) {
51
- const unicode = data[i]!;
52
- const encoded = data[i + 1]!;
53
- forwardMap.set(unicode, encoded);
54
- reverseMap.set(encoded, unicode);
55
- }
56
- }
57
-
58
- // Initialize mappings at module load time
59
- decodeMappings(GBK_DATA, unicodeToGbk, gbkToUnicode);
60
- decodeMappings(BIG5_DATA, unicodeToBig5, big5ToUnicode);
61
-
62
40
  /**
63
41
  * Get GBK bytes for a Unicode character
64
- * @param unicode - Unicode code point
65
- * @returns GBK byte pair [high, low] or null if not found
42
+ * 先查精简表,查不到再懒加载完整表
66
43
  */
67
44
  export function getGbkBytes(unicode: number): [number, number] | null {
68
- const gbk = unicodeToGbk.get(unicode);
69
- if (gbk !== undefined) {
45
+ // ASCII 直接返回
46
+ if (unicode >= 0x20 && unicode <= 0x7E) {
47
+ return [0, unicode];
48
+ }
49
+
50
+ // 先查精简表
51
+ const gbk = binarySearchGbk(unicode);
52
+ if (gbk !== null) {
70
53
  return [(gbk >> 8) & 0xff, gbk & 0xff];
71
54
  }
55
+
56
+ // 非常用字,懒加载完整表
57
+ if (isInCommonRange(unicode)) {
58
+ const { GBK_DATA } = loadFullData();
59
+ for (let i = 0; i < GBK_DATA.length; i += 2) {
60
+ if (GBK_DATA[i] === unicode) {
61
+ const gbkValue = GBK_DATA[i + 1];
62
+ if (gbkValue !== undefined) {
63
+ return [(gbkValue >> 8) & 0xff, gbkValue & 0xff];
64
+ }
65
+ }
66
+ }
67
+ }
68
+
72
69
  return null;
73
70
  }
74
71
 
75
72
  /**
76
73
  * Get Unicode character from GBK bytes
77
- * @param high - High byte
78
- * @param low - Low byte
79
- * @returns Unicode code point or null if not found
74
+ * 懒加载完整表
80
75
  */
81
76
  export function getUnicodeFromGbk(high: number, low: number): number | null {
82
77
  const gbk = (high << 8) | low;
83
- return gbkToUnicode.get(gbk) ?? null;
78
+
79
+ // 先查缓存
80
+ const cached = gbkToUnicode.get(gbk);
81
+ if (cached !== undefined) return cached;
82
+
83
+ // 懒加载完整表
84
+ const { GBK_DATA } = loadFullData();
85
+ for (let i = 0; i < GBK_DATA.length; i += 2) {
86
+ if (GBK_DATA[i + 1] === gbk) {
87
+ const result = GBK_DATA[i];
88
+ return result ?? null;
89
+ }
90
+ }
91
+
92
+ return null;
84
93
  }
85
94
 
86
95
  /**
87
96
  * Get Big5 bytes for a Unicode character
88
- * @param unicode - Unicode code point
89
- * @returns Big5 byte pair or null if not found
97
+ * 懒加载完整表
90
98
  */
91
99
  export function getBig5Bytes(unicode: number): [number, number] | null {
92
- const big5 = unicodeToBig5.get(unicode);
93
- if (big5 !== undefined) {
94
- return [(big5 >> 8) & 0xff, big5 & 0xff];
100
+ // 先查缓存
101
+ const cached = unicodeToBig5.get(unicode);
102
+ if (cached !== undefined) {
103
+ const cachedValue = cached;
104
+ return [(cachedValue >> 8) & 0xff, cachedValue & 0xff];
105
+ }
106
+
107
+ // 懒加载完整表
108
+ const { BIG5_DATA } = loadFullData();
109
+ for (let i = 0; i < BIG5_DATA.length; i += 2) {
110
+ if (BIG5_DATA[i] === unicode) {
111
+ const big5 = BIG5_DATA[i + 1];
112
+ if (big5 !== undefined) {
113
+ return [(big5 >> 8) & 0xff, big5 & 0xff];
114
+ }
115
+ }
95
116
  }
117
+
96
118
  return null;
97
119
  }
98
120
 
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Taro Bluetooth Print Library
3
- * A lightweight, high-performance Bluetooth printing library for Taro
3
+ * 轻量级、高性能的蓝牙打印库
4
4
  *
5
5
  * @packageDocumentation
6
6
  */
@@ -10,28 +10,27 @@ export { BluetoothPrinter } from './core/BluetoothPrinter';
10
10
  export type { PrinterEvents } from './core/BluetoothPrinter';
11
11
  export { EventEmitter } from './core/EventEmitter';
12
12
 
13
- // Drivers
14
- export { EscPos } from './drivers/EscPos';
13
+ // Drivers - 打印机驱动
14
+ export * from './drivers';
15
15
 
16
- // Adapters
16
+ // Adapters - 平台适配器
17
17
  export { TaroAdapter } from './adapters/TaroAdapter';
18
+ export { AlipayAdapter } from './adapters/AlipayAdapter';
19
+ export { BaiduAdapter } from './adapters/BaiduAdapter';
20
+ export { ByteDanceAdapter } from './adapters/ByteDanceAdapter';
21
+ export { WebBluetoothAdapter } from './adapters/WebBluetoothAdapter';
18
22
  export { AdapterFactory } from './adapters/AdapterFactory';
19
23
  export { BaseAdapter } from './adapters/BaseAdapter';
20
- export { WebBluetoothAdapter } from './adapters/WebBluetoothAdapter';
21
24
  export type { WebBluetoothRequestOptions } from './adapters/WebBluetoothAdapter';
22
25
 
23
- // Services
24
- export { ConnectionManager } from './services/ConnectionManager';
25
- export type {
26
- ConnectionManagerConfig,
27
- ConnectionManagerEvents,
28
- } from './services/ConnectionManager';
26
+ // Services - 服务层
27
+ export * from './services';
29
28
 
30
- // Device Management
29
+ // Device Management - 设备管理
31
30
  export { DeviceManager } from './device/DeviceManager';
32
31
  export type { BluetoothDevice, ScanOptions, DeviceManagerEvents } from './device/DeviceManager';
33
32
 
34
- // Print Queue
33
+ // Print Queue - 打印队列
35
34
  export { PrintQueue } from './queue/PrintQueue';
36
35
  export type {
37
36
  PrintJob,
@@ -41,11 +40,11 @@ export type {
41
40
  PrintQueueEvents,
42
41
  } from './queue/PrintQueue';
43
42
 
44
- // Offline Cache
43
+ // Offline Cache - 离线缓存
45
44
  export { OfflineCache } from './cache/OfflineCache';
46
45
  export type { CachedJob, CacheConfig, CacheStats } from './cache/OfflineCache';
47
46
 
48
- // Template Engine
47
+ // Template Engine - 模板引擎
49
48
  export { TemplateEngine } from './template/TemplateEngine';
50
49
  export type {
51
50
  TemplateType,
@@ -56,32 +55,32 @@ export type {
56
55
  ValidationResult,
57
56
  } from './template/TemplateEngine';
58
57
 
59
- // Barcode Generator
58
+ // Barcode Generator - 条码生成
60
59
  export { BarcodeGenerator, BarcodeFormat } from './barcode/BarcodeGenerator';
61
60
  export type { BarcodeOptions } from './barcode/BarcodeGenerator';
62
61
 
63
- // Text Formatter
62
+ // Text Formatter - 文本格式化
64
63
  export { TextFormatter, TextAlign } from './formatter/TextFormatter';
65
64
  export type { TextStyle } from './formatter/TextFormatter';
66
65
 
67
- // Preview Renderer
66
+ // Preview Renderer - 打印预览
68
67
  export { PreviewRenderer } from './preview/PreviewRenderer';
69
68
  export type { PreviewOptions, PreviewResult } from './preview/PreviewRenderer';
70
69
 
71
- // Encoding Service
70
+ // Encoding - 编码服务
72
71
  export { EncodingService } from './encoding/EncodingService';
73
72
  export type { EncodingConfig } from './encoding/EncodingService';
74
73
 
75
- // Utilities
74
+ // Utilities - 工具函数
76
75
  export { Logger, LogLevel } from './utils/logger';
77
76
  export { Encoding } from './utils/encoding';
78
77
  export { ImageProcessing } from './utils/image';
79
78
  export { PlatformType, detectPlatform, isPlatformSupported } from './utils/platform';
80
79
 
81
- // Error handling
80
+ // Error handling - 错误处理
82
81
  export { BluetoothPrintError, ErrorCode } from './errors/BluetoothError';
83
82
 
84
- // Configuration
83
+ // Configuration - 配置
85
84
  export { DEFAULT_CONFIG, mergeConfig } from './config/PrinterConfig';
86
85
  export type {
87
86
  PrinterConfig,
@@ -90,5 +89,10 @@ export type {
90
89
  LoggingConfig,
91
90
  } from './config/PrinterConfig';
92
91
 
93
- // Types
92
+ // Plugin System - 插件系统
93
+ export { PluginManager } from './plugins/PluginManager';
94
+ export { createLoggingPlugin, createRetryPlugin } from './plugins';
95
+ export type { Plugin, PluginHooks, PluginOptions, PluginFactory } from './plugins/types';
96
+
97
+ // Types - 类型定义
94
98
  export * from './types';
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Plugin Manager
3
+ * Manages plugin lifecycle and hook execution
4
+ */
5
+
6
+ import { Plugin, PluginHooks, PluginOptions } from './types';
7
+ import { BluetoothPrintError, ErrorCode } from '@/errors/BluetoothError';
8
+ import { PrinterState } from '@/types';
9
+ import { Logger } from '@/utils/logger';
10
+
11
+ /**
12
+ * Manages plugins for BluetoothPrinter
13
+ */
14
+ export class PluginManager {
15
+ private plugins: Map<string, Plugin> = new Map();
16
+ private readonly logger = Logger.scope('PluginManager');
17
+
18
+ /**
19
+ * Register a plugin
20
+ * @param plugin - Plugin to register
21
+ * @param options - Plugin options
22
+ * @throws {BluetoothPrintError} If plugin with same name already exists
23
+ */
24
+ async register(plugin: Plugin, options?: PluginOptions): Promise<void> {
25
+ if (this.plugins.has(plugin.name)) {
26
+ throw new BluetoothPrintError(
27
+ ErrorCode.INVALID_CONFIGURATION,
28
+ `Plugin "${plugin.name}" is already registered`
29
+ );
30
+ }
31
+
32
+ this.logger.info(
33
+ `Registering plugin: ${plugin.name}${plugin.version ? ` v${plugin.version}` : ''}`
34
+ );
35
+
36
+ if (plugin.init) {
37
+ await plugin.init(options);
38
+ }
39
+
40
+ this.plugins.set(plugin.name, plugin);
41
+ this.logger.debug(`Plugin registered: ${plugin.name}`);
42
+ }
43
+
44
+ /**
45
+ * Unregister a plugin
46
+ * @param name - Plugin name to unregister
47
+ */
48
+ async unregister(name: string): Promise<void> {
49
+ const plugin = this.plugins.get(name);
50
+ if (!plugin) {
51
+ this.logger.warn(`Plugin not found: ${name}`);
52
+ return;
53
+ }
54
+
55
+ if (plugin.destroy) {
56
+ await plugin.destroy();
57
+ }
58
+
59
+ this.plugins.delete(name);
60
+ this.logger.info(`Plugin unregistered: ${name}`);
61
+ }
62
+
63
+ /**
64
+ * Get a registered plugin
65
+ * @param name - Plugin name
66
+ * @returns Plugin instance or undefined
67
+ */
68
+ get(name: string): Plugin | undefined {
69
+ return this.plugins.get(name);
70
+ }
71
+
72
+ /**
73
+ * Get all registered plugin names
74
+ * @returns Array of plugin names
75
+ */
76
+ getNames(): string[] {
77
+ return Array.from(this.plugins.keys());
78
+ }
79
+
80
+ /**
81
+ * Check if a plugin is registered
82
+ * @param name - Plugin name
83
+ * @returns True if registered
84
+ */
85
+ has(name: string): boolean {
86
+ return this.plugins.has(name);
87
+ }
88
+
89
+ /**
90
+ * Execute a hook across all plugins
91
+ * @param hookName - Name of the hook to execute
92
+ * @param args - Arguments to pass to the hook
93
+ * @returns Result from hooks (last non-void result)
94
+ */
95
+ async executeHook<K extends keyof PluginHooks>(
96
+ hookName: K,
97
+ ...args: Parameters<NonNullable<PluginHooks[K]>>
98
+ ): Promise<unknown> {
99
+ let result: unknown = undefined;
100
+
101
+ for (const [name, plugin] of this.plugins) {
102
+ const hook = plugin.hooks[hookName];
103
+ if (hook) {
104
+ try {
105
+ // @ts-expect-error - TypeScript can't infer the correct types here
106
+ const hookResult = await hook(...args);
107
+ if (hookResult !== undefined) {
108
+ result = hookResult;
109
+ }
110
+ } catch (error) {
111
+ this.logger.error(`Plugin "${name}" hook "${hookName}" failed:`, error);
112
+ // Continue to next plugin
113
+ }
114
+ }
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * Execute beforeConnect hooks
122
+ */
123
+ async beforeConnect(deviceId: string): Promise<string> {
124
+ const result = await this.executeHook('beforeConnect', deviceId);
125
+ return (result as string) || deviceId;
126
+ }
127
+
128
+ /**
129
+ * Execute afterConnect hooks
130
+ */
131
+ async afterConnect(deviceId: string): Promise<void> {
132
+ await this.executeHook('afterConnect', deviceId);
133
+ }
134
+
135
+ /**
136
+ * Execute beforeDisconnect hooks
137
+ */
138
+ async beforeDisconnect(deviceId: string): Promise<void> {
139
+ await this.executeHook('beforeDisconnect', deviceId);
140
+ }
141
+
142
+ /**
143
+ * Execute afterDisconnect hooks
144
+ */
145
+ async afterDisconnect(deviceId: string): Promise<void> {
146
+ await this.executeHook('afterDisconnect', deviceId);
147
+ }
148
+
149
+ /**
150
+ * Execute beforePrint hooks
151
+ */
152
+ async beforePrint(buffer: Uint8Array): Promise<Uint8Array> {
153
+ const result = await this.executeHook('beforePrint', buffer);
154
+ return (result as Uint8Array) || buffer;
155
+ }
156
+
157
+ /**
158
+ * Execute afterPrint hooks
159
+ */
160
+ async afterPrint(bytesSent: number): Promise<void> {
161
+ await this.executeHook('afterPrint', bytesSent);
162
+ }
163
+
164
+ /**
165
+ * Execute onError hooks
166
+ * @returns True if error should be suppressed
167
+ */
168
+ async onError(error: BluetoothPrintError): Promise<boolean> {
169
+ const result = await this.executeHook('onError', error);
170
+ return result === true;
171
+ }
172
+
173
+ /**
174
+ * Execute onStateChange hooks
175
+ */
176
+ async onStateChange(state: PrinterState, previousState: PrinterState): Promise<void> {
177
+ await this.executeHook('onStateChange', state, previousState);
178
+ }
179
+
180
+ /**
181
+ * Execute onProgress hooks
182
+ */
183
+ async onProgress(sent: number, total: number): Promise<void> {
184
+ await this.executeHook('onProgress', sent, total);
185
+ }
186
+
187
+ /**
188
+ * Clear all plugins
189
+ */
190
+ async clear(): Promise<void> {
191
+ for (const name of this.plugins.keys()) {
192
+ await this.unregister(name);
193
+ }
194
+ }
195
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Logging Plugin
3
+ * Provides detailed logging for printer operations
4
+ */
5
+
6
+ import { Plugin, PluginFactory, PluginOptions } from '../types';
7
+ import { Logger, LogLevel } from '@/utils/logger';
8
+
9
+ export interface LoggingPluginOptions extends PluginOptions {
10
+ /** Log level (default: DEBUG) */
11
+ level?: LogLevel;
12
+ /** Include timestamps (default: true) */
13
+ timestamps?: boolean;
14
+ /** Log progress updates (default: false, can be noisy) */
15
+ logProgress?: boolean;
16
+ }
17
+
18
+ /**
19
+ * Creates a logging plugin instance
20
+ */
21
+ export const createLoggingPlugin: PluginFactory = (options?: LoggingPluginOptions): Plugin => {
22
+ const opts: Required<LoggingPluginOptions> = {
23
+ level: options?.level ?? LogLevel.DEBUG,
24
+ timestamps: options?.timestamps ?? true,
25
+ logProgress: options?.logProgress ?? false,
26
+ };
27
+
28
+ const logger = Logger.scope('PrinterLog');
29
+ let startTime: number;
30
+
31
+ const formatTime = (): string => {
32
+ if (!opts.timestamps) return '';
33
+ return `[${new Date().toISOString()}] `;
34
+ };
35
+
36
+ return {
37
+ name: 'logging',
38
+ version: '1.0.0',
39
+ description: 'Detailed logging for printer operations',
40
+
41
+ hooks: {
42
+ beforeConnect: (deviceId: string) => {
43
+ startTime = Date.now();
44
+ logger.info(`${formatTime()}Connecting to device: ${deviceId}`);
45
+ },
46
+
47
+ afterConnect: (deviceId: string) => {
48
+ const elapsed = Date.now() - startTime;
49
+ logger.info(`${formatTime()}Connected to ${deviceId} (${elapsed}ms)`);
50
+ },
51
+
52
+ beforeDisconnect: (deviceId: string) => {
53
+ logger.info(`${formatTime()}Disconnecting from: ${deviceId}`);
54
+ },
55
+
56
+ afterDisconnect: (deviceId: string) => {
57
+ logger.info(`${formatTime()}Disconnected from: ${deviceId}`);
58
+ },
59
+
60
+ beforePrint: (buffer: Uint8Array) => {
61
+ startTime = Date.now();
62
+ logger.info(`${formatTime()}Starting print job: ${buffer.length} bytes`);
63
+ },
64
+
65
+ afterPrint: (bytesSent: number) => {
66
+ const elapsed = Date.now() - startTime;
67
+ const speed = ((bytesSent / elapsed) * 1000).toFixed(2);
68
+ logger.info(
69
+ `${formatTime()}Print complete: ${bytesSent} bytes in ${elapsed}ms (${speed} B/s)`
70
+ );
71
+ },
72
+
73
+ onError: error => {
74
+ logger.error(`${formatTime()}Error [${error.code}]: ${error.message}`);
75
+ return false; // Don't suppress the error
76
+ },
77
+
78
+ onStateChange: (state, previousState) => {
79
+ logger.debug(`${formatTime()}State: ${previousState} → ${state}`);
80
+ },
81
+
82
+ onProgress: (sent, total) => {
83
+ if (opts.logProgress) {
84
+ const percent = ((sent / total) * 100).toFixed(1);
85
+ logger.debug(`${formatTime()}Progress: ${sent}/${total} (${percent}%)`);
86
+ }
87
+ },
88
+ },
89
+
90
+ init: () => {
91
+ Logger.setLevel(opts.level);
92
+ logger.info('Logging plugin initialized');
93
+ },
94
+
95
+ destroy: () => {
96
+ logger.info('Logging plugin destroyed');
97
+ },
98
+ };
99
+ };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Retry Plugin
3
+ * Provides automatic retry with exponential backoff for failed operations
4
+ */
5
+
6
+ import { Plugin, PluginFactory, PluginOptions } from '../types';
7
+ import { Logger } from '@/utils/logger';
8
+ import { BluetoothPrintError, ErrorCode } from '@/errors/BluetoothError';
9
+
10
+ export interface RetryPluginOptions extends PluginOptions {
11
+ /** Maximum retry attempts (default: 3) */
12
+ maxRetries?: number;
13
+ /** Initial delay in ms (default: 1000) */
14
+ initialDelay?: number;
15
+ /** Maximum delay in ms (default: 10000) */
16
+ maxDelay?: number;
17
+ /** Backoff multiplier (default: 2) */
18
+ backoffMultiplier?: number;
19
+ /** Error codes that should trigger retry */
20
+ retryableErrors?: ErrorCode[];
21
+ }
22
+
23
+ /**
24
+ * Creates a retry plugin instance
25
+ */
26
+ export const createRetryPlugin: PluginFactory = (options?: RetryPluginOptions): Plugin => {
27
+ const opts: Required<RetryPluginOptions> = {
28
+ maxRetries: options?.maxRetries ?? 3,
29
+ initialDelay: options?.initialDelay ?? 1000,
30
+ maxDelay: options?.maxDelay ?? 10000,
31
+ backoffMultiplier: options?.backoffMultiplier ?? 2,
32
+ retryableErrors: options?.retryableErrors ?? [
33
+ ErrorCode.CONNECTION_FAILED,
34
+ ErrorCode.CONNECTION_TIMEOUT,
35
+ ErrorCode.WRITE_FAILED,
36
+ ErrorCode.WRITE_TIMEOUT,
37
+ ],
38
+ };
39
+
40
+ const logger = Logger.scope('RetryPlugin');
41
+ let retryCount = 0;
42
+ let currentDelay = opts.initialDelay;
43
+
44
+ const shouldRetry = (error: BluetoothPrintError): boolean => {
45
+ return retryCount < opts.maxRetries && opts.retryableErrors.includes(error.code);
46
+ };
47
+
48
+ const sleep = (ms: number): Promise<void> => {
49
+ return new Promise(resolve => setTimeout(resolve, ms));
50
+ };
51
+
52
+ return {
53
+ name: 'retry',
54
+ version: '1.0.0',
55
+ description: 'Automatic retry with exponential backoff',
56
+
57
+ hooks: {
58
+ beforeConnect: () => {
59
+ // Reset retry state on new connection attempt
60
+ retryCount = 0;
61
+ currentDelay = opts.initialDelay;
62
+ },
63
+
64
+ beforePrint: () => {
65
+ // Reset retry state on new print job
66
+ retryCount = 0;
67
+ currentDelay = opts.initialDelay;
68
+ },
69
+
70
+ onError: async (error: BluetoothPrintError) => {
71
+ if (shouldRetry(error)) {
72
+ retryCount++;
73
+ logger.warn(
74
+ `Retryable error occurred (attempt ${retryCount}/${opts.maxRetries}): ${error.code}`
75
+ );
76
+ logger.info(`Waiting ${currentDelay}ms before retry...`);
77
+
78
+ await sleep(currentDelay);
79
+
80
+ // Exponential backoff
81
+ currentDelay = Math.min(currentDelay * opts.backoffMultiplier, opts.maxDelay);
82
+
83
+ // Note: Returning false means error is not suppressed
84
+ // The actual retry logic would need to be implemented in the printer
85
+ // This plugin mainly provides the delay and logging
86
+ return false;
87
+ }
88
+
89
+ if (retryCount > 0) {
90
+ logger.error(`Failed after ${retryCount} retries: ${error.code} - ${error.message}`);
91
+ }
92
+
93
+ return false;
94
+ },
95
+ },
96
+
97
+ init: () => {
98
+ logger.info(
99
+ `Retry plugin initialized (max: ${opts.maxRetries}, delay: ${opts.initialDelay}ms)`
100
+ );
101
+ },
102
+ };
103
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Plugin System Exports
3
+ */
4
+
5
+ export { PluginManager } from './PluginManager';
6
+ export type { Plugin, PluginHooks, PluginOptions, PluginFactory } from './types';
7
+
8
+ // Built-in plugins
9
+ export { createLoggingPlugin } from './builtin/LoggingPlugin';
10
+ export { createRetryPlugin } from './builtin/RetryPlugin';